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
@@ -583,6 +583,8 @@ object ConfigLoader {
stageTimeoutMs = asLong(orchestrationSection["stage_timeout_ms"], 180_000),
journalCompactionTokenThreshold =
asInt(orchestrationSection["journal_compaction_token_threshold"], 2_000),
resumeAbandonedMaxAgeMinutes =
asLong(orchestrationSection["resume_abandoned_max_age_minutes"], 1_440),
)
val modelsSettings = ModelsSettings(
@@ -28,6 +28,12 @@ data class CorrexConfig(
data class OrchestrationKnobs(
val stageTimeoutMs: Long = 180_000,
val journalCompactionTokenThreshold: Int = 2_000,
/**
* Boot-time abandoned-session pickup only resumes sessions whose last event is at most
* this old; anything staler stays parked (POST /sessions/{id}/resume revives it on demand).
* 0 disables auto-resume entirely.
*/
val resumeAbandonedMaxAgeMinutes: Long = 1_440,
)
@Serializable
@@ -84,6 +84,7 @@ object CorrexConfigWriter {
b.section("orchestration")
b.kv("stage_timeout_ms", cfg.orchestration.stageTimeoutMs)
b.kv("journal_compaction_token_threshold", cfg.orchestration.journalCompactionTokenThreshold)
b.kv("resume_abandoned_max_age_minutes", cfg.orchestration.resumeAbandonedMaxAgeMinutes)
b.section("personalization")
b.kv("enabled", cfg.personalization.enabled)
@@ -175,6 +175,13 @@ object EditableConfig {
getString = { it.orchestration.journalCompactionTokenThreshold.toString() },
withString = { c, v -> orc(c) { it.copy(journalCompactionTokenThreshold = int(JCT_KEY, v)) } },
),
EditableField(
"orchestration.resume_abandoned_max_age_minutes", ConfigFieldType.LONG,
getString = { it.orchestration.resumeAbandonedMaxAgeMinutes.toString() },
withString = { c, v ->
orc(c) { it.copy(resumeAbandonedMaxAgeMinutes = lng("orchestration.resume_abandoned_max_age_minutes", v)) }
},
),
)
private const val JCT_KEY = "orchestration.journal_compaction_token_threshold"