Skip to content

Commit 9fb65d1

Browse files
author
SqlRush
committed
Accept adjacent sidechain lifecycle aliases
1 parent 049d456 commit 9fb65d1

5 files changed

Lines changed: 194 additions & 12 deletions

File tree

docs/cc-100-roadmap.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
| M7 初始 TUI 层 | 已完成轻量 terminal frame renderer、PromptInput 状态机、history 导航、ctrl-p/ctrl-n history navigation、shift-enter 多行输入、多行 prompt 行内 ctrl-a/ctrl-e/ctrl-u/ctrl-k 和 wrap/render/cursor、共享 kill ring、ctrl-b/ctrl-f/ctrl-u/ctrl-k/ctrl-w 行编辑、alt-b/alt-f/alt-d/alt-backspace word 编辑、ctrl-left/ctrl-right/alt-left/alt-right word motion、ctrl-y yank 和 alt-y yank-pop 初版、reverse-search cursor/word 编辑/kill/yank/yank-pop 初版、ctrl-c interrupt/双击退出事件、ctrl-d delete-forward/空输入双击退出事件、ctrl-l 重绘事件、ctrl-o/ctrl-t 全局切换事件、ctrl-g/ctrl-s/ctrl-x chord chat 事件、reverse-search 状态/渲染/脚本断言/空结果/选择回填/cursor 断言、paste/image hint 输入和 OSC ST/base64 filename 兼容、text/image pasted-content 引用/metadata 脚本断言/提交展开/history entry restoration、SGR mouse 解析、alternate terminal navigation key sequences including modified Home/End/Delete/PageUp/PageDown、滚轮滚动、修饰键滚轮/左键、左键拖动选择、viewport 半页/顶部/底部可配置滚动、viewport 点击选择和 dialog action 点击、focus/blur 事件、resize 视口保持、keybinding resolver/config/chord pending/null-unbind/key/action camelCase alias、JSON config loader 和 focus/mouse/paste/image key name 覆盖、vim insert/normal/j/k/word/WORD/ge/gE/line-local ^/$/0/|/I/A/D/quote/bracket text-object/yank/register/paste/delete/count/replace/undo/find/till/repeat/matching-pair %/dot-repeat/G/gg/toggle/join/open-line/indent/substitute 动作、normal-mode arrow/backspace/delete 映射和 operator linewise/字符范围、REPL screen、permission/task dialog builder、dialog kind/id routing/runtime/status line、runtime 到 REPL screen 的 dialog/status 同步、runtime-aware interaction script runner、prompt text/cursor/expanded/vim mode/register/task state/dialog result/runtime mutation/task bulk-cancel/permission cancel/keybinding mutation/status negative/snapshot negative/screen size/event-sequence/event-count/no-event/dialog-result-count/no-dialog-result 脚本断言、viewport 脚本断言、named-key 脚本输入、script JSON/JSONL/wrapper loader、script file runner 和 runtime/task camel field aliases、stale dialog race guard、cancel active、permission id/all cancellation、queued permission promotion、active task dialog refresh、task lifecycle/bulk-cancel 初版、idempotent alternate screen lifecycle/reset/reassert interactive 初版、mouse/focus/bracketed-paste terminal mode lifecycle/reconciliation、ANSI snapshot 基础、snapshot corpus write/compare/script-file compare/missing-baseline/diff/batch/strict unexpected-baseline 状态、scripted interaction runner/assertions/multi-key/text/paste/image/pasted-content metadata 初版、status/dialog/message components、viewport/selection |
3333
| 全量测试 | 当前 `go test ./...` 通过 |
3434

35+
M6 补充:sidechain/subagent lifecycle state loader 现在接受 `subagent_started``agentStarted``task_failed``sidechainCompleted` 等相邻 subtype aliases,支持 `taskID`/`workerId`/`runId``agentName`/`kind``resultText`/`finalMessage` 等字段,并在 failed/cancelled subtype 没有显式 status 时自动归一状态。
36+
3537
M6 补充:transcript resume 的嵌套 content block 现在接受 `toolUseId`/`toolUseID``isError``cacheControl``cacheReference` 字段别名,并保留 cache edit 的 `cacheReference`
3638

