The artifact tool takes inline: true to also show the produced version below
its transcript card: Markdown/text rendered, HTML/SVG in the sandboxed preview
frame. Drawings fill the frame's width (an SVG with a viewBox or an absolute
size scales to it; an HTML page that is only an SVG or image too) and the frame
takes the content height the preview reports, capped at 70vh. Every viewer sees
it, guests included (owner decision). The TUI renders inline Markdown and text.
SVG previews now carry the diagnostics bridge in CDATA (its && broke the XML).
Several places truncate text by UTF-16 code units with `slice()`. When
the cut lands between the two halves of a surrogate pair (an emoji or
other character outside the Basic Multilingual Plane), the result keeps
a lone high or low surrogate. That is invalid UTF-16.
Example: a grep match line with 499 ASCII characters followed by `😀` is
cut at 500 units, so the output ends in `\uD83D... [truncated]`.
The bundled providers strip lone surrogates before sending
(`sanitizeSurrogates` in pi-ai), so the usual effect is a dropped
character. But the broken text still reaches the session file, RPC and
JSON clients (strict UTF-8 encoders such as Rust's serde_json reject
lone surrogates), extensions, and custom providers.
This adds `sliceHead(text, n)` and `sliceTail(text, n)` to
`core/tools/truncate.ts`. They behave like `slice(0, n)` and `slice(-n)`
but return one unit fewer when the cut would split a pair, and are used
where truncated text reaches the model:
- `truncateLine` (grep match lines), the case reported in #7121
- codemode `truncateOutput` head and tail, and its argument and error
previews
- compaction `truncateForSummary` (tool results sent to the summarizer)
- MCP server summaries in the system prompt and stderr tails in
connection errors
The cost is two `charCodeAt` checks per truncation, and nothing on the
untruncated path.
The surrogate check follows the fix in #7122 by @IKEASven69, which
@cristinaponcela reviewed in a follow-up branch. That PR was closed
before merging.
Not changed: TUI-only previews (renderers, visual truncation) and
line-based slicing. pi-private's pi-durable codemode `output.ts` has the
same head/tail slicing and needs a separate change there.
**Testing**
- New `test/truncate-surrogates.test.ts`: an emoji at the cut in
`truncateLine` is dropped whole, an emoji ending exactly at the limit is
kept, `sliceHead`/`sliceTail` never leave a lone surrogate for any
length, and they match `slice` on text without surrogates.
- `test/tools.test.ts`, compaction, MCP extension, and codemode tests
pass.
- `npm run check` passes.
fixes#7121
This PR is AI-generated.
Commits queued behind a running commit step share the next step's backend
transaction: each keeps its own step, seq, outbox commit and adoption in
queue order, the metadata is written once. A refusal rolls the group back
and its admitted commits run again alone; series, exports, replays and empty
commits end a group; backend failures while closing are latched; any throw
after COMMIT stops the store with memory at the durable group.
SqliteStorage.commit(batches) reads the check state of all batches up front,
checks each batch in memory against what the earlier ones left (same
prefix-stop refusals, no savepoints), and writes the accepted batches per
table: small rows as one JSON array parameter (json_each), rows with much
text bound column by column within 100 parameters. A group commit over a
remote SQLite takes a few round trips instead of about eight per batch;
local groups are faster too. Randomized differential test against
MemoryStorage, raw-table and statement-count guards.
HTTP servers without an Authorization header sign in with OAuth: stored
tokens in mcp-auth.json, refreshed without a browser. /mcp/sign-in/<server>
returns to a loopback callback when the browser runs on the p2 host (the
device owner, or a relayed/public owner whose browser reaches a nonce probe
on 127.0.0.1), else to /mcp/callback on the origin p2 is reached through.
Pasting the final redirect URL always completes a waiting sign-in.
Web: an MCP sheet (status, restart, sign in/out, paste). TUI: /mcp and
/mcp paste. The sidebar footer's owner entries move into a More menu.
- Switching branches (navigate, select) no longer waits for the main conversation: a branch left while
running keeps working in the background, read-only with Stop allowed.
- Branch rows in the sidebar show the working dot; background branches' answers count as unread.
- Fence refusals name a running background branch instead of a subagent #N.
- Subagent reports go to the conversation that sent the message; team send to "parent" goes to the
conversation that last spawned or messaged the subagent.
Servers with auth.provider, such as Radius, were reported as needing sign-in because the shell command sent no token. pi mcp now reads and refreshes the provider's /login token, and its hints and login/logout point to /login <provider>.
- Runtime: ctx.panel shows one component right of the screen's body (about 40%,
hidden below 100 columns, dock stays full width). F2 moves the focus into it
(viewport and select keys scroll it, Escape returns), /close-panel closes it.
Closes on conversation switch and facet reload, like overlays.
- Artifacts: Enter opens the artifact in the panel when the terminal fits it,
else inline as before; the panel follows the artifact's newest version.
- Fix: /artifacts rows are keyed by name and version, so every version of an
artifact can be selected.
An end-to-end reload suite (a kit app, a worker, a real Chromium page; one test per row of design's reload table).
The kit gains timeouts and base (base bundles' inputs and worker entries mark base). Fixes: a call crossing a failed
or a removal gets the cause, not not_ready; a call sent with an old method index is decoded by the names its consumer
had until the new bound is acked; a page retries a failed script fetch; applyChanges runs one at a time.
Branch rows are hidden until the session row's expand control (the
subagents' chevron) opens them, above the subagents; the control also
appears for a session with two or more branches and no subagents, with
the branch count beside the subagent count. Showing a subagent still
expands the list; showing any other non-main conversation no longer
does (right after a /tree switch the page shows the new branch before
the row names it main).
A hold unbinding with loading (a restart, a parked swap, a live failure retrying) keeps its calls in flight; each
gets one answer from what it waits on (the instance it runs on, or the gate). Calls sent by the old method names
before the consumer learns of the loading wait at the gate too. failed and removal still reject.
Pages and TUIs loaded before a deploy hold the contracts they were built
with; without accepts they wait unbound (contract_mismatch) until they
reload. The histories are now written with evolve from the released
versions, and the providers accept them:
- Sessions 3 (rows without main and branches) and 4 (a null branch label
reads as the session title); v4's treeSelf arguments pass as they are
- Catalog 2 and 3, the same row mappings
- SessionTree 1: the tree without labels (no branches, no node
conversation); rename is new and needs nothing
test/p2/contract-versions.test.ts binds the released declarations
against the current server.
p2.main version 2 stores a label per branch: { conversation, label }.
A new branch is labelled in the commit that forks it, with the start of
the message the owner picked (the answer for "Branch from here", the
edited user message for "Edit"). Conversation 1 keeps null, which every
UI shows as the session title. Version 1 migrates to null labels; at
start the worker fills the other branches once from their fork points.
- SessionTree v2: rename (owner), Tree.branches, TreeNode.conversation
- Sessions v5 / Catalog v4: a branch label may be null
- the worker reports p2.main changes; the derived labels are gone
- web: a pencil beside each sidebar branch row opens a rename sheet
- TUI: /tree shows branch labels; Shift+L renames the selected branch
quiet-tools draws every tool call as one line; Ctrl+O, a click or a failure
shows the full content. Its /settings row switches back to the tools' own cards.
TuiSettingFields lets plugins add /settings rows, stored at
settings.plugins.<plugin>.<name> of the visitor's prefs. Tool renderer
resolvers receive the settings; a change redraws every card.
Each branch is a top-level conversation forked from another at an entry;
the session-scoped p2.main document lists them and names the current one.
Earlier branches are read-only. The main agent's per-agent state
(subagents, artifacts, voice and chat mode, pending move) stays on
conversation 1 for every branch.
- tree plugin: SessionTree (tree, navigate, select); the worker reports
main and branches to the catalog (Sessions v4, catalog schema 4)
- TUI: /tree and double Escape (pi's default "tree"), a tree selector
adapted from coding-agent
- web: Edit on user messages, Branch from here on answers, branch rows
under the session in the sidebar
- main-agent checks use the branch list instead of conversation 1
- search indexes each branch's own entries
Design: apps/p2/docs/tree.md
Archiving from the session sheet navigates away first, which unmounted SessionOptions and released its Sessions
reference; Chord cancels a released reference's calls, so the archive call rejected with "p2.sessions: released",
shown as the composer toast over the owner-notice lane. The archive now holds a reference of its own until it is done,
and the server runs its steps in a root of its own.
The archive stopped the worker with Hosts.stop, which restarts it for its holders: the page's lingering
conversation started a worker the archived session refused (resolve_failed, "Worker: Session is archived" notice).
SessionWorkerControl.suspend stops it and leaves its holders suspended until a restore (retry) or their release;
archive, a failed fork's archived copy and an archived session's index rebuild use it.
The server drops a page's own chord.* attributes and writes chord.host and the principal hook's result itself; tests
for the hook returning nothing, the NOOP adapter and calls over a link.
The four telemetry gaps (rows on commits, chord.host, microsecond times, a read-only store) and: pages record and
send span batches over their link to the server's ingest object (checked, capped, rate-limited, stamped browser and
with the link's principal); workers send theirs too, so the server is the store's only writer; principalAttributes
lets an app choose what a span records about its principal. The tutorial shows a real page's click traced to the
worker, with the trace viewer.
- type-check subagent-extension.test.ts against the extension view like the other subagent test
- update the test to current ToolRenderContext and ExtensionToolContext types
- use a decodable PNG in the image-only steering test; the fake data was omitted by image processing
The shipped subagent example could say a child was done while it was
still working, lose the useful part of process failures, and return a
normal-looking tool result when the delegated work had actually failed.
Single and chain output could also exceed the normal tool limit.
This also adds a dedicated regression suite covering progress state,
cancellation, process failures, output limits, cwd-based agent
selection, and persisted tool-result error state.
After this change:
- queued, running, completed, failed, and aborted work stay distinct
throughout the run;
- interrupting the parent run preserves partial child results, stops
queued work from starting, and escalates when a child ignores SIGTERM;
- signal exits, spawn failures, provider errors, stderr, and assistant
output remain visible instead of collapsing to `(no output)`;
- failed subagents become failed tool results, so the parent model can
react instead of treating them as successful calls;
- single and chain results stay within the normal 50 KB / 2,000-line
limit;
- project agents are selected and confirmed from each task's actual
working directory rather than the dispatcher's directory.
## Further potential improvements
These are useful follow-ups, but they are separate product and storage
decisions rather than part of the failure-handling fixes in this PR.
<details>
<summary>Potential follow-ups</summary>
### Keep persisted transcripts bounded
Subagent tool results persist the full child message history in
`details`, which can make parent session files large and slow to reload.
A follow-up could compact that history or spill it to disk.
### Define an aggregate parallel-output budget
Parallel mode still gives every task its own 50 KB budget. A follow-up
could keep every task represented while enforcing one overall
tool-result limit.
### Report child usage to the calling session
Per-child token and cost usage is visible in result cards but is not
added to the calling session's totals.
### Isolate malformed agent definitions
One malformed YAML agent can still break discovery for every valid agent
in the same directory.
### Limit subagent recursion
A nesting limit would prevent accidental process and paid-request
multiplication when subagents can invoke the subagent tool again.
* https://github.com/earendil-works/pi/issues/8195
### Add a configurable timeout
Unattended runs would benefit from a generous opt-in timeout for
children that never complete.
</details>
## Related issues
### Spawn failures lose their diagnostics
This directly addresses the reported case where a nonexistent child
working directory produces `(no output)` instead of the actual spawn
error. The regression suite includes that same reproduction.
* https://github.com/earendil-works/pi/issues/8129
### Persisted child history can bloat sessions
This PR intentionally keeps full child history in tool-result details,
so session compaction or spill-to-disk remains follow-up work rather
than something fixed here.
* https://github.com/earendil-works/pi/issues/7452
use llama.cpp's default port and remove the stale `--jinja` note
same change as #11616 by @pcuenca with the original commit preserved
disclaimer: my julien-cto agent helped me write this
Co-authored-by: Pedro Cuenca <pedro@huggingface.co>
Chord's telemetry schema; spans for calls, methods, relays, holds, activations, natives, loads, reloads, resolves,
revocations, routines, jobs and HTTP requests; trace carriers on hold and call (bounded, printable ASCII) continued by
relays and workers; events and host-wide metrics; the app kit passes an adapter to its hosts. renderTrace escapes
control characters. Example 62 prints a trace across three hosts.
## What
Hardens the plan-mode example extension's step-progress tracking.
Previously, completion markers were only recognized in the exact
`[DONE:n]` form inside visible text blocks, so steps frequently never
got checked off during execution.
## Changes
- **`getTextContent`** now reads `thinking` blocks as well as text
blocks, so `[DONE:n]` tags emitted during reasoning are honored.
- **Marker tolerance** (`extractDoneSteps`): accepts `[DONE: 1]`, `[DONE
1]`, `Done: n`, and prose phrasings like "Step n done / is complete /
finished" — all adjacency-anchored so negations ("Step 9 is not done")
never match.
- **Whole-plan fallback** (`allStepsComplete`): if the run's final
message says the plan is done (e.g. "All steps complete"), all remaining
steps are marked at `agent_end`.
- **Plan extraction dedup**: only the last `Plan:` section is extracted,
so a plan drafted in a thinking block no longer duplicates the steps
from the text block.
- **No more truncation**: step text is kept in full (previously cut at
50 chars); the widget truncates for display only. The execution prompt
now shows the complete step text.
- **No more dropped steps**: steps whose text starts with a backtick
(e.g. `1. \`index.ts\` — …`) are no longer skipped.
- **Widget refresh** happens on every turn instead of only when a marker
matched.
- **Plan-complete card** is delivered with `nextTurn` instead of
triggering an extra LLM turn.
## Tests
`packages/coding-agent/test/plan-mode-utils.test.ts` updated for the new
behavior (truncation, backtick steps, marker-count semantics) and
extended with coverage for the new cases (marker variants, prose
phrasings, negation safety, thinking-draft dedup, markdown titles,
whole-plan completion).
All 43 tests in `plan-mode-utils.test.ts` +
`plan-mode-extension.test.ts` pass (`npx vitest run`).
Note: the repo-wide type-check currently fails on `main` with
pre-existing `ModelCatalog`/`never` errors unrelated to this change;
this branch adds none (same count with and without the diff).
The `read` and `write` tool views find a file's language from its
extension with `getLanguageFromPath()`. That function's extension table
had no entries for Dart, Nix, or Groovy. Their highlight.js grammars are
loaded at startup, but the lookup returned `undefined`, so these files
were shown as plain uncolored text.
This change adds `dart → dart`, `nix → nix`, `groovy → groovy`, and
`gradle → groovy` to the table in
`packages/coding-agent/src/modes/interactive/theme/theme.ts`.
HTML export keeps its own separate extension map and a vendored
highlight bundle without a Dart grammar. This change does not touch
either.
## Testing
- Added a test to `test/syntax-highlight.test.ts` that checks the new
extension mappings. It fails without the fix and passes with it.
- `node ../../node_modules/vitest/dist/cli.js --run
test/syntax-highlight.test.ts` passes.
- Before, `getLanguageFromPath("a.dart")` (and `.nix`, `.groovy`,
`build.gradle`) returned `undefined` from the built dist. After, they
return `dart`, `nix`, `groovy`, and `groovy`.
- `npm run check` reports 6 type errors in `packages/ai/test` (model ID
types). Main without this change has the same 6 errors.
Fixes#11643
Requested by @badlogic
Co-authored-by: triagent <triagent@localhost>
The `openai-responses` client sent the session-affinity header as
`session_id`. Proxies such as nginx with `underscores_in_headers off`
silently drop headers with underscores, and Envoy can be configured to
reject them. Codex now sends `session-id`, and the
`openai-codex-responses` path already followed it (#4967).
With the default `openai` session-affinity format, `openai-responses`
now sends `session-id`. `x-client-request-id` and `prompt_cache_key` are
unchanged. The `openai-nosession` and `openrouter` formats behave as
before.
The compat schema description for `sessionAffinityFormat`
(`openai-responses`) and `docs/models-configuration.md` are updated to
match.
Not changed: `openai-completions` still sends `session_id`. I did not
check whether xAI or other backends on the Responses API depend on the
underscored name. Their `prompt_cache_key` is still sent, so prompt
caching should not depend on the header.
Testing:
- `test/openai-responses-compat.test.ts` now captures `session-id`. It
also checks that `session_id` is no longer sent, and the
explicit-override test uses the new name. `test/xai-responses.test.ts`
expects `session-id` and no `session_id`.
- Without the source change, these tests fail (3 failures); with it,
both files pass (50 tests).
- `npm run check` passes.
- The full `packages/ai` suite passes, except
`generate-models-strict.test.ts`. That failure is unrelated: the test
depends on live model catalog data.
Fixes#11639
Requested by @badlogic
Co-authored-by: triagent <triagent@localhost>
## Summary
- remove delivered queue entries even when the user message has no text
- keep image-only steering and follow-up pending counts in sync with the
agent queue
- cover image-only steering delivery with a regression test
## Testing
- `npm run check`
- `./test.sh`
Fixes#8581
Fixes#8409
## Summary
- Pass the request abort signal through lazy stream setup wrappers.
- Report setup failures as aborted when the request signal is already
aborted.
- Added a regression test for aborting during a tool execution before
next auth setup.