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
9 changes: 9 additions & 0 deletions .changeset/stable-session-prompt-context.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@schlessera/brain-backend-claude": patch
"@schlessera/brain-backend-pi": patch
"@schlessera/brain-ui-sdk": patch
"@schlessera/brain-ui-react": patch
"@schlessera/brain-ui-server": patch
---

Retain session-stable prompt appends and deliver changed host facts in owned new tail context, preserving user history and unknown device capabilities.
3 changes: 3 additions & 0 deletions api-report/ui-sdk.txt
Original file line number Diff line number Diff line change
Expand Up @@ -889,6 +889,7 @@ export "./internal" (./src/internal.ts)
type RestrictedEnvelope
STALE_NATIVE_WRITE
SUBPROCESS_ENV
type SessionPromptFacts
type StateLedger
TURN_TEMP_BUDGET_ENV
TURN_TEMP_BYTES_ENV
Expand Down Expand Up @@ -961,6 +962,7 @@ export "./internal" (./src/internal.ts)
createWorkerFileObserver
createWorkerStartupView
denyWorkerCommand
describeClient
describeLimitHits
execWrapperSpawnOptions
handleTurnTemp
Expand Down Expand Up @@ -1014,6 +1016,7 @@ export "./internal" (./src/internal.ts)
retainWorkerStartupView
runMaintenanceWorker
runMaintenanceWorkerSync
sessionPromptContext
startInferenceRelay
turnTempFullMessage
turnTempLimit
Expand Down
1 change: 1 addition & 0 deletions docs/decisions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ somebody already learn the hard way?*
| [experiment-tier.md](experiment-tier.md) | Why measurement-harness tests live under `experiments/`, outside product discovery, and run as a receipt-checked gate before each release, with the file- and assertion-level inventory behind #1386. |
| [runtime-requirements.md](runtime-requirements.md) | Why package ranges and package/host runtime minimums compose, which loaded/executed identities are checked, how unknown versions and overrides behave, and why compatibility stays separate from measurement. |
| [codex-subscription-auth.md](codex-subscription-auth.md) | Why pi and the Codex CLI use separate ChatGPT OAuth grants, the source-based no-sharing ruling, and the observed separate-login limits. |
| [session-prompt-context.md](session-prompt-context.md) | Why the exact session append is retained, changed facts use owned new-tail context, and replay preserves the actual user text. |
| [pi-endpoint-routing.md](pi-endpoint-routing.md) | Why declared built-in pi models use the configured inference runtime, native per-request authentication endpoints take precedence, and the routing migration requires real adapter destination proof. |
| [reranker-activation.md](reranker-activation.md) | Why model judgment requires persistent opt-in across search entry points, why credentials and modes cannot enable it, and why the measured Jev provider and seam remain available. |
| [gemini-agent-contract.md](gemini-agent-contract.md) | Why Gemini embeds the single installed contract, how migration preserves user text and refuses ambiguous markers, and why sync leaves exact root/nested instruction conflicts for judgment. |
Expand Down
2 changes: 1 addition & 1 deletion docs/decisions/action-notifications.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ but add the Actions-aware aggregate and durable constituent/destination records.
This decision authorizes no new channel, extension seam or daemon.

The client already reads a zone for client environment
(`const zone`, `packages/ui-react/src/lib/client-environment.ts:107-108`).
(`const zone`, `packages/ui-react/src/lib/client-environment.ts:109-110`).
Push rebind sends the subscription and label
(`rebindPushSubscriptionAfterLogin`, `packages/ui-react/src/lib/push-registration.ts:62-76`);
the server subscribe path does not persist that client's delivery zone
Expand Down
4 changes: 2 additions & 2 deletions docs/decisions/async-collaboration.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,11 @@ requires one enforcement mechanism with different named memberships for voice
and unattended work. Keep that requirement when implementing availability
control. Existing mandatory backend conformance supports safe rejection of an
unsupported restricted request; silently ignoring it is forbidden. The current
optional inputs are (`enforceAllowedTools?: boolean`, `packages/ui-sdk/src/server/backend.ts:535`) and (`noGrantSurface?: boolean`, `packages/ui-sdk/src/server/backend.ts:559`). These are permission primitives,
optional inputs are (`enforceAllowedTools?: boolean`, `packages/ui-sdk/src/server/backend.ts:539`) and (`noGrantSurface?: boolean`, `packages/ui-sdk/src/server/backend.ts:563`). These are permission primitives,
not a claim of filesystem or network containment.

