instance_runtime_tasks.updated_at is written in two mutually-unsortable text formats, so every ORDER BY updated_at DESC … LIMIT over the board mirror ranks a 00:30 runner task above an escalation raised at 22:00 the same day.
Found in the D1 sweep prompted by #438 (no test in this repo executes SQL). This is the #587 shape one level down: not two queries disagreeing, but two writers disagreeing about the byte format of the column a reader sorts on.
Verified — the column has two writers with two formats
datetime('now') → SQLite's YYYY-MM-DD HH:MM:SS (space at index 10):
workers/api/src/lib/work-card.ts:22-23 — upsertWorkCard, the ONE upsert every board domain goes through:
INSERT INTO instance_runtime_tasks (id, instance_id, user_id, type, status, payload, created_at, updated_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, datetime('now'), datetime('now'))
Callers: lib/coding-board.ts:91 (coding-session cards), lib/pipeline-board.ts:54 (pipeline runs), lib/coding-authority.ts:355, lib/repo-policy-act.ts:241 and :275, workflows/coding-session.ts:255.
workers/api/src/lib/work-card.ts:63 — closeWorkCards: updated_at = datetime('now')
workers/api/src/lib/work-card.ts:93 — setWorkCardProgress: updated_at = datetime('now')
workers/api/src/workflows/agent-loop.ts:312-313 — the escalation card, i.e. "a supervised agent needs a decision":
VALUES (?1, ?2, ?3, 'escalation', 'needs_human', ?4, datetime('now'), datetime('now'))
workers/api/src/lib/loop-drivers.ts:268-269 — the delegation card, same shape.
workers/api/src/workflows/job-apply.ts:141, routes/instances-apply.ts:246, routes/instances-browse.ts:183 — updated_at = datetime('now') on failure.
ISO-8601 with T/Z (index 10 is T):
workers/api/src/routes/instances-runtime.ts:250-262 — mirrorRuntimeTask binds ?8 from
taskTimestamp(task.updatedAt ?? task.updated_at ?? createdAt), and taskTimestamp is
workers/api/src/routes/instances-runtime.ts:155-159:
export function taskTimestamp(value: unknown): string {
return typeof value === "string" && value.trim()
? value
: new Date().toISOString();
}
The runner-supplied value is ISO too — packages/browser-runner/src/runner.ts:180,195,235,243,253,260,474,1079,1140,1153,1172 all assign task.updatedAt = new Date().toISOString().
workers/api/src/routes/instances-tasks.ts:242-245 — SET status = 'running', updated_at = ?1 bound with new Date().toISOString().
workers/api/src/routes/instances-apply.ts:104 and routes/instances-browse.ts:108 — the single-flight claim rows bind nowIso.
Verified — SQLite orders these by bytes, so the two formats never interleave
D1/SQLite compares TEXT with BINARY collation. ' ' is 0x20, 'T' is 0x54, so for any two rows with the same date every ISO row sorts above every datetime('now') row regardless of the time of day:
$ sqlite3 :memory: "create table t(u text);
insert into t values('2026-08-16T01:00:00.000Z'),('2026-08-16 23:00:00'),('2026-08-16T02:00:00.000Z'),('2026-08-16 09:00:00');
select u from t order by u desc;"
2026-08-16T02:00:00.000Z
2026-08-16T01:00:00.000Z
2026-08-16 23:00:00
2026-08-16 09:00:00
Verified — three readers sort on it, and each pairs the sort with a LIMIT
The sort is not cosmetic: it decides which rows are returned at all.
workers/api/src/routes/instances-runtime.ts:281-286 — mirroredRuntimeTasks:
SELECT payload FROM instance_runtime_tasks
WHERE instance_id = ?1 AND user_id = ?2 AND hidden = 0
ORDER BY updated_at DESC
LIMIT ?3
Default limit = 200 (:279).
workers/api/src/lib/instance-work.ts:220-224 — recentWorkForInstances, one branch per subordinate:
SELECT instance_id, id, type, status, payload, updated_at
FROM instance_runtime_tasks
WHERE instance_id = ?${p} AND user_id = ?1 AND hidden = 0
ORDER BY updated_at DESC LIMIT ?2
clampPer(perInstance, 8) at :216 — 8 rows per instance.
workers/api/src/lib/board.ts:295 calls mirroredRuntimeTasks(env, instanceId, userId, BOARD_TASK_LIMIT) with BOARD_TASK_LIMIT = 1000 (lib/board.ts:127).
User-visible impact
- The supervision surface is the sharp one.
lib/connectors/supervision.ts:358 calls
recentWorkForInstances(ctx.env, userId, ids, limit) — this is what a supervisor agent reads to
answer "what have my subordinates been doing". Only 8 cards come back per subordinate, and any
subordinate with more than 8 non-hidden cards gets its 8 chosen by this ordering. A coding-session
card, a pipeline card, a delegation card and an escalation card are all datetime('now')
writers, so they lose to every runner-mirrored task stamped anywhere earlier the same day. The
card that says "a supervised agent needs a decision" (workflows/agent-loop.ts:312) is exactly
the one that drops off.
GET /v1/instances/:id/tasks (routes/instances-tasks.ts:81 and :121) returns
mirroredRuntimeTasks(...) at the 200 default with no pagination and no truncation flag, so
the console has no way to know rows were dropped or that the drop was arbitrary. (The board path
does report truncated — lib/board.ts:415 — but only at 1000.)
Not claimed / inferred
lib/board.ts:415 re-sorts the assembled items in JS with Date.parse(item.updatedAt) off the
payload, and the payload's updatedAt is mixed the same way (work-card.ts:62 and :92 write
json_set(payload, '$.updatedAt', datetime('now')) while runner payloads are ISO). V8 parses
"YYYY-MM-DD HH:MM:SS" as local time and the Workers runtime is UTC, so this JS sort is probably
correct today. Not verified, and not part of the claim above — the SQL ordering is broken
independently of it, and the payload mixing is worth fixing for the same reason.
- I have not measured a production row count, so I cannot say how often the 200/1000 caps bind. The
8-per-instance supervision cap binds at nine cards.
The repo already knows this hazard exists
workers/api/src/lib/error-log.ts:93 carries a private one-liner for exactly this conversion:
const sqlTime = (ms: number) => new Date(ms).toISOString().slice(0, 19).replace("T", " ");
grep -rn "sqlTime" workers/api/src → 2 hits, both in error-log.ts. lib/admin.ts:116-118 (sinceTs), routes/admin-instance-detail.ts:166-168 (thirtyDaysAgo) and lib/external-usage.ts:170-173 each re-derive the same three lines locally. Four independent copies of the fix, none of them shared, and instance_runtime_tasks got none of them.
Suggested shape of a fix (not prescriptive)
Pick one format for the column, convert the other writers, and pin it with a test that inserts through both writer paths and asserts the SQL ordering — which is the class of test #438 says does not exist yet. A shared sqlTime/nowSql helper in lib/ would give the four existing copies somewhere to collapse into.
Refs #438.
instance_runtime_tasks.updated_atis written in two mutually-unsortable text formats, so everyORDER BY updated_at DESC … LIMITover the board mirror ranks a 00:30 runner task above an escalation raised at 22:00 the same day.Found in the D1 sweep prompted by #438 (no test in this repo executes SQL). This is the #587 shape one level down: not two queries disagreeing, but two writers disagreeing about the byte format of the column a reader sorts on.
Verified — the column has two writers with two formats
datetime('now')→ SQLite'sYYYY-MM-DD HH:MM:SS(space at index 10):workers/api/src/lib/work-card.ts:22-23—upsertWorkCard, the ONE upsert every board domain goes through:lib/coding-board.ts:91(coding-session cards),lib/pipeline-board.ts:54(pipeline runs),lib/coding-authority.ts:355,lib/repo-policy-act.ts:241and:275,workflows/coding-session.ts:255.workers/api/src/lib/work-card.ts:63—closeWorkCards:updated_at = datetime('now')workers/api/src/lib/work-card.ts:93—setWorkCardProgress:updated_at = datetime('now')workers/api/src/workflows/agent-loop.ts:312-313— the escalation card, i.e. "a supervised agent needs a decision":workers/api/src/lib/loop-drivers.ts:268-269— thedelegationcard, same shape.workers/api/src/workflows/job-apply.ts:141,routes/instances-apply.ts:246,routes/instances-browse.ts:183—updated_at = datetime('now')on failure.ISO-8601 with
T/Z(index 10 isT):workers/api/src/routes/instances-runtime.ts:250-262—mirrorRuntimeTaskbinds?8fromtaskTimestamp(task.updatedAt ?? task.updated_at ?? createdAt), andtaskTimestampisworkers/api/src/routes/instances-runtime.ts:155-159:packages/browser-runner/src/runner.ts:180,195,235,243,253,260,474,1079,1140,1153,1172all assigntask.updatedAt = new Date().toISOString().workers/api/src/routes/instances-tasks.ts:242-245—SET status = 'running', updated_at = ?1bound withnew Date().toISOString().workers/api/src/routes/instances-apply.ts:104androutes/instances-browse.ts:108— the single-flight claim rows bindnowIso.Verified — SQLite orders these by bytes, so the two formats never interleave
D1/SQLite compares TEXT with BINARY collation.
' 'is0x20,'T'is0x54, so for any two rows with the same date every ISO row sorts above everydatetime('now')row regardless of the time of day:Verified — three readers sort on it, and each pairs the sort with a LIMIT
The sort is not cosmetic: it decides which rows are returned at all.
workers/api/src/routes/instances-runtime.ts:281-286—mirroredRuntimeTasks:limit = 200(:279).workers/api/src/lib/instance-work.ts:220-224—recentWorkForInstances, one branch per subordinate:clampPer(perInstance, 8)at:216— 8 rows per instance.workers/api/src/lib/board.ts:295callsmirroredRuntimeTasks(env, instanceId, userId, BOARD_TASK_LIMIT)withBOARD_TASK_LIMIT = 1000(lib/board.ts:127).User-visible impact
lib/connectors/supervision.ts:358callsrecentWorkForInstances(ctx.env, userId, ids, limit)— this is what a supervisor agent reads toanswer "what have my subordinates been doing". Only 8 cards come back per subordinate, and any
subordinate with more than 8 non-hidden cards gets its 8 chosen by this ordering. A coding-session
card, a pipeline card, a
delegationcard and anescalationcard are alldatetime('now')writers, so they lose to every runner-mirrored task stamped anywhere earlier the same day. The
card that says "a supervised agent needs a decision" (
workflows/agent-loop.ts:312) is exactlythe one that drops off.
GET /v1/instances/:id/tasks(routes/instances-tasks.ts:81and:121) returnsmirroredRuntimeTasks(...)at the 200 default with no pagination and no truncation flag, sothe console has no way to know rows were dropped or that the drop was arbitrary. (The board path
does report
truncated—lib/board.ts:415— but only at 1000.)Not claimed / inferred
lib/board.ts:415re-sorts the assembled items in JS withDate.parse(item.updatedAt)off thepayload, and the payload's
updatedAtis mixed the same way (work-card.ts:62and:92writejson_set(payload, '$.updatedAt', datetime('now'))while runner payloads are ISO). V8 parses"YYYY-MM-DD HH:MM:SS"as local time and the Workers runtime is UTC, so this JS sort is probablycorrect today. Not verified, and not part of the claim above — the SQL ordering is broken
independently of it, and the payload mixing is worth fixing for the same reason.
8-per-instance supervision cap binds at nine cards.
The repo already knows this hazard exists
workers/api/src/lib/error-log.ts:93carries a private one-liner for exactly this conversion:grep -rn "sqlTime" workers/api/src→ 2 hits, both inerror-log.ts.lib/admin.ts:116-118(sinceTs),routes/admin-instance-detail.ts:166-168(thirtyDaysAgo) andlib/external-usage.ts:170-173each re-derive the same three lines locally. Four independent copies of the fix, none of them shared, andinstance_runtime_tasksgot none of them.Suggested shape of a fix (not prescriptive)
Pick one format for the column, convert the other writers, and pin it with a test that inserts through both writer paths and asserts the SQL ordering — which is the class of test #438 says does not exist yet. A shared
sqlTime/nowSqlhelper inlib/would give the four existing copies somewhere to collapse into.Refs #438.