Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/governance-scope-off-event-loop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@schlessera/brain-ui-server": patch
"@schlessera/brain-ui-sdk": patch
---

Changed: the hosted server commits governance events raised outside a journal transaction, the session's first taint, roadblock records and turn scope rows on its SQLite write worker at `synchronous=FULL`, in submission order with journal and Activity writes, instead of on the event loop (#2850). A turn's egress connection sends its first byte only after its taint has committed, and admission counts that taint from the moment it is submitted.
Changed: the egress proxy awaits a promise returned from its `admitted` hook before the connection's first upstream byte.
2 changes: 1 addition & 1 deletion docs/decisions/answer-delivery.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ carries, no longer by position.

The host also re-sends a session's pending questions after `session_resume`
(`resendPendingAsks(host, ws, msg.sessionId)`,
`packages/ui-server/src/ws/dispatch.ts:640`). Before this, a resume replaced
`packages/ui-server/src/ws/dispatch.ts:641`). Before this, a resume replaced
the transcript and left only a history card behind, which was the second
symptom.

Expand Down
2 changes: 1 addition & 1 deletion docs/decisions/async-collaboration.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ suppressed-per-model Action; unknown usage cannot become a guessed zero.
Subscription billing moves the turn counter even when spend is zero. The activity
sum exposes unpriced runs (`sumEffectiveCost`,
`packages/ui-server/src/activity/store.ts:179-194`) and settles only after execution
(`store.rollupRun(runId);`, `packages/ui-server/src/activity/recorder.ts:723`).
(`store.rollupRun(runId);`, `packages/ui-server/src/activity/recorder.ts:746`).
Retrospective totals alone cannot enforce admission.

The maintainer's [2026-10-01 ruling on #678](https://github.com/schlessera/brain-kit/issues/678#issuecomment-5926691626)
Expand Down
4 changes: 2 additions & 2 deletions docs/decisions/claude-code-runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ next to it.
of this binary. The only version probe in the server is for the `brain` CLI
(`Probe the selected content CLI`,
`packages/ui-server/src/brain/client.ts:142-210`, called at
`probeBrainCliVersion(config.brainPath`, `packages/ui-server/src/app.ts:434`).
`probeBrainCliVersion(config.brainPath`, `packages/ui-server/src/app.ts:433`).
`brain doctor` runs `claude mcp list` from `PATH` (`which("claude")`,
`packages/core/src/cli/commands/doctor.ts:501-505`) — the user's own Claude
Code on their own machine, to check the MCP registration, not the server's
Expand Down Expand Up @@ -212,7 +212,7 @@ unnecessary.
`node_modules/@anthropic-ai/claude-agent-sdk/sdk.mjs`. The boot probe below
turns either into a refusal to start, for the same reason a missing backend
refuses to boot (`A missing (or unrecognized) agent backend`,
`packages/ui-server/src/app.ts:372-377`).
`packages/ui-server/src/app.ts:371-376`).
- **The published range is still a caret, and that bounds what this repo can
guarantee.** `@schlessera/brain-backend-claude` depends on `^0.3.241`, and a
host resolves it in its own lockfile. A host can bump the SDK — and so the
Expand Down
35 changes: 20 additions & 15 deletions docs/decisions/governed-writable-workers.md
Original file line number Diff line number Diff line change
Expand Up @@ -1072,7 +1072,7 @@ principal authority remain the caller's responsibility. The policy publisher
creates, writes, fsyncs and removes its operation-owned staging name entirely
inside that synchronous callback, between the two serialized observations
(`await options.observeUserPolicy`,
`packages/ui-server/src/brain/policy-proposal.ts:197-224`). First publication
`packages/ui-server/src/brain/policy-proposal.ts:198-225`). First publication
also removes the temporary hardlink before the final observation. No filename
prefix is ignored; a similarly named real agent change is still retained.
Failed staging preserves the original policy, while failed recording after
Expand Down Expand Up @@ -1334,7 +1334,7 @@ queues the capture
(`function held<T>`, `packages/ui-server/src/brain/turn-journal.ts:191-210`),
and the scope monitor's hold freezes every active turn's productive workers
until the capture completes (`before() {`,
`packages/ui-server/src/brain/turn-scope.ts:134-142`). Pi's agent loop runs in
`packages/ui-server/src/brain/turn-scope.ts:158-166`). Pi's agent loop runs in
its worker, so a frozen worker cannot send its next provider request until the
resume. On the native fixture the gap between a result and the next request
tracked the hold, not the bridge call.
Expand Down Expand Up @@ -1443,7 +1443,7 @@ unmet rather than relaxed.

