-
Notifications
You must be signed in to change notification settings - Fork 3
plat 131
PLAT-131 — session enrichment erased the identity the frontend needs, so a global-activity pill could not switch workflows
| Coordination | Value |
|---|---|
| Assigned agent | unassigned |
| Ticket state |
implemented — fail-before/pass-after against the exact live payload; live reverify pending a server restart |
| Last synchronized | 2026-08-18 |
- Priority: P2 — no data loss and no wrong execution, but a live activity pill silently navigates to the wrong workflow, and the only visible symptom is "clicking does nothing useful."
-
Owner:
cmd/server/polling.go(buildActiveSessionInfoSummary)
Reported live: rtslatency shows in the top global activity monitor, but
clicking it does not switch to that workflow — it opens a Schedule tab under
whichever workflow is currently on screen (build-in-public), and the pill
survives a page refresh.
buildActiveSessionInfoSummary enriches each session with data from its
tracked execution. Both enrichment branches assigned three identity fields
unconditionally:
active := trackedExecutionToActive(exec)
enriched.PresetQueryID = active.PresetQueryID // unguarded
enriched.PresetName = active.PresetName // unguarded
enriched.WorkspacePath = active.WorkspacePath // unguarded
if active.TriggeredBy != "" { // guarded
enriched.TriggeredBy = active.TriggeredBy
}A tracked execution is a narrower record than the session. A
workflow-builder background execution (workshop_background) legitimately
carries no preset and no workspace path — so for a session whose only running
execution was one of those, all three fields were overwritten with "".
WorkflowName survived only because its own assignment is guarded
(if active.PresetName != ""), and TriggeredBy survived for the same
reason. The three fields between them were not — an inconsistency inside a
single block, which is what makes this an oversight rather than a design.
The session had resolved its identity correctly at registration:
server.go sets WorkspacePath and derives WorkflowName from it in the
same guarded write, so a session cannot legitimately have one without the
other. Enrichment then destroyed half of it.
Captured live from /api/sessions/active while the pill was on screen (the
session had been running ~3h, driven by scheduled Pulse):
{
"session_id": "schedule-cron--42eca39a_1787009443095002000",
"workflow_name": "rtslatency", // survived (guarded assignment)
"workflow_label": "rtslatency", // survived (guarded assignment)
"agent_mode": "workflow_phase"
// workspace_path: ABSENT // erased (unguarded)
// preset_query_id: ABSENT // erased (unguarded)
// preset_name: ABSENT // erased (unguarded)
}Every other scheduled session in the same response (build-in-public,
upwork, tectonicusadaytrading) carried a correct workspace_path; those
had no running workshop_background execution to trigger the clobber.
The name-without-path shape is itself the proof: nothing sets WorkflowName
for this path except a write that sets WorkspacePath alongside it.
findWorkflowPresetForSession (frontend/src/utils/workflowSessionRestore.ts)
resolves a session's workflow by exactly the two fields that were erased:
const presetId = session.preset_query_id || runningWorkflow?.preset_query_id
if (presetId) { /* match by id */ }
const workspacePath = normalizeWorkspacePath(runningWorkflow?.workspace_path || session.workspace_path)
if (!workspacePath) return undefined // ← both gone: bailReturning undefined sends openCanonicalActivitySession down its fallback,
openActiveSession, which opens/activates a chat tab but never calls
selectWorkflowPreset(preset) — the one call that actually changes the
displayed workflow, and which only the openWorkflowPresetPage path makes.
Hence the exact reported behaviour: a Schedule tab opens under the already-open workflow, the header never changes, and nothing is logged because nothing errors.
Guard the three assignments in both enrichment branches, matching the
TriggeredBy guard that was already there. A tracked execution that does
carry identity still wins — it is the more specific record; it simply can no
longer erase what the session already knew.
Two tests in cmd/server/polling_test.go:
-
TestBuildActiveSessionInfoSummaryKeepsSessionIdentityWhenTrackedExecutionOmitsItreproduces the live shape (a runningworkshop_backgroundexecution with no identity over a session that has one) and asserts all four fields survive. Confirmed fail-before with the exact production symptom —workspace_path = ""— by revertingpolling.goalone. -
TestBuildActiveSessionInfoSummaryStillAdoptsTrackedExecutionIdentityWhenPresentguards the opposite direction: a tracked execution carrying real identity must still override the session's. Passes both before and after, proving the guard did not turn into "never update".
Full run across cmd/server, guidance, virtual-tools, and
step_based_workflow: 6 pre-existing failures before and after (the PLAT-129
remainder), zero new.
-
The session is a zombie. The same live payload shows five
synthetic-turn:steer-message-*child executions stuckrunningsince ~00:00 and ~00:29 UTC,raw_session_status: "error"alongsidephase: "running", and a session alive for over three hours. That is why the pill never disappears. It is the PLAT-116/117 orphan-settling family, not this bug — this ticket only restores the identity so the pill navigates correctly while it is shown. -
The frontend has no fallback.
findWorkflowPresetForSessioncould match onworkflow_name/workflow_label, which were correct throughout this incident, and would have degraded gracefully instead of silently opening the wrong workflow. Worth adding as defence in depth; not required now that the backend stops erasing the primary keys.
Auto-synced from docs/ on main. Edit there, not here.