Skip to content

Commit b8b9cd8

Browse files
author
SqlRush
committed
Accept memory fact source aliases
1 parent cd86c0e commit b8b9cd8

5 files changed

Lines changed: 158 additions & 15 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 补充: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。
190190

191+
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 为字符串。
192+
191193
M6 补充:compact runner 的 summary 响应现在也接受 provider-style `choices`/`outputs`/`candidates`/`generations` wrapper,可在构建 compact plan 前从 `message.content`、content-block array、`content.parts[].text` 和 fenced `json` code block 中恢复 visible summary text。
192194

193195
M6/M7 补充:Runner 会把 `RelevantMemoryDir` 透传到 tool metadata 的 internal auto-memory path context,让 Read tool freshness prefix 和 permission internal-path policy 在同一 memory dir 配置下生效。

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,7 @@ test/parity/ # golden tests against TS/official behavior
300300
- 本轮补充:session-memory recall agent 现在接受 `summaries``selectedSummaries``relevantSummaries``candidateSummaries` 等 summary collection aliases,并会继续从嵌套 `summary.sessionId`/`summaryId` 恢复模型排序的 session IDs。
301301
- 本轮补充:session-memory recall agent 现在也接受 `sessionUri`/`sessionUrl``uri`/`url`/`href` 等 summary link aliases,能从 `file://.../summary.md` 或 API URL path 中恢复 session ID 并按模型顺序匹配 summary。
302302
- 本轮补充: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。
303+
- 本轮补充:model-backed memory fact extraction 现在接受 `sourceMessageUUID`/`source_message_uuid``sourceEventId`/`source_event_id``originId``turn`/`event` source object 等来源别名,并把 numeric source ID 保留为字符串。
303304
- 本轮补充:compact runner 的 summary 响应现在也接受 provider-style `choices`/`outputs`/`candidates`/`generations` wrapper,可在构建 compact plan 前从 `message.content`、content-block array、`content.parts[].text` 和 fenced `json` code block 中恢复 visible summary text。
304305
- 本轮补充:remote history `SDKEvent` type 现在会把 provider-style aliases 归一化为现有 canonical 事件类型,包括 `assistant_message`/`assistant_delta``userMessage`/`humanMessage``system-event``result_event`/`finalResult`/`response.completed``errorEvent`/`failureEvent``status_update`/`statusMessage`/`progress`,single-object page 与 transcript materialization 不再因事件类型拼写相邻而丢消息。
305306
- 本轮补充: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 阈值来源分叉。

