Skip to content

Commit 0738174

Browse files
author
SqlRush
committed
Accept wrapped memory agent selections
1 parent d645613 commit 0738174

5 files changed

Lines changed: 213 additions & 1 deletion

File tree

docs/cc-100-roadmap.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,8 @@ M6 补充:relevant memory prefetch 现在有可取消 runtime,并在 `Memory
172172

173173
M6 补充:model-backed relevant memory selector prompt 现在包含 recent successful tools 和 already-surfaced memory paths 的有界上下文,模型侧选择与 deterministic prefilter 的 tool/surfaced 约束更一致。
174174

175+
M6 补充:session-memory recall agent 和 relevant-memory selector 现在递归解包 `data`/`payload`/`body`、JSON:API `resource`/`attributes`/`properties`/`attrs`,以及 GraphQL `viewer`/`edge`/`node`/`nodes`/`edges` selection wrapper,API-shaped model response 中的 session IDs 和 memory paths 会按模型顺序保留。
176+
175177
M6/M7 补充:Runner 会把 `RelevantMemoryDir` 透传到 tool metadata 的 internal auto-memory path context,让 Read tool freshness prefix 和 permission internal-path policy 在同一 memory dir 配置下生效。
176178

177179
M6 补充:transcript resume 在 fallback 转换 attachment message 时会保留 raw attachment payload,确保恢复出的 `relevant_memories` attachment 仍可被 request 构建路径展开为 system-reminder。
@@ -502,6 +504,8 @@ M7 补充:prompt history `LogEntry` 读取现在接受 `sessionID`/`session`/`
502504

503505
本轮补充:conversation runner 现在会在用户消息入队后计算 compact token warning state,并在触发 warning/error/auto-compact/blocking 阈值时发出 `token_warning` event;warning state 接入 blocking-limit override,auto-compact threshold 判断接入 `CLAUDE_AUTOCOMPACT_PCT_OVERRIDE`,使 runtime warning 和自动压缩使用同一套 window 输入。
504506

507+
本轮补充:session-memory recall agent 和 relevant-memory selector 现在递归解包 `data`/`payload`/`body`、JSON:API `resource`/`attributes`/`properties`/`attrs`,以及 GraphQL `viewer`/`edge`/`node`/`nodes`/`edges` selection wrapper,API-shaped model response 中的 session IDs 和 memory paths 会按模型顺序保留。
508+
505509
本轮补充:microcompact disk cache loader 现在读取 Go 默认、camelCase、snake_case 和相邻实现常见字段别名/包装形态,包括 `result`/`data`/`cache`/`value` wrapper、`content`/`text` summary、`cacheKey`/`key`/`hash` digest、cache-hit 别名、计数字段别名/数字字符串、RFC3339/Unix 秒/Unix 毫秒时间字段,以及 `createdAt` + `ttlSeconds`/`ttlMs`/`expiresIn`/`maxAge` 等相对 TTL 推导,提升 cached microcompact 文件在不同实现/版本间的恢复率。
506510