**Why.** Every governed turn queues an automatic index refresh after it
(`host.governedTurns?.refresh(host.workerLock)`,
`packages/ui-server/src/ws/run-session.ts:899`). The refresh usually finishes
`packages/ui-server/src/ws/run-session.ts:909`). The refresh usually finishes
while the next turn runs, so that turn's finish observation found a rewritten
`brain.db` and copied a whole new version of it into retained objects, inside
the observation's worker hold. #2831 measured 36 versions of a ~120 MB index in
Expand Down Expand Up @@ -1484,10 +1484,10 @@ timestamps only, with no review action. Preview reports `metadata_only` and
download has nothing to serve. History revert and Undo read that same
presentation and exclude the family as `metadata_only`, so a version retained
by an earlier release is not restored either
(`reason = "metadata_only"`, `packages/ui-server/src/brain/turn-revert.ts:43`).
(`reason = "metadata_only"`, `packages/ui-server/src/brain/turn-revert.ts:46`).
The operation's ordinary index refresh rebuilds it, so that exclusion alone
does not make the operation partial
(`const partial`, `packages/ui-server/src/brain/turn-revert.ts:145`). Existing
(`const partial`, `packages/ui-server/src/brain/turn-revert.ts:148`). Existing
retained history is not erased; ordinary bounded retention expires it.