docs/first-second-parity-audit.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ M6 progress now includes:
155155
- `internal/memory`: model-backed memory fact extraction now also unwraps provider-style `choices`, `outputs`, `candidates`, and `generations` responses plus top-level `message`/`content`/`text`/`output_text` envelopes, recovering JSON facts payloads from nested `message.content`, content-block arrays, `content.parts[].text`, and fenced `json` code blocks.
156156
- `internal/memory`: model-backed memory fact extraction now also traverses API-shaped fact collections such as `observations`, `notes`, `findings`, `records`, `resources`, `data.resource.attributes`, and `edge.node`, while accepting text aliases such as `note`, `description`, `body`, `message`, `observation`, and `finding`.
157157
- `internal/memory`: model-backed memory fact extraction now accepts additional kind aliases such as `user_pref`, `requirement`, `action_item`, `outcome`, `conclusion`, `tool_usage`, and `command_run`, normalizing them into preference/request/decision/tool facts.
158+
- `internal/memory`: model-backed memory fact extraction now accepts additional source aliases such as `sourceMessageUUID`, `source_message_uuid`, `sourceEventId`, `source_event_id`, `originId`, and nested `turn`/`event` source objects, preserving numeric source IDs as strings.
158159
- `internal/session`: remote-history response parsing now also unwraps provider-style `choices`, `outputs`, `candidates`, and `generations` responses plus top-level `message`/`content`/`text`/`output_text` envelopes, recovering event page JSON plus pagination from nested `message.content`, content-block arrays, `content.parts[].text`, and fenced `json` code blocks.
159160
- `internal/session`: remote-history pagination now accepts `starting_after`/`startingAfter`/`after*` cursor aliases from page fields and link URL query parameters when deriving the next `before_id`.
160161
- `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.

internal/memory/agent.go

Lines changed: 124 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"net/url"
99
"path/filepath"
1010
"sort"
11+
"strconv"
1112
"strings"
1213

1314
"ccgo/internal/api/anthropic"
@@ -2400,18 +2401,18 @@ func rawMemoryFactFromMap(value map[string]any) (rawMemoryFact, bool) {
24002401
MessageText: stringMapField(value, "message"),
24012402
Observation: stringMapField(value, "observation"),
24022403
Finding: stringMapField(value, "finding"),
2403-
SourceUUID: stringMapField(value, "source_uuid"),
2404-
SourceUUIDCamel: stringMapField(value, "sourceUuid"),
2405-
SourceID: stringMapField(value, "source_id"),
2406-
SourceIDCamel: stringMapField(value, "sourceId"),
2407-
Source: nestedIDFromValue(value["source"]),
2408-
MessageUUID: stringMapField(value, "message_uuid"),
2409-
MessageUUIDCamel: stringMapField(value, "messageUuid"),
2410-
MessageID: stringMapField(value, "message_id"),
2411-
MessageIDCamel: stringMapField(value, "messageId"),
2412-
SourceMessageID: stringMapField(value, "source_message_id"),
2413-
SourceMessageIDCamel: stringMapField(value, "sourceMessageId"),
2414-
UUID: stringMapField(value, "uuid"),
2404+
SourceUUID: idMapField(value, "source_uuid", "sourceUUID"),
2405+
SourceUUIDCamel: idMapField(value, "sourceUuid"),
2406+
SourceID: idMapField(value, "source_id", "sourceID", "source_event_id", "source_event_uuid", "origin_id", "origin_uuid"),
2407+
SourceIDCamel: idMapField(value, "sourceId", "sourceEventId", "sourceEventID", "sourceEventUuid", "sourceEventUUID", "originId", "originUuid", "originUUID"),
2408+
Source: sourceIDFromFactMap(value),
2409+
MessageUUID: idMapField(value, "message_uuid", "messageUUID"),
2410+
MessageUUIDCamel: idMapField(value, "messageUuid"),
2411+
MessageID: idMapField(value, "message_id", "messageID"),
2412+
MessageIDCamel: idMapField(value, "messageId"),
2413+
SourceMessageID: idMapField(value, "source_message_id", "source_message_uuid", "sourceMessageID", "sourceMessageUUID"),
2414+
SourceMessageIDCamel: idMapField(value, "sourceMessageId", "sourceMessageUuid"),
2415+
UUID: idMapField(value, "uuid"),
24152416
}
24162417
if fact.MessageUUID == "" {
24172418
if _, ok := value["message"].(map[string]any); ok {
@@ -2433,6 +2434,19 @@ func rawMemoryFactFromMap(value map[string]any) (rawMemoryFact, bool) {
24332434
return fact, true
24342435
}
24352436

2437+
func idMapField(value map[string]any, keys ...string) string {
2438+
return idMapFieldDepth(value, 0, keys...)
2439+
}
2440+
2441+
func idMapFieldDepth(value map[string]any, depth int, keys ...string) string {
2442+
for _, key := range keys {
2443+
if id := nestedIDFromValueDepth(value[key], depth+1); id != "" {
2444+
return id
2445+
}
2446+
}
2447+
return ""
2448+
}
2449+
24362450
func stringMapField(value map[string]any, keys ...string) string {
24372451
for _, key := range keys {
24382452
if text := textFromValue(value[key]); text != "" {
@@ -2442,15 +2456,75 @@ func stringMapField(value map[string]any, keys ...string) string {
24422456
return ""
24432457
}
24442458

2459+
func sourceIDFromFactMap(value map[string]any) string {
2460+
for _, key := range []string{
2461+
"source",
2462+
"origin",
2463+
"source_message",
2464+
"sourceMessage",
2465+
"source_event",
2466+
"sourceEvent",
2467+
"source_turn",
2468+
"sourceTurn",
2469+
"event",
2470+
"turn",
2471+
} {
2472+
if id := nestedIDFromValue(value[key]); id != "" {
2473+
return id
2474+
}
2475+
}
2476+
return ""
2477+
}
2478+
24452479
func nestedIDFromValue(value any) string {
2446-
if text := directStringValue(value); text != "" {
2447-
return text
2480+
return nestedIDFromValueDepth(value, 0)
2481+
}
2482+
2483+
func nestedIDFromValueDepth(value any, depth int) string {
2484+
if depth > 6 {
2485+
return ""
2486+
}
2487+
if id := directIDValue(value); id != "" {
2488+
return id
24482489
}
24492490
object, ok := value.(map[string]any)
24502491
if !ok {
24512492
return ""
24522493
}
2453-
return stringMapField(object, "source_uuid", "sourceUuid", "source_id", "sourceId", "message_uuid", "messageUuid", "message_id", "messageId", "uuid", "id")
2494+
if id := idMapFieldDepth(object, depth+1,
2495+
"source_uuid", "sourceUuid", "sourceUUID",
2496+
"source_id", "sourceId", "sourceID",
2497+
"source_message_uuid", "sourceMessageUuid", "sourceMessageUUID",
2498+
"source_message_id", "sourceMessageId", "sourceMessageID",
2499+
"message_uuid", "messageUuid", "messageUUID",
2500+
"message_id", "messageId", "messageID",
2501+
"event_uuid", "eventUuid", "eventUUID",
2502+
"event_id", "eventId", "eventID",
2503+
"turn_uuid", "turnUuid", "turnUUID",
2504+
"turn_id", "turnId", "turnID",
2505+
"origin_uuid", "originUuid", "originUUID",
2506+
"origin_id", "originId", "originID",
2507+
"uuid", "id",
2508+
); id != "" {
2509+
return id
2510+
}
2511+
for _, key := range []string{
2512+
"source",
2513+
"origin",
2514+
"source_message",
2515+
"sourceMessage",
2516+
"source_event",
2517+
"sourceEvent",
2518+
"source_turn",
2519+
"sourceTurn",
2520+
"event",
2521+
"turn",
2522+
} {
2523+
if id := nestedIDFromValueDepth(object[key], depth+1); id != "" {
2524+
return id
2525+
}
2526+
}
2527+
return ""
24542528
}
24552529

24562530
func textFromValue(value any) string {
@@ -2481,6 +2555,41 @@ func directStringValue(value any) string {
24812555
return strings.TrimSpace(text)
24822556
}
24832557

2558+
func directIDValue(value any) string {
2559+
switch typed := value.(type) {
2560+
case string:
2561+
return strings.TrimSpace(typed)
2562+
case json.Number:
2563+
return strings.TrimSpace(typed.String())
2564+
case float64:
2565+
return strings.TrimSpace(strconv.FormatFloat(typed, 'f', -1, 64))
2566+
case float32:
2567+
return strings.TrimSpace(strconv.FormatFloat(float64(typed), 'f', -1, 32))
2568+
case int:
2569+
return strconv.Itoa(typed)
2570+
case int8:
2571+
return strconv.FormatInt(int64(typed), 10)
2572+
case int16:
2573+
return strconv.FormatInt(int64(typed), 10)
2574+
case int32:
2575+
return strconv.FormatInt(int64(typed), 10)
2576+
case int64:
2577+
return strconv.FormatInt(typed, 10)
2578+
case uint:
2579+
return strconv.FormatUint(uint64(typed), 10)
2580+
case uint8:
2581+
return strconv.FormatUint(uint64(typed), 10)
2582+
case uint16:
2583+
return strconv.FormatUint(uint64(typed), 10)
2584+
case uint32:
2585+
return strconv.FormatUint(uint64(typed), 10)
2586+
case uint64:
2587+
return strconv.FormatUint(typed, 10)
2588+
default:
2589+
return ""
2590+
}
2591+
}
2592+
24842593
type rawMemoryFact struct {
24852594
Kind string `json:"kind"`
24862595
Type string `json:"type"`

internal/memory/memory_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1359,6 +1359,36 @@ func TestMemoryAgentExtractsNestedFactSourceObjects(t *testing.T) {
13591359
}
13601360
}
13611361

1362+
func TestMemoryAgentExtractsAdditionalFactSourceAliases(t *testing.T) {
1363+
client := &fakeMemoryClient{response: &anthropic.Response{
1364+
ID: "msg_memory_source_aliases",
1365+
Type: "message",
1366+
Role: "assistant",
1367+
Model: "sonnet",
1368+
Content: []contracts.ContentBlock{contracts.NewTextBlock(`{"facts":[
1369+
{"kind":"preference","text":"prefer source message uuid aliases","sourceMessageUUID":"user_1"},
1370+
{"type":"decision","content":"keep source event aliases","source_event_id":1234},
1371+
{"category":"request","summary":"support turn source objects","turn":{"id":"turn_1"}}
1372+
]}`)},
1373+
}}
1374+
result, err := (Agent{Client: client}).Extract(context.Background(), []contracts.Message{msgs.UserText("Remember more source aliases")}, ExtractOptions{})
1375+
if err != nil {
1376+
t.Fatal(err)
1377+
}
1378+
if result.Fallback || len(result.Facts) != 3 {
1379+
t.Fatalf("result = %#v", result)
1380+
}
1381+
if !hasMemoryFact(result.Facts, FactPreference, "prefer source message uuid aliases", "user_1") {
1382+
t.Fatalf("sourceMessageUUID fact missing = %#v", result.Facts)
1383+
}
1384+
if !hasMemoryFact(result.Facts, FactDecision, "keep source event aliases", "1234") {
1385+
t.Fatalf("source event alias fact missing = %#v", result.Facts)
1386+
}
1387+
if !hasMemoryFact(result.Facts, FactRequest, "support turn source objects", "turn_1") {
1388+
t.Fatalf("turn source object fact missing = %#v", result.Facts)
1389+
}
1390+
}
1391+
13621392
func TestMemoryAgentExtractsStructuredFactText(t *testing.T) {
13631393
client := &fakeMemoryClient{response: &anthropic.Response{
13641394
ID: "msg_memory_structured_text",

0 commit comments

Comments
 (0)