3739
M6 补充:transcript resume 的 nested content block `id`/`tool_use_id`/`toolUseId` 现在接受 JSON number,并保留为字符串 tool-use ID。

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ test/parity/ # golden tests against TS/official behavior
185185
- 本轮补充:sidechain agent metadata sidecar 读取接受 `type`/`subagentType`/`agentName`/`name``workspacePath`/`workspace`/`path`/`directory``taskDescription`/`prompt`/`input`/`command`/`title` 等字段别名,避免历史或第三方 subagent sidecar 在 resume/list 时丢失 agent 类型、worktree 路径或任务描述。
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 的 ID 等 string-like 字段接受 JSON number 和 Go 数字标量,numeric subagent ID 会保留为字符串并可用于 state/list/resume 查找。
188+
- 本轮补充: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。
188189
- 本轮补充:transcript metadata loader 为 file-history snapshot 和 attribution snapshot 建立 `messageId` 索引,并接受 `message_id`/`messageUuid`/`id` 等字段别名,保留 raw list 的同时支持按消息恢复 snapshot。
189190
- 本轮补充:transcript message/index/session list 读取 `gitBranch`,兼容 `git_branch`/`branch` 别名,并让 session search 可以按分支名命中,贴近官方 lite metadata 的 branch 展示和检索行为。
190191
- 本轮补充:full transcript `TitleFromTranscript` 的标题优先级和 indexed/lite 路径对齐,按 custom title、AI title、首个用户 prompt、last-prompt、summary 顺序兜底。

