A max-tokens response that included a tool call persisted assembler-transformed
content next to replay metadata projected from the untransformed native message,
so the next request died in history reconstruction with INVALID_REPLAY_STATE and
the session stayed permanently stuck.
Write side: the finish chunk's replayState becomes a typed ReplayEnvelope —
opaque response-level metadata plus optional per-block entries aligned with the
emitted block sequence. BlockAssembler computes one keep/drop decision for
blocks and entries together, so stored metadata always describes stored content
and retained blocks keep their signatures. pi-ai splits its state into a
version-2 response half and per-block signature entries.
Read side: durable content is authoritative. toPiAssistant degrades any
unusable state — foreign kind, other versions (including the flat v1 form
already on disk), malformed metadata, or content/block mismatches — to the
existing provider-neutral conversion with an onReplayDegrade diagnostic instead
of failing the request, which un-bricks sessions poisoned before this change.
Covered by assembler and replay unit tests, an agent-loop continuation
regression, keyless real-composition continuation tests (native pruned-envelope
replay and legacy flat-state degrade), and the authored keyless snapshot
scenario max-tokens-continue through the assembled ACP app.
tool-bash-persistent overwrote the backend's PS1, so terminal-bash prompt
readiness never matched and every send degraded to the 3.5s silence tier
(idleSilenceMs + handoffGraceMs) under production defaults.
The controlled PROMPT_COMMAND now re-asserts PS1 before every prompt, so an
in-shell override never survives to the next prompt. The tool initializes
with stty -echo alone and detects the no-end-marker fallback through the
seam's stdin_read wait reason instead of matching its own prompt text.
Tool calls drop from 7180/3560/3566 ms to 355/88/91 ms (spawn+init+echo,
echo, pwd; darwin, production defaults). The loader composition suite now
pins the fast path by pushing idleSilenceMs beyond the send bound, and a
real-PTY case proves PS1 self-healing.
Fixes#2585
The shipped bundles keep ctx.sessionQuery mounted but set the new
session-query-sqlite `openAt: never` phase: searchSessions/searchEvents
fail with the typed SESSION_QUERY_SEARCH_DISABLED code before any request
normalization, node:sqlite is never imported or opened, and no source
observation or reconciliation runs. Every inherited exact read, filter,
and trace — session export descendants, subagent-fork Workspace
inheritance, title reads — keeps working, and the Web sidebar search
degrades to its designed local title/workspace matching. Enabling content
search is a one-line openAt override in a later patch layer; the web e2e
scaffold keeps it enabled as the assembled opt-in coverage.
The transport schema requires both `code` and `description`, but the tool
description and both SDK instruction flavors described the call as passing a
program. `description` was reachable only through the parameter schema, so a
model following the prose emitted `{code}` alone and lost the whole written
program to an INVALID_ARGS rejection.
The length and format guidance stays in RUN_CODE_DESCRIPTION_PARAM_DESCRIPTION
alone, so the schema and the prompt cannot drift.
Fixes#2426
A plugin that registered a settings namespace could not reach the browser
configuration page: the api-proxy filtered every read and gated every
write through two hardcoded namespace lists, and the plugin configuration
section rendered an unordered list of cards carrying an opaque id rather
than the namespace they edit. Both gates lived in this repository, so a
user-authored plugin was configurable only by hand-editing settings.yaml.
The proxy now serves whatever ctx.settings.describe() returns and adds no
boundary of its own; a name no registration answers folds into the seam's
own settings-rejected, and the settings-not-exposed code retires. The
settings seam is untouched: which client may read a namespace, and which
page renders it, are facts about consumers.
settings.plugin.item becomes a keyed slot whose key is the namespace a
card edits, following tool.call.toolview. The section reads describe once
and dispatches the intersection of the slot ledger and the served set, so
a namespace another surface owns renders nothing without declaring
anything, and a card for an uncomposed plugin is never dispatched.
turn/start precedes the first claim and pre-step, a rejected or empty first claim closes a zero-step turn, llm/stream and tools/* are their own domains, and agent/turn-stopping is serial. Regenerates the lifecycle diagram from the same facts and names --patch overlays exactly.
Correct the reworked English wording in the CLI profile section, architecture map, and llm-streaming cross-reference, then bring every edited pair's Chinese side along and re-record the pairing hashes.