The ordinary Claude assembly loads project settings and appends bridge tools
(`createClaudeSdkTurn`, `packages/ui-backend-claude/src/sdk-options.ts:85-263`).
(`createClaudeSdkTurn`, `packages/ui-backend-claude/src/sdk-options.ts:88-399`).
Its environment is already filtered (`envSnapshot`,
`packages/ui-backend-claude/src/config/env.ts:182-190`), with profile credentials
and operator extras. An autonomous envelope needs its own narrower credential
Expand Down
2 changes: 1 addition & 1 deletion docs/decisions/claude-2.1.287-compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ The property under test is: with `enforceAllowedTools: true`, a `Bash` call
outside the turn's roster cannot execute unless the host bridge grants that
call, including when settings or a rewrite could otherwise auto-approve it.
The backend builds the actual turn options and production permission handlers
(`createClaudeSdkTurn`, `packages/ui-backend-claude/src/sdk-options.ts:85-202`).
(`createClaudeSdkTurn`, `packages/ui-backend-claude/src/sdk-options.ts:88-399`).
The explicit ask lives in
(`enforcementHook`, `packages/ui-backend-claude/src/permission-hooks.ts:183-212`)
and is registered under enforcement in
Expand Down
10 changes: 5 additions & 5 deletions docs/decisions/claude-code-runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ next to it.
`packages/ui-server/src/agent/backend.ts:497`), read back as a string
(`const claudeCodePath`, `packages/ui-backend-claude/src/module.ts:266-270`)
and handed to the SDK (`backend.claudeCodePath`,
`packages/ui-backend-claude/src/sdk-options.ts:262`). Because of the `||`
`packages/ui-backend-claude/src/sdk-options.ts:285`). Because of the `||`
default the value was never empty, so **the server always overrode the SDK's
own binary**. The variable is withheld from every subprocess
(`CLAUDE_CODE_PATH: NONE`,
Expand Down Expand Up @@ -388,7 +388,7 @@ until re-measured.
| `The runtime precedence modelled by`, `packages/ui-backend-claude/tests/allowlist-enforcement.test.ts:11-20` | The precedence `runToolCall` models, (a)–(e). The test cannot re-measure it. | The cases above plus the composition case. A changed result changes the model in the test in the same PR. |
| `the one answer that beats`, `packages/ui-backend-claude/tests/no-grant-surface.test.ts:13-18` | The same three opinions, and that `ask` is what forces the decision. | Same probe cases. |
| `That third vector is stated here`, `docs/decisions/voice-permission.md:645-671` | An in-process `deny` beats a project-settings `allow`. | Probe case: settings `allow` against in-process `deny`, with the positive control of the settings hook alone running the tool. |
| `Three measured examples from the Claude SDK`, `docs/extending/agent-backends.md:291-304` | The same three mechanisms and the `ask`, restated for backend authors with no version attached. | Updated in the same PR as the constant whenever a probe result changes. |
| `Three measured examples from the Claude SDK`, `docs/extending/agent-backends.md:294-307` | The same three mechanisms and the `ask`, restated for backend authors with no version attached. | Updated in the same PR as the constant whenever a probe result changes. |
| `createSdkMcpServer({ alwaysLoad: true })`, `docs/decisions/design-kit/tool-loading.md:21-44` (D44) | Two different kinds of claim. That `createSdkMcpServer({ alwaysLoad })` stamps `_meta["anthropic/alwaysLoad"]` is SDK behaviour, asserted keylessly by `"anthropic/alwaysLoad"`, `packages/ui-backend-claude/tests/sdk-options-mcp.test.ts:82` and `"anthropic/alwaysLoad"`, `tests/bridge-tools.test.ts:816,834`. That the CLI honours the stamp, and that first-frame latency did not move, is CLI behaviour. | The SDK half by the existing tests. The CLI half needs a live run of both arms — stamp set and unset — on the new pair, recording the pair from `init` and observing whether the bridge tools reached the model undeferred. `scripts/measure-show-block.ts` can run either arm (with and without `--always-load`; `ALWAYS_LOAD`, `scripts/measure-show-block.ts:304,369`), but it records no version and nothing in it compares the two arms or checks deferral, so it does not re-check this as it stands. Extending it is part of #209. `--tokens` prices schemas through the API and never runs the CLI, so it re-checks nothing here. |

> **2026-09-30 — Implementation context (D44's harness gap in the sites table).**
Expand Down Expand Up @@ -453,9 +453,9 @@ into them.
- **The default profile passes both through.** The built-in `claude` profile
declares no credential (`DEFAULT_PROFILES`, `packages/ui-backend-claude/src/profiles.ts:139-141`).
A turn's environment is the filtered agent environment plus the profile's
additions (`export function turnEnv`, `packages/ui-backend-claude/src/sdk-options.ts:65-71`,
additions (`export function turnEnv`, `packages/ui-backend-claude/src/sdk-options.ts:68-74`,
`envSnapshot`, `packages/ui-backend-claude/src/config/env.ts:182-190`), handed to the SDK
whole (`sdkOptions.env = childEnv`, `sdk-options.ts:332`). A declared bearer-token profile clears both
whole (`sdkOptions.env = childEnv`, `sdk-options.ts:356`). A declared bearer-token profile clears both
ambient credentials (`input.authTokenEnv !== undefined`, `profiles.ts:116-122`); a declared API-key profile sets
the key on purpose (`input.apiKeyEnv !== undefined`, `profiles.ts:123-125`).
- **Billing originally relied on ambient credential classification.** The
Expand Down Expand Up @@ -603,7 +603,7 @@ subscription authenticates.
environment is necessary, whether or not a subscription credential is
present. It is not sufficient: the CLI also takes an API key from an
`apiKeyHelper` in settings — the backend loads the brain repo's project
settings (`settingSources: ["project"]`, `sdk-options.ts:210`) — and from a stored Console login, reported
settings (`settingSources: ["project"]`, `sdk-options.ts:232`) — and from a stored Console login, reported
as `/login managed key` (`sdk.d.ts:5585`). So the turn has to check which
credential the CLI selected **before the prompt is sent**, and end the turn
if it is not a subscription. The check reads the account from the SDK's
Expand Down
2 changes: 1 addition & 1 deletion docs/decisions/design-kit/answer-blocks.md
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,7 @@ turns the complete roster the model reached for was `bash`, `show_block`,
`brain_read`, `grep`, `brain_search`, `read_file`, `brain_list`, `brain_graph`:
no search-then-load round trip, ever. The brief is in the prompt on every turn
unconditionally (`block: "show_block"`,
`packages/ui-backend-pi/src/session-resources.ts:193`, not behind a capability
`packages/ui-backend-pi/src/session-resources.ts:200`, not behind a capability
check like the four bridge tools beside it). So pi is the structural twin of
D43's `--always-load` arm and has never run any other configuration.

Expand Down
2 changes: 1 addition & 1 deletion docs/decisions/design-kit/backend-measurements.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ and each closed before the measured runs:

- **The brain has to live outside the operator's home directory, not only
outside a checkout.** `settingSources: ["project"]`,
`packages/ui-backend-claude/src/sdk-options.ts:210`, makes the CLI walk up
`packages/ui-backend-claude/src/sdk-options.ts:232`, makes the CLI walk up
from the cwd, and at every ancestor it reads `.claude/CLAUDE.md`,
`.claude/skills/` and `.claude/agents/`. A brain anywhere under a home
directory therefore loads `~/.claude/CLAUDE.md` as *project* instructions,
Expand Down
2 changes: 1 addition & 1 deletion docs/decisions/design-kit/show-block-brief.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ is in the prompt on every pi turn by construction. That makes pi's shipping
configuration the structural equivalent of this record's `--always-load`
**brief** arm — and only that one. pi has no no-brief arm and no supported way
to have one: `block: "show_block"`,
`packages/ui-backend-pi/src/session-resources.ts:193` passes the
`packages/ui-backend-pi/src/session-resources.ts:200` passes the
block brief unconditionally, where the four lines above it gate their briefs on
a capability. So pi can corroborate the loaded *rate* and can say nothing at
all about whether the brief matters; the 77%/77% here and #148's 76%/77% are
Expand Down
2 changes: 1 addition & 1 deletion docs/decisions/design-kit/tool-loading.md
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ Two things stop that residue being read as a like-for-like gap, and both cut
against reading pi as a second replication of the brief result. pi is on a
different harness driving the model directly, and — this is the one that matters
— **pi has no no-brief arm and no supported way to have one.**
`block: "show_block"`, `packages/ui-backend-pi/src/session-resources.ts:193`
`block: "show_block"`, `packages/ui-backend-pi/src/session-resources.ts:200`
passes the block brief into `buildSystemPromptAppend` unconditionally, not
behind a capability check like `askUser`, `location`, `activity` and `mask` on
the lines above it. So every pi number was measured with the brief present. The
Expand Down
2 changes: 1 addition & 1 deletion docs/decisions/governed-writable-workers.md
Original file line number Diff line number Diff line change
Expand Up @@ -958,7 +958,7 @@ classification:
`packages/ui-backend-claude/src/tool-policy.ts:246`). The parent process is
shared across sessions, so the lock is too.
- Pi takes its lock inside the worker (`brain: createBrainAccess(init.backend.brainPath), lock: init.sharedLock ?`,
`packages/ui-backend-pi/src/worker-entry.ts:115`). Under `governed`, pi's tool
`packages/ui-backend-pi/src/worker-entry.ts:117`). Under `governed`, pi's tool
lock must instead be the server's keyed lock, acquired over the worker RPC.
Otherwise two pi sessions can interleave `git add` and `git commit`.

Expand Down
2 changes: 1 addition & 1 deletion docs/decisions/live-conversation.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ tool admission and execution, permissions, cancellation, terminal state and
resynchronization. Engine task requests are advisory until mapped to committed
user input. No native search, code-execution or alternate permission loop is
enabled. Both adapters can delegate through the existing `AgentBackend`
(`export interface AgentBackend`, `packages/ui-sdk/src/server/backend.ts:640-670`);
(`export interface AgentBackend`, `packages/ui-sdk/src/server/backend.ts:644-674`);
the host serializes work in a session and uses capability-checked follow-ups.

The [voice-permission decision](voice-permission.md) binds this interface.
Expand Down
2 changes: 1 addition & 1 deletion docs/decisions/pi-endpoint-routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ The concrete adapter integration points are:
| Declared-model validation (`export function toModel`, `packages/ui-backend-pi/src/profiles.ts:57-77`) | Keep built-in validation and identity refusal, then select the configured model from the inference runtime. The current function returns the raw catalog object. |
| Ordinary/autonomous new sessions (`async newSession`, `packages/ui-backend-pi/src/native-session-runtime.ts:41-72`) | Supply the native configured runtime and its selected model to the real `createAgentSession` call. The worker uses `SessionManager.inMemory`; the server persists ordinary JSONL over pipes, and autonomous turns remain nonpersistent. |
| Ordinary persisted resumes (`async openSession`, `packages/ui-backend-pi/src/native-session-runtime.ts:74-109`) | Supply the configured runtime without a new model override; reject/dispose if `modelFallbackMessage` is present. |
| Native resources (`const agentDir = getAgentDir()`, `packages/ui-backend-pi/src/session-resources.ts:86-120`) | Use the same native agent-directory discovery as settings/resources. Preserve loader failure behavior and its inline permission gate. |
| Native resources (`const agentDir = getAgentDir()`, `packages/ui-backend-pi/src/session-resources.ts:89-128`) | Use the same native agent-directory discovery as settings/resources. Preserve loader failure behavior and its inline permission gate. |

Runtime construction belongs at these concrete session boundaries. No runtime
factory option, new public seam or authentication wrapper is needed. Each hosted turn reconstructs its native inference runtime inside a fresh
Expand Down
2 changes: 1 addition & 1 deletion docs/decisions/policy-write-boundary.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ application, refusal and recovery.

This is a change to the execution architecture. Current Claude assembly creates
SDK options and in-process bridge tools (`createClaudeSdkTurn`,
`packages/ui-backend-claude/src/sdk-options.ts:85-261`); pi builds its worker-local resource
`packages/ui-backend-claude/src/sdk-options.ts:88-399`); pi builds its worker-local resource
loader and calls `reload()` (`createSessionResources`,
`packages/ui-backend-pi/src/session-resources.ts:29-113`). A child-only wrapper
cannot be treated as proof for those parent-side paths. The investigation records
Expand Down
2 changes: 1 addition & 1 deletion 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:134-142` | 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:896` | 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:899` | queued entries re-mint `turnId` in the same slot and may come from another socket |
| Password login | `acquirePasswordVerification(key)`, `auth.ts:740` | argon2id verify, failure counting, in-flight reservation, then `issueSessionCookie` |
| Passkey login | `issueLoginSession`, `packages/ui-server/src/middleware/passkeys.ts:442` | assertion verified, then `issueSessionCookie`; `row.id` is in scope |
| Passkey **registration** | `INSERT INTO passkey_credentials`, `passkeys.ts:540` | inserts a credential; calls **neither** helper |
Expand Down
44 changes: 44 additions & 0 deletions docs/decisions/session-prompt-context.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Session-stable appends and owned new-turn context

The maintainer selected option B on [#2047](https://github.com/schlessera/brain-kit/issues/2047#issuecomment-6212451069)
on 2026-10-16, within the provider-history work tracked by
[#2053](https://github.com/schlessera/brain-kit/issues/2053).

A conversation retains one exact system append in internal session storage.
The model receives relevant changed facts only in newly appended user content
or equivalent new-turn hook context. Earlier provider messages, tool results,
tool arrays and the stored append remain unchanged. Authoritative host state,
change tracking and replay provenance remain internal. This is the selected
exception to the parent epic's internal-only wording, not an exception to
append-only provider history.

The fixed append carries static configured inference warn/stop limits.
Relevant device/capability, sandbox/profile/protected-path, writable-area,
journal/revert and actionable threshold changes belong in new context. Routine
running totals do not. Unknown device probes never mean absent hardware.
Enforcement remains in worker boundaries and permission/budget checks; a
prompt paragraph grants no authority.

Replay uses structured internal message ownership to present the actual
user's text. It never strips substrings or host-looking patterns. Literal
user-authored lookalikes remain intact. Delivery tracking must survive resumed
workers and retry after a send that never reached saved history.

Keeping every changed fact internal was rejected because the model would
retain stale device and operating facts until a refusal. Updating only
operational constraints was rejected because device and reading-layout facts
would remain stale. Rebuilding the system append was rejected because it
either changes the provider's cached prefix or is ignored by native system
snapshot recording. The selected new-tail addition costs context tokens on
later requests while preserving the existing prefix.

The provider-history guard's existing scope excludes native `cache_control`
breakpoint movement from message-content comparison: changing cache metadata
does not edit prior content. Other provider content remains compared exactly.
Offline fake-origin request-byte proof does not establish first-party provider
cache hits, savings or current account-specific snapshot availability.

[Session prompt context](../hosting/session-prompt-context.md) describes
first-turn facts, changes, unknowns, retries, persistence and the independent
Claude snapshot-off control. The broader provider-history record and its
cross-cause guards remain the scope of #2053.
Loading
Loading