Skip to content

Commit 4fa1f61

Browse files
author
SqlRush
committed
Accept content replacement metadata aliases
1 parent 40feae8 commit 4fa1f61

6 files changed

Lines changed: 68 additions & 6 deletions

File tree

docs/cc-100-roadmap.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ M6 补充:summary/custom-title/ai-title/last-prompt metadata 现在接受 `con
5454

5555
M6 补充:tag、agent-name、agent-color、agent-setting 和 mode metadata 现在接受 `label`/`name`/`color`/`setting`/`status` 等值字段别名,full loader、metadata loader 和 transcript index 的 agent/session 状态恢复保持一致。
5656

57+
M6 补充:content-replacement metadata 现在接受 `records`/`contentReplacements` 等 record wrapper,以及 record 内 `type`/`content`/`hash` 等字段别名,full loader、metadata loader 和 transcript index 的 replacement 恢复保持一致。
58+
5759
M6 补充:remote history GraphQL/connection 分页现在接受 `hasPrevious`/`hasPreviousPage``hasOlder`/`more` 继续分页标记,以及 `previousCursor`/`prevCursor`/`beforeCursor`/`olderCursor` 等 before-id cursor 别名,避免只返回第一页历史。
5860

5961
M6 补充:remote history response parser 现在会递归解包 `data.session.events``data.projectSession.eventConnection``conversation``remoteHistory` 等 GraphQL/session wrapper,继续复用 `nodes`/`edges[].node``pageInfo` pagination 解析。

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ test/parity/ # golden tests against TS/official behavior
206206
- 本轮补充:task-summary metadata 接受 `taskSummary`/`task_summary`/`content`/`text` 摘要别名和 `createdAt`/`created_at` timestamp 别名,旧任务摘要记录不会只保留 session id。
207207
- 本轮补充:summary/custom-title/ai-title/last-prompt metadata 接受 `content`/`text`/`title`/`name`/`prompt` 等值字段别名,full loader、metadata loader 和 transcript index 的标题/摘要恢复保持一致。
208208
- 本轮补充:tag、agent-name、agent-color、agent-setting 和 mode metadata 接受 `label`/`name`/`color`/`setting`/`status` 等值字段别名,full loader、metadata loader 和 transcript index 的 agent/session 状态恢复保持一致。
209+
- 本轮补充:content-replacement metadata 接受 `records`/`contentReplacements` 等 record wrapper,以及 record 内 `type`/`content`/`hash` 等字段别名,full loader、metadata loader 和 transcript index 的 replacement 恢复保持一致。
209210
- 本轮补充:remote history `SDKEvent` 解码接受顶层 `sessionID` 作为事件 session id 别名,materialize transcript 时会填充 record 与嵌套 message 的 session id(覆盖测试:`TestRemoteHistoryTranscriptMessagesAcceptsSessionIDUpperAlias`)。
210211
- 本轮补充:remote history `SDKEvent` 解码接受 `parentUUID``parentId`/`parentID`/`parent_id``parentMessageId`/`parentMessageID`/`parent_message_id` parent aliases,materialize transcript 时会保留 parent chain(覆盖测试:`TestRemoteHistoryTranscriptMessagesAcceptsParentIDAliases`)。
211212
- 本轮补充:memory 层补齐官方 `memoryAge`/freshness note 语义,`ReadDocumentsWithOptions` 可为超过 1 天的 memory 文档前缀 system-reminder,提示模型把 memory 当作 point-in-time observation 并核对当前代码。

docs/first-second-parity-audit.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ M6 progress now includes:
140140
- `internal/session`: task-summary metadata now accepts alternate summary and timestamp field names in both full and lightweight metadata loaders.
141141
- `internal/session`: summary/title/last-prompt metadata now accepts alternate value field names across the full loader, lightweight metadata loader, and transcript index.
142142
- `internal/session`: tag, agent, and mode metadata now accepts alternate value field names across the full loader, lightweight metadata loader, and transcript index.
143+
- `internal/session`: content-replacement metadata now accepts alternate record wrapper and record value/hash field names across the full loader, lightweight metadata loader, and transcript index.
143144
- `internal/session`: remote-history parsing now also accepts connection-style `history`/`messages` wrappers with `nodes`/`edges[].node` event lists and `pageInfo`/`page_info` `hasNextPage`/`endCursor`/`startCursor` pagination aliases.
144145
- `internal/session`: remote-history connection edges now use `edges[].cursor` as the event cursor when the nested node lacks an event ID, preserving pagination even when pageInfo omits a cursor.
145146
- `internal/session`: remote-history parsing now also accepts `eventList`/`event_list`, `sessionEvents`/`session_events`, and connection aliases such as `connection`, `eventConnection`, and `sessionEventsConnection`.

internal/session/transcript_aliases.go

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -315,26 +315,42 @@ func (r *ContentReplacementRecord) UnmarshalJSON(data []byte) error {
315315
type ContentReplacementRecordJSON ContentReplacementRecord
316316
var aux struct {
317317
*ContentReplacementRecordJSON
318-
ToolUseIDUpper string `json:"toolUseID"`
319-
ToolUseIDSnake string `json:"tool_use_id"`
320-
BlockIDUpper string `json:"blockID"`
321-
BlockIDSnake string `json:"block_id"`
322-
OriginalHashSnake string `json:"original_hash"`
318+
KindType string `json:"type"`
319+
KindReplacementCamel string `json:"replacementKind"`
320+
KindReplacementSnake string `json:"replacement_kind"`
321+
ToolUseIDUpper string `json:"toolUseID"`
322+
ToolUseIDSnake string `json:"tool_use_id"`
323+
BlockIDUpper string `json:"blockID"`
324+
BlockIDSnake string `json:"block_id"`
325+
ReplacementContent string `json:"content"`
326+
ReplacementText string `json:"text"`
327+
ReplacementValue string `json:"value"`
328+
ReplacementOutput string `json:"output"`
329+
OriginalHashSnake string `json:"original_hash"`
330+
OriginalHashShort string `json:"hash"`
331+
OriginalContentHashCamel string `json:"originalContentHash"`
332+
OriginalContentHashSnake string `json:"original_content_hash"`
323333
}
324334
base := ContentReplacementRecordJSON{}
325335
aux.ContentReplacementRecordJSON = &base
326336
if err := json.Unmarshal(data, &aux); err != nil {
327337
return err
328338
}
329339
*r = ContentReplacementRecord(base)
340+
if r.Kind == "" {
341+
r.Kind = firstNonEmptyString(aux.KindType, aux.KindReplacementCamel, aux.KindReplacementSnake)
342+
}
330343
if r.ToolUseID == "" {
331344
r.ToolUseID = firstNonEmptyString(aux.ToolUseIDUpper, aux.ToolUseIDSnake)
332345
}
333346
if r.BlockID == "" {
334347
r.BlockID = firstNonEmptyString(aux.BlockIDUpper, aux.BlockIDSnake)
335348
}
349+
if r.Replacement == "" {
350+
r.Replacement = firstNonEmptyString(aux.ReplacementContent, aux.ReplacementText, aux.ReplacementValue, aux.ReplacementOutput)
351+
}
336352
if r.OriginalHash == "" {
337-
r.OriginalHash = aux.OriginalHashSnake
353+
r.OriginalHash = firstNonEmptyString(aux.OriginalHashSnake, aux.OriginalHashShort, aux.OriginalContentHashCamel, aux.OriginalContentHashSnake)
338354
}
339355
return nil
340356
}

internal/session/transcript_metadata_fields.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,9 +189,37 @@ func parseContentReplacementMetadata(line []byte) (ContentReplacementEntry, bool
189189
"subagentId", "subagentID", "subagent_id",
190190
)
191191
}
192+
if len(entry.Replacements) == 0 {
193+
entry.Replacements = parseContentReplacementRecords(fields.rawValue(
194+
"replacements",
195+
"replacementRecords", "replacement_records",
196+
"records",
197+
"contentReplacements", "content_replacements",
198+
"items",
199+
))
200+
}
192201
return entry, true
193202
}
194203

204+
func parseContentReplacementRecords(raw json.RawMessage) []ContentReplacementRecord {
205+
if len(raw) == 0 {
206+
return nil
207+
}
208+
var records []ContentReplacementRecord
209+
if err := json.Unmarshal(raw, &records); err == nil {
210+
return records
211+
}
212+
var record ContentReplacementRecord
213+
if err := json.Unmarshal(raw, &record); err != nil || record.isEmpty() {
214+
return nil
215+
}
216+
return []ContentReplacementRecord{record}
217+
}
218+
219+
func (r ContentReplacementRecord) isEmpty() bool {
220+
return r.Kind == "" && r.ToolUseID == "" && r.BlockID == "" && r.Replacement == "" && r.OriginalHash == ""
221+
}
222+
195223
func parseTombstoneMetadata(line []byte) (TombstoneEntry, bool) {
196224
var entry TombstoneEntry
197225
if err := json.Unmarshal(line, &entry); err != nil {

internal/session/transcript_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,7 @@ func TestLoadTranscriptCollectsMetadataEntries(t *testing.T) {
643643
`{"type":"content-replacement","sessionId":"s1","agentId":"agent_1","replacements":[{"toolUseId":"toolu_2","replacement":"agent stub"}]}`,
644644
`{"type":"content_replacement","session_id":"s3","agent_id":"agent_2","replacements":[{"tool_use_id":"toolu_4","block_id":"block_4","original_hash":"hash_4","replacement":"snake agent stub"}]}`,
645645
`{"type":"content-replacement","sessionId":"s1","agentID":"agent_3","replacements":[{"toolUseID":"toolu_5","blockID":"block_5","originalHash":"hash_5","replacement":"upper agent stub"}]}`,
646+
`{"type":"content-replacement","sessionID":"s9","records":{"type":"tool-result","toolUseID":"toolu_9","blockID":"block_9","hash":"hash_9","content":"record alias stub"}}`,
646647
`{"type":"marble_origami_commit","session_id":"s3","collapse_id":"c1","summary_uuid":"sum1","summary_content":"collapsed","summary":"short","first_archived_uuid":"u1","last_archived_uuid":"a1"}`,
647648
`{"type":"marble-origami-commit","sessionID":"s4","collapseID":"c2","summaryID":"sum2","content":"upper collapsed","firstArchivedID":"u2","lastArchivedID":"a2"}`,
648649
`{"type":"marble-origami-snapshot","sessionId":"s1","armed":true,"lastSpawnTokens":42}`,
@@ -712,6 +713,9 @@ func TestLoadTranscriptCollectsMetadataEntries(t *testing.T) {
712713
if got := transcript.ContentReplacements["agent_3"]; len(got) != 1 || got[0].ToolUseID != "toolu_5" || got[0].BlockID != "block_5" || got[0].Replacement != "upper agent stub" {
713714
t.Fatalf("upper agent content replacements = %#v", got)
714715
}
716+
if got := transcript.ContentReplacements["s9"]; len(got) != 1 || got[0].Kind != "tool-result" || got[0].ToolUseID != "toolu_9" || got[0].BlockID != "block_9" || got[0].OriginalHash != "hash_9" || got[0].Replacement != "record alias stub" {
717+
t.Fatalf("record alias content replacements = %#v", got)
718+
}
715719
if len(transcript.ContextCollapseCommits) != 2 || transcript.ContextCollapseCommits[0].CollapseID != "c1" || transcript.ContextCollapseCommits[0].SummaryUUID != "sum1" || transcript.ContextCollapseCommits[1].CollapseID != "c2" || transcript.ContextCollapseCommits[1].SummaryUUID != "sum2" || transcript.ContextCollapseCommits[1].FirstArchivedUUID != "u2" {
716720
t.Fatalf("collapse commit = %#v", transcript.ContextCollapseCommits)
717721
}
@@ -782,6 +786,9 @@ func TestLoadTranscriptCollectsMetadataEntries(t *testing.T) {
782786
if got := metadata.ContentReplacements["agent_3"]; len(got) != 1 || got[0].ToolUseID != "toolu_5" || got[0].BlockID != "block_5" || got[0].Replacement != "upper agent stub" {
783787
t.Fatalf("metadata upper agent replacements = %#v", got)
784788
}
789+
if got := metadata.ContentReplacements["s9"]; len(got) != 1 || got[0].Kind != "tool-result" || got[0].ToolUseID != "toolu_9" || got[0].BlockID != "block_9" || got[0].OriginalHash != "hash_9" || got[0].Replacement != "record alias stub" {
790+
t.Fatalf("metadata record alias replacements = %#v", got)
791+
}
785792
if len(metadata.ContextCollapseCommits) != 2 || metadata.ContextCollapseCommits[0].CollapseID != "c1" || metadata.ContextCollapseCommits[0].SummaryUUID != "sum1" || metadata.ContextCollapseCommits[1].CollapseID != "c2" || metadata.ContextCollapseCommits[1].SummaryUUID != "sum2" || metadata.ContextCollapseCommits[1].FirstArchivedUUID != "u2" {
786793
t.Fatalf("metadata collapse commit = %#v", metadata.ContextCollapseCommits)
787794
}
@@ -802,6 +809,13 @@ func TestLoadTranscriptCollectsMetadataEntries(t *testing.T) {
802809
if agentIndex.Tag != "aliased-tag" || agentIndex.AgentName != "Aliased Agent" || agentIndex.AgentColor != "green" || agentIndex.AgentSetting != "auditor" || agentIndex.Mode != "review" {
803810
t.Fatalf("index agent value aliases = %#v", agentIndex)
804811
}
812+
contentIndex, err := LoadTranscriptIndex(path, "s9")
813+
if err != nil {
814+
t.Fatal(err)
815+
}
816+
if contentIndex.ContentReplacementCount != 1 {
817+
t.Fatalf("index content replacement aliases = %#v", contentIndex)
818+
}
805819
}
806820

807821
func TestLoadTranscriptMetadataAcceptsSessionIDAndNumericStrings(t *testing.T) {

0 commit comments

Comments
 (0)