Skip to content

Commit 01d652b

Browse files
author
SqlRush
committed
Accept numeric content replacement IDs
1 parent af003e2 commit 01d652b

6 files changed

Lines changed: 53 additions & 43 deletions

File tree

docs/cc-100-roadmap.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ M6 补充:tag、agent-name、agent-color、agent-setting 和 mode metadata 现
5656

5757
M6 补充:content-replacement metadata 现在接受 `records`/`contentReplacements` 等 record wrapper,以及 record 内 `type`/`content`/`hash` 等字段别名,full loader、metadata loader 和 transcript index 的 replacement 恢复保持一致。
5858

59+
M6 补充:content-replacement metadata 的 `agentId`、record `toolUseId``blockId` 现在也接受 JSON number,并在 full/lightweight loader 中保留为字符串 ID。
60+
5961
M6 补充:remote history GraphQL/connection 分页现在接受 `hasPrevious`/`hasPreviousPage``hasOlder`/`more` 继续分页标记,以及 `previousCursor`/`prevCursor`/`beforeCursor`/`olderCursor` 等 before-id cursor 别名,避免只返回第一页历史。
6062

6163
M6 补充:remote history pagination bool 字段现在除 JSON bool 和 `true`/`false` 字符串外,也接受 `1`/`0``yes`/`no``on`/`off` 等数值/字符串布尔形态,避免 wrapper/pageInfo 中的非严格布尔值中断分页。

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ test/parity/ # golden tests against TS/official behavior
200200
- 本轮补充:transcript metadata loader 接受 `sessionID`/`session` 作为 session-scoped metadata ID 别名,并容忍 `prNumber``timeSavedMs``lastSpawnTokens` 等计数字段使用数字字符串。
201201
- 本轮补充:transcript metadata ID helper 现在复用 contract `ID` JSON 解码,`messageID`/`sessionID` 等 metadata ID 字段可接受 JSON number 并保留为字符串。
202202
- 本轮补充:context-collapse commit metadata 的 collapse/summary/archived ID 字段现在也走 metadata ID helper,支持 JSON number ID 并保持 full/lightweight loader 一致。
203+
- 本轮补充:content-replacement metadata 的 `agentId`、record `toolUseId``blockId` 现在也接受 JSON number,并在 full/lightweight loader 中保留为字符串 ID。
203204
- 本轮补充:context-collapse snapshot metadata 接受 `isArmed`/`enabled` bool 别名、`spawnTokens`/`tokenCount` 计数字段别名,以及 `stagedMessages`/`items` staged payload wrapper,full loader 和 metadata loader 保持一致。
204205
- 本轮补充:transcript message 和嵌套 contract message 接受顶层 `sessionID` 作为 session id 别名,`LoadTranscript``LoadTranscriptIndex` 和 indexed resume 会保留该 session id(覆盖测试:`TestLoadTranscriptAcceptsSessionIDUpperAlias`)。
205206
- 本轮补充:嵌套 contract message 接受 `parentUUID``parentId`/`parentID`/`parent_id``parentMessageId`/`parentMessageID`/`parent_message_id` 和 parent-message UUID 别名,transcript/remote history payload 自带 parent alias 时不会丢失嵌套 parent。

docs/first-second-parity-audit.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ M6 progress now includes:
163163
- `internal/session`: transcript metadata loading now accepts `sessionID` and `session` as session-scoped ID aliases and tolerates numeric strings for counters such as `prNumber`, `timeSavedMs`, and `lastSpawnTokens`.
164164
- `internal/session`: transcript metadata ID helpers now reuse contract `ID` JSON decoding so metadata IDs such as `messageID` and `sessionID` can be JSON numbers preserved as strings.
165165
- `internal/session`: context-collapse commit metadata now routes collapse, summary, and archived ID fields through metadata ID decoding so JSON number IDs are preserved in full and lightweight metadata loaders.
166+
- `internal/session`: content-replacement metadata now preserves JSON number agent, tool-use, and block IDs as strings in full and lightweight metadata loaders.
166167
- `internal/session`: context-collapse snapshot metadata now accepts alternate armed, token-count, and staged payload field names in both full and lightweight metadata loaders.
167168
- `internal/session` and `internal/contracts`: transcript records and nested contract messages now accept top-level `sessionID` as a session id alias, preserving it through `LoadTranscript`, `LoadTranscriptIndex`, and indexed resume (`TestLoadTranscriptAcceptsSessionIDUpperAlias`).
168169
- `internal/contracts`/`internal/session`: remote-history `SDKEvent` decoding now accepts top-level `sessionID` as an event session id alias and preserves it during transcript materialization (`TestRemoteHistoryTranscriptMessagesAcceptsSessionIDUpperAlias`).

