Skip to content

Commit 0aaa9df

Browse files
author
SqlRush
committed
Traverse sidechain lifecycle collections
1 parent 3b5d12e commit 0aaa9df

5 files changed

Lines changed: 75 additions & 1 deletion

File tree

docs/cc-100-roadmap.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,8 @@ M7 补充:prompt history `LogEntry` 读取现在接受 `sessionID`/`session`/`
502502

503503
本轮补充:sidechain/subagent lifecycle content 读取现在也递归解包 GraphQL/JSON:API 风格的 `edge`/`node`/`attrs` wrapper,wrapped start/summary event 可继续恢复 ID、status、summary 和 agent metadata。
504504

505+
本轮补充:sidechain/subagent lifecycle 字段提取现在也会穿透 `edges`/`nodes`/`included` 等 collection wrapper 和数组元素,GraphQL connection 或 JSON:API included 风格的 start/summary payload 可继续恢复 state/list/resume 所需字段。
506+
505507
本轮补充:sidechain/subagent lifecycle content 的 ID 等 string-like 字段现在接受 JSON number/数字标量,numeric subagent ID 会保留为字符串并能通过 resume fallback 找回对应 sidechain。
506508

507509
本轮补充:sidechain runtime 现在会拒绝同一 sidechain ID 的 running 状态重复 start;已完成后重新 start 会被视为新 lifecycle,state loader 会清空上一轮 summary/endedAt 并使用新的 startedAt。

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ test/parity/ # golden tests against TS/official behavior
186186
- 本轮补充:sidechain/subagent lifecycle content 读取会递归解包 `payload`/`data`/`body`/`result`/`response`/`metadata` 等 wrapper,嵌套的 subagent ID、status/outcome、summary、agent type、workspace path 和 task description 都可参与 state/list/resume 恢复。
187187
- 本轮补充:sidechain/subagent lifecycle content 读取现在也接受 JSON:API/resource-style 的 `resource`/`attributes`/`properties` wrapper;外层 resource `id` 可作为 sidechain ID fallback,内层 metadata、status/outcome 和 summary 仍会参与 state/list/resume 恢复。
188188
- 本轮补充:sidechain/subagent lifecycle content 读取现在也递归解包 GraphQL/JSON:API 风格的 `edge`/`node`/`attrs` wrapper,wrapped start/summary event 可继续恢复 ID、status、summary 和 agent metadata。
189+
- 本轮补充:sidechain/subagent lifecycle 字段提取现在也会穿透 `edges`/`nodes`/`included` 等 collection wrapper 和数组元素,GraphQL connection 或 JSON:API included 风格的 start/summary payload 可继续恢复 state/list/resume 所需字段。
189190
- 本轮补充:sidechain/subagent lifecycle 的 ID 等 string-like 字段接受 JSON number 和 Go 数字标量,numeric subagent ID 会保留为字符串并可用于 state/list/resume 查找。
190191
- 本轮补充:sidechain/subagent lifecycle subtype 现在接受 `subagent_started``agentStarted``task_failed``sidechainCompleted` 等相邻事件名,并从 `taskID`/`workerId`/`runId``agentName`/`kind``resultText`/`finalMessage` 等字段恢复 state/list/resume 信息;`*_failed`/`*_cancelled` subtype 无显式 status 时会归一到 failed/cancelled。
191192
- 本轮补充:sidechain/subagent lifecycle status 归一化现在接受 compact/camel aliases,包括 `inProgress``completedSuccessfully``cancelledByUser`/`canceledByUser``failedError`/`failedWithError``timedOut`,并继续写回 canonical running/completed/cancelled/failed 状态。

docs/first-second-parity-audit.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ M6 progress now includes:
184184
- `internal/session`: sidechain lifecycle content loading now recurses through wrapper objects such as `payload`, `data`, `result`, `response`, and `metadata`, preserving nested subagent IDs, status, summaries, agent type, worktree path, and task descriptions during state/list/resume recovery.
185185
- `internal/session`: sidechain lifecycle content loading now also accepts JSON:API/resource-style `resource`/`attributes`/`properties` wrappers, preserving outer resource IDs as sidechain ID fallback while recovering nested metadata, status, and summary fields.
186186
- `internal/session`: sidechain lifecycle content loading now also recurses through GraphQL/JSON:API `edge`/`node`/`attrs` wrappers, preserving wrapped start and summary event fields during state/list/resume recovery.
187+
- `internal/session`: sidechain lifecycle field extraction now also traverses collection wrappers such as `edges`, `nodes`, and `included`, plus array elements, so GraphQL connection or JSON:API included start/summary payloads can still recover state/list/resume fields.
187188
- `internal/session`: sidechain lifecycle scalar fields now accept JSON number values for IDs and other string-like fields, preserving numeric subagent IDs as strings across state/list/resume recovery.
188189
- `internal/session`: sidechain runtime finish now canonicalizes status aliases such as `success`, `error`, and `canceled` before writing summary records, keeping sidechain and main transcript lifecycle output stable.
189190
- `internal/session`: sidechain runtime now rejects duplicate starts while the same sidechain ID is running, and state loading treats a later start as a fresh lifecycle by clearing stale summary/end metadata.

internal/session/sidechain_status.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -361,13 +361,19 @@ func firstStringFieldDepth(value any, keys []string, depth int) string {
361361
}
362362
}
363363
}
364-
for _, key := range []string{"payload", "data", "body", "content", "result", "response", "record", "entry", "item", "event", "edge", "node", "resource", "attributes", "properties", "attrs", "metadata", "details", "value", "output"} {
364+
for _, key := range []string{"payload", "data", "body", "content", "result", "response", "record", "records", "entry", "entries", "item", "items", "event", "events", "edge", "edges", "node", "nodes", "resource", "resources", "attributes", "properties", "attrs", "metadata", "details", "value", "values", "output", "outputs", "included", "collection", "list", "children"} {
365365
if raw, ok := fields[key]; ok {
366366
if value := firstStringFieldDepth(raw, keys, depth+1); value != "" {
367367
return value
368368
}
369369
}
370370
}
371+
case []any:
372+
for _, item := range fields {
373+
if value := firstStringFieldDepth(item, keys, depth+1); value != "" {
374+
return value
375+
}
376+
}
371377
case map[string]string:
372378
for _, key := range keys {
373379
if raw := fields[key]; raw != "" {

internal/session/sidechain_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,70 @@ func TestLoadSidechainStateAcceptsGraphQLResourceLifecycleWrappers(t *testing.T)
487487
}
488488
}
489489

490+
func TestLoadSidechainStateAcceptsLifecycleCollectionWrappers(t *testing.T) {
491+
sessionPath := filepath.Join(t.TempDir(), "session.jsonl")
492+
sessionID := contracts.ID("sess_1")
493+
if err := AppendSidechainMessage(sessionPath, sessionID, "collection-resource", TranscriptMessage{
494+
Type: "system",
495+
UUID: "start_1",
496+
Timestamp: "2026-01-01T00:00:01Z",
497+
Subtype: "agent_start",
498+
Content: map[string]any{
499+
"edges": []any{
500+
map[string]any{
501+
"node": map[string]any{
502+
"attrs": map[string]any{
503+
"subagentId": "collection_worker",
504+
"agentKind": "collector",
505+
"workspacePath": "/tmp/collection-worktree",
506+
"taskDescription": "collect wrapped lifecycle",
507+
"lifecycleState": "inProgress",
508+
},
509+
},
510+
},
511+
},
512+
},
513+
}); err != nil {
514+
t.Fatal(err)
515+
}
516+
if err := AppendSidechainMessage(sessionPath, sessionID, "collection-resource", TranscriptMessage{
517+
Type: "system",
518+
UUID: "summary_1",
519+
Timestamp: "2026-01-01T00:00:02Z",
520+
Subtype: "sidechainCompleted",
521+
Content: map[string]any{
522+
"included": []any{
523+
map[string]any{
524+
"resource": map[string]any{
525+
"attributes": map[string]any{
526+
"agentID": "collection_worker",
527+
"outcome": "completedSuccessfully",
528+
"finalMessage": "collection lifecycle complete",
529+
},
530+
},
531+
},
532+
},
533+
},
534+
}); err != nil {
535+
t.Fatal(err)
536+
}
537+
538+
states, err := ListSidechainStates(sessionPath, sessionID)
539+
if err != nil {
540+
t.Fatal(err)
541+
}
542+
if len(states) != 1 {
543+
t.Fatalf("states = %#v", states)
544+
}
545+
state := states[0]
546+
if state.ID != "collection_worker" || state.Status != SidechainStatusCompleted || state.Summary != "collection lifecycle complete" {
547+
t.Fatalf("state = %#v", state)
548+
}
549+
if state.Metadata.AgentType != "collector" || state.Metadata.WorktreePath != "/tmp/collection-worktree" || state.Metadata.Description != "collect wrapped lifecycle" {
550+
t.Fatalf("metadata = %#v", state.Metadata)
551+
}
552+
}
553+
490554
func TestLoadSidechainStateAcceptsNumericLifecycleIDs(t *testing.T) {
491555
sessionPath := filepath.Join(t.TempDir(), "session.jsonl")
492556
sessionID := contracts.ID("sess_1")

0 commit comments

Comments
 (0)