Skip to content

Commit caaf5ee

Browse files
author
SqlRush
committed
Accept memory recall query aliases
1 parent 4e338c5 commit caaf5ee

5 files changed

Lines changed: 111 additions & 21 deletions

File tree

docs/cc-100-roadmap.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,8 @@ M6 补充:session-memory recall agent 和 relevant-memory selector 现在递
188188

189189
M6 补充:session-memory recall agent 和 relevant-memory selector 现在也接受 provider-style `choices`/`outputs`/`candidates`/`generations` response wrapper,可从 `message.content`、content-block array、`content.parts[].text` 和 fenced `json` code block 里递归恢复 JSON selection payload。
190190

191+
M6 补充:session-memory recall agent 和 relevant-memory selector 的 query 解析现在接受 `user_query`/`userQuery``question``prompt``input``search``search_text`/`searchText` 等相邻别名,模型返回非 canonical query key 时仍能保留改写后的检索语义。
192+
191193
M6 补充:model-backed memory fact extraction 现在也接受 provider-style `choices`/`outputs`/`candidates`/`generations` response wrapper 以及顶层 `message`/`content`/`text` envelope,可从 `message.content`、content-block array、`content.parts[].text` 和 fenced `json` code block 里递归恢复 JSON facts payload。
192194

193195
M6 补充:model-backed memory fact extraction 现在接受更多 fact source aliases,包括 `sourceMessageUUID`/`source_message_uuid``sourceEventId`/`source_event_id``originId` 以及 `turn`/`event` source object,并保留 numeric source IDs 为字符串。

docs/claude-code-go-rewrite-plan.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,7 @@ test/parity/ # golden tests against TS/official behavior
299299
- 本轮补充:model-backed relevant memory selector prompt 现在包含 recent successful tools 和 already-surfaced memory paths 的有界上下文,模型侧选择与 deterministic prefilter 的 tool/surfaced 约束更一致。
300300
- 本轮补充:session-memory recall agent 和 relevant-memory selector 现在递归解包 `data`/`payload`/`body`、JSON:API `resource`/`attributes`/`properties`/`attrs``included`,以及 GraphQL `viewer`/`edge`/`node`/`nodes`/`edges``collection`/`list`/`children`/`values` selection wrapper;带明确非 memory/session `type` 的 resource 不再用裸 `id` 污染选择顺序,API-shaped model response 中的 session IDs 和 memory paths 会按模型顺序保留。
301301
- 本轮补充:session-memory recall agent 和 relevant-memory selector 现在也接受 provider-style `choices`/`outputs`/`candidates`/`generations` response wrapper 以及顶层 `message`/`content`/`text` envelope,可从 `message.content`、content-block array、`content.parts[].text` 和 fenced `json` code block 里递归恢复 JSON selection payload。
302+
- 本轮补充:session-memory recall agent 和 relevant-memory selector 的 query 解析现在接受 `user_query`/`userQuery``question``prompt``input``search``search_text`/`searchText` 等相邻别名,模型返回非 canonical query key 时仍能保留改写后的检索语义。
302303
- 本轮补充:relevant-memory selector 现在接受 `uri`/`url`/`href``fileUri`/`fileUrl` 等 memory path aliases,并能从 `file://` URI 或 API URL path basename 恢复候选 memory;顶层 `memories`/`matches`/`filesList` 集合在同时带 `query` 时也会参与 selection 解析,避免 query 快路径丢掉模型选择。
303304
- 本轮补充:session-memory recall agent 现在接受 `summaries``selectedSummaries``relevantSummaries``candidateSummaries` 等 summary collection aliases,并会继续从嵌套 `summary.sessionId`/`summaryId` 恢复模型排序的 session IDs。
304305
- 本轮补充:session-memory recall agent 现在也接受 `sessionUri`/`sessionUrl``uri`/`url`/`href` 等 summary link aliases,能从 `file://.../summary.md` 或 API URL path 中恢复 session ID 并按模型顺序匹配 summary。

