Commit Graph

663 Commits

Author SHA1 Message Date
Katia Bulatova abaeebbefa fix(webapp): make a repeated watch card close leave the revision alone
`settleInvestigationStateAndCloseCard()` bumped the revision before it looked at
the transcript, so a redelivered or replayed action settled the row a second time
while the append refused the duplicate card. The row then held revision 2 and the
transcript's terminal card revision 1: the live run rendered 2, a refresh rendered
1, for a tool advertised as idempotent on the action id.

The transaction now locks the investigation, checks the tenancy triple, locks the
chat, and looks for the message id before it writes anything. An action already in
the transcript returns the stored card and the current revision untouched.

Lock order stays investigation then chat, matching `persistTurn` and the sweep's
`settleInvestigationAndCloseCard`; taking the chat first here would deadlock
against them.

A missing or deleted chat was indistinguishable from an already-closed card —
both came back `closed: false` — and the settle committed anyway, which is exactly
the terminal-row-without-a-card the transaction exists to prevent. It is now
`{ ok: false, error: "chat_missing" }`, and the watch lane logs it as the race it
is rather than a fault.
2026-08-06 13:56:42 +00:00
Katia Bulatova d52c224306 fix(dashboard-agent): merge the step breakpoint into the message's anthropic options
Setting the breakpoint replaced the whole `anthropic` provider-options object on
the last message, dropping any other Anthropic option it carried.
2026-08-06 13:42:20 +00:00
Katia Bulatova afc0cfaac8 fix(webapp): close a consented watch investigation's card atomically
The watch lane settled the investigation with `settleOpenInvestigations`, then
appended the terminal card as a separate write — and swallowed that write's
error, logging it and reporting success. So the row went terminal, the card
never arrived, the stale sweep stopped selecting the row because it was no
longer `in_progress`, and the user was left on "Working…" with nothing able to
repair it. Nothing in production calls the action again on its own, which is
exactly why the swallowed error mattered.

The lane now writes through `settleInvestigationStateAndCloseCard`: the terminal
revision and the closing card commit in one transaction, under the lane's own
message id, and the error propagates so the task's retry is a real retry. If the
card cannot be rendered the settle rolls back, leaving the `in_progress` row the
sweep still selects. The duplicated revision bump is gone with it — one outcome
is now one revision.

The regression test drives a failing close in one action; it no longer proves
recovery by calling the action a second time by hand.
2026-08-06 13:23:15 +00:00
Katia Bulatova f6f753c45a fix(webapp): merge the transcript under the row lock instead of replacing it
`persistTurn` and `persistMessages` stored the whole `messages` array they were
handed. The array is the snapshot the turn started from, so anything another
process appended in between was deleted: a wake delivery, a watch consent
record, or the terminal card of an investigation the stale sweep had just
settled. That last one is unrecoverable — the row is already terminal, so the
sweep never selects it again, and the panel is back to "Working…" for ever.

Both writes now read the row under `select ... for update` inside the
transaction and merge by stable message id: incoming order is kept, a stored
message the snapshot does not have goes at the end, and no id appears twice. A
message with no id falls back to its content so it cannot be carried over twice.

An append-only `chat_messages` table is the better long-term shape; merging
under the lock is enough for this architecture and needs no migration.
2026-08-06 13:23:14 +00:00
Katia Bulatova 2e23a098de fix(webapp): keep the other Anthropic options when the step breakpoint rolls off 2026-08-06 13:04:08 +00:00
Katia Bulatova 37c56aaa15 test(webapp): pin the settlement failure window and the open panel
Covers the two halves the earlier pass left open. Against a real database, a
stale card that cannot be rendered now proves the sweep rolls the settle back
with it: the row stays `in_progress` at revision 0, the chat stays empty, and
the row is still in the next run's selection.

The panel half is covered over `liveProgress`, the code that decides whether
"Working…" is shown: a mounted panel holding the unconcluded card re-reads the
stored transcript, merges by stable id, and the progress line goes away without
a reload. Re-reading repeatedly cannot add a second copy of the card.
2026-08-06 12:42:27 +00:00
Katia Bulatova 5bf3612ecf fix(webapp): settle an investigation and its closing card in one write
The row settle, the transcript and the session state were three separate
operations. Once the row was terminal, a failed transcript write left a card
that read `in_progress` forever: the stale sweep only selects `in_progress`
rows, so nothing was left to repair it.