docs/first-second-parity-audit.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ M6 progress now includes:
174174
- `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.
175175
- `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.
176176
- `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.
177+
- `internal/session`: sidechain lifecycle state loading now accepts adjacent subtype aliases such as `subagent_started`, `agentStarted`, `task_failed`, and `sidechainCompleted`, with broader task/worker/run ID fields plus `resultText`/`finalMessage` summaries and subtype-derived failed/cancelled defaults.
177178
- `internal/session`: transcript metadata loading now indexes file-history and attribution snapshots by message ID, with aliases such as `message_id`, `messageUuid`, and `id`, while preserving raw snapshot lists.
178179
- `internal/session`: transcript indexes and session search now recover message `gitBranch` values, accept `git_branch`/`branch` aliases, and can match sessions by branch name.
179180
- `internal/session`: full transcript title derivation now matches indexed/lite fallback order: custom title, AI title, first user prompt, last-prompt metadata, then summary.

internal/session/sidechain_status.go

Lines changed: 107 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"sort"
88
"strconv"
99
"strings"
10+
"unicode"
1011

1112
"ccgo/internal/contracts"
1213
)
@@ -28,6 +29,57 @@ type SidechainState struct {
2829
Metadata SidechainMetadata
2930
}
3031

32+
var sidechainLifecycleIDFields = []string{
33+
"sidechainId", "sidechainID", "sidechain_id",
34+
"subagentId", "subagentID", "subagent_id",
35+
"agentId", "agentID", "agent_id",
36+
"taskId", "taskID", "task_id",
37+
"workerId", "workerID", "worker_id",
38+
"runId", "runID", "run_id",
39+
"id",
40+
}
41+
42+
var sidechainLifecycleAgentTypeFields = []string{
43+
"agentType", "agent_type",
44+
"subagentType", "subagent_type",
45+
"agentKind", "agent_kind",
46+
"agentName", "agent_name",
47+
"kind", "name", "agent",
48+
}
49+
50+
var sidechainLifecycleWorktreeFields = []string{
51+
"worktreePath", "worktree_path",
52+
"worktree", "worktreeDir", "worktree_dir",
53+
"workingDirectory", "working_directory",
54+
"cwd",
55+
"workspacePath", "workspace_path", "workspace",
56+
"path", "directory",
57+
}
58+
59+
var sidechainLifecycleDescriptionFields = []string{
60+
"description", "description_text", "descriptionText",
61+
"desc", "summary",
62+
"task", "taskDescription", "task_description",
63+
"prompt", "input", "command", "title",
64+
}
65+
66+
var sidechainLifecycleStartStatusFields = []string{
67+
"status", "state", "phase", "lifecycle", "lifecycle_state", "lifecycleState",
68+
}
69+
70+
var sidechainLifecycleSummaryStatusFields = []string{
71+
"status", "state", "result", "outcome", "phase", "lifecycle", "lifecycle_state", "lifecycleState",
72+
}
73+
74+
var sidechainLifecycleSummaryFields = []string{
75+
"summary", "summary_text", "summaryText",
76+
"finalSummary", "final_summary",
77+
"resultSummary", "result_summary",
78+
"resultText", "result_text",
79+
"finalMessage", "final_message",
80+
"message", "text", "output", "value", "final",
81+
}
82+
3183
func LoadSidechainState(info SidechainInfo) (SidechainState, error) {
3284
transcript, err := LoadTranscript(info.Path)
3385
if err != nil {
@@ -74,19 +126,19 @@ func LoadSidechainState(info SidechainInfo) (SidechainState, error) {
74126
state.StartedAt = msg.Timestamp
75127
state.EndedAt = ""
76128
state.Summary = ""
77-
if sidechainID := firstStringField(msg.Content, "sidechainId", "sidechainID", "sidechain_id", "subagentId", "subagentID", "subagent_id", "agentId", "agentID", "agent_id", "id"); sidechainID != "" {
129+
if sidechainID := firstStringField(msg.Content, sidechainLifecycleIDFields...); sidechainID != "" {
78130
state.ID = sidechainID
79131
}
80-
if agentType := firstStringField(msg.Content, "agentType", "agent_type", "subagentType", "subagent_type", "agentKind", "agent_kind", "agent"); agentType != "" && state.Metadata.AgentType == "" {
132+
if agentType := firstStringField(msg.Content, sidechainLifecycleAgentTypeFields...); agentType != "" && state.Metadata.AgentType == "" {
81133
state.Metadata.AgentType = agentType
82134
}
83-
if worktreePath := firstStringField(msg.Content, "worktreePath", "worktree_path", "worktree", "worktreeDir", "worktree_dir", "workingDirectory", "working_directory", "cwd", "workspacePath", "workspace_path", "workspace", "path", "directory"); worktreePath != "" && state.Metadata.WorktreePath == "" {
135+
if worktreePath := firstStringField(msg.Content, sidechainLifecycleWorktreeFields...); worktreePath != "" && state.Metadata.WorktreePath == "" {
84136
state.Metadata.WorktreePath = worktreePath
85137
}
86-
if description := firstStringField(msg.Content, "description", "description_text", "descriptionText", "desc", "summary", "task", "taskDescription", "task_description", "prompt", "input", "command", "title"); description != "" && state.Metadata.Description == "" {
138+
if description := firstStringField(msg.Content, sidechainLifecycleDescriptionFields...); description != "" && state.Metadata.Description == "" {
87139
state.Metadata.Description = description
88140
}
89-
if status := sidechainStatusField(msg.Content, "status", "state", "phase", "lifecycle", "lifecycle_state", "lifecycleState"); status != "" {
141+
if status := sidechainStatusField(msg.Content, sidechainLifecycleStartStatusFields...); status != "" {
90142
state.Status = status
91143
} else {
92144
state.Status = SidechainStatusRunning
@@ -95,15 +147,17 @@ func LoadSidechainState(info SidechainInfo) (SidechainState, error) {
95147
if isSidechainSummarySubtype(msg.Subtype) {
96148
finished = true
97149
state.EndedAt = msg.Timestamp
98-
if sidechainID := firstStringField(msg.Content, "sidechainId", "sidechainID", "sidechain_id", "subagentId", "subagentID", "subagent_id", "agentId", "agentID", "agent_id", "id"); sidechainID != "" {
150+
if sidechainID := firstStringField(msg.Content, sidechainLifecycleIDFields...); sidechainID != "" {
99151
state.ID = sidechainID
100152
}
101-
if status := sidechainStatusField(msg.Content, "status", "state", "result", "outcome", "phase", "lifecycle", "lifecycle_state", "lifecycleState"); status != "" {
153+
if status := sidechainStatusField(msg.Content, sidechainLifecycleSummaryStatusFields...); status != "" {
154+
state.Status = status
155+
} else if status := defaultSidechainSummaryStatus(msg.Subtype); status != "" {
102156
state.Status = status
103157
} else {
104158
state.Status = SidechainStatusCompleted
105159
}
106-
state.Summary = firstStringField(msg.Content, "summary", "summary_text", "summaryText", "finalSummary", "final_summary", "resultSummary", "result_summary", "message", "text")
160+
state.Summary = firstStringField(msg.Content, sidechainLifecycleSummaryFields...)
107161
}
108162
}
109163
if state.Status == SidechainStatusUnknown {
@@ -121,23 +175,64 @@ func LoadSidechainState(info SidechainInfo) (SidechainState, error) {
121175
}
122176

123177
func isSidechainStartSubtype(subtype string) bool {
124-
switch strings.TrimSpace(subtype) {
125-
case "sidechain_start", "sidechainStart", "subagent_start", "subagentStart", "agent_start", "agentStart", "task_start", "taskStart":
178+
switch sidechainSubtypeAction(subtype) {
179+
case "start", "started":
126180
return true
127181
default:
128182
return false
129183
}
130184
}
131185

132186
func isSidechainSummarySubtype(subtype string) bool {
133-
switch strings.TrimSpace(subtype) {
134-
case "sidechain_summary", "sidechainSummary", "sidechain_end", "sidechainEnd", "sidechain_finish", "sidechainFinish", "subagent_summary", "subagentSummary", "subagent_end", "subagentEnd", "subagent_finish", "subagentFinish", "agent_summary", "agentSummary", "agent_end", "agentEnd", "agent_finish", "agentFinish", "task_summary", "taskSummary", "task_end", "taskEnd", "task_finish", "taskFinish":
187+
switch sidechainSubtypeAction(subtype) {
188+
case "summary", "summarized", "summarised",
189+
"end", "ended",
190+
"finish", "finished",
191+
"complete", "completed",
192+
"success", "succeeded", "done",
193+
"fail", "failed", "failure",
194+
"error", "errored",
195+
"cancel", "cancelled", "canceled",
196+
"abort", "aborted":
135197
return true
136198
default:
137199
return false
138200
}
139201
}
140202

203+
func defaultSidechainSummaryStatus(subtype string) string {
204+
switch sidechainSubtypeAction(subtype) {
205+
case "complete", "completed", "success", "succeeded", "done":
206+
return SidechainStatusCompleted
207+
case "fail", "failed", "failure", "error", "errored":
208+
return SidechainStatusFailed
209+
case "cancel", "cancelled", "canceled", "abort", "aborted":
210+
return SidechainStatusCancelled
211+
default:
212+
return ""
213+
}
214+
}
215+
216+
func sidechainSubtypeAction(subtype string) string {
217+
normalized := normalizeSidechainSubtype(subtype)
218+
for _, prefix := range []string{"sidechain", "subagent", "agent", "task"} {
219+
if strings.HasPrefix(normalized, prefix) {
220+
return strings.TrimPrefix(normalized, prefix)
221+
}
222+
}
223+
return ""
224+
}
225+
226+
func normalizeSidechainSubtype(subtype string) string {
227+
var builder strings.Builder
228+
for _, r := range strings.TrimSpace(subtype) {
229+
if unicode.IsLetter(r) || unicode.IsDigit(r) {
230+
builder.WriteRune(unicode.ToLower(r))
231+
}
232+
}
233+
return builder.String()
234+
}
235+
141236
func sidechainStatusField(value any, keys ...string) string {
142237
return normalizeSidechainStatus(firstStringField(value, keys...))
143238
}

internal/session/sidechain_test.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,89 @@ func TestLoadSidechainStateAcceptsSubtypeAndStatusAliases(t *testing.T) {
221221
}
222222
}
223223

224+
func TestLoadSidechainStateAcceptsAdjacentLifecycleSubtypeAliases(t *testing.T) {
225+
sessionPath := filepath.Join(t.TempDir(), "session.jsonl")
226+
sessionID := contracts.ID("sess_1")
227+
if err := AppendSidechainMessage(sessionPath, sessionID, "worker-failed", TranscriptMessage{
228+
Type: "system",
229+
UUID: "failed_start",
230+
Timestamp: "2026-01-01T00:00:01Z",
231+
Subtype: "subagent_started",
232+
Content: map[string]any{
233+
"taskID": "worker_9",
234+
"agentName": "researcher",
235+
"workspace": "/tmp/research-worktree",
236+
"task": "trace lifecycle",
237+
},
238+
}); err != nil {
239+
t.Fatal(err)
240+
}
241+
if err := AppendSidechainMessage(sessionPath, sessionID, "worker-failed", TranscriptMessage{
242+
Type: "system",
243+
UUID: "failed_summary",
244+
Timestamp: "2026-01-01T00:00:02Z",
245+
Subtype: "task_failed",
246+
Content: map[string]any{
247+
"workerId": "worker_9",
248+
"resultText": "agent crashed",
249+
},
250+
}); err != nil {
251+
t.Fatal(err)
252+
}
253+
if err := AppendSidechainMessage(sessionPath, sessionID, "worker-completed", TranscriptMessage{
254+
Type: "system",
255+
UUID: "completed_start",
256+
Timestamp: "2026-01-01T00:00:03Z",
257+
Subtype: "agentStarted",
258+
Content: map[string]any{
259+
"runID": "worker_10",
260+
"kind": "reviewer",
261+
"cwd": "/tmp/review-worktree",
262+
"input": "review the diff",
263+
},
264+
}); err != nil {
265+
t.Fatal(err)
266+
}
267+
if err := AppendSidechainMessage(sessionPath, sessionID, "worker-completed", TranscriptMessage{
268+
Type: "system",
269+
UUID: "completed_summary",
270+
Timestamp: "2026-01-01T00:00:04Z",
271+
Subtype: "sidechainCompleted",
272+
Content: map[string]any{
273+
"runId": "worker_10",
274+
"finalMessage": "review complete",
275+
},
276+
}); err != nil {
277+
t.Fatal(err)
278+
}
279+
280+
states, err := ListSidechainStates(sessionPath, sessionID)
281+
if err != nil {
282+
t.Fatal(err)
283+
}
284+
if len(states) != 2 {
285+
t.Fatalf("states = %#v", states)
286+
}
287+
byID := map[string]SidechainState{}
288+
for _, state := range states {
289+
byID[state.ID] = state
290+
}
291+
failed := byID["worker_9"]
292+
completed := byID["worker_10"]
293+
if failed.ID != "worker_9" || failed.Status != SidechainStatusFailed || failed.Summary != "agent crashed" {
294+
t.Fatalf("failed state = %#v", failed)
295+
}
296+
if failed.Metadata.AgentType != "researcher" || failed.Metadata.WorktreePath != "/tmp/research-worktree" || failed.Metadata.Description != "trace lifecycle" {
297+
t.Fatalf("failed metadata = %#v", failed.Metadata)
298+
}
299+
if completed.ID != "worker_10" || completed.Status != SidechainStatusCompleted || completed.Summary != "review complete" {
300+
t.Fatalf("completed state = %#v", completed)
301+
}
302+
if completed.Metadata.AgentType != "reviewer" || completed.Metadata.WorktreePath != "/tmp/review-worktree" || completed.Metadata.Description != "review the diff" {
303+
t.Fatalf("completed metadata = %#v", completed.Metadata)
304+
}
305+
}
306+
224307
func TestLoadSidechainStateAcceptsWrappedLifecycleContent(t *testing.T) {
225308
sessionPath := filepath.Join(t.TempDir(), "session.jsonl")
226309
sessionID := contracts.ID("sess_1")

0 commit comments

Comments
 (0)