docs/first-second-parity-audit.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ M6 progress now includes:
149149
- `internal/memory`: session-memory summary text now falls back to frontmatter aliases such as `summaryText`, `summary`, `content`, `text`, `resultSummary`, and `finalSummary` when the markdown body is empty, while preserving body precedence.
150150
- `internal/memory`: session-memory recall and relevant-memory model selectors now recurse through GraphQL/JSON:API selection wrappers such as `data`, `payload`, `body`, `resource`, `attributes`, `properties`, `attrs`, `viewer`, `edge`, `node`, `nodes`, and `edges`, preserving model-ranked session IDs and memory paths from API-shaped responses.
151151
- `internal/memory`: session-memory recall and relevant-memory model selectors now also unwrap provider-style `choices`, `outputs`, `candidates`, and `generations` responses plus top-level `message`/`content`/`text`/`output_text` envelopes, recovering JSON selection payloads from nested `message.content`, content-block arrays, `content.parts[].text`, and fenced `json` code blocks.
152+
- `internal/memory`: session-memory recall and relevant-memory model selectors now preserve rewritten search text from adjacent query aliases such as `user_query`, `question`, `prompt`, `input`, `search`, and `search_text`, not only canonical `query`/`search_query` keys.
152153
- `internal/memory`: relevant-memory model selection now accepts URI/link path aliases such as `uri`, `url`, `href`, `fileUri`, and `fileUrl`, matching `file://` URIs and API URL path basenames back to candidate memory files; top-level `memories`, `matches`, and `filesList` collections are also scanned when a response includes `query`.
153154
- `internal/memory`: relevant-memory model selection now also treats file-selection aliases such as `selectedFiles`, `relevantFiles`, `candidateFiles`, `selectedFilePaths`, and `relevantFilePaths` as memory selections, including nested file objects and link objects.
154155
- `internal/memory`: session-memory recall model selection now accepts summary collection aliases such as `summaries`, `selectedSummaries`, `relevantSummaries`, and `candidateSummaries`, including nested `summary.sessionId` and `summaryId` fields.

