detached 5d30b85

fix(subagents): pair live trace activity

commit
5d30b85460e5a878eb34c9219b761d7d3ea71fb3
author
brice · · 2026-08-12 12:33 UTC
committer
brice · · 2026-08-12 12:33 UTC
parent
3cb020e
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
5 files · +44 -13
M packages/agent/subagent_worker.go +3 -1
@@ -249,7 +249,6 @@ func runSubagentWorkerMode(ctx context.Context, args Args, version string) (runE
249249 }
250250 turnID := fmt.Sprintf("turn-%d", step)
251251 em.setTurnID(turnID)
252- defer em.setTurnID("")
253252 c, cancel := subagentTurnContext(ctx, args.SubagentTurnTimeout)
254253 cancelFn = cancel
255254 mu.Unlock()
@@ -356,6 +355,9 @@ func runSubagentWorkerMode(ctx context.Context, args Args, version string) (runE
356355
357356 cancel()
358357 mu.Lock()
358+ // Clear the emitter identity before reopening the busy gate: a newly
359+ // admitted turn must not race this completed turn's cleanup.
360+ em.setTurnID("")
359361 busyTurn = false
360362 cancelFn = nil
361363 mu.Unlock()
@@ -249,7 +249,6 @@ func runSubagentWorkerMode(ctx context.Context, args Args, version string) (runE
249 }249 }
250 turnID := fmt.Sprintf("turn-%d", step)250 turnID := fmt.Sprintf("turn-%d", step)
251 em.setTurnID(turnID)251 em.setTurnID(turnID)
252- defer em.setTurnID("")
253 c, cancel := subagentTurnContext(ctx, args.SubagentTurnTimeout)252 c, cancel := subagentTurnContext(ctx, args.SubagentTurnTimeout)
254 cancelFn = cancel253 cancelFn = cancel
255 mu.Unlock()254 mu.Unlock()
@@ -356,6 +355,9 @@ func runSubagentWorkerMode(ctx context.Context, args Args, version string) (runE
356 355
357 cancel()356 cancel()
358 mu.Lock()357 mu.Lock()
358+ // Clear the emitter identity before reopening the busy gate: a newly
359+ // admitted turn must not race this completed turn's cleanup.
360+ em.setTurnID("")
359 busyTurn = false361 busyTurn = false
360 cancelFn = nil362 cancelFn = nil
361 mu.Unlock()363 mu.Unlock()
M packages/agent/subagents/projection.go +7 -3
@@ -193,12 +193,15 @@ func ProjectTrace(events []TraceEvent) map[string]AgentTraceView {
193193 }
194194
195195 // ObservationFor returns the last observation only when it belongs to this
196-// operation's turn. A prior turn's stream is not live activity for a resumed
197-// or subsequent turn.
196+// operation's turn and, when identified, its tool call. A prior turn's stream
197+// or another tool's output is not live activity for this operation.
198198 func (v AgentTraceView) ObservationFor(operation Operation) *LiveObservation {
199199 if v.LastObservation == nil || operation.TurnID == "" || v.LastObservation.TurnID == "" || operation.TurnID != v.LastObservation.TurnID {
200200 return nil
201201 }
202+ if v.LastObservation.CallID != "" && v.LastObservation.CallID != operation.CallID {
203+ return nil
204+ }
202205 return v.LastObservation
203206 }
204207
@@ -222,9 +225,10 @@ func traceObservation(event TraceEvent) *LiveObservation {
222225 source, _ = event.Data["worker_event"].(string)
223226 }
224227 name, _ := event.Data["name"].(string)
228+ callID, _ := event.Data["call_id"].(string)
225229 switch typeName {
226230 case "assistant.stream.observed", "reasoning.stream.observed", "tool.output.observed", "worker.protocol.observed":
227- return &LiveObservation{Type: typeName, Source: source, At: event.Timestamp, TurnID: event.TurnID, Name: name}
231+ return &LiveObservation{Type: typeName, Source: source, At: event.Timestamp, TurnID: event.TurnID, CallID: callID, Name: name}
228232 }
229233 return nil
230234 }
@@ -193,12 +193,15 @@ func ProjectTrace(events []TraceEvent) map[string]AgentTraceView {
193 }193 }
194 194
195 // ObservationFor returns the last observation only when it belongs to this195 // ObservationFor returns the last observation only when it belongs to this
196-// operation's turn. A prior turn's stream is not live activity for a resumed196+// operation's turn and, when identified, its tool call. A prior turn's stream
197-// or subsequent turn.197+// or another tool's output is not live activity for this operation.
198 func (v AgentTraceView) ObservationFor(operation Operation) *LiveObservation {198 func (v AgentTraceView) ObservationFor(operation Operation) *LiveObservation {
199 if v.LastObservation == nil || operation.TurnID == "" || v.LastObservation.TurnID == "" || operation.TurnID != v.LastObservation.TurnID {199 if v.LastObservation == nil || operation.TurnID == "" || v.LastObservation.TurnID == "" || operation.TurnID != v.LastObservation.TurnID {
200 return nil200 return nil
201 }201 }
202+ if v.LastObservation.CallID != "" && v.LastObservation.CallID != operation.CallID {
203+ return nil
204+ }
202 return v.LastObservation205 return v.LastObservation
203 }206 }
204 207
@@ -222,9 +225,10 @@ func traceObservation(event TraceEvent) *LiveObservation {
222 source, _ = event.Data["worker_event"].(string)225 source, _ = event.Data["worker_event"].(string)
223 }226 }
224 name, _ := event.Data["name"].(string)227 name, _ := event.Data["name"].(string)
228+ callID, _ := event.Data["call_id"].(string)
225 switch typeName {229 switch typeName {
226 case "assistant.stream.observed", "reasoning.stream.observed", "tool.output.observed", "worker.protocol.observed":230 case "assistant.stream.observed", "reasoning.stream.observed", "tool.output.observed", "worker.protocol.observed":
227- return &LiveObservation{Type: typeName, Source: source, At: event.Timestamp, TurnID: event.TurnID, Name: name}231+ return &LiveObservation{Type: typeName, Source: source, At: event.Timestamp, TurnID: event.TurnID, CallID: callID, Name: name}
228 }232 }
229 return nil233 return nil
230 }234 }
M packages/agent/subagents/projection_test.go +19 -0
@@ -79,6 +79,25 @@ func TestProjectTraceSelectsSpecificActivityAndProjectsResultDelivery(t *testing
7979 }
8080 }
8181
82+func TestProjectTraceOmitsObservationForAnotherToolInSameTurn(t *testing.T) {
83+ started := time.Date(2026, 8, 12, 7, 0, 0, 0, time.UTC)
84+ view := ProjectTrace([]TraceEvent{
85+ {Timestamp: started, Type: "turn.started", AgentID: "agent-1", TurnID: "turn-1"},
86+ {Timestamp: started.Add(time.Second), Type: "tool.started", AgentID: "agent-1", TurnID: "turn-1", Data: map[string]any{"call_id": "one", "name": "bash"}},
87+ {Timestamp: started.Add(2 * time.Second), Type: "tool.started", AgentID: "agent-1", TurnID: "turn-1", Data: map[string]any{"call_id": "two", "name": "read"}},
88+ {Timestamp: started.Add(3 * time.Second), Type: "tool.output.observed", AgentID: "agent-1", TurnID: "turn-1", Data: map[string]any{"call_id": "two", "name": "read"}},
89+ })["agent-1"]
90+ if view.PrimaryOperation == nil || view.PrimaryOperation.CallID != "one" {
91+ t.Fatalf("primary operation = %#v, want call one", view.PrimaryOperation)
92+ }
93+ if observation := view.ObservationFor(*view.PrimaryOperation); observation != nil {
94+ t.Fatalf("another tool's observation = %#v, want omitted", observation)
95+ }
96+ if got, want := view.Summary(), "tool bash open"; got != want {
97+ t.Fatalf("summary = %q, want %q", got, want)
98+ }
99+}
100+
82101 func TestProjectTraceOmitsPriorTurnObservation(t *testing.T) {
83102 started := time.Date(2026, 8, 12, 7, 0, 0, 0, time.UTC)
84103 view := ProjectTrace([]TraceEvent{
@@ -79,6 +79,25 @@ func TestProjectTraceSelectsSpecificActivityAndProjectsResultDelivery(t *testing
79 }79 }
80 }80 }
81 81
82+func TestProjectTraceOmitsObservationForAnotherToolInSameTurn(t *testing.T) {
83+ started := time.Date(2026, 8, 12, 7, 0, 0, 0, time.UTC)
84+ view := ProjectTrace([]TraceEvent{
85+ {Timestamp: started, Type: "turn.started", AgentID: "agent-1", TurnID: "turn-1"},
86+ {Timestamp: started.Add(time.Second), Type: "tool.started", AgentID: "agent-1", TurnID: "turn-1", Data: map[string]any{"call_id": "one", "name": "bash"}},
87+ {Timestamp: started.Add(2 * time.Second), Type: "tool.started", AgentID: "agent-1", TurnID: "turn-1", Data: map[string]any{"call_id": "two", "name": "read"}},
88+ {Timestamp: started.Add(3 * time.Second), Type: "tool.output.observed", AgentID: "agent-1", TurnID: "turn-1", Data: map[string]any{"call_id": "two", "name": "read"}},
89+ })["agent-1"]
90+ if view.PrimaryOperation == nil || view.PrimaryOperation.CallID != "one" {
91+ t.Fatalf("primary operation = %#v, want call one", view.PrimaryOperation)
92+ }
93+ if observation := view.ObservationFor(*view.PrimaryOperation); observation != nil {
94+ t.Fatalf("another tool's observation = %#v, want omitted", observation)
95+ }
96+ if got, want := view.Summary(), "tool bash open"; got != want {
97+ t.Fatalf("summary = %q, want %q", got, want)
98+ }
99+}
100+
82 func TestProjectTraceOmitsPriorTurnObservation(t *testing.T) {101 func TestProjectTraceOmitsPriorTurnObservation(t *testing.T) {
83 started := time.Date(2026, 8, 12, 7, 0, 0, 0, time.UTC)102 started := time.Date(2026, 8, 12, 7, 0, 0, 0, time.UTC)
84 view := ProjectTrace([]TraceEvent{103 view := ProjectTrace([]TraceEvent{
M packages/agent/subagents/runner.go +3 -1
@@ -909,7 +909,9 @@ func recordWorkerTrace(agent *Agent, ev Event) {
909909 switch ev.Type {
910910 case "turn_start", "turn_end":
911911 if nestedTurn {
912- if ev.Type == "turn_end" {
912+ if ev.Type == "turn_start" {
913+ traceType = "provider.request.started"
914+ } else {
913915 traceType = "provider.request.finished"
914916 }
915917 } else if ev.Type == "turn_start" {
@@ -909,7 +909,9 @@ func recordWorkerTrace(agent *Agent, ev Event) {
909 switch ev.Type {909 switch ev.Type {
910 case "turn_start", "turn_end":910 case "turn_start", "turn_end":
911 if nestedTurn {911 if nestedTurn {
912- if ev.Type == "turn_end" {912+ if ev.Type == "turn_start" {
913+ traceType = "provider.request.started"
914+ } else {
913 traceType = "provider.request.finished"915 traceType = "provider.request.finished"
914 }916 }
915 } else if ev.Type == "turn_start" {917 } else if ev.Type == "turn_start" {
M packages/agent/subagents/runner_test.go +12 -8
@@ -807,6 +807,7 @@ func TestRecordWorkerTraceNormalizesSafeLiveObservations(t *testing.T) {
807807 agent := &Agent{ID: "agent-1", trace: trace}
808808 recordWorkerTrace(agent, Event{Type: EventMessageDelta, TurnID: "turn-1", Data: map[string]any{"delta": "sensitive answer"}})
809809 recordWorkerTrace(agent, Event{Type: "request_started", TurnID: "turn-1", Data: map[string]any{"provider": "test", "model": "test"}})
810+ recordWorkerTrace(agent, Event{Type: "turn_start", TurnID: "turn-1", Data: map[string]any{"nested_turn": true}})
810811 recordWorkerTrace(agent, Event{Type: "turn_end", TurnID: "turn-1", Data: map[string]any{"nested_turn": true}})
811812 recordWorkerTrace(agent, Event{Type: "tool_call", TurnID: "turn-1", Data: map[string]any{"id": "call-1", "name": "bash", "args": "sensitive arguments"}})
812813 recordWorkerTrace(agent, Event{Type: "future_event", TurnID: "turn-1", Data: map[string]any{"secret": "hidden"}})
@@ -814,7 +815,7 @@ func TestRecordWorkerTraceNormalizesSafeLiveObservations(t *testing.T) {
814815 t.Fatal(err)
815816 }
816817 events := trace.Events()
817- if len(events) != 5 {
818+ if len(events) != 6 {
818819 t.Fatalf("events = %#v", events)
819820 }
820821 if got, want := events[0].Type, "assistant.stream.observed"; got != want {
@@ -826,25 +827,28 @@ func TestRecordWorkerTraceNormalizesSafeLiveObservations(t *testing.T) {
826827 if got, want := events[1].Type, "provider.request.started"; got != want {
827828 t.Fatalf("type = %q, want %q", got, want)
828829 }
829- if got, want := events[2].Type, "provider.request.finished"; got != want {
830+ if got, want := events[2].Type, "provider.request.started"; got != want {
830831 t.Fatalf("type = %q, want %q", got, want)
831832 }
832- if got, want := events[3].Type, "tool.started"; got != want {
833+ if got, want := events[3].Type, "provider.request.finished"; got != want {
833834 t.Fatalf("type = %q, want %q", got, want)
834835 }
835- if got, _ := events[3].Data["call_id"].(string); got != "call-1" {
836+ if got, want := events[4].Type, "tool.started"; got != want {
837+ t.Fatalf("type = %q, want %q", got, want)
838+ }
839+ if got, _ := events[4].Data["call_id"].(string); got != "call-1" {
836840 t.Fatalf("tool call_id = %q", got)
837841 }
838- if _, found := events[3].Data["args"]; found {
842+ if _, found := events[4].Data["args"]; found {
839843 t.Fatalf("tool arguments leaked: %#v", events[2].Data)
840844 }
841- if got, want := events[4].Type, "worker.protocol.observed"; got != want {
845+ if got, want := events[5].Type, "worker.protocol.observed"; got != want {
842846 t.Fatalf("type = %q, want %q", got, want)
843847 }
844- if got, _ := events[4].Data["source_event"].(string); got != "future.event" {
848+ if got, _ := events[5].Data["source_event"].(string); got != "future.event" {
845849 t.Fatalf("source_event = %q", got)
846850 }
847- if _, found := events[4].Data["secret"]; found {
851+ if _, found := events[5].Data["secret"]; found {
848852 t.Fatalf("worker payload leaked: %#v", events[3].Data)
849853 }
850854 view := ProjectTrace(append([]TraceEvent{{Type: "turn.started", AgentID: "agent-1", TurnID: "turn-1"}}, events...))["agent-1"]
@@ -807,6 +807,7 @@ func TestRecordWorkerTraceNormalizesSafeLiveObservations(t *testing.T) {
807 agent := &Agent{ID: "agent-1", trace: trace}807 agent := &Agent{ID: "agent-1", trace: trace}
808 recordWorkerTrace(agent, Event{Type: EventMessageDelta, TurnID: "turn-1", Data: map[string]any{"delta": "sensitive answer"}})808 recordWorkerTrace(agent, Event{Type: EventMessageDelta, TurnID: "turn-1", Data: map[string]any{"delta": "sensitive answer"}})
809 recordWorkerTrace(agent, Event{Type: "request_started", TurnID: "turn-1", Data: map[string]any{"provider": "test", "model": "test"}})809 recordWorkerTrace(agent, Event{Type: "request_started", TurnID: "turn-1", Data: map[string]any{"provider": "test", "model": "test"}})
810+ recordWorkerTrace(agent, Event{Type: "turn_start", TurnID: "turn-1", Data: map[string]any{"nested_turn": true}})
810 recordWorkerTrace(agent, Event{Type: "turn_end", TurnID: "turn-1", Data: map[string]any{"nested_turn": true}})811 recordWorkerTrace(agent, Event{Type: "turn_end", TurnID: "turn-1", Data: map[string]any{"nested_turn": true}})
811 recordWorkerTrace(agent, Event{Type: "tool_call", TurnID: "turn-1", Data: map[string]any{"id": "call-1", "name": "bash", "args": "sensitive arguments"}})812 recordWorkerTrace(agent, Event{Type: "tool_call", TurnID: "turn-1", Data: map[string]any{"id": "call-1", "name": "bash", "args": "sensitive arguments"}})
812 recordWorkerTrace(agent, Event{Type: "future_event", TurnID: "turn-1", Data: map[string]any{"secret": "hidden"}})813 recordWorkerTrace(agent, Event{Type: "future_event", TurnID: "turn-1", Data: map[string]any{"secret": "hidden"}})
@@ -814,7 +815,7 @@ func TestRecordWorkerTraceNormalizesSafeLiveObservations(t *testing.T) {
814 t.Fatal(err)815 t.Fatal(err)
815 }816 }
816 events := trace.Events()817 events := trace.Events()
817- if len(events) != 5 {818+ if len(events) != 6 {
818 t.Fatalf("events = %#v", events)819 t.Fatalf("events = %#v", events)
819 }820 }
820 if got, want := events[0].Type, "assistant.stream.observed"; got != want {821 if got, want := events[0].Type, "assistant.stream.observed"; got != want {
@@ -826,25 +827,28 @@ func TestRecordWorkerTraceNormalizesSafeLiveObservations(t *testing.T) {
826 if got, want := events[1].Type, "provider.request.started"; got != want {827 if got, want := events[1].Type, "provider.request.started"; got != want {
827 t.Fatalf("type = %q, want %q", got, want)828 t.Fatalf("type = %q, want %q", got, want)
828 }829 }
829- if got, want := events[2].Type, "provider.request.finished"; got != want {830+ if got, want := events[2].Type, "provider.request.started"; got != want {
830 t.Fatalf("type = %q, want %q", got, want)831 t.Fatalf("type = %q, want %q", got, want)
831 }832 }
832- if got, want := events[3].Type, "tool.started"; got != want {833+ if got, want := events[3].Type, "provider.request.finished"; got != want {
833 t.Fatalf("type = %q, want %q", got, want)834 t.Fatalf("type = %q, want %q", got, want)
834 }835 }
835- if got, _ := events[3].Data["call_id"].(string); got != "call-1" {836+ if got, want := events[4].Type, "tool.started"; got != want {
837+ t.Fatalf("type = %q, want %q", got, want)
838+ }
839+ if got, _ := events[4].Data["call_id"].(string); got != "call-1" {
836 t.Fatalf("tool call_id = %q", got)840 t.Fatalf("tool call_id = %q", got)
837 }841 }
838- if _, found := events[3].Data["args"]; found {842+ if _, found := events[4].Data["args"]; found {
839 t.Fatalf("tool arguments leaked: %#v", events[2].Data)843 t.Fatalf("tool arguments leaked: %#v", events[2].Data)
840 }844 }
841- if got, want := events[4].Type, "worker.protocol.observed"; got != want {845+ if got, want := events[5].Type, "worker.protocol.observed"; got != want {
842 t.Fatalf("type = %q, want %q", got, want)846 t.Fatalf("type = %q, want %q", got, want)
843 }847 }
844- if got, _ := events[4].Data["source_event"].(string); got != "future.event" {848+ if got, _ := events[5].Data["source_event"].(string); got != "future.event" {
845 t.Fatalf("source_event = %q", got)849 t.Fatalf("source_event = %q", got)
846 }850 }
847- if _, found := events[4].Data["secret"]; found {851+ if _, found := events[5].Data["secret"]; found {
848 t.Fatalf("worker payload leaked: %#v", events[3].Data)852 t.Fatalf("worker payload leaked: %#v", events[3].Data)
849 }853 }
850 view := ProjectTrace(append([]TraceEvent{{Type: "turn.started", AgentID: "agent-1", TurnID: "turn-1"}}, events...))["agent-1"]854 view := ProjectTrace(append([]TraceEvent{{Type: "turn.started", AgentID: "agent-1", TurnID: "turn-1"}}, events...))["agent-1"]

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close