fix(router,server,config,tui-go): close QA findings #2 #3 #9 #10 #12

#2: approval pauses narrate from ApprovalRequestedEvent (carries
tool/tier/preview/stage directly) instead of OrchestrationPaused +
a pending-approvals map the next event had not yet populated — the
narrator always lost that race and produced generic text.

#3: NarrationTrigger now carries the triggering event's stageId; the
narration status line and RouterNarrationEvent.stageId use it instead
of the router projection's currentStageId, which terminal events have
already cleared ('Stage: none').

#9: ExecutionPlanLocked -> plan.locked and ArtifactValidated ->
artifact.validated mapped to the TUI (Go: feed lines, plan shows the
compiled stage chain); ArtifactValidating explicitly dropped.

#10: blank router turn guard — a length-finish with empty text now
emits an explanatory turn naming the max_tokens budget instead of a
silent blank; blank steering responses no longer emit steering notes.

#12: resumeAbandoned only auto-resumes sessions whose last event is
within orchestration.resume_abandoned_max_age_minutes (default 24h,
0 disables); staler sessions stay parked for POST /resume. Knob is
TUI-editable and persisted.
This commit is contained in:
2026-06-12 13:20:04 +04:00
parent a4f0c6d0a2
commit a455762dd5
13 changed files with 171 additions and 36 deletions
+9
View File
@@ -227,6 +227,15 @@ func (m *Model) applyServer(msg protocol.ServerMessage) {
if s := m.session(msg.SessionID); s != nil {
s.addEvent(nowMillis(), "ArtifactCreated", msg.ArtifactID)
}
case protocol.TypeArtifactValid:
if s := m.session(msg.SessionID); s != nil {
s.addEvent(nowMillis(), "ArtifactValidated", msg.ArtifactID)
}
case protocol.TypePlanLocked:
if s := m.session(msg.SessionID); s != nil {
s.addEvent(nowMillis(), "PlanLocked", strings.Join(msg.StageIDs, " → "))
s.LastOutput = "plan locked: " + strings.Join(msg.StageIDs, " → ")
}
case protocol.TypeWorkspaceBound:
if s := m.session(msg.SessionID); s != nil {
s.WorkspaceRoot = msg.WorkspaceRoot
@@ -39,6 +39,8 @@ const (
TypeProviderStatus = "provider.status_changed"
TypeProtocolError = "protocol_error"
TypeArtifactCreated = "artifact.created"
TypeArtifactValid = "artifact.validated"
TypePlanLocked = "plan.locked"
TypeRouterResponse = "router.response"
TypeSnapshotComplete = "snapshot_complete"
TypeWorkflowList = "workflow.list"
@@ -81,6 +83,9 @@ type ServerMessage struct {
Outcome string `json:"outcome"` // approval.resolved: APPROVED | REJECTED | AUTO_APPROVED
WorkspaceRoot string `json:"workspaceRoot"` // session.workspace_bound: the bound cwd
// plan.locked: the compiled phase-2 stage ids in execution order
StageIDs []string `json:"stageIds"`
// inference.retry
AttemptNumber int `json:"attemptNumber"`
MaxAttempts int `json:"maxAttempts"`
@@ -224,6 +229,7 @@ func (m ServerMessage) IsEventBearing() bool {
TypeToolStarted, TypeToolCompleted, TypeToolFailed, TypeToolRejected,
TypeToolAssessed,
TypeApprovalRequired, TypeApprovalResolved, TypeWorkspaceBound, TypeArtifactCreated,
TypeArtifactValid, TypePlanLocked,
TypeRouterNarration:
return true
default: