Repository navigation
046_RepairAutomaticSettlementTimestamps misses pre-fix auto-settle events tagged actor_kind: 'client' #10937
Description
Activity
Triage
Confirmed on current
main. Migration 046 only repairs server-attributedserver:auto-settle:%events. Client-attributedthread.settledrows withsettledAt == occurred_atare skipped on purpose (046’s tests treat them as manual). That leaves a hole for the pre-reactor / client-dispatched auto-settle burst in this report.What the code does
inferActorKindinOrchestrationEventStorelabels a commandclientunlesscommand_idstarts withserver:orprovider:. Since #8600,ThreadSettlementReactoralways dispatchesthread.auto-settlewithserver:auto-settle:${threadId}:${uuid}, so those events areactor_kind = 'server'.The decider still stamps a fresh
thread.settle(the client command) assettledAt = occurredAt.thread.auto-settleuses the command’ssettledAt(last activity since #9254).046’s
automatic_settlementsCTE requires all of:actor_kind = 'server'command_id LIKE 'server:auto-settle:%'json_extract(payload_json, '$.settledAt') = occurred_at
The 046 test helper maps any non-
server:command id toclientand leaves that row unrepaired (thread-manual). That matches this machine: 42 client events withsettledAt == occurred_atin a ~90s burst, several months off last activity.resolveSettledThreadTimestampreturnssettledAtwhen present, so the Settled shelf’s “X days ago” labels and order all collapse to the sweep.047–049 do not touch
settled_at. No open PR widens 046. This user is already on 0.0.40 (09e8de9), so 046 has already run — changing 046 in place will not re-run.Going forward the reactor is fine. This is historical projection repair only. Event payloads can stay as recorded (same rationale as 046: engine/projectors bootstrap from projection rows and cursors).
Related
- [Bug]: Auto-settlement marks old threads fresh and sends completion pushes on first launch #9057 (closed) / fix(server): preserve automatic settlement timestamps #9254 (merged) — same wrong-stamp symptom for the
server:auto-settlepath. This is the leftover client-attributed cohort, not a reopen. - fix(server): stop stale settlement notifications #9081 (open) — suppresses stale settlement notifications. Does not repair
settled_at. - [Bug]: Old auto-settled threads re-appeared as unsettled in sidebar after version upgrade #9600 (closed) / fix(server): settle inactive threads with open PRs #9610 (merged) — open-PR inactivity guard. Different symptom.
- Imported agent sessions duplicate T3-native threads: import dedup never consults the native thread namespace #10933 (open) — import-namespace collision. Reporter has no
import:threads; not a duplicate.
Suggested fix
Add a follow-up migration (next id is 050). Do not edit 046.
Reuse 046’s projection-only update: set
projection_threads.settled_atto the latest message/turn activity at or before the event’soccurred_at, elsecreated_at. Leaveorchestration_eventsalone. Keep the “projectionsettled_atstill equals the bad event stamp” guard so a later manual re-settle is not rewritten.Do not match every
actor_kind = 'client' AND settledAt = occurred_atrow. That is also a genuine one-offthread.settletoday. Discriminate automated sweeps, e.g.:- a tight cluster of many such client
thread.settledevents (this report: 34+ in ~90s), and/or - a bounded cutover (desktop
0.0.35–0.0.38/ around the fix(server): settle threads server-side #8600 window, e.g. through ~2026-09-04), and - optionally a large gap between
settled_atand last activity.
Reporter already ran that repair locally with permission (41/42 rows; events unchanged).
Labels:
bug,accepted,via-triage- addedbugSomething is broken or behaving incorrectly.Something is broken or behaving incorrectly.acceptedfeature request acceptedfeature request acceptedvia-triageFiled through npx t3 triageFiled through npx t3 triage
on Sep 9, 2026 - added a commit that references this issue
on Oct 8, 2026
What happened
After updating, recent threads that used Codex appeared to be "gone," and threads using Codex that the user didn't recognize as recent appeared in their place. Separately, almost all threads — including ones months old — were labeled as settled "~4 days old," which was not correct.
Diagnosis
Nothing was actually deleted: every thread with a
thread.createdevent inorchestration_eventswas still present inprojection_threads(58/58). The real problem is thesettled_attimestamp used by the sidebar to sort and label the Settled section (resolveSettledThreadTimestampinpackages/client-runtime/src/state/threadSort.ts, which returnssettledAtwhen present).During an earlier update (before this user's most recent one), an automatic settlement sweep ran a burst of 34+
thread.settledcommands roughly one per second over ~90 seconds, settling nearly every idle thread across every project. Each event carriesactor_kind: 'client'and a plain random-UUIDcommand_id— not theserver:auto-settle:<threadId>:<uuid>/actor_kind: 'server'shape the currentThreadSettlementReactoruses. Each event'spayload_json.settledAtequals its ownoccurred_at(the sweep time), not the thread's real last activity — the exact bug described in the comment onapps/server/src/persistence/Migrations/046_RepairAutomaticSettlementTimestamps.ts("Server auto-settlement used to stamp settledAt with the sweep time instead of the thread's last activity").Migration 046 exists specifically to repair this, but its detection query is scoped to:
Because the pre-fix implementation apparently attributed these commands to
actor_kind: 'client'with a plain UUIDcommand_id(rather than the currentserver:auto-settle:shape), migration 046 silently skips every one of them. On this machine, comparing each burst-settled thread'ssettled_atagainst its true last activity (max ofprojection_thread_messages.created_at/projection_turns.requested_at|started_at|completed_at) found 42 threads misdated, several by months (e.g. a thread last active 2026-04-14 showedsettled_at = 2026-09-04).Net user-visible effect: the Settled section's "X days ago" labels cluster within a few days of the sweep regardless of a thread's true age, burying recently-relevant threads under old ones that now look deceptively recent, and making genuinely old, unfamiliar threads (e.g. ones run from a non-project directory like
~/Downloads) look like new/foreign activity.Steps to reproduce
ThreadSettlementReactor/server:auto-settle:commands existed).thread.settledevent hasactor_kind: 'client', a randomcommand_id, andpayload_json.settledAt == occurred_at.projection_threads.settled_atfor threads settled in step 2 — the value still equals the sweep time, not the thread's real last activity.Version
Server 0.0.40 (tag
v0.0.40, commit09e8de9). Bad data originated on desktop app 0.0.35–0.0.38 per eventmetadata_json.origin.appVersion.Environment
macOS 24.6.0 (Darwin), Apple silicon, Node v26.8.1. Desktop app, local server (
http://127.0.0.1:3773). Providers in use: codex, claudeAgent.Evidence
Related issues
None found matching this specific migration gap. #9600 ("Old auto-settled threads re-appeared as unsettled in sidebar after version upgrade", closed) is a related but distinct symptom from around the same time window. Not a duplicate of #10933 (agent-session import namespace collision) — no
import:-prefixed threads exist on this machine; every affected thread here is native.Fix applied or workaround
With the user's explicit permission, ran a corrected version of migration 046's repair query against their local
projection_threads, scoped toactor_kind = 'client'events withsettledAt == occurred_atin the affected time window, recomputingsettled_atfrom each thread's real last message/turn activity (falling back tocreated_at). 41 of 42 candidate rows were updated (one already matched). No source files were changed; noorchestration_eventsrows were modified — only theprojection_threads.settled_atprojection.Suggested upstream fix: broaden migration 046's detection (or add a follow-up migration) to also match
actor_kind = 'client'settlement events whosepayload_json.settledAtequalsoccurred_at, excluding genuine one-off manual archives if possible (e.g. by requiring the event be part of a tight burst of many such events, or by shipping a fixed cutover date for when the server-side reactor replaced the older path).Filed by
claude (opus-5) via t3 triage