The Convex-to-Postgres port (#3107) and the tree retirement (#3125) dropped behaviours that 0.4 had. A six-way audit found 30-odd; this is the ledger for the ones with no PR yet.
Method, because it decides what counts: services/platform/convex/ survived #3107 as dead-or-shimmed code and was deleted by #3125, so an identifier existing in the tree never proved it ran. Every item below was verified by reading the live 0.5 path, and each names the file that should own the fix. backend/MIGRATION.md's PENDING: notes are the authority on what was deferred on purpose; nothing labelled REGRESSION here has one.
In flight
| Finding |
PR |
| Retrieval gate served unbound, superseded, orphaned and mid-index refs to any member |
#3141 |
| Erasure cascade missed 5 more subject-data categories |
#3122 |
| Chat entity legs compared a whole question as one substring |
#3123 |
| Emailed attachments are never indexed or retrievable |
#3121 (no PR) |
Also being worked, no PR yet: unrevoked LLM gateway keys, audit-log and conversation-write role gates, 2FA audit rows, the retention sweeps, the agent-comment automation loop, the dead chat search legs, and the docs that still describe a tale-convex service.
Security and cost
Sandbox sessions never revoke their LLM gateway key. revokeVirtualKey has one caller where 0.4 had four. The expiry sweep flips status only (domains/sandbox/watchdogs.ts:32), admin destroy revokes the session token but not the key (domains/sandbox/sessions.ts:459), and the task-agent lane never revokes (domains/tasks/agent-runs.ts:206). The mint body states the invariant twice: the gateway has no native TTL, and reset_duration: '1M' is safe because teardown revokes. It does not. Each abandoned key can spend one session's budget against the org's provider keys every month, indefinitely, one per expired or destroyed session.
An agent-authored task comment can start the task's owning automation. 0.4 called the trigger only from the user-comment path. 0.5 has one addTaskComment for both lanes and calls maybeTriggerOwningAutomation unconditionally (domains/tasks/comments.ts:144). The template is directly below it: dispatchMentionedProjectAgent gates on authorType !== 'user' at line 534. Reachable from the workflow task.comment native, the task-agent's task_comment tool, and the overdue cron. The live-run guard blocks a concurrent second start, not a sequential loop.
Audit-log reads are open to every non-disabled role (domains/audit_logs/routes.ts, six sites; helper at auth/access.ts:94). 0.4 was admin and owner only. The log names data subjects and the lawful grounds for erasing them.
A read-only member can write to conversations — reply, send outbound mail, close, bulk-act, delete (domains/conversations/service.ts:125, routes at 170/207/318/469/502). The assignment-privacy half is correctly ported; the write-role gate is what is gone. Outbound mail leaves the building under the org's name.
2FA and passkey lifecycle events no longer audit. Only failures do (domains/two_factor/service.ts:97, auth/auth.ts:321). An attacker who adds a passkey and disables 2FA leaves no successful-action trail.
Retention destruction emits no audit rows. Sixteen 0.4 action names exist nowhere in the tree (domains/retention/service.ts:849).
Correctness
Legal holds are checked once, then not again for the rest of the cascade. 0.4 re-read them inside all 19 erasure arms and named the reason: FRCP 37(e) spoliation. 0.5 gates once at domains/erasure/service.ts:540 and then runs ten passes that are not one transaction, two of which fan out per-thread and per-document deletes.
The erasure receipt cannot distinguish "found nothing" from "never looked", and the drawer's Full breakdown renders blank. counts is now Record<string, number>; the renderer skips non-objects (app/features/settings/governance/data-subject-requests/request-detail-drawer.tsx:530), so every entry is dropped before the zero-count branch. The headline numbers still render, so it looks populated.
Two retention categories never sweep. An admin sets an externalConversations window and nothing deletes; message bodies are kept forever (domains/retention/service.ts, no reference to app.conversations). chatFilterEvents is the same shape but latent — no production writer yet. Separately login_block_counters went from a 30-day window to 90 with no note, and two_factor_attempts lost its 30-day sweep entirely, so a user who fails 2FA once and never returns keeps that row forever.
Bulk task edits and moveTask emit no notifications, open no review gate and fire no automation triggers (domains/tasks/service.ts:2582, :1504).
updateTask never clears startNotifiedAt/slaLevel, so moving a due date never re-fires the ladder (domains/tasks/service.ts:891).
setTaskReviewer is a bare column write — no subscribe, no re-target of a pending request, no notify (domains/tasks/service.ts:903). Distinct from #3130's fix, which gates the respond door rather than the designation door.
Agent transcript merge became "keep the longer array", so a disjoint later ring segment overwrites history at parity (domains/tasks/agent-turn-shim.ts:537).
Comment edits never re-resolve mentions. Editing a comment to add @someone notifies nobody and the stored mention set goes stale. addedMentions survives at core/tasks/mentions.ts:127 with no caller.
Two chat search legs are stubbed empty under false comments. kind="website" returns [] "because there is no 0.5 table" — migration 0045 created app.websites and listWebsites exists. kind="mail-attachment" likewise, while migration 0037 shipped the columns and the binder writes them. Both are advertised to the model on every turn. The task and project legs also lost their zero-hit listing fallback, leaving listedSource unreachable.
A search after a replacement upload can answer from the superseded version. #3141 closes the read half; the indexer-side guard and the orphaned corpus row remain (domains/documents/replacement.ts:755 moves file_ref on and never purges).
Scheduling and hygiene
The three daily governance crons were packed into one 30-minute window (jobs/schedules.ts:29). 0.4 spaced them and said why: releases at 01:00, audit integrity at 02:00, retention at 04:00, so the verifier never walks the chain while retention deletes audit prefixes. 0.5 runs releases at 04:15 and integrity at 04:30, inside the retention window. This compounds with the documented anchor change, where the verifier trusts the oldest surviving row.
The expiry sweep can expire a sandbox session under a live turn — it guards on the admin pin, not on a running op (domains/sandbox/watchdogs.ts:32).
The admission-ticket reap runs last, behind up to 375s of spawner probes against a 300s job expiry, so it never runs when tickets are piling up (domains/sandbox/watchdogs.ts:46).
Sandbox drift reconcile only heals phantoms — a dropped pin is never re-asserted and a missing pinned container is never rebuilt (domains/sandbox/service.ts:175).
No expired-session reaper. Inherited from 0.4, not a regression, but the session table grows without bound; the idle sweep deliberately skips already-expired rows.
No exhaustiveness gate on the chat shim map. The sandbox bridge has exactly this walker as a CI gate; backend/domains/chat/ has no test file. That absence is how #3112's and #3128's missing handlers shipped. The map is complete today.
The 0.4 privacy suite for the conversations chat leg was deleted and not replaced — 26 cases, including "a plain member sees nothing" and "does not reach into another organization". What survives is a unit test on the predicate plus one integration assertion no workflow invokes.
Checked and not regressions
policyAcknowledgements and wfExecutions have no 0.5 table, so their missing erasure arms are moot — though the docs and locale catalogs still promise both, which is a docs gap. Budget enforcement was never wired in 0.4 either. The prompt-injection boundary is fully ported; the untrusted_content zero-hit signal was a rename to lib/chat/untrusted-content.ts with 12 live call sites. pg-boss schedules do fire — verified empirically against a throwaway database, since config.schedule defaults true independently of supervise.
The Convex-to-Postgres port (#3107) and the tree retirement (#3125) dropped behaviours that 0.4 had. A six-way audit found 30-odd; this is the ledger for the ones with no PR yet.
Method, because it decides what counts:
services/platform/convex/survived #3107 as dead-or-shimmed code and was deleted by #3125, so an identifier existing in the tree never proved it ran. Every item below was verified by reading the live 0.5 path, and each names the file that should own the fix.backend/MIGRATION.md'sPENDING:notes are the authority on what was deferred on purpose; nothing labelled REGRESSION here has one.In flight
Also being worked, no PR yet: unrevoked LLM gateway keys, audit-log and conversation-write role gates, 2FA audit rows, the retention sweeps, the agent-comment automation loop, the dead chat search legs, and the docs that still describe a
tale-convexservice.Security and cost
Sandbox sessions never revoke their LLM gateway key.
revokeVirtualKeyhas one caller where 0.4 had four. The expiry sweep flips status only (domains/sandbox/watchdogs.ts:32), admin destroy revokes the session token but not the key (domains/sandbox/sessions.ts:459), and the task-agent lane never revokes (domains/tasks/agent-runs.ts:206). The mint body states the invariant twice: the gateway has no native TTL, andreset_duration: '1M'is safe because teardown revokes. It does not. Each abandoned key can spend one session's budget against the org's provider keys every month, indefinitely, one per expired or destroyed session.An agent-authored task comment can start the task's owning automation. 0.4 called the trigger only from the user-comment path. 0.5 has one
addTaskCommentfor both lanes and callsmaybeTriggerOwningAutomationunconditionally (domains/tasks/comments.ts:144). The template is directly below it:dispatchMentionedProjectAgentgates onauthorType !== 'user'at line 534. Reachable from the workflowtask.commentnative, the task-agent'stask_commenttool, and the overdue cron. The live-run guard blocks a concurrent second start, not a sequential loop.Audit-log reads are open to every non-disabled role (
domains/audit_logs/routes.ts, six sites; helper atauth/access.ts:94). 0.4 was admin and owner only. The log names data subjects and the lawful grounds for erasing them.A read-only member can write to conversations — reply, send outbound mail, close, bulk-act, delete (
domains/conversations/service.ts:125, routes at 170/207/318/469/502). The assignment-privacy half is correctly ported; the write-role gate is what is gone. Outbound mail leaves the building under the org's name.2FA and passkey lifecycle events no longer audit. Only failures do (
domains/two_factor/service.ts:97,auth/auth.ts:321). An attacker who adds a passkey and disables 2FA leaves no successful-action trail.Retention destruction emits no audit rows. Sixteen 0.4 action names exist nowhere in the tree (
domains/retention/service.ts:849).Correctness
Legal holds are checked once, then not again for the rest of the cascade. 0.4 re-read them inside all 19 erasure arms and named the reason: FRCP 37(e) spoliation. 0.5 gates once at
domains/erasure/service.ts:540and then runs ten passes that are not one transaction, two of which fan out per-thread and per-document deletes.The erasure receipt cannot distinguish "found nothing" from "never looked", and the drawer's Full breakdown renders blank.
countsis nowRecord<string, number>; the renderer skips non-objects (app/features/settings/governance/data-subject-requests/request-detail-drawer.tsx:530), so every entry is dropped before the zero-count branch. The headline numbers still render, so it looks populated.Two retention categories never sweep. An admin sets an
externalConversationswindow and nothing deletes; message bodies are kept forever (domains/retention/service.ts, no reference toapp.conversations).chatFilterEventsis the same shape but latent — no production writer yet. Separatelylogin_block_counterswent from a 30-day window to 90 with no note, andtwo_factor_attemptslost its 30-day sweep entirely, so a user who fails 2FA once and never returns keeps that row forever.Bulk task edits and
moveTaskemit no notifications, open no review gate and fire no automation triggers (domains/tasks/service.ts:2582,:1504).updateTasknever clearsstartNotifiedAt/slaLevel, so moving a due date never re-fires the ladder (domains/tasks/service.ts:891).setTaskRevieweris a bare column write — no subscribe, no re-target of a pending request, no notify (domains/tasks/service.ts:903). Distinct from #3130's fix, which gates the respond door rather than the designation door.Agent transcript merge became "keep the longer array", so a disjoint later ring segment overwrites history at parity (
domains/tasks/agent-turn-shim.ts:537).Comment edits never re-resolve mentions. Editing a comment to add
@someonenotifies nobody and the stored mention set goes stale.addedMentionssurvives atcore/tasks/mentions.ts:127with no caller.Two chat search legs are stubbed empty under false comments.
kind="website"returns[]"because there is no 0.5 table" — migration 0045 createdapp.websitesandlistWebsitesexists.kind="mail-attachment"likewise, while migration 0037 shipped the columns and the binder writes them. Both are advertised to the model on every turn. The task and project legs also lost their zero-hit listing fallback, leavinglistedSourceunreachable.A search after a replacement upload can answer from the superseded version. #3141 closes the read half; the indexer-side guard and the orphaned corpus row remain (
domains/documents/replacement.ts:755movesfile_refon and never purges).Scheduling and hygiene
The three daily governance crons were packed into one 30-minute window (
jobs/schedules.ts:29). 0.4 spaced them and said why: releases at 01:00, audit integrity at 02:00, retention at 04:00, so the verifier never walks the chain while retention deletes audit prefixes. 0.5 runs releases at 04:15 and integrity at 04:30, inside the retention window. This compounds with the documented anchor change, where the verifier trusts the oldest surviving row.The expiry sweep can expire a sandbox session under a live turn — it guards on the admin pin, not on a running op (
domains/sandbox/watchdogs.ts:32).The admission-ticket reap runs last, behind up to 375s of spawner probes against a 300s job expiry, so it never runs when tickets are piling up (
domains/sandbox/watchdogs.ts:46).Sandbox drift reconcile only heals phantoms — a dropped pin is never re-asserted and a missing pinned container is never rebuilt (
domains/sandbox/service.ts:175).No expired-session reaper. Inherited from 0.4, not a regression, but the
sessiontable grows without bound; the idle sweep deliberately skips already-expired rows.No exhaustiveness gate on the chat shim map. The sandbox bridge has exactly this walker as a CI gate;
backend/domains/chat/has no test file. That absence is how #3112's and #3128's missing handlers shipped. The map is complete today.The 0.4 privacy suite for the conversations chat leg was deleted and not replaced — 26 cases, including "a plain member sees nothing" and "does not reach into another organization". What survives is a unit test on the predicate plus one integration assertion no workflow invokes.
Checked and not regressions
policyAcknowledgementsandwfExecutionshave no 0.5 table, so their missing erasure arms are moot — though the docs and locale catalogs still promise both, which is a docs gap. Budget enforcement was never wired in 0.4 either. The prompt-injection boundary is fully ported; theuntrusted_contentzero-hit signal was a rename tolib/chat/untrusted-content.tswith 12 live call sites. pg-boss schedules do fire — verified empirically against a throwaway database, sinceconfig.scheduledefaults true independently ofsupervise.