**Measured.** `scripts/prove-native-host.ts --backend=pi --app-uid=1000
Expand Down Expand Up @@ -1531,7 +1531,7 @@ The brain's git history stays the durable record of its content.
- **The Activity recorder and its rollups** price those events under the
[cost-tracking rules](cost-tracking.md). An unknown price is never counted as
zero. The session total that counts unknown as zero
(`msg.costUsd ?? 0`, `packages/ui-server/src/ws/session-catalog.ts:364`)
(`msg.costUsd ?? 0`, `packages/ui-server/src/ws/session-catalog.ts:368`)
must not feed any budget.

**Enforcement:**
Expand Down Expand Up @@ -1724,7 +1724,7 @@ limits from the same policy inputs and states independent hard limits.

**What exists.** The audit trail is the Activity span record: tool name, the
full input up to its cap, output and approval decisions
(`"tool_input"`, `packages/ui-server/src/activity/recorder.ts:313`).
(`"tool_input"`, `packages/ui-server/src/activity/recorder.ts:334`).

**What changes:**

Expand Down Expand Up @@ -1805,7 +1805,7 @@ reports which applies as `governed.egress`. Making the relay work without
managed storage is follow-up work. Each connection is recorded as a governance event, and a recording
failure ends the turn. Without a governance recorder the policy still applies
and only the records are absent (`prepareTurnEgress`,
`packages/ui-server/src/brain/turn-egress.ts:59-135`). The note says egress is
`packages/ui-server/src/brain/turn-egress.ts:60-166`). The note says egress is
recorded only when the backend actually routed the worker through the proxy.
API-key Claude profiles (2026-10-22) and pi turns (2026-10-23) are routed
too. A pi turn whose credential the relay cannot carry is refused rather than
Expand Down Expand Up @@ -2496,7 +2496,7 @@ every turn without a proven recipient:
provenance shows every item and checkpoint of its origin thread compatible
with those masks, checked once the masks are exact and before any thread
text is read into the prompt (`admission = await admitOriginThread`,
`packages/ui-server/src/inbox/autonomous-turn.ts:373`). The dispatcher's
`packages/ui-server/src/inbox/autonomous-turn.ts:377`). The dispatcher's
context is built afterwards, from the admission's arrival read and in the
transaction that revalidates it, and both reads revalidate in their own
transaction before building any payload (`function authorizeRead`,
Expand Down Expand Up @@ -3310,7 +3310,7 @@ outward host skips the card only while every classified call in progress is a
visible read, or an approved outward action that names that host. A GitHub
write names the GitHub API hosts, and a push or HTTP write names its target's
host. Any unknown or opaque call in progress withholds the exception
(`createOutwardCalls`, `packages/ui-server/src/brain/turn-egress.ts:35-57`).
(`createOutwardCalls`, `packages/ui-server/src/brain/turn-egress.ts:36-58`).
A background process left by an earlier call can still connect inside such a
window; the proxy records it either way.

Expand All @@ -3324,7 +3324,7 @@ listen on other ports: `gmail.googleapis.com`, `graph.microsoft.com`,
`smtp.sendgrid.net`, `api.mailgun.net`, `api.eu.mailgun.net`,
`smtp.mailgun.org`, `smtp.eu.mailgun.org`, `api.postmarkapp.com`,
`smtp.postmarkapp.com`, `api.resend.com` and `smtp.resend.com`
(`MAIL_SUBMISSION_PORTS`, `packages/ui-server/src/brain/turn-egress.ts:16-19`).
(`MAIL_SUBMISSION_PORTS`, `packages/ui-server/src/brain/turn-egress.ts:17-20`).
Microsoft Graph and Exchange Online carry more than mail, so their reads by
an opaque script also wait on the card. The coordinator chose this list for
#1665; the maintainer may override it.
Expand Down Expand Up @@ -3374,7 +3374,12 @@ server", following the rule above that erring towards taint costs only tighter
rules (maintainer ruling, 2026-10-22). The first source is the session's one
`taint` event. A write that fails stops the turn rather than leaving its
outward rules loose. The check and write share a SQLite transaction
(`recordFirstTaint`, `packages/ui-server/src/governance/session-taint.ts:17-22`).
(`recordFirstTaint`, `packages/ui-server/src/governance/session-taint.ts:17-23`).
The hosted server runs that transaction on its write worker
(`insertFirstTaint`, `packages/ui-server/src/governance/session-taint.ts:29-35`;
[#2850](https://github.com/schlessera/brain-kit/issues/2850)). A taint counts
for admission from the moment it is submitted, and a connection's first byte
waits for its taint's commit.

The admitting host hands both backends a live read of that record with the
turn's command policy. While it reports a taint, no `outward.allow` rule
Expand All @@ -3395,7 +3400,7 @@ session's proxies already admitted (a record with charged bytes; the proxy
charges only after admission); otherwise it is refused and recorded as an
`egress_denial` with reason `taint_allowlist`. The taint list narrows and never
widens: deny and `allowlist` mode still apply first (`prepareTurnEgress`,
`packages/ui-server/src/brain/turn-egress.ts:59-135`). Hosts without managed
`packages/ui-server/src/brain/turn-egress.ts:60-166`). Hosts without managed
native storage have no proxy, so the Bash classifier remains their shell rule
and nothing narrows their egress.

Expand Down Expand Up @@ -3543,7 +3548,7 @@ providers or a claim of capturing intervening external writes. The copy-out
boundary is `const write`, `packages/ui-backend-claude/src/worker-state.ts:210-224`;
retention/finalization is `persistClaudeMemory`,
`packages/ui-server/src/brain/turn-journal.ts:524-554`; destination-bound recovery
is `const target`, `packages/ui-server/src/brain/turn-revert.ts:110-120`.
is `const target`, `packages/ui-server/src/brain/turn-revert.ts:113-123`.

### Concurrent edits

Expand Down Expand Up @@ -3576,7 +3581,7 @@ exclusive "mine" or "theirs" ownership. A conflict requires two distinct
observed after-versions sharing an overlapping turn; one version remains shared
attribution. User choices use the existing authenticated history operation,
comparing current raw state with the reviewed latest observation before effects
(`resolveSelection`, `packages/ui-server/src/brain/turn-revert.ts:52-75`).
(`resolveSelection`, `packages/ui-server/src/brain/turn-revert.ts:55-78`).
Keep both creates the other version beside the selected file with sync's
numbered `-remote` convention, using exclusive descriptor-rooted creation,
existing CAS leases and explicit coverage for its final generated name
Expand Down
4 changes: 2 additions & 2 deletions docs/decisions/managed-worktrees.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ unknown; what happens next is in [hygiene and cleanup](#hygiene-and-cleanup).
Each governed or strict turn whose journal is recording opens one worker
generation, and every worker of that turn binds the generation's root-owned
directory whole at `/tmp/brain-worktrees`, inside the turn's own `/tmp`
(`// Managed worktrees (#2430): a journaled turn's workers bind one empty generation.`, `packages/ui-server/src/ws/run-session.ts:565-577`;
(`// Managed worktrees (#2430): a journaled turn's workers bind one empty generation.`, `packages/ui-server/src/ws/run-session.ts:566-578`;
`// Managed worktrees (#2430): the whole access generation`, `packages/ui-sdk/src/server/worker-launcher.ts:848-859`).
A granted worktree appears there as `/tmp/brain-worktrees/<worktree id>/` on the
worker's next path lookup; the worker cannot create entries beside it, and a
Expand Down Expand Up @@ -199,7 +199,7 @@ The host side is `createReferenceTools`
start, the host records its principal and what those workers bind: the public
view, the principal's private view and the turn's worker generation, if one
opened
(`// Reference tools (#2432): the turn's workers bind the principal's source`, `packages/ui-server/src/ws/run-session.ts:578-584`).
(`// Reference tools (#2432): the turn's workers bind the principal's source`, `packages/ui-server/src/ws/run-session.ts:579-585`).
A call carries only a repository, a revision or a worktree identity. The bridge
adds the turn's own principal, and a call for a turn the host did not prepare,
or under another principal, refuses
Expand Down
4 changes: 2 additions & 2 deletions docs/decisions/scheduled-tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ Source inspected at `b5c30c180df5584584f261b446c4d440380d2441`:
(`else if (item.status === "scheduled"`, `packages/ui-server/src/inbox/actions.ts:196-197`).
The runtime ticks every minute (`INBOX_TICK_MS`, `packages/ui-server/src/inbox/runtime.ts:11-13`).
- Production app wiring supplies recovery and budgets, without a dispatcher
(`// Recovery/heartbeat only.`, `packages/ui-server/src/app.ts:491-507`).
(`// Recovery/heartbeat only.`, `packages/ui-server/src/app.ts:490-506`).
A scripted nonempty ready-item test still reports no claim, attempt or
Activity root. That proves disabled wiring, not safe execution of a schedule.
- Headless plumbing checks backend support, a usable principal, lifetime and
reservation ownership (`runAutonomousTurn`, `packages/ui-server/src/inbox/autonomous-turn.ts:111-148`).
reservation ownership (`runAutonomousTurn`, `packages/ui-server/src/inbox/autonomous-turn.ts:112-149`).
Capability flags and these checks do not prove credential/configuration,
filesystem or egress containment at the actual backend boundary.
- The generic core runner has a different interface
Expand Down
4 changes: 2 additions & 2 deletions docs/decisions/session-principals.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ Both reviewers verified every row below against the source.
| WS upgrade | `ws/connection.ts:262-264` | `createWsUpgrade` ignores the request context |
| WS admission | `ws/connection.ts:84` | `clients.add(ws)` — the socket's identity is unknown |
| Turn record | `ws/run-session.ts:136-144` | recorder built from `{turnId, sessionId, billing}`; `RunningTurn` (`ws/turns.ts:55-77`) has no actor |
| Follow-up queue | `QueuedFollowUp`, `ws/turns.ts:61-103`; `slot.queue.push(entry)`, `run-session.ts:1090` | queued entries re-mint `turnId` in the same slot and may come from another socket |
| Follow-up queue | `QueuedFollowUp`, `ws/turns.ts:61-103`; `slot.queue.push(entry)`, `run-session.ts:1100` | queued entries re-mint `turnId` in the same slot and may come from another socket |
| Password login | `acquirePasswordVerification(key)`, `auth.ts:749` | argon2id verify, failure counting, in-flight reservation, then `issueSessionCookie` |
| Passkey login | `issueLoginSession`, `packages/ui-server/src/middleware/passkeys.ts:448` | assertion verified, then `issueSessionCookie`; `row.id` is in scope |
| Passkey **registration** | `INSERT INTO passkey_credentials`, `passkeys.ts:547` | inserts a credential; calls **neither** helper |
Expand Down Expand Up @@ -204,7 +204,7 @@ credentials, not people.
(`upsertRollup`, `activity/sql.ts:19-24`) — the rollup is what survives span
pruning, so cost-by-actor dies at prune time without it. The precedent is
profile/billing: a root-span attr that the rollup reads (`spanId: rootSpanId`,
`activity/recorder.ts:222-237`). Nullable for cron (`origin: "cron"`,
`activity/recorder.ts:243-258`). Nullable for cron (`origin: "cron"`,
`activity/span-sink.ts:102`, `cron/run-job.ts:113`) and for pre-migration rows;
nothing is backfilled.