Both lanes now commit the pair atomically. The live turn hands its pending
settlements to `persistTurn`, which upserts the revisions, appends their cards
and writes the session in one transaction; the process-local entry survives
until that commits, so a retried `onTurnComplete` still settles. The sweep goes
through `settleInvestigationAndCloseCard`, whose rollback restores the
`in_progress` row the sweep already selects.

That also removes the last reader of the per-run `chatOwners` map, which had no
`delete` and grew for the life of the worker: the failure record now travels in
the transcript write, which needs no userId.

Separately, the consented watch investigation gets the rolling step cache. Its
ten-step `streamText` re-sent every accumulated tool output uncached; the
breakpoint helper and the per-step cache telemetry now live in `step-cache.ts`
and both lanes use them, wrapping any `prepareStep` the resolved options carry.
2026-08-06 12:42:27 +00:00
Katia Bulatova 26f93da1a9 fix(webapp): have the stale-investigation sweep close the card it settles
The sweep settled the row and appended nothing, so it visibly fixed nothing: the
chat kept rendering the last card it had, which was still "Working…". The settle
now returns the state and revision it wrote, and the sweep appends that as the
closing card revision on the chat — id-deduped on
`investigation-settlement:{id}:{revision}`, so a retried run can neither stack a
second card nor open a second investigation.

The append is scoped by chat id: a sweep runs off any session and has no user in
context, unlike the turn lane.
2026-08-06 11:34:59 +00:00
Katia Bulatova f7b8374792 fix(webapp): put a turn's settled investigation card in the transcript
Settling the investigations row was invisible to the user. The panel builds the
winning revision from the transcript's own `tool-render_view` parts and never
reads that table, so a turn that ran out of steps left the card at
`in_progress` forever: the database believed the investigation had finished
while a refresh still showed "Working...".

`settleOpenInvestigations` now returns the revisions it committed, and
`onTurnComplete` appends each as one more card revision — after the transcript
write and id-deduped on `investigation-settlement:{id}:{revision}`, so a failed
append leaves the card visibly unclosed rather than silently lost, and a retry
can't stack a second card.

