Repository navigation
Description
What is wrong
actsInWindow returns every consequential act on the instance during a run's time window. It is meant to return only the acts of the session that run was driving. A Coder instance holds several repos, each with its own session, and those sessions can run concurrently. When two runs on one instance overlap in time, each one reports the other repo's pushes, merges and deletes as its own. That happens on the three surfaces a human or a supervisor reads:
- the run's outcome note and board card
- the resume note handed to the next run
check_delegation
This is a code-path finding, not confirmed in production. I have not measured whether two runs on different repos of one instance have actually overlapped on this account. Everything below is read from the code at b47cf05e.
Mechanism (verified)
1. The query does not filter by session. workers/api/src/lib/instance-work.ts:400-404:
`SELECT ${ACT_COLUMNS}
FROM agent_events
WHERE instance_id = ?1 AND user_id = ?2 AND event = ?5 AND ts >= ?3 AND ts <= ?4
ORDER BY ts ASC LIMIT ?6`,Its own doc comment (:381-390) states the intent it does not enforce: "The window is precise enough — it is the interval during which this run was the one driving the session." It keys on time instead of trace_id only because a console terminal poll can drain a run's acts and stamp the session id first. That argues against keying on the trace id, not for including other sessions.
2. Every act already records its session. workers/api/src/lib/engine-acts.ts:609-616:
context: {
act: act.kind,
command: act.command,
target: act.target,
irreversible: act.irreversible,
ok: act.ok,
sessionId: ctx.sessionId,
},It has been there since acts were first recorded (9958d7a9, #294). act.consequential is written nowhere else, so no act row lacks it. context is TEXT -- JSON extras (migrations/0038_agent_events.sql:17), written with JSON.stringify(...).slice(0, 4000) (lib/events.ts:72). The runner caps command at 400 characters and target at 120 (engine-acts.ts:81,504,517), so the slice cannot cut off sessionId.
3. The run's own card. workers/api/src/workflows/coding-session.ts:206-207:
const acts = event.payload.loopRunId || event.payload.boardTaskId
? await actsInWindow(env, userId, instanceId, runStartedAt, Date.now()).catch(() => [])sessionId is in scope: it is destructured from event.payload at :73 and never reassigned.
4. The resume note. workers/api/src/lib/coding-resume-note.ts:164-167:
const prev = await lastUnfinishedRunForRepo(env, params.userId, params.instanceId, params.sessionId, now);
if (!prev) return null;
// The window is the interval during which that run was the one driving its session.
const acts = await actsInWindow(env, params.userId, params.instanceId, prev.startedAt, prev.finishedAt ?? now, 100);prev carries sessionId (agent-loop-store.ts:179). Since f8ded1bb the predecessor is found by repo, so the note is exactly the reader that must not pick up another repo's acts.
5. check_delegation. workers/api/src/lib/connectors/supervision.ts:736-742:
const acts = await actsInWindow(
ctx.env,
userId,
run.instanceId,
run.startedAt,
run.finishedAt ?? Date.now(),
).catch(() => [] as ActItem[]);run comes from getLoopRun, whose view carries sessionId: string | null (agent-loop-store.ts:155,179).
Fix
- Add a
sessionId: string | nullparameter toactsInWindow. When it is set, addAND json_valid(context) AND json_extract(context, '$.sessionId') = ?. That is the same guarded formroutes/admin-trace.ts:89already uses on this table. - When it is
null, return[]without querying. Only the chat driver creates a run row with no session (loop-drivers.ts:113-125), and a chat run drives no engine. Any coding act inside its window belongs to some other session, so "none" is the correct reading. - Pass the session at all three call sites:
sessionId,prev.sessionIdandrun.sessionId. - Leave
recentActsForInstancesinstance-wide on purpose. It is a supervisor's feed across instances, not one run's record.
Test plan
- A real-schema test (
d1-sqlite, thecoding-resume-note-repo.test.tspattern): one instance, two repos, two sessions, overlapping windows, acts written for each session. Assert each call returns only its own session's acts, and assert anullsession returns[]without querying. - End to end through
pendingCodingResumeNote: a successor on repo R must not be told about a push that repo O's run made in the same window. - Mutation check: remove the session predicate and watch the tests fail.
Refs #806 (where this was noticed and left unfiled), #294 (acts on delegated runs), #766 (the ts = 0 misattribution fix, same family).
Activity
fix(coding): scope actsInWindow to the run's session, not the whole i…
serge-ivo commentedon Sep 13, 2026
Fixed in
cda8ef91. CI ✅ (run) and Deploy API Worker ✅ (run).What changed
actsInWindow(lib/instance-work.ts) now takes asessionId: string | null. When it is set, the query addsAND json_valid(context) AND json_extract(context, '$.sessionId') = ?7. When it is null, it returns[]without querying.workflows/coding-session.ts:sessionIdlib/coding-resume-note.ts:prev.sessionId, the predecessor's session rather than the caller's, which sincef8ded1bbis usually a later sessioncheck_delegation,lib/connectors/supervision.ts:run.sessionIdrecentActsForInstancesstays instance-wide. It is a supervisor's feed across instances, not one run's record.Tests
instance-work-session.test.ts(new, real migrated schema). Two repos on one instance, with overlapping runs:coding-resume-note-repo.test.ts: a repo-O act inside run A's window no longer reaches repo R's successor. The fixture's acts now carrysessionId, as real rows do.instance-work.test.ts: the bind shape, and a null session issues no query.supervision.test.ts: the A delegated run records its outcome but not its consequential acts — merging to main is invisible #294 fixture gainssession_id, as every coding driver writes it.Mutation-checked:
json_validfails the malformed-context test, becausejson_extractraises on bad JSON.Suites: full suite 11,514/11,514. API typecheck (source and tests),
pnpm -r typecheck, biome, everycheck-*.mjsguard anddocs:driftare clean. The change is line-neutral in both pinned files.Not verified
check_delegation, where it previously showed whatever coding acts fell inside its window. That is the intended reading, since a chat run drives no engine, and it is the one visible behaviour change.feat(loop): a continued run inherits what the stopped Pilot had worke…