Expand Down
27 changes: 22 additions & 5 deletions docs/decisions/sqlite-write-worker.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,28 @@ thread waits for it.
snapshot as the app's pricing instance and reloads the table whenever the
app's refresh replaces it. A refresh whose cache write failed leaves the
worker on the previous table until the next successful write.
- **Other app-connection writers stay on the loop**: governance events written
outside a journal transaction, the writer gate, turn budgets and scope, the
session catalog, inbox and settings. Each commits at FULL on the loop, and
while a worker procedure holds its turn, such a write also waits there for
the worker's commit to finish. Those writers are tracked separately
- **Governance events and turn scope moved next**
([#2850](https://github.com/schlessera/brain-kit/issues/2850)). Every
governance event raised outside a journal transaction, the session's first
taint, roadblock classification and turn scope rows commit on the same
worker (`packages/ui-server/src/governance/event-writes.ts`,
`packages/ui-server/src/brain/turn-scope-writes.ts`). An event is validated
and its clock taken where it is raised. Three reads that followed those
writes now wait for them: a connection's first byte waits for its taint's
commit, while admission counts a taint from submission; a scope decision
runs only after the turn's earlier scope writes commit, and settles after
its own; and a turn's outcome waits for its egress records. The scope
summary the client hears of is sent once its row commits. A process without
a write worker, such as the cron runner, runs the same procedures on its own
connection.
- **Other app-connection writers stay on the loop for now**: the writer gate,
turn budgets, the session catalog and message sources, the Activity
notifier and push ticks, auth sessions, inbox and settings. Each commits at
FULL on the loop, and while a worker procedure holds its turn, such a write
also waits there for the worker's commit to finish, including a journal
procedure that writes only `journal.db`, because the ledger's
`BEGIN IMMEDIATE` locks every attached file. Moving each of them to the
worker queue ends that wait too
([#2850](https://github.com/schlessera/brain-kit/issues/2850)).

## Rejected
Expand Down
Loading
Loading