internal/session/transcript_aliases.go

Lines changed: 26 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -312,46 +312,36 @@ func (m *SnipMetadata) UnmarshalJSON(data []byte) error {
312312
}
313313

314314
func (r *ContentReplacementRecord) UnmarshalJSON(data []byte) error {
315-
type ContentReplacementRecordJSON ContentReplacementRecord
316315
var aux struct {
317-
*ContentReplacementRecordJSON
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"`
333-
}
334-
base := ContentReplacementRecordJSON{}
335-
aux.ContentReplacementRecordJSON = &base
316+
KindType string `json:"type"`
317+
Kind string `json:"kind"`
318+
KindReplacementCamel string `json:"replacementKind"`
319+
KindReplacementSnake string `json:"replacement_kind"`
320+
ToolUseID contracts.ID `json:"toolUseId"`
321+
ToolUseIDUpper contracts.ID `json:"toolUseID"`
322+
ToolUseIDSnake contracts.ID `json:"tool_use_id"`
323+
BlockID contracts.ID `json:"blockId"`
324+
BlockIDUpper contracts.ID `json:"blockID"`
325+
BlockIDSnake contracts.ID `json:"block_id"`
326+
Replacement string `json:"replacement"`
327+
ReplacementContent string `json:"content"`
328+
ReplacementText string `json:"text"`
329+
ReplacementValue string `json:"value"`
330+
ReplacementOutput string `json:"output"`
331+
OriginalHash string `json:"originalHash"`
332+
OriginalHashSnake string `json:"original_hash"`
333+
OriginalHashShort string `json:"hash"`
334+
OriginalContentHashCamel string `json:"originalContentHash"`
335+
OriginalContentHashSnake string `json:"original_content_hash"`
336+
}
336337
if err := json.Unmarshal(data, &aux); err != nil {
337338
return err
338339
}
339-
*r = ContentReplacementRecord(base)
340-
if r.Kind == "" {
341-
r.Kind = firstNonEmptyString(aux.KindType, aux.KindReplacementCamel, aux.KindReplacementSnake)
342-
}
343-
if r.ToolUseID == "" {
344-
r.ToolUseID = firstNonEmptyString(aux.ToolUseIDUpper, aux.ToolUseIDSnake)
345-
}
346-
if r.BlockID == "" {
347-
r.BlockID = firstNonEmptyString(aux.BlockIDUpper, aux.BlockIDSnake)
348-
}
349-
if r.Replacement == "" {
350-
r.Replacement = firstNonEmptyString(aux.ReplacementContent, aux.ReplacementText, aux.ReplacementValue, aux.ReplacementOutput)
351-
}
352-
if r.OriginalHash == "" {
353-
r.OriginalHash = firstNonEmptyString(aux.OriginalHashSnake, aux.OriginalHashShort, aux.OriginalContentHashCamel, aux.OriginalContentHashSnake)
354-
}
340+
r.Kind = firstNonEmptyString(aux.Kind, aux.KindType, aux.KindReplacementCamel, aux.KindReplacementSnake)
341+
r.ToolUseID = firstNonEmptyString(string(aux.ToolUseID), string(aux.ToolUseIDUpper), string(aux.ToolUseIDSnake))
342+
r.BlockID = firstNonEmptyString(string(aux.BlockID), string(aux.BlockIDUpper), string(aux.BlockIDSnake))
343+
r.Replacement = firstNonEmptyString(aux.Replacement, aux.ReplacementContent, aux.ReplacementText, aux.ReplacementValue, aux.ReplacementOutput)
344+
r.OriginalHash = firstNonEmptyString(aux.OriginalHash, aux.OriginalHashSnake, aux.OriginalHashShort, aux.OriginalContentHashCamel, aux.OriginalContentHashSnake)
355345
return nil
356346
}
357347

