Skip to content

Commit ce8858f

Browse files
author
SqlRush
committed
Accept numeric content block tool IDs
1 parent 01d652b commit ce8858f

5 files changed

Lines changed: 54 additions & 9 deletions

File tree

docs/cc-100-roadmap.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434

3535
M6 补充:transcript resume 的嵌套 content block 现在接受 `toolUseId`/`toolUseID``isError``cacheControl``cacheReference` 字段别名,并保留 cache edit 的 `cacheReference`
3636

37+
M6 补充:transcript resume 的 nested content block `id`/`tool_use_id`/`toolUseId` 现在接受 JSON number,并保留为字符串 tool-use ID。
38+
3739
M6 补充:嵌套 contract message 现在接受 `parentUUID``parentId`/`parentID`/`parent_id``parentMessageId`/`parentMessageID`/`parent_message_id` 和 parent-message UUID 别名,transcript/remote history payload 自带 parent alias 时不会丢失嵌套 parent。
3840

3941
M6 补充:嵌套 contract message 现在接受 `messageId`/`messageID`/`message_id``messageUuid`/`messageUUID`/`message_uuid` 作为自身 ID/UUID 别名,indexed resume 会保留 payload 自带的 nested message id。

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ test/parity/ # golden tests against TS/official behavior
196196
- 本轮补充:contract `ID` JSON 读取现在接受 JSON number/null,remote history event/message/session/parent ID alias 可继承数字 ID 兼容面并在 transcript materialization 中保留为字符串。
197197
- 本轮补充:remote history response parser 会递归解包 `data.session.events``data.projectSession.eventConnection``conversation``remoteHistory` 等 GraphQL/session wrapper,继续复用 `nodes`/`edges[].node``pageInfo` pagination 解析。
198198
- 本轮补充:transcript resume 的嵌套 content block 接受 `toolUseId`/`toolUseID``isError``cacheControl``cacheReference` 字段别名,并保留 cache edit 的 `cacheReference`
199+
- 本轮补充:transcript resume 的 nested content block `id`/`tool_use_id`/`toolUseId` 现在接受 JSON number,并保留为字符串 tool-use ID。
199200
- 本轮补充:lightweight transcript metadata loader 在 `system`/`compact_boundary` 后清空旧 `marble-origami-commit`/`marble-origami-snapshot` 状态,和 full loader/官方 sessionStorage compact-boundary 语义一致。
200201
- 本轮补充:transcript metadata loader 接受 `sessionID`/`session` 作为 session-scoped metadata ID 别名,并容忍 `prNumber``timeSavedMs``lastSpawnTokens` 等计数字段使用数字字符串。
201202
- 本轮补充:transcript metadata ID helper 现在复用 contract `ID` JSON 解码,`messageID`/`sessionID` 等 metadata ID 字段可接受 JSON number 并保留为字符串。

docs/first-second-parity-audit.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ M6 progress now includes:
129129
- `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 responses, before_id resume, fallback field fill, and duplicate-aware parent linking during transcript materialization.
130130
- `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.
131131
- `internal/session`: transcript full loader and line-offset index now accept entry type aliases such as `role`, `entry_type`, and `messageType`, plus `createdAt`/`created_at` timestamp aliases.
132-
- `internal/contracts`/`internal/session`: transcript resume now accepts nested content block aliases for `toolUseId`/`toolUseID`, `isError`, `cacheControl`, `cacheReference`, and cache edit `cacheReference`.
132+
- `internal/contracts`/`internal/session`: transcript resume now accepts nested content block aliases for `toolUseId`/`toolUseID`, `isError`, `cacheControl`, `cacheReference`, and cache edit `cacheReference`; tool-use content block IDs also accept JSON numbers and preserve them as strings.
133133
- `internal/contracts`/`internal/session`: nested contract messages now accept parent aliases such as `parentUUID`, `parentId`/`parentID`/`parent_id`, `parentMessageId`/`parentMessageID`/`parent_message_id`, and parent-message UUID aliases, preserving payload-supplied parents in transcript and remote-history resume paths.
134134
- `internal/contracts`/`internal/session`: nested contract messages now accept `messageId`/`messageID`/`message_id` and `messageUuid`/`messageUUID`/`message_uuid` as their own ID/UUID aliases, preserving payload-supplied nested message IDs in indexed resume output.
135135
- `internal/contracts`/`internal/session`: the base `SessionEntry` JSONL loader now accepts role/type, message ID/UUID, parent ID/UUID, and session ID/UUID aliases so older entry files keep type, parent, and session metadata when loaded through `session.Load`.

internal/contracts/messages.go

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,19 +40,44 @@ type ContentBlock struct {
4040
}
4141