The card-building and the latest-revision reader move out of the watch lane and
into the runtime both lanes share, so there is one shape, not two. The watch
lane keeps its own message id: it dedupes on the action, not the revision.
2026-08-06 11:34:58 +00:00
Katia Bulatova 7b2f9c6933 chore(dashboard-agent-db): drop the submission's external-notification patch
A recorded outcome is immutable now, so nothing calls it.
2026-08-06 11:31:33 +00:00
Katia Bulatova d646809b0c chore(rbac): keep the delegated-token invariant, drop the essay 2026-08-06 10:49:06 +00:00
Katia Bulatova 79482dea37 chore(webapp): drop the comments the tests already say 2026-08-06 10:49:05 +00:00
Katia Bulatova 478b30cc2c chore(dashboard-agent): drop the test comments the assertions already say 2026-08-06 10:28:07 +00:00
Katia Bulatova 240799d0aa chore(dashboard-agent): drop the comments the code already says 2026-08-06 10:28:06 +00:00
Katia Bulatova 210863ea76 fix(webapp): separate a watch's last look from its last check in the batch order
A tick that could read nothing now moves the group's fairness key only, so a watch
with a permanently broken reader stops crowding out the rest of an over-cap group.
Dueness and the streak facts still follow the last real check.
2026-08-06 10:28:05 +00:00
Katia Bulatova 9daa8f92eb docs(dashboard-agent): tell the judge the error category is derived, the message withheld
Also reuse the policy's errored-output check instead of a second copy of it.
2026-08-06 10:28:02 +00:00
Katia Bulatova fb99526809 feat(dashboard-agent): tell the eval judge a failure's kind without its message
Classify a failed tool result locally into one of seven categories and send only
the label; the message is dropped with every other free-text field. Unrecognised
failures are unknown rather than guessed, and a bare string error field is now
withheld too.
2026-08-06 10:28:01 +00:00
Katia Bulatova 05b3472801 Merge branch 'main' into feat/dashboard-agent-flows
Route conflicts were the tab-title work meeting the agent page-context handle: both
sides kept, duplicate meta exports resolved to pageMeta, duplicate imports merged
with unused bindings dropped. Lockfile regenerated from the merged manifests.
2026-08-06 10:27:43 +00:00
Katia Bulatova d9ff853f09 style(webapp): format the agent eval and step-cache tests 2026-08-06 09:42:46 +00:00
Katia Bulatova 488621e033 perf(webapp): cache the agent's accumulated tool results between steps
A step-level 5-minute cache breakpoint rolls onto the growing history once it is large enough for the write to pay for itself, and each step logs the provider's cache write and read counts.
2026-08-06 09:42:45 +00:00
Katia Bulatova 1aee9cac35 fix(webapp): send the eval judge only the turn's structural facts
The judged turn now passes an allow-list of structural fields instead of a deny-list of known-sensitive names, unfolds the tool-output envelope so error text is redacted too, truncates inputs as well as outputs, and caps the whole turn.
2026-08-06 09:42:45 +00:00
Katia Bulatova 5f2f5fe241 fix(webapp): scope the watch submission ledger and its alerts to one tenant
The chat id a fresh submit derives, the transcript appends, the create's chat read
and the ledger replay are all owner-scoped now, the fire callback alerts once per
terminal outcome, and a failed email subscription is stated instead of dropped.
2026-08-06 09:40:28 +00:00
Katia Bulatova 4846cf8ea0 fix(dashboard-agent): finish a wake in the transcript, or don't count it
A settled investigation now lands as one more revision of the same card, so the
in-progress card and its working line end. An empty narration writes nothing and
says so, and only an acknowledged append marks a wake delivered.
2026-08-06 09:39:17 +00:00
Katia Bulatova d51cb9a736 fix(webapp): stop the OSS fallback handing a delegated token blanket access
Without an RBAC plugin, a user-actor token got the same permissive ability a personal access token gets. It now gets only what its own scope cap allows, and reads only when it declares no cap. Personal access tokens are unchanged.
2026-08-06 09:24:08 +00:00
Katia Bulatova 880bf60f48 test(tsql): pin that no mutating query can pass the query boundary
The agent's run_query path compiles TRQL, which has no write statements, so a
mutation cannot parse. Cover the deny-list evasions a keyword filter would miss
(casing, comments, a statement smuggled after a semicolon) and keep positive
controls so the negatives can't pass by rejecting everything.
2026-08-06 09:10:22 +00:00
Katia Bulatova 50fd10bf1b fix(webapp): replay a watch submission's recorded outcome instead of re-running it
A retried card submit was only repairable while the first attempt's watch was
still active. Once it had fired, expired, or answered in one shot, the retry
re-evaluated the condition and created a second operation.

A watch_submissions ledger, keyed (chat_id, client_request_id), is now written
before the condition is read and carries the outcome once there is one. A retry
looks it up first: a recorded outcome is replayed, a different draft under the
same id conflicts, and only a pending row proceeds - converging on the watch id
reserved up front rather than creating another.
2026-08-06 07:14:40 +00:00
Katia Bulatova 49a953871a fix(webapp): record a watch request before the watch starts, and repair a retried submit 2026-08-06 01:54:44 +00:00
Katia Bulatova d719c9b2d3 fix(webapp): start the wake poll for a browser that has only an active watch
The page load reported unread wakes only, so a fresh browser whose watch was
created elsewhere and hasn't fired yet never started polling: the wake landed
without a toast or a dot until a reload. The loader now returns the active-watch
presence too, in one read per page load.
2026-08-06 01:53:16 +00:00
Katia Bulatova e599f1299c fix(dashboard-agent): rotate an over-cap watch group instead of starving it
The batch took the 500 soonest-expiring watches of a group every tick, so a group
larger than the cap could leave the rest unchecked until the first 500 expired.
The group is now ordered least-recently-checked first, with a generated
cadence_minutes column and an index so the due predicate no longer re-parses the
spec JSON per tick.
2026-08-06 01:53:15 +00:00
Katia Bulatova a3be7bda6b fix(dashboard-agent): cap the output of the summariser and the wake narration 2026-08-06 01:41:05 +00:00
Katia Bulatova 4e39228be1 fix(dashboard-agent): pin only a live investigation into a compacted context 2026-08-06 01:41:04 +00:00
Katia Bulatova ddc6d25a66 fix(dashboard-agent): describe a value at the redaction depth cap instead of passing it to the judge 2026-08-06 01:41:04 +00:00
Katia Bulatova 0c41a70fa4 perf(dashboard-agent): cap search_docs to a few short excerpts 2026-08-06 00:49:01 +00:00
Katia Bulatova 42a76fb93d perf(dashboard-agent): say a settled watch outcome without a model, and narrate the rest with Haiku 2026-08-06 00:49:01 +00:00
Katia Bulatova 1d083f3ea9 test(dashboard-agent): budget the real prompt prefix with ceilings and a committed snapshot 2026-08-06 00:49:00 +00:00
Katia Bulatova 99850acc2d feat(dashboard-agent): summarise an oversized conversation, keeping live investigations and watches 2026-08-06 00:48:59 +00:00
Katia Bulatova fea1a29220 refactor(dashboard-agent): split the watch tick into delivery, condition, batch and adapters
watch-tick.ts keeps the two task definitions and the per-watch check; the wake
delivery, the condition lifecycle, the group tick and the db/session/callback
adapters each move to their own module. A pure move: watch-tick.ts re-exports
everything it exported before.
2026-08-06 00:13:24 +00:00
Katia Bulatova 31698fa0db refactor(dashboard-agent): split the tool set into one module per responsibility
tools.ts now only assembles ready adapters, in the same frozen key order: the
HTTP/JWT client, result curation, the docs client, the source-read ledger,
evidence canonicalisation, investigation persistence, and the api/navigation/
watch/alert tool groups each own their own module. A pure move.

