Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
fix(coderd/x/chatd): project memory extraction only creates memories
A question-only turn led the extractor to rewrite a correct memory with
hallucinated content. Existing memories are now updated only by the main
agent's tool or the UI; extraction proposals that collide with an
existing name are dropped.
  • Loading branch information
f0ssel committed Sep 11, 2026
commit db834ae21a3a8f78f4706b364814520bef8c345c
26 changes: 17 additions & 9 deletions coderd/x/chatd/projectmemory_extract.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const projectMemoryExtractionPrompt = "You review a completed coding-chat turn a
chattool.ProjectMemoryGuidance + " " +
"Record only facts the user stated or explicitly confirmed in this turn. " +
"Never record that something is unknown, unspecified, undecided, or pending, and never record questions or the assistant's own guesses. " +
"Skip facts that already appear in the memory index unless the user changed them, and reuse the existing name when updating. " +
"Skip anything already covered by a memory in the index; existing memories are updated by the main agent, not by you. " +
"Most turns contain nothing new: return an empty list in that case."

type projectMemoryExtraction struct {
Expand Down Expand Up @@ -141,21 +141,29 @@ func (p *Server) extractProjectMemories(ctx context.Context, logger slog.Logger,
}
}

// applyProjectMemoryUpsert records a memory the extractor proposed. It only
// creates: dogfooding showed the extractor rewriting a correct memory with
// hallucinated content after a question-only turn, so updates to existing
// memories are reserved for the main agent's tool and the UI.
func applyProjectMemoryUpsert(ctx context.Context, store database.Store, chat database.Chat, upsert projectMemoryExtractionUpsert) error {
normalized, err := normalizeProjectMemoryExtraction(upsert)
if err != nil {
return err
}
name, memoryType, description, body := normalized.Name, normalized.Type, normalized.Description, normalized.Body
_, existingErr := store.GetChatProjectMemoryByName(ctx, database.GetChatProjectMemoryByNameParams{ProjectID: chat.ProjectID.UUID, Name: name})
if existingErr != nil {
count, countErr := store.CountChatProjectMemoriesByProjectID(ctx, chat.ProjectID.UUID)
if countErr != nil {
return xerrors.Errorf("count project memories: %w", countErr)
}
if count >= chattool.MaxProjectMemories {
return xerrors.New("project memory limit reached")
}
if existingErr == nil {
return xerrors.Errorf("memory %q already exists", name)
}
if !errors.Is(existingErr, sql.ErrNoRows) {
return xerrors.Errorf("look up project memory: %w", existingErr)
}
count, countErr := store.CountChatProjectMemoriesByProjectID(ctx, chat.ProjectID.UUID)
if countErr != nil {
return xerrors.Errorf("count project memories: %w", countErr)
}
if count >= chattool.MaxProjectMemories {
return xerrors.New("project memory limit reached")
}
_, err = store.UpsertChatProjectMemoryByName(ctx, database.UpsertChatProjectMemoryByNameParams{
ProjectID: chat.ProjectID.UUID, OrganizationID: chat.OrganizationID, Type: memoryType,
Expand Down
47 changes: 46 additions & 1 deletion coderd/x/chatd/projectmemory_extract_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,8 @@ func TestExtractProjectMemories(t *testing.T) {
db.EXPECT().GetChatProjectMemoryByName(gomock.Any(), database.GetChatProjectMemoryByNameParams{
ProjectID: chat.ProjectID.UUID,
Name: "release_notes",
}).Return(database.GetChatProjectMemoryByNameRow{}, nil),
}).Return(database.GetChatProjectMemoryByNameRow{}, sql.ErrNoRows),
db.EXPECT().CountChatProjectMemoriesByProjectID(gomock.Any(), chat.ProjectID.UUID).Return(int64(0), nil),
db.EXPECT().UpsertChatProjectMemoryByName(gomock.Any(), validUpsert).Return(database.ChatProjectMemory{}, nil),
db.EXPECT().UpsertChatProjectMemoryCursor(gomock.Any(), database.UpsertChatProjectMemoryCursorParams{
ChatID: chat.ID,
Expand All @@ -290,6 +291,50 @@ func TestExtractProjectMemories(t *testing.T) {
require.NotContains(t, capturedPrompt, `"deletes"`)
})

t.Run("NeverOverwritesExistingMemory", func(t *testing.T) {
t.Parallel()

ctrl := gomock.NewController(t)
db := dbmock.NewMockStore(ctrl)
chat := newChat()
server := newServer(t, db, roundTripFunc(func(req *http.Request) (*http.Response, error) {
response := objectResponse(t, map[string]any{
"upserts": []map[string]any{{
"name": "release_notes",
"type": database.ChatProjectMemoryTypeProject,
"description": "Hallucinated rewrite",
"body": "Deploy day is Friday.",
}},
})
response.Request = req
return response, nil
}))
gomock.InOrder(
db.EXPECT().GetChatByID(gomock.Any(), chat.ID).Return(chat, nil),
db.EXPECT().GetChatProjectMemoryCursor(gomock.Any(), chat.ID).Return(
database.ChatProjectMemoryCursor{ChatID: chat.ID, HistoryVersion: 3}, nil,
),
db.EXPECT().GetChatMessagesForPromptByChatID(gomock.Any(), chat.ID).Return([]database.ChatMessage{
message(t, 1, database.ChatMessageRoleUser, "when do we deploy?", 5),
}, nil),
db.EXPECT().GetChatProjectMemoriesByProjectID(gomock.Any(), chat.ProjectID.UUID).Return(nil, nil),
)
expectModelResolution(db, chat)
gomock.InOrder(
db.EXPECT().GetChatProjectMemoryByName(gomock.Any(), database.GetChatProjectMemoryByNameParams{
ProjectID: chat.ProjectID.UUID,
Name: "release_notes",
}).Return(database.GetChatProjectMemoryByNameRow{}, nil),
// No UpsertChatProjectMemoryByName: the existing memory is left alone.
db.EXPECT().UpsertChatProjectMemoryCursor(gomock.Any(), database.UpsertChatProjectMemoryCursorParams{
ChatID: chat.ID,
HistoryVersion: chat.HistoryVersion,
}).Return(database.ChatProjectMemoryCursor{}, nil),
)

server.extractProjectMemories(t.Context(), slogtest.Make(t, nil), chat)
})

t.Run("RespectsCapForNewNames", func(t *testing.T) {
t.Parallel()

Expand Down
Loading