507511
本轮补充:microcompact disk cache loader 现在也接受 `cacheEntry`/`cache_entry``micro_compact`/`micro_compact_result` wrapper,以及 `summaryMarkdown`/`resultSummary`/`compressedText``cacheDigest`/`digestHash`/`fingerprint``summarizedCount`/`retainedCount``formatVersion``ttlMilliseconds`/`expiresInMilliseconds`/`maxAgeMilliseconds` 等相邻实现字段别名。

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ test/parity/ # golden tests against TS/official behavior
247247
- 本轮补充:memory 层增加可取消 `PrefetchRelevantMemories` runtime,复用现有 gating/selector/surfacing 逻辑返回 plan、selection 和 attachments;conversation `RunTurn` 会在用户消息进入后启动 relevant memory prefetch,并在第一轮 model request 消费结果,预取文件系统错误 fail-open 且不阻断主请求。
248248
- 本轮补充:relevant memory prefetch 接入 model-backed sideQuery selector:当 `MemoryAgentClient` 可用时,先向模型提供候选 memory manifest,请模型返回 `memory_paths`/`memoryPaths`/`filePath`/`matches`/嵌套 selection 等路径别名,按模型顺序读取附件;模型错误或无效路径会 fail-open 回落 deterministic selector。完整官方 prompt/telemetry parity 仍继续推进。
249249
- 本轮补充:model-backed relevant memory selector prompt 现在包含 recent successful tools 和 already-surfaced memory paths 的有界上下文,模型侧选择与 deterministic prefilter 的 tool/surfaced 约束更一致。
250+
- 本轮补充:session-memory recall agent 和 relevant-memory selector 现在递归解包 `data`/`payload`/`body`、JSON:API `resource`/`attributes`/`properties`/`attrs`,以及 GraphQL `viewer`/`edge`/`node`/`nodes`/`edges` selection wrapper,API-shaped model response 中的 session IDs 和 memory paths 会按模型顺序保留。
250251
- 本轮补充:conversation runner 会在用户消息入队后基于 compact window 计算 token warning state,并在达到 warning/error/auto-compact/blocking 阈值时发出 `token_warning` event;warning state 接入 blocking-limit env override,auto-compact 判断接入既有 `CLAUDE_AUTOCOMPACT_PCT_OVERRIDE`,避免 warning 和 compact 阈值来源分叉。
251252
- 本轮补充:microcompact disk cache loader 现在读取 Go 默认、camelCase 和 snake_case 字段别名,并容忍计数字段的数字字符串,以及 RFC3339、Unix 秒、Unix 毫秒时间字段,提升 cached microcompact 文件在不同实现/版本间的恢复率。
252253
- 本轮补充:microcompact disk cache loader 现在接受 `cacheEntry`/`cache_entry``micro_compact`/`micro_compact_result` wrapper,以及 `summaryMarkdown`/`resultSummary`/`compressedText``cacheDigest`/`digestHash`/`fingerprint``summarizedCount`/`retainedCount``formatVersion``ttlMilliseconds`/`expiresInMilliseconds`/`maxAgeMilliseconds` 等相邻实现字段别名。

