Audit · PR #1289 · codex/task-activity-timeline · 13 iul 2026

Cine a scris nota asta?

Până acum, Voost Control nu putea răspunde: un task avea o singură notă mutabilă, fără autor, fără istoric. PR #1289 o înlocuiește cu Task Activity — comentarii care nu se mai pot suprascrie. Am auditat schimbarea cu 16 agenți + un review independent Codex. Asta a ieșit.

48fișiere atinse
+1.475linii adăugate
0probleme P1
2P2 confirmate
16+1agenți de audit + Codex
Verdict: merge

Feature-ul merită producția — rezolvă o pierdere reală de informație, fără consumatori inventați. Dar cere 2 fixuri înainte de merge, ambele în același loc: podul de compatibilitate din migrare.

ACTUL 1

Nota care se ștergea singură

Câmpul reviewNote era folosit de tine și de mașini în același timp. Dialogul de Review îți punea în față toată nota și salva ce lăsai tu — peste tot ce scriseseră agenții.

Demo — suprascrierea (comportamentul de pe main)
Follow-up ofertă — lead Necesit (instalații electrice)
[agent-work-evidence] pachet #a41f: 3 apeluri, transcript promovat, outcome=partial [reconciliation] Task înlocuit de follow-up-ul din 8 iul
Client 360 Review Policy Digest reconciliere
Contextul mașinilor a dispărut. Dialogul salvează câmpul întreg — evidence-ul agentului și markerul de reconciliere s-au dus, iar cei trei consumatori de mai sus citesc acum o notă goală de context. Fără autor, fără timestamp, fără urmă.

Asta nu e ipoteză de laborator: pe main, dialogul chiar preîncarcă tot reviewNote și salvează înlocuirea integrală, pe același câmp în care Agent Work Evidence și markerii [reconciliation] își țin datele. Un singur edit de-al tău putea orbi trei sisteme de siguranță.

ACTUL 2

Ce a construit Codex în loc

O tabelă nouă, append-only: comentarii care se pot doar adăuga, niciodată edita sau șterge. „Activity” e doar o proiecție de citire — comentariile + schimbările de status, în ordine cronologică. Nu apare o a doua autoritate pentru status.

Anatomia unui Task Comment — apasă pe câmpuri
task_comments
body created_by created_at source task_id
Un comentariu de-al tău bifează și reviewedAt pe task — semnalul „owner-ul a văzut”, păstrat.
Același scenariu, cu Task Activity
autor indisponibil[legacy] nota istorică, migrată cu timestamp-ul ei real
system[agent-work-evidence] pachet #a41f: 3 apeluri, outcome=partial
statusin_progress → review · de neo
Nimic nu s-a pierdut. Fiecare rând are autor și oră; nota ta stă lângă evidence-ul mașinii, nu peste el. Aceeași istorie o văd UI-ul, voost tasks show, contextul agenților și timeline-ul.

Compatibilitatea nu a fost ruptă: cei 9 scriitori vechi de note (reconciliere, delivery guard, import Necesit, e-Factura Conta, work reporting…) scriu acum în ambele locuri, în aceeași tranzacție, până când consumatorii lor structurați migrează și ei. Aici începe însă partea cu probleme.

ACTUL 3

Auditul: 8 lentile, apoi avocatul diavolului

Opt agenți au auditat câte o dimensiune. Fiecare acuzație serioasă (P1/P2) a primit apoi un verificator advers cu o singură misiune: s-o dărâme citind codul. Ce n-a supraviețuit, a fost respins.