internal/memory/agent.go

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,11 @@ func parseRecallAgentJSON(raw string) (string, []contracts.ID, bool) {
487487
if text, ok := selectionProviderResponseText(raw); ok {
488488
return parseRecallAgentJSON(text)
489489
}
490+
var rawObject map[string]json.RawMessage
491+
queryFromAliases := ""
492+
if err := json.Unmarshal([]byte(raw), &rawObject); err == nil {
493+
queryFromAliases = selectionQueryFromRawObject(rawObject)
494+
}
490495
var object struct {
491496
Query string `json:"query"`
492497
SearchQuery string `json:"search_query"`
@@ -752,6 +757,9 @@ func parseRecallAgentJSON(raw string) (string, []contracts.ID, bool) {
752757
if query == "" {
753758
query = strings.TrimSpace(object.ExpandedQueryCamel)
754759
}
760+
if query == "" {
761+
query = queryFromAliases
762+
}
755763
if len(ids) == 0 {
756764
query, ids = recallSelectionFromNestedPayloads(query,
757765
object.Selection,
@@ -818,7 +826,7 @@ func parseRelevantMemoryAgentJSON(raw string) (string, []string, bool) {
818826
}
819827
var rawObject map[string]json.RawMessage
820828
if err := json.Unmarshal([]byte(raw), &rawObject); err == nil {
821-
query := relevantMemoryQueryFromRawObject(rawObject)
829+
query := selectionQueryFromRawObject(rawObject)
822830
ids := relevantMemoryIDsFromRawObject(rawObject)
823831
if len(ids) == 0 {
824832
query, ids = relevantMemorySelectionFromNestedPayloads(query, rawObjectValues(rawObject,
@@ -1020,25 +1028,7 @@ func parseRelevantMemoryAgentJSON(raw string) (string, []string, bool) {
10201028
object.Resources,
10211029
)
10221030
}
1023-
query := strings.TrimSpace(object.Query)
1024-
if query == "" {
1025-
query = strings.TrimSpace(object.SearchQuery)
1026-
}
1027-
if query == "" {
1028-
query = strings.TrimSpace(object.SearchQueryCamel)
1029-
}
1030-
if query == "" {
1031-
query = strings.TrimSpace(object.RewrittenQuery)
1032-
}
1033-
if query == "" {
1034-
query = strings.TrimSpace(object.RewrittenQueryCamel)
1035-
}
1036-
if query == "" {
1037-
query = strings.TrimSpace(object.ExpandedQuery)
1038-
}
1039-
if query == "" {
1040-
query = strings.TrimSpace(object.ExpandedQueryCamel)
1041-
}
1031+
query := firstNonEmpty(object.Query, object.SearchQuery, object.SearchQueryCamel, object.RewrittenQuery, object.RewrittenQueryCamel, object.ExpandedQuery, object.ExpandedQueryCamel)
10421032
if len(ids) == 0 {
10431033
query, ids = relevantMemorySelectionFromNestedPayloads(query,
10441034
object.Selected,
@@ -1083,7 +1073,7 @@ func parseRelevantMemoryAgentJSON(raw string) (string, []string, bool) {
10831073
return "", nil, false
10841074
}
10851075

1086-
func relevantMemoryQueryFromRawObject(object map[string]json.RawMessage) string {
1076+
func selectionQueryFromRawObject(object map[string]json.RawMessage) string {
10871077
for _, key := range []string{
10881078
"query",
10891079
"search_query",
@@ -1092,6 +1082,14 @@ func relevantMemoryQueryFromRawObject(object map[string]json.RawMessage) string
10921082
"rewrittenQuery",
10931083
"expanded_query",
10941084
"expandedQuery",
1085+
"user_query",
1086+
"userQuery",
1087+
"question",
1088+
"prompt",
1089+
"input",
1090+
"search",
1091+
"search_text",
1092+
"searchText",
10951093
} {
10961094
if value := stringFromRawJSON(object[key]); value != "" {
10971095
return value

internal/memory/memory_test.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,44 @@ func TestMemoryAgentSelectRelevantMemoriesUsesModelPaths(t *testing.T) {
405405
}
406406
}
407407

408+
func TestMemoryAgentSelectRelevantMemoriesParsesAdditionalQueryAliases(t *testing.T) {
409+
dir := t.TempDir()
410+
dbPath := filepath.Join(dir, "db.md")
411+
opsPath := filepath.Join(dir, "ops.md")
412+
writeFile(t, dbPath, "---\ndescription: database permissions migration\n---\ndb rules\n")
413+
writeFile(t, opsPath, "---\ndescription: deployment runbook\n---\nops rules\n")
414+
client := &fakeMemoryClient{response: &anthropic.Response{
415+
ID: "msg_memory_query_aliases",
416+
Type: "message",
417+
Role: "assistant",
418+
Model: "sonnet",
419+
Content: []contracts.ContentBlock{contracts.NewTextBlock(`{"user_query":"database access","memory_paths":["db.md"]}`)},
420+
}}
421+
422+
result, err := (Agent{Client: client}).SelectRelevantMemories(context.Background(), dir, "database permissions", RelevantMemorySelectorOptions{Limit: 1})
423+
if err != nil {
424+
t.Fatal(err)
425+
}
426+
if result.Fallback || result.Query != "database access" || strings.Join(result.SelectedIDs, ",") != "db.md" {
427+
t.Fatalf("user_query result = %#v", result)
428+
}
429+
if len(result.Selected) != 1 || result.Selected[0].Path != dbPath {
430+
t.Fatalf("user_query selected = %#v", result.Selected)
431+
}
432+
433+
client.response.Content = []contracts.ContentBlock{contracts.NewTextBlock(`{"question":"deployment runbook","memoryPaths":["ops.md"]}`)}
434+
result, err = (Agent{Client: client}).SelectRelevantMemories(context.Background(), dir, "deployment", RelevantMemorySelectorOptions{Limit: 1})
435+
if err != nil {
436+
t.Fatal(err)
437+
}
438+
if result.Fallback || result.Query != "deployment runbook" || strings.Join(result.SelectedIDs, ",") != "ops.md" {
439+
t.Fatalf("question result = %#v", result)
440+
}
441+
if len(result.Selected) != 1 || result.Selected[0].Path != opsPath {
442+
t.Fatalf("question selected = %#v", result.Selected)
443+
}
444+
}
445+
408446
func TestMemoryAgentSelectRelevantMemoriesParsesNestedAliasesAndFallsBack(t *testing.T) {
409447
dir := t.TempDir()
410448
dbPath := filepath.Join(dir, "db.md")
@@ -1733,6 +1771,56 @@ func TestMemoryAgentRecallParsesAlternateModelResponseKeys(t *testing.T) {
17331771
}
17341772
}
17351773

1774+
func TestMemoryAgentRecallParsesAdditionalQueryAliases(t *testing.T) {
1775+
root := filepath.Join(t.TempDir(), "session-memory")
1776+
for _, item := range []struct {
1777+
id contracts.ID
1778+
summary string
1779+
updated int64
1780+
}{
1781+
{id: "prior", summary: "database access policy notes", updated: 200},
1782+
{id: "other", summary: "credential rotation notes", updated: 100},
1783+
} {
1784+
if _, err := WriteSessionSummary(SessionSummaryOptions{
1785+
Root: root,
1786+
SessionID: item.id,
1787+
Summary: item.summary,
1788+
UpdatedAt: time.Unix(item.updated, 0).UTC(),
1789+
}); err != nil {
1790+
t.Fatal(err)
1791+
}
1792+
}
1793+
client := &fakeMemoryClient{response: &anthropic.Response{
1794+
ID: "msg_recall_query_aliases",
1795+
Type: "message",
1796+
Role: "assistant",
1797+
Model: "sonnet",
1798+
Content: []contracts.ContentBlock{contracts.NewTextBlock(`{"user_query":"database access","session_ids":["prior"]}`)},
1799+
}}
1800+
result, err := (Agent{Client: client}).Recall(context.Background(), root, "what did we decide about db access?", RecallOptions{Limit: 1})
1801+
if err != nil {
1802+
t.Fatal(err)
1803+
}
1804+
if result.Fallback || result.Query != "database access" || strings.Join(contractIDStrings(result.SelectedIDs), ",") != "prior" {
1805+
t.Fatalf("user_query result = %#v", result)
1806+
}
1807+
if len(result.Matches) != 1 || result.Matches[0].Summary.SessionID != "prior" {
1808+
t.Fatalf("user_query matches = %#v", result.Matches)
1809+
}
1810+
1811+
client.response.Content = []contracts.ContentBlock{contracts.NewTextBlock(`{"question":"credential rotation","selectedSessions":[{"sessionId":"other"}]}`)}
1812+
result, err = (Agent{Client: client}).Recall(context.Background(), root, "what did we decide about credential rotation?", RecallOptions{Limit: 1})
1813+
if err != nil {
1814+
t.Fatal(err)
1815+
}
1816+
if result.Fallback || result.Query != "credential rotation" || strings.Join(contractIDStrings(result.SelectedIDs), ",") != "other" {
1817+
t.Fatalf("question result = %#v", result)
1818+
}
1819+
if len(result.Matches) != 1 || result.Matches[0].Summary.SessionID != "other" {
1820+
t.Fatalf("question matches = %#v", result.Matches)
1821+
}
1822+
}
1823+
17361824
func TestMemoryAgentRecallParsesNestedModelSelections(t *testing.T) {
17371825
root := filepath.Join(t.TempDir(), "session-memory")
17381826
for _, item := range []struct {

0 commit comments

Comments
 (0)