internal/session/transcript_metadata_fields.go

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -224,23 +224,32 @@ func parseSnapshotMessageID(line []byte) contracts.ID {
224224
}
225225

226226
func parseContentReplacementMetadata(line []byte) (ContentReplacementEntry, bool) {
227-
var entry ContentReplacementEntry
228-
if err := json.Unmarshal(line, &entry); err != nil {
229-
return ContentReplacementEntry{}, false
230-
}
231227
fields, err := parseTranscriptMetadataFields(line)
232228
if err != nil {
233229
return ContentReplacementEntry{}, false
234230
}
231+
var aux struct {
232+
Type string `json:"type"`
233+
SessionID contracts.ID `json:"sessionId"`
234+
Replacements json.RawMessage `json:"replacements"`
235+
}
236+
if err := json.Unmarshal(line, &aux); err != nil {
237+
return ContentReplacementEntry{}, false
238+
}
239+
entry := ContentReplacementEntry{
240+
Type: aux.Type,
241+
SessionID: aux.SessionID,
242+
Replacements: parseContentReplacementRecords(aux.Replacements),
243+
}
235244
if entry.SessionID == "" {
236245
entry.SessionID = fields.sessionIDValue()
237246
}
238247
if entry.AgentID == "" {
239-
entry.AgentID = fields.stringValue(
240-
"agentID", "agent_id", "agent",
248+
entry.AgentID = string(fields.idValue(
249+
"agentId", "agentID", "agent_id", "agent",
241250
"sidechainId", "sidechainID", "sidechain_id",
242251
"subagentId", "subagentID", "subagent_id",
243-
)
252+
))
244253
}
245254
if len(entry.Replacements) == 0 {
246255
entry.Replacements = parseContentReplacementRecords(fields.rawValue(

internal/session/transcript_test.go

Lines changed: 7 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":"s10","agentId":7001,"replacements":[{"toolUseId":7002,"blockId":7003,"replacement":"numeric agent stub"}]}`,
646647
`{"type":"content-replacement","sessionID":"s9","records":{"type":"tool-result","toolUseID":"toolu_9","blockID":"block_9","hash":"hash_9","content":"record alias stub"}}`,
647648
`{"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"}`,
648649
`{"type":"marble-origami-commit","sessionID":"s4","collapseID":"c2","summaryID":"sum2","content":"upper collapsed","firstArchivedID":"u2","lastArchivedID":"a2"}`,
@@ -714,6 +715,9 @@ func TestLoadTranscriptCollectsMetadataEntries(t *testing.T) {
714715
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" {
715716
t.Fatalf("upper agent content replacements = %#v", got)
716717
}
718+
if got := transcript.ContentReplacements["7001"]; len(got) != 1 || got[0].ToolUseID != "7002" || got[0].BlockID != "7003" || got[0].Replacement != "numeric agent stub" {
719+
t.Fatalf("numeric agent content replacements = %#v", got)
720+
}
717721
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" {
718722
t.Fatalf("record alias content replacements = %#v", got)
719723
}
@@ -787,6 +791,9 @@ func TestLoadTranscriptCollectsMetadataEntries(t *testing.T) {
787791
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" {
788792
t.Fatalf("metadata upper agent replacements = %#v", got)
789793
}
794+
if got := metadata.ContentReplacements["7001"]; len(got) != 1 || got[0].ToolUseID != "7002" || got[0].BlockID != "7003" || got[0].Replacement != "numeric agent stub" {
795+
t.Fatalf("metadata numeric agent replacements = %#v", got)
796+
}
790797
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" {
791798
t.Fatalf("metadata record alias replacements = %#v", got)
792799
}

0 commit comments

Comments
 (0)