Scor pe dimensiuni (1–10)
valoare produs
9
API
8
UI operator
8
integrări
8
teste
8
docs & skills
8
migrare DB
7
simplitate
6
Simplitatea e nota mică: ~100 din 129 de linii de migrare există doar pentru fereastra de rollback. Restul PR-ului e disciplinat.
Acuzațiile mari — apasă să vezi dacă au supraviețuit verificării
P2Cast-ul de xid din trigger crapă după primul wraparound — orice commit pe review_note ar eșua
prisma/migrations/20260713050000_add_task_comments/migration.sql:81
CONFIRMATVerificat pe PostgreSQL 16 local și în sursa Postgres: ('4294967296')::xid aruncă eroare, nu trunchiază. Demo-ul e mai jos, în Actul 4.
P2Logica de dedup există de două ori (TypeScript + plpgsql) și nimic nu le forțează să rămână identice
src/lib/tasks/task-comments.ts:17 · migration.sql:51–63
CONFIRMATVerificatorul n-a putut respinge — și a găsit el însuși un caz în care cele două diverg azi (whitespace). Codex a lovit independent exact același punct. Demo-ul e în Actul 4.
P2„Podul de rollback din migrare e ceremonie — topologia ta (1 prod + 2 tenanți) nu-l justifică”
migration.sql (trigger-ul întreg)
RESPINSPremisele acuzației erau greșite: fără pod, o notă scrisă în fereastra de deploy ar lipsi din Activity, iar un re-backfill naiv ar duplica rânduri. Podul e conservator, dar funcțional și ieftin la rulare. Rămâne valabilă doar partea de „nu are dată de scoatere”.
P2„Invariantul append-only nu are test negativ și nici gardă în DB”
src/app/api/tasks/[id]/comments/route.ts
RESPINSFaptele sunt adevărate, dar nu există nicio cale reală de scriere care să editeze sau să șteargă: ruta e doar POST, iar codul cheamă exclusiv create. Acuzația cerea PR-ului să inventeze o convenție de testare pe care repo-ul n-o are nicăieri.
P2 → P3„Backfill-ul de migrare rulează complet netestat — transformă datele de producție o singură dată, la deploy”
migration.sql:111
CONFIRMAT, retrogradatGolul de acoperire e real (runner-ul clonează DB-uri goale), dar riscul e mic: review_note rămâne intact, iar rândurile migrate sunt marcate cu source și regenerabile. Debt, nu pericol.
P2 → P3„Rutele de reconcile resping acum actori pe care ieri îi acceptau, iar API.md nu spune”
docs/API.md:146
CONFIRMAT, retrogradatÎnăsprirea e intenționată (atribuire onestă) și documentată în blocul de convenții + ADR; apelanții fără actor primesc default-uri și nu simt nimic. Rămâne un nit de completat în docs, nu un contract rupt.
ACTUL 4

Cele două probleme reale — declanșează-le

Ambele stau în podul de compatibilitate: un trigger în Postgres care prinde notele scrise de un runtime vechi (fereastra de deploy sau un rollback) și le copiază în Activity. Ideea e bună. Două detalii nu sunt.

P2 nr. 1 — bomba cu ceas din cast · apasă butonul

Trigger-ul se dedublează întrebând: „comentariul ăsta a fost scris în tranzacția curentă?”. Pentru asta compară id-ul de tranzacție — printr-un cast care merge azi și crapă mâine:

xmin = (pg_current_xact_id()::text)::xid
epoch xid (azi: 0)0
tranzacții consumate5.233.093
valoarea castată5.233.093 — încape în 32 de biți ✓
commit pe review_noteOK
ERROR: value "4294967296" is out of range for type xid — verificat pe PostgreSQL 16 local. După primul epoch, castul aruncă eroare la fiecare commit care atinge un review_note, pe fiecare bază tenant care poartă migrarea — la ani după ce toată lumea a uitat că trigger-ul există. Fixul e o linie (compară pe 64 de biți), dar trebuie să intre înainte ca podul să fie uitat.
sursă: migration.sql:81 · verificat adversarial pe PG 16.13 + sursa Postgres (uint32in_subr) · 13 iul 2026
P2 nr. 2 — aceeași regulă, scrisă în două limbi · declanșează divergența

Ca să nu creeze duplicate, trigger-ul compară byte cu byte corpul comentariului scris de aplicație cu bucata de notă pe care o calculează el singur, cu aceeași euristică rescrisă în plpgsql. Egalitatea ține azi — dar n-o păzește niciun test. Un singur spațiu o rupe:

Duplicat anonim în Activity. Cu spațiul la final, nota veche (trimuită de aplicație) nu mai e prefix al celei noi, așa că trigger-ul cade pe ramura „notă întreagă” — alt corp, egalitatea pică, și inserează el însuși un al doilea comentariu, cu autor indisponibil. Codex și verificatorul Fable au găsit independent exact acest caz. Fixul minim: un test care pinuiește egalitatea + contractul scris la call site-uri; fixul curat: o singură implementare a regulii, nu două.
Egalitate perfectă → zero duplicate. Așa arată toate cele 9 call site-uri azi. Problema nu e că e stricat — e că nimic nu-l ține așa.
fidel implementării: reviewNoteCommentBody (task-comments.ts:17) vs. capture_task_review_note_activity (migration.sql:51–63) · 13 iul 2026
ACTUL 5