docs/first-second-parity-audit.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ M6 progress now includes:
137137
- `internal/compact`: microcompact disk cache loading now treats a `value` text content-block object as a direct summary payload while still recovering digest, version, timestamp, and related cache metadata from that same `value` sidecar object when present.
138138
- `internal/conversation`: optional auto-compact can now run before the main request, fail open with a consecutive-failure circuit breaker, emit runtime `token_warning` events from compact warning state, persist compact boundary metadata to transcript, write a session-memory summary, inject recalled session-memory snippets into API request context when enabled, and optionally extract turn-end memory facts into session memory.
139139
- `internal/memory`: session-memory summaries can now be loaded with frontmatter field aliases, rolled up/pruned into an archive summary with prior rollup archives excluded from candidate compaction and merged across archive IDs, rune-safe rollup truncation, recalled by query with deterministic scoring and recency ordering, ranked through a model-assisted candidate/session-id selection path with fallback when selected IDs are invalid or excluded, alternate/camel response-key parsing, fenced/prose JSON extraction, scalar session-id parsing, nested/wrapped/collection-alias selection parsing, and nested selected-memory item parsing, and injected into resume context through the same optional recall agent path; deterministic and model-backed memory fact extraction can summarize user preferences, requests, decisions, and tool-use facts, including fenced/prose JSON, wrapped facts responses, alternate and structured fact field names, nested source objects, nested fact response shapes, and fact kind aliases.
140+
- `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.
140141
- `internal/session`: project session listing and pagination, prompt-history lock/buffered flush/field aliases, lightweight transcript index/title/text-preview inference, line-offset transcript indexing/window/byte-budget-window/parent-chain/resume/tail/byte-budget-tail loading, AI-title/last-prompt/task/agent/PR/worktree transcript metadata loading and type/field aliases, transcript message/session UUID field aliases, tombstone metadata loading with target/session/reason aliases and delete/relink replay, agent-scoped content replacement metadata/record field-alias loading, session-scoped metadata reappend including AI-title/last-prompt/task-summary, streaming transcript search snippets for resume/search UI, official `subagents/agent-*.jsonl` transcript layout with legacy sidechain listing, agent metadata sidecar read/write/field aliases, sidechain runtime start/append/finish/cancel/fail summary bridging plus parent-chain append/finish, sidechain manager orchestration for spawn/append/finish/cancel/fail/list/resume, sidechain state/list/resume support with content-field aliases, sidechain resume context construction, sidechain conversation and agent-scoped content-replacement reconstruction, transcript tail/window loading and byte-budget tail loading for bounded-memory resume/UI paths, lightweight transcript metadata loading, remote-history token refresh retry, page-field/event-list/records/entries/last-id/cursor/event-id/has-next aliases plus wrapped-data/links/paging/bare-array/keyed-event-map responses, before_id resume, fallback field fill, and duplicate-aware parent linking during transcript materialization.
141142
- `internal/session`: transcript full loader and line-offset index now accept top-level record UUID aliases such as `messageUuid`, `messageUUID`, `message_id`, `messageId`, and `id`, plus parent message UUID/ID aliases for progress bridge and indexed resume paths.
142143
- `internal/session`: indexed resume chains now distinguish budget-truncated parents from genuinely missing parents, exposing both markers while preserving bounded transcript reads.

internal/memory/agent.go

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -525,8 +525,22 @@ func parseRecallAgentJSON(raw string) (string, []contracts.ID, bool) {
525525
CandidateMemoriesCamel []json.RawMessage `json:"candidateMemories"`
526526
Candidates []json.RawMessage `json:"candidates"`
527527
Results []json.RawMessage `json:"results"`
528+
Nodes []json.RawMessage `json:"nodes"`
529+
Edges []json.RawMessage `json:"edges"`
530+
Items []json.RawMessage `json:"items"`
531+
Resources []json.RawMessage `json:"resources"`
528532
Selection json.RawMessage `json:"selection"`
529533
Selected json.RawMessage `json:"selected"`
534+
Data json.RawMessage `json:"data"`
535+
Payload json.RawMessage `json:"payload"`
536+
Body json.RawMessage `json:"body"`
537+
Resource json.RawMessage `json:"resource"`
538+
Attributes json.RawMessage `json:"attributes"`
539+
Properties json.RawMessage `json:"properties"`
540+
Attrs json.RawMessage `json:"attrs"`
541+
Viewer json.RawMessage `json:"viewer"`
542+
Edge json.RawMessage `json:"edge"`
543+
Node json.RawMessage `json:"node"`
530544
Result json.RawMessage `json:"result"`
531545
Response json.RawMessage `json:"response"`
532546
Recall json.RawMessage `json:"recall"`
@@ -584,6 +598,10 @@ func parseRecallAgentJSON(raw string) (string, []contracts.ID, bool) {
584598
object.CandidateMemoriesCamel,
585599
object.Candidates,
586600
object.Results,
601+
object.Nodes,
602+
object.Edges,
603+
object.Items,
604+
object.Resources,
587605
)
588606
}
589607
query := strings.TrimSpace(object.Query)
@@ -606,7 +624,25 @@ func parseRecallAgentJSON(raw string) (string, []contracts.ID, bool) {
606624
query = strings.TrimSpace(object.ExpandedQueryCamel)
607625
}
608626
if len(ids) == 0 {
609-
query, ids = recallSelectionFromNestedPayloads(query, object.Selection, object.Selected, object.Result, object.Response, object.Recall, object.MemoryRecall, object.MemoryRecallCamel)
627+
query, ids = recallSelectionFromNestedPayloads(query,
628+
object.Selection,
629+
object.Selected,
630+
object.Data,
631+
object.Payload,
632+
object.Body,
633+
object.Resource,
634+
object.Attributes,
635+
object.Properties,
636+
object.Attrs,
637+
object.Viewer,
638+
object.Edge,
639+
object.Node,
640+
object.Result,
641+
object.Response,
642+
object.Recall,
643+
object.MemoryRecall,
644+
object.MemoryRecallCamel,
645+
)
610646
}
611647
return query, ids, query != "" || len(ids) > 0
612648
}
@@ -646,6 +682,20 @@ func parseRelevantMemoryAgentJSON(raw string) (string, []string, bool) {
646682
query, ids = relevantMemorySelectionFromNestedPayloads(query, rawObjectValues(rawObject,
647683
"selected",
648684
"selection",
685+
"data",
686+
"payload",
687+
"body",
688+
"resource",
689+
"attributes",
690+
"properties",
691+
"attrs",
692+
"viewer",
693+
"edge",
694+
"node",
695+
"nodes",
696+
"edges",
697+
"items",
698+
"resources",
649699
"result",
650700
"response",
651701
"memory_selection",
@@ -702,8 +752,22 @@ func parseRelevantMemoryAgentJSON(raw string) (string, []string, bool) {
702752
Memories []json.RawMessage `json:"memories"`
703753
FilesList []json.RawMessage `json:"files_list"`
704754
FilesListCamel []json.RawMessage `json:"filesList"`
755+
Nodes []json.RawMessage `json:"nodes"`
756+
Edges []json.RawMessage `json:"edges"`
757+
Items []json.RawMessage `json:"items"`
758+
Resources []json.RawMessage `json:"resources"`
705759
Selected json.RawMessage `json:"selected"`
706760
Selection json.RawMessage `json:"selection"`
761+
Data json.RawMessage `json:"data"`
762+
Payload json.RawMessage `json:"payload"`
763+
Body json.RawMessage `json:"body"`
764+
Resource json.RawMessage `json:"resource"`
765+
Attributes json.RawMessage `json:"attributes"`
766+
Properties json.RawMessage `json:"properties"`
767+
Attrs json.RawMessage `json:"attrs"`
768+
Viewer json.RawMessage `json:"viewer"`
769+
Edge json.RawMessage `json:"edge"`
770+
Node json.RawMessage `json:"node"`
707771
Result json.RawMessage `json:"result"`
708772
Response json.RawMessage `json:"response"`
709773
MemorySelection json.RawMessage `json:"memory_selection"`
@@ -753,6 +817,10 @@ func parseRelevantMemoryAgentJSON(raw string) (string, []string, bool) {
753817
object.Memories,
754818
object.FilesList,
755819
object.FilesListCamel,
820+
object.Nodes,
821+
object.Edges,
822+
object.Items,
823+
object.Resources,
756824
)
757825
}
758826
query := strings.TrimSpace(object.Query)
@@ -778,6 +846,16 @@ func parseRelevantMemoryAgentJSON(raw string) (string, []string, bool) {
778846
query, ids = relevantMemorySelectionFromNestedPayloads(query,
779847
object.Selected,
780848
object.Selection,
849+
object.Data,
850+
object.Payload,
851+
object.Body,
852+
object.Resource,
853+
object.Attributes,
854+
object.Properties,
855+
object.Attrs,
856+
object.Viewer,
857+
object.Edge,
858+
object.Node,
781859
object.Result,
782860
object.Response,
783861
object.MemorySelection,
@@ -1092,6 +1170,26 @@ var recallNestedItemKeys = []string{
10921170
"candidateSession",
10931171
"candidate_memory",
10941172
"candidateMemory",
1173+
"data",
1174+
"payload",
1175+
"body",
1176+
"result",
1177+
"response",
1178+
"resource",
1179+
"attributes",
1180+
"properties",
1181+
"attrs",
1182+
"viewer",
1183+
"edge",
1184+
"node",
1185+
"nodes",
1186+
"edges",
1187+
"items",
1188+
"resources",
1189+
"record",
1190+
"entry",
1191+
"item",
1192+
"value",
10951193
}
10961194

10971195
var relevantMemoryNestedItemKeys = []string{
@@ -1106,6 +1204,26 @@ var relevantMemoryNestedItemKeys = []string{
11061204
"relevantMemory",
11071205
"candidate_memory",
11081206
"candidateMemory",
1207+
"data",
1208+
"payload",
1209+
"body",
1210+
"result",
1211+
"response",
1212+
"resource",
1213+
"attributes",
1214+
"properties",
1215+
"attrs",
1216+
"viewer",
1217+
"edge",
1218+
"node",
1219+
"nodes",
1220+
"edges",
1221+
"items",
1222+
"resources",
1223+
"record",
1224+
"entry",
1225+
"item",
1226+
"value",
11091227
}
11101228

11111229
func stripMarkdownFence(raw string) string {

internal/memory/memory_test.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,44 @@ func TestMemoryAgentSelectRelevantMemoriesParsesNestedAliasesAndFallsBack(t *tes
442442
}
443443
}
444444

445+
func TestMemoryAgentSelectRelevantMemoriesParsesGraphQLResourceSelections(t *testing.T) {
446+
dir := t.TempDir()
447+
dbPath := filepath.Join(dir, "db.md")
448+
opsPath := filepath.Join(dir, "ops.md")
449+
writeFile(t, dbPath, "---\ndescription: database permissions migration\n---\ndb rules\n")
450+
writeFile(t, opsPath, "---\ndescription: deployment runbook\n---\nops rules\n")
451+
client := &fakeMemoryClient{response: &anthropic.Response{
452+
ID: "msg_memory_graphql",
453+
Type: "message",
454+
Role: "assistant",
455+
Model: "sonnet",
456+
Content: []contracts.ContentBlock{contracts.NewTextBlock(`{
457+
"data":{
458+
"viewer":{
459+
"memorySelection":{
460+
"query":"database access",
461+
"edges":[
462+
{"node":{"attrs":{"filePath":"ops.md"}}},
463+
{"edge":{"node":{"properties":{"memoryPath":"db.md"}}}}
464+
]
465+
}
466+
}
467+
}
468+
}`)},
469+
}}
470+
471+
result, err := (Agent{Client: client}).SelectRelevantMemories(context.Background(), dir, "database permissions", RelevantMemorySelectorOptions{Limit: 2})
472+
if err != nil {
473+
t.Fatal(err)
474+
}
475+
if result.Fallback || result.Query != "database access" || strings.Join(result.SelectedIDs, ",") != "ops.md,db.md" {
476+
t.Fatalf("result = %#v", result)
477+
}
478+
if len(result.Selected) != 2 || result.Selected[0].Path != opsPath || result.Selected[1].Path != dbPath {
479+
t.Fatalf("selected = %#v", result.Selected)
480+
}
481+
}
482+
445483
func TestPrefetchRelevantMemoriesCanUseMemoryAgentSelector(t *testing.T) {
446484
dir := t.TempDir()
447485
dbPath := filepath.Join(dir, "db.md")
@@ -1291,6 +1329,56 @@ func TestMemoryAgentRecallParsesWrappedSelectionsAndScalarID(t *testing.T) {
12911329
}
12921330
}
12931331

1332+
func TestMemoryAgentRecallParsesGraphQLResourceSelections(t *testing.T) {
1333+
root := filepath.Join(t.TempDir(), "session-memory")
1334+
for _, item := range []struct {
1335+
id contracts.ID
1336+
summary string
1337+
updated int64
1338+
}{
1339+
{id: "prior", summary: "database access policy notes", updated: 200},
1340+
{id: "other", summary: "credential rotation notes", updated: 100},
1341+
} {
1342+
if _, err := WriteSessionSummary(SessionSummaryOptions{
1343+
Root: root,
1344+
SessionID: item.id,
1345+
Summary: item.summary,
1346+
UpdatedAt: time.Unix(item.updated, 0).UTC(),
1347+
}); err != nil {
1348+
t.Fatal(err)
1349+
}
1350+
}
1351+
client := &fakeMemoryClient{response: &anthropic.Response{
1352+
ID: "msg_recall_graphql",
1353+
Type: "message",
1354+
Role: "assistant",
1355+
Model: "sonnet",
1356+
Content: []contracts.ContentBlock{contracts.NewTextBlock(`{
1357+
"data":{
1358+
"viewer":{
1359+
"memoryRecall":{
1360+
"searchQuery":"database access",
1361+
"edges":[
1362+
{"node":{"attributes":{"sessionId":"prior"}}},
1363+
{"edge":{"node":{"properties":{"summaryId":"other"}}}}
1364+
]
1365+
}
1366+
}
1367+
}
1368+
}`)},
1369+
}}
1370+
result, err := (Agent{Client: client}).Recall(context.Background(), root, "what did we decide about db access?", RecallOptions{Limit: 2})
1371+
if err != nil {
1372+
t.Fatal(err)
1373+
}
1374+
if result.Fallback || result.Query != "database access" || strings.Join(contractIDStrings(result.SelectedIDs), ",") != "prior,other" {
1375+
t.Fatalf("result = %#v", result)
1376+
}
1377+
if len(result.Matches) != 2 || result.Matches[0].Summary.SessionID != "prior" || result.Matches[1].Summary.SessionID != "other" {
1378+
t.Fatalf("matches = %#v", result.Matches)
1379+
}
1380+
}
1381+
12941382
func TestMemoryAgentRecallExtractsFencedJSONFromModelProse(t *testing.T) {
12951383
root := filepath.Join(t.TempDir(), "session-memory")
12961384
if _, err := WriteSessionSummary(SessionSummaryOptions{

0 commit comments

Comments
 (0)