4242
func (b *ContentBlock) UnmarshalJSON(data []byte) error {
43-
type contentBlockJSON ContentBlock
44-
var base contentBlockJSON
45-
if err := json.Unmarshal(data, &base); err != nil {
43+
var aux struct {
44+
Type ContentBlockType `json:"type"`
45+
Text string `json:"text"`
46+
Source any `json:"source"`
47+
ID ID `json:"id"`
48+
Name string `json:"name"`
49+
Input json.RawMessage `json:"input"`
50+
Content any `json:"content"`
51+
IsError bool `json:"is_error"`
52+
ToolUseID ID `json:"tool_use_id"`
53+
CacheControl *CacheControl `json:"cache_control"`
54+
CacheReference string `json:"cache_reference"`
55+
Edits []CacheEdit `json:"edits"`
56+
}
57+
if err := json.Unmarshal(data, &aux); err != nil {
4658
return err
4759
}
48-
*b = ContentBlock(base)
60+
*b = ContentBlock{
61+
Type: aux.Type,
62+
Text: aux.Text,
63+
Source: aux.Source,
64+
ID: string(aux.ID),
65+
Name: aux.Name,
66+
Input: aux.Input,
67+
Content: aux.Content,
68+
IsError: aux.IsError,
69+
ToolUseID: string(aux.ToolUseID),
70+
CacheControl: aux.CacheControl,
71+
CacheReference: aux.CacheReference,
72+
Edits: aux.Edits,
73+
}
4974

5075
fields := map[string]json.RawMessage{}
5176
if err := json.Unmarshal(data, &fields); err != nil {
5277
return err
5378
}
5479
if b.ToolUseID == "" {
55-
b.ToolUseID = stringJSONField(fields, "toolUseId", "toolUseID")
80+
b.ToolUseID = idJSONField(fields, "toolUseId", "toolUseID")
5681
}
5782
if _, hasCanonical := fields["is_error"]; !hasCanonical {
5883
if value, ok := boolJSONField(fields, "isError"); ok {
@@ -117,6 +142,20 @@ func stringJSONField(fields map[string]json.RawMessage, names ...string) string
117142
return ""
118143
}
119144

145+
func idJSONField(fields map[string]json.RawMessage, names ...string) string {
146+
for _, name := range names {
147+
raw, ok := fields[name]
148+
if !ok {
149+
continue
150+
}
151+
var value ID
152+
if err := json.Unmarshal(raw, &value); err == nil {
153+
return string(value)
154+
}
155+
}
156+
return ""
157+
}
158+
120159
func boolJSONField(fields map[string]json.RawMessage, names ...string) (bool, bool) {
121160
for _, name := range names {
122161
raw, ok := fields[name]

internal/session/transcript_test.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -513,8 +513,8 @@ func TestBuildResumeConversationConvertsTranscriptChain(t *testing.T) {
513513

514514
func TestBuildResumeConversationAcceptsContentBlockFieldAliases(t *testing.T) {
515515
path := writeTranscript(t, []string{
516-
`{"type":"assistant","uuid":"a1","sessionId":"s1","timestamp":"2026-01-01T00:00:01Z","message":{"type":"assistant","content":[{"type":"tool_use","id":"toolu_1","name":"Read","cacheControl":{"type":"ephemeral","ttl":"5m"}},{"type":"cache_edits","cacheReference":"cache_ref_1","edits":[{"type":"clear","cacheReference":"cache_ref_0"}]}]}}`,
517-
`{"type":"user","uuid":"tr1","sessionId":"s1","parentUuid":"a1","timestamp":"2026-01-01T00:00:02Z","content":[{"type":"tool_result","toolUseId":"toolu_1","isError":true,"content":"denied"}]}`,
516+
`{"type":"assistant","uuid":"a1","sessionId":"s1","timestamp":"2026-01-01T00:00:01Z","message":{"type":"assistant","content":[{"type":"tool_use","id":9001,"name":"Read","cacheControl":{"type":"ephemeral","ttl":"5m"}},{"type":"cache_edits","cacheReference":"cache_ref_1","edits":[{"type":"clear","cacheReference":"cache_ref_0"}]}]}}`,
517+
`{"type":"user","uuid":"tr1","sessionId":"s1","parentUuid":"a1","timestamp":"2026-01-01T00:00:02Z","content":[{"type":"tool_result","toolUseId":9001,"isError":true,"content":"denied"}]}`,
518518
})
519519
resume, err := BuildResumeConversation(path, "tr1")
520520
if err != nil {
@@ -530,14 +530,17 @@ func TestBuildResumeConversationAcceptsContentBlockFieldAliases(t *testing.T) {
530530
if len(assistant.Content) != 2 {
531531
t.Fatalf("assistant content = %#v", assistant.Content)
532532
}
533+
if assistant.Content[0].ID != "9001" {
534+
t.Fatalf("numeric tool use id = %#v", assistant.Content[0])
535+
}
533536
if assistant.Content[0].CacheControl == nil || assistant.Content[0].CacheControl.Type != "ephemeral" || assistant.Content[0].CacheControl.TTL != "5m" {
534537
t.Fatalf("cacheControl alias = %#v", assistant.Content[0].CacheControl)
535538
}
536539
if assistant.Content[1].CacheReference != "cache_ref_1" || len(assistant.Content[1].Edits) != 1 || assistant.Content[1].Edits[0].CacheReference != "cache_ref_0" {
537540
t.Fatalf("cacheReference aliases = %#v", assistant.Content[1])
538541
}
539542
user := resume.Messages[1]
540-
if len(user.Content) != 1 || user.Content[0].ToolUseID != "toolu_1" || !user.Content[0].IsError {
543+
if len(user.Content) != 1 || user.Content[0].ToolUseID != "9001" || !user.Content[0].IsError {
541544
t.Fatalf("tool result aliases = %#v", user.Content)
542545
}
543546
}

0 commit comments

Comments
 (0)