Restul listei + a doua opinie

P3-uri reale, niciunul blocant — plus ce a adus review-ul independent Codex (gpt-5.6-sol).

P3 — de pus pe listă, nu în calea merge-ului · apasă pentru detalii
P3Contextul tău de operator nu vede comentariile noi
src/lib/context/context-read-model.ts:414 — fișier neatins de PR
VERIFICAT DIRECTBucket-ul „task-uri cu note” filtrează pe reviewNote not null. Un task adnotat doar prin noul comment box nu apare acolo. E debt de tranziție asumat, dar merită în issue-ul de follow-up.
P3Empty state-ul Activity e un placeholder cu bordură punctată, nu tăcere
task-activity-section.tsx · gustul tău declarat: „zeros = silence”
Plus copy stângaci „Created to <status>” pe evenimentul de creare. Amândouă sunt fixuri de fast lane.
P3Două referințe de skill încă trimit agenții spre calea veche
references/api-full.md + housekeeping-task-decision-reconciliation.md
Ambele marcate „legacy” și dual-write-ul le ține funcționale, deci impact mic — dar canonul spune acum voost tasks comment, iar referințele n-au aflat.
P3Limita de statusChanges din contextul agenților a scăzut tacit din 20 în 12
entity-context-resolvers.ts:638 — verificat direct pe diff
Probabil intenționat (aliniere la limita generală), dar e o schimbare de comportament nedocumentată pentru agenți.
Codex (gpt-5.6-sol), review independent — 154k tokeni de explorare

Zero P1. Șase P2 candidate, din care după verificare:

1 CONFIRMĂexact edge-ul de whitespace din Actul 4 — găsit independent, fără să vadă raportul Fable
Convergența a două modele diferite pe același bug e cel mai puternic semnal din tot auditul că problema e reală.
3 RESPINSEpreexistente pe main, nu regresii ale PR-ului
Race-ul PATCH/DELETE (cod neschimbat — iar PR-ul chiar îl repară pe calea de comments, cu FOR UPDATE), limita globală de body de 1 GB (config pentru upload-uri de call artifacts, comună tuturor rutelor) și regex-ul lowercase de actori (schema din issue #771, doar refolosită).
2 MINOREedge-uri de rollback multi-append + NUL byte → 500 în loc de 400
Primul cere un runtime vechi care face două append-uri în aceeași tranzacție (podul reține doar ultimul suffix); al doilea e un nit de validare. Ambele P3 în practică.
ACTUL 6

Verdictul, pe criteriile tale

Barele tale: „cleanest code, minimum complexity, never build ahead of the first real consumer” + cele trei coloane din VISION. Aplicate:

Trece

Consumator real?

Toți există azi: pagina de Task (pagina ta cea mai folosită), voost tasks comment pentru agenți, cei 5 scriitori de note din producție. Replies/edit/paginare — explicit amânate în ADR-0091.

Trece

Funcționalitate reală?

Da: elimină o pierdere de date demonstrată (edit-ul tău peste evidence-ul mașinii) și dă atribuire nefalsificabilă. Trust + Visibility + Context, la propriu.

Cu rezervă

Complexitate minimă?

Suprafața de produs — da (UI-ul chiar a scăzut cu 109 linii). Migrarea — nu: podul de rollback e conservator pentru 1 prod + 2 tenanți și n-are dată de scoatere. Funcțional, dar peste nevoie.

Next steps — în ordine
  • 1Fix de o linie pe cast-ul xid în trigger (comparație pe 64 de biți) — cerut pe PR #1289 înainte de merge.blochează merge
  • 2Pinuiește invariantul dual-write: un test care rupe build-ul dacă corpul comentariului diverge de incrementul de review_note (cazul cu whitespace devine test), + contractul scris la call site-uri.blochează merge
  • 3Issue de follow-up: data de scoatere a trigger-ului (legată de „nu mai suportăm rollback pre-Activity”) + migrarea context-read-model de pe reviewNote pe comments.înainte de deploy
  • 4Merge + deploy, apoi fast lane pentru gustul UI: empty state tăcut, copy-ul „Created to”.după 1–3
Concluzia

PR-ul răspunde la „cine a scris asta?”. Mai rămâne de răspuns la „cine scoate trigger-ul?”.