The prompt-prefix fingerprints are unchanged, which is what keeps the head-start
and agent prefixes byte-identical.
2026-08-06 00:13:23 +00:00
Katia Bulatova 99b89338c3 docs(dashboard-agent): make the eval docs match the code
The docs claimed every turn is judged; the sampled rate, the redaction rule, the code-mode
skip, the opt-out and the retention period are now written down in one place.
2026-08-06 00:11:26 +00:00
Katia Bulatova 263b32467d feat(dashboard-agent): give the golden runs their own eval sample rate
The CI lane reads its own variable and defaults to every turn, so neither lane can change
the other's rate.
2026-08-06 00:11:25 +00:00
Katia Bulatova a4b4ef1b55 feat(webapp): let an organization turn off judging of its agent turns
A per-org feature flag the agent checks before every judged turn, and a turn is judged only
on an explicit yes.
2026-08-06 00:11:23 +00:00
Katia Bulatova 3c5b2d5d6b feat(dashboard-agent): never judge a turn that read source 2026-08-06 00:11:22 +00:00
Katia Bulatova 42ddcffef0 feat(dashboard-agent): keep customer data out of the judged-turn row and the judge
Payloads, outputs, query rows and file contents are replaced by their shape before a turn
reaches the judge, and the row now keeps the derived verdict only.
2026-08-06 00:11:22 +00:00
Katia Bulatova 7f13add1a0 feat(webapp): retire judged-turn rows after 30 days
The table is append-only quality data with no reader, so the sweep now drops rows past
the period in one bounded statement per run.
2026-08-06 00:11:21 +00:00
Katia Bulatova 18dffa6364 refactor(dashboard-agent): give the watch tests their own file
The wake narration and watch investigation cases move verbatim to
watch-actions.test.ts; the scaffolding both test files use moves to
test-support.ts. Same 146 passed / 2 skipped.
2026-08-05 23:42:35 +00:00
Katia Bulatova 46290d1ca6 refactor(dashboard-agent): move the watch wake and investigate lanes out of the agent
Pure move. The wake narration and the consented investigation now live in
watch-actions.ts, reached from one call in onAction; the runtime they share with
the agent's own hooks moves to agent-runtime.ts. Every export is re-exported from
dashboard-agent.ts, so no import path changes.
2026-08-05 23:42:35 +00:00
Katia Bulatova 2ed5d52260 refactor(dashboard-agent-db): move the watch tables into their own schema module
Pure move, re-exported from schema.ts so drizzle-kit reads the same six tables
and the generated SQL is unchanged.
2026-08-05 23:42:34 +00:00
Katia Bulatova 30ec754a37 refactor(dashboard-agent-db): move the watch queries into their own module
Pure move: the watch, wake and batch-chain queries leave queries.ts for
watch-queries.ts, re-exported so every import path still resolves.
2026-08-05 23:42:33 +00:00
Katia Bulatova b6ebe55500 fix(dashboard-agent): escape the prefix separator so the file reviews as text 2026-08-05 23:23:40 +00:00
Katia Bulatova be1cb0458e perf(dashboard-agent-db): index the open investigations the sweep scans 2026-08-05 22:17:43 +00:00