Skip to content

feat(durable): Postgres storage on the grouped commit contract - #5

Open
rgarcia wants to merge 600 commits into
hypeship/durable-postgres-storagefrom
hypeship/postgres-grouped-commit
Open

rgarcia wants to merge 600 commits into
hypeship/durable-postgres-storagefrom
hypeship/postgres-grouped-commit

Conversation

@rgarcia

@rgarcia rgarcia commented Oct 30, 2026

Copy link
Copy Markdown

Summary

Rebases the Postgres storage adapter onto upstream main (which now carries pi-durable's group commit, e5c6182) and ports it to the batched Storage.commit(batches, receive, context) contract.

  • A group of batches is one round trip: a new commit_batches plpgsql function locks the session row and checks the owner once, then runs the existing commit_batch per batch inside a subtransaction. A refusal by one of the adapter's checks (SQLSTATE DU003 to DU009) keeps the batches before it and returns the refusal; any other error, ownership included, raises and rolls the call back.
  • A batch containing a document.copy starts a new call once the batches before it are durable, because the copy's source is materialized while preparing. Everything else is one call.
  • A failed database read while preparing a copy is an unknown outcome (rejects the call), never a refusal.
  • Schema version 3 also widens the task status check to admit sleeping, added upstream in 1.4.0.
  • kernel/pi main was fast-forwarded to upstream main (50af8c1); the base branch hypeship/durable-postgres-storage starts there.

Tests

PI_DURABLE_POSTGRES_URL=... vitest --run test/postgres-storage.test.ts: 72 tests pass against Postgres 17, including upstream's new conformance cases (prefix kept before a refusal, a malformed later batch, a batched copy against the earlier batches' state, receipts before resolution) on both the plain and the reopen-per-commit storages, plus a new round-trip test: one query for three plain batches, three for a group whose last batch copies a document the first two wrote.

Root tsc --noEmit and biome are clean.

davidbrai and others added 30 commits October 21, 2026 10:10
models.dev deprecated several Baseten, GitHub Copilot, and Together models,
which broke type checks and the default-model test.

- Default GitHub Copilot model: gpt-5.4 -> gpt-6.1-sol
- Move Copilot live tests to gpt-6-luna/gpt-6-sol
- Remove tests and generator entries for retired models, including the
  Together reasoning-only model list
Sign in with ChatGPT on a ChatGPT Business plan can fail with
subscription_sharing_workplace_individual_usage_limit_exceeded. Only the
workspace admin can raise this limit, so retrying never helps.

- Mark the error as non-retryable
- Add a hint to the error message telling the user to ask their
  workspace admin to increase the limit
Status codes inside request ids or other numbers no longer make an error retryable.

closes earendil-works#11057
…ution into pi-ai

pi-ai gets a ClientIdentity request option and createModels() default that choose the User-Agent (detailed with version, short, or custom) and provider attribution headers for OpenRouter, NVIDIA NIM, and OpenCode. Attribution is off by default in pi-ai and on by default in pi-agent-core and pi-durable (HarnessOptions.identity). pi-coding-agent sets its identity from the telemetry setting instead of adding headers itself.
- ClientAttribution is { name, url, categories? } with required name and url, so
  OpenRouter always gets an HTTP-Referer. identity.ts maps the fields to each
  provider's headers; NVIDIA gets the name with its first letter upper-cased.
  attribution: null turns it off. PI_ATTRIBUTION is pi's, including the
  cli-agent category; agent-core, durable, and the coding agent use it.
- Categories are a string array, joined for OpenRouter.
- withClientIdentity() is a typed wrapper instead of a Proxy and keeps a
  MutableModels mutable. Durable no longer caches wrappers in a WeakMap.
- The Codex originator stays pi; login agentName no longer follows attribution.
- Models and the coding agent's ModelRuntime add x-opencode-session for custom
  providers pointing at OpenCode, replacing the coding agent's transformHeaders.
…ite replication

Portable modules under sqlite/replication: a WAL scanner that returns the newest page versions of committed transactions and stops at the last valid commit frame, incremental whole-database checksums from per-page hashes, the shared segment and base container with header validation, and object keys and start markers. A Node-only zstd page compressor sits beside them. None of it is exported from the package yet.
Reverts 48c8f32 and 4759f09. Session sync replicates pi-durable commits instead of SQLite pages, so NodeSqliteDatabase.exclusive(), the replicated connection wrapper, the WAL reader, page checksums, the segment container, start-marker keys and the zstd page compressor have no user.
Every built-in storage implements ReplayableStorage: head(), replay() at an explicit sequence, and export() of the current state as a log that replays to the same reads. SQLite schema version 3 adds sequence indexes so export streams. @earendil-works/pi-durable/sync adds ReplicatingStorage, which appends each committed batch to an outbox with fork copies resolved, compactRecords(), and versioned JSON log lines.
This reverts commit 4c25119. The work continues on the replication branch.
…ch recorded its loadout

Resume and /tree restored exactly the tools the branch recorded, so extension
tools registered later stayed inactive. Registered extension tools that the
branch never added or removed are now activated unless they use
defaultActive: false. The whole branch is searched, so tools removed before a
compaction stay inactive. Also fixes the /tree case from earendil-works#11075.

closes earendil-works#11062
The built-in grep tool spawned rg with the inherited environment, so
RIPGREP_CONFIG_PATH (e.g. a config containing --fixed-strings) silently
changed tool semantics: literal: false searches became literal and the
agent got incomplete results with no error.

Pass --no-config so the tool contract is deterministic. Users who want
custom rg behavior can use the bash tool or wrap grep in an extension.

closes earendil-works#11203
…l call

Pre-prompt recovery of a recoverable length stop omitted the assistant
message but not the tool results that answered its calls, because only
the end-of-run caller passed them. The orphaned results were persisted
and providers rejected every later request.

_omitRecoveryAttempt now finds the matching tool results in the
projected context itself, so every caller omits the pair together.

closes earendil-works#11227
Jev (TypeSafe, Workers AI), Clef (Workers AI), and OpenRouter decision
models now accept ClassifierContext.images. Each service takes its own
wire form: TypeSafe assets with the opt-in header, Clef images data URLs,
or OpenRouter image_url parts in the state. A new SystemOneCompat
(imageInput, sendImageOptInHeader) selects the form; catalog entries set
it explicitly and models.json provider compat now applies to classifiers.

Adds Clef Omni and Jev 1.14 on Workers AI and fixes Clef Flash and
Workers AI Jev catalog pricing.
Move two post-release entries out of the 1.3.0 sections, add missing entries, duplicate user-facing ai and tui changes into coding-agent, and add New Features.
)

Add Usage.cost.totalFromProvider to record whether cost.total is the
billed cost reported by the provider or an estimate from catalog rates.
Catalog-derived totals set it to false; a missing value also means
false. combineUsage marks a sum as provider-reported only when both
parts are.
Use authoritative OpenRouter billing for Chat Completions and Anthropic Messages while retaining catalog component estimates and fallback behavior.

Fixes earendil-works#9980
Bump vitest to 4.1.12 (vite 8.3.4) and the gondolin example extension to 0.14.0, which drops node-forge. Gondolin's undici moves to 6.29.1.
* feat: publish preview packages to pkg.earendil.com

CI packs every public package after the build, with internal dependencies
pointing at the same commit's builds. A workflow_run job publishes them to
the earendil-pkg worker with the job's GitHub OIDC token, moves main or
pr-<n>, and comments install commands on the PR. Closing a PR deletes its
ref. pi-env is left out because its daemons are only built for releases.

* feat: publish preview packages only for main and same-repo PRs

pkg.earendil.com requires a consumer token, so fork authors could not
install their previews anyway. Fork PRs are no longer packed or
published, which keeps fork code out of the registry and removes the PR
lookup: the PR number now comes from the workflow_run event. The PR
comment is dropped; the job summary lists the install commands.

* fix: pick the PR into main and describe only the workflow in preview-packages.yml

- Read the PR number from the workflow_run pull request list with jq,
  selecting the PR into main; a branch can also have PRs into other
  branches, and the index expression tripped workflow linters.
- Comments describe what the workflow does and what it needs from
  pkg.earendil.com, not how the service works.

* fix: gate preview publishing to main pushes and open PRs into main

- Publish only CI runs for pushes to main and for pull requests. A branch
  can make its own copy of CI run on push; those runs are unreviewed and
  were uploaded as push builds.
- Move pr-<number> only while the PR is open into main with this commit
  as its head, checked live, so a late publish cannot recreate the ref of
  a closed PR; otherwise publish without a ref and print a notice.
- Set concurrency per job, so skipped fork runs never wait in a group.

* docs: drop the service rule from the preview-packages.yml header

* docs: name the earendil-works/pkg worker in the preview script
A mid-conversation system message was placed before checking whether the
next assistant message converts to any blocks. When it did not (empty text,
or another model's reasoning-only reply), the update landed before a user
message and Claude rejected the request with a 400.

closes earendil-works#11091
…n Chord, explicit watch retirement

Breaking changes:
- Durable Storage.commit(writes, adopt, context): storages call adopt(seq) once,
  synchronously, when the batch is durable and before their next commit
- Chord traceIn(options, fn, context): context last
- WatchHandle and WatchEnd move to Chord (Durable re-exports them); end reasons
  ended (was session_closed) and failed (was session_failed); retirement is an
  explicit source event, and an ended watch drops its references into the Session
- Removed Session.documentState(), Conversation.viewState(), Harness.taskGraph()
  and DocumentState; use watchDoc(), watch(), watchTaskGraph()
badlogic and others added 30 commits October 29, 2026 12:15
…il-works#11565)

In regular TUI mode, user and assistant messages wrote OSC 133
(`A`/`B`/`C`) shell-integration markers straight to the terminal.
Terminals that display OSC 7501 program status (Rex, iTerm2 3.7.5+) read
`133;A` as a new shell prompt and drop the current status. As a result,
the `working` indicator disappeared right after a run started and only
`done` showed up at the end. Fullscreen mode was not affected because it
already strips these markers before writing.

## Change
- `TuiMainScreen.doRender()` (`packages/tui/src/tui-main-screen.ts`) now
strips OSC 133 markers from the start of each rendered line before
compositing and writing. It uses the same pattern as fullscreen
(`OSC133_ZONE_PREFIX`).
- The message components still produce the markers. Fullscreen uses them
internally for prompt navigation and already strips them before writing,
so its behavior is unchanged.
- In regular mode, the terminal's own jump-to-prompt shortcuts no longer
stop at pi messages (added in earendil-works#1805).

## Testing
- New test in `packages/tui/test/tui-render.test.ts`: no `ESC]133;`
reaches the terminal across a full render and a differential render,
with both BEL and ST terminators. It fails without the change and passes
with it.
- These pass:
  - `packages/tui` `npm test`
- `test/user-message.test.ts` and `test/assistant-message.test.ts` in
coding-agent
  - `npm run check`
- I ran `pi -ne --tui-mode regular` with `PI_PROGRAM_STATUS=1` and a
streaming test provider, capturing all terminal output with `script`.
- Before: `7501 working`, then repeated `133;A`/`133;B`/`133;C`, then
`7501 done`.
  - After: only `7501 idle` → `working` → `done`, and no OSC 133.

Fixes earendil-works#11519

Requested by @davidbrai

Co-authored-by: triagent <triagent@localhost>
…dil-works#11566)

At startup, interactive mode printed every skill diagnostic under the
heading `[Skill conflicts]`. Most of these are validation warnings for
skills that still load, for example `description exceeds 1024 characters
(1508)` or name-format warnings. The heading made them look like name
collisions.

This change renames the heading to `[Skill warnings]`. Validation and
loading behavior are unchanged. The prompt and theme headings are left
as they are.

**Testing**
- Updated the assertion in
`packages/coding-agent/test/interactive-mode-status.test.ts` ("still
shows diagnostics on quiet startup when requested") to expect `[Skill
warnings]`. It fails against the old heading and passes with the change.
All 37 tests in that file pass.
- `npm run check` reports six type errors in `packages/ai` tests about
model IDs. Unchanged `origin/main` reports the same six errors, so they
are unrelated to this change. It reports nothing in `coding-agent`.

Fixes earendil-works#11521

Requested by @davidbrai

Co-authored-by: triagent <triagent@localhost>
…works#11572)

When a Zhipu/Z.ai (`open.bigmodel.cn`) plan hits its usage cap, the API
returns HTTP 429 with error code `1308`. The cap resets after hours, but
`isRetryableAssistantError()` still marked the error as retryable,
because the bare `429` in the message matched the transient-error
pattern. The agent-level retry then re-sent a request that could not
succeed and slowed down the final error. With default settings that is 3
attempts, about 14 s.

This change adds the Zhipu `"code":"1308"` error code to the
non-retryable provider-limit patterns in
`packages/ai/src/utils/retry.ts`, next to the existing quota and
subscription-limit entries. The pattern matches the JSON code field, not
a bare number, so request IDs or byte counts that contain 1308 do not
trigger it.

Not changed: the provider-level HTTP retry (`retry.provider.maxRetries`,
off by default). It still retries every 429 based on the status code
alone.

Testing:
- `npm run check` passes.
- `test/retry.test.ts` passes.
- Calling `isRetryableAssistantError` directly on the reported message,
`429 {"code":"1308","message":"已达到 5 小时的使用上限。…"}`, returned `true`
before the change and returns `false` after.

Fixes earendil-works#11537

Requested by @davidbrai

Co-authored-by: triagent <triagent@localhost>
…ed version unchanged (earendil-works#11571)

## What was wrong

`pi update --extensions` first looks up the newest version of each npm
package with `npm view`. If that version is newer than the installed
one, pi runs `npm install <name>@latest`. The two commands read
different registry documents, and each is cached separately. Shortly
after a publish, `npm view` can already report the new version while
`npm install` still resolves the old one. npm then exits successfully,
pi prints "Updated packages", and the startup banner lists the same
package again on the next launch. The user can't tell that nothing was
actually updated.

## What the change does

- `shouldUpdateNpmSource` now returns the installed version and the
registry version along with its decision. Both are carried on
`NpmUpdateTarget`.
- After `npm install`, `updateNpmBatch` re-reads each package's
installed version. If the registry lookup succeeded and the version did
not change, it emits a `progress` event with the message: `<source> is
still X (registry reports Y); npm registry metadata may be stale, try
again in a few minutes`.
- The package CLI now prints `progress` events in yellow. Until now
nothing emitted them.

Install behavior is unchanged: pi still installs `@latest`.

## Testing

- New test in `test/package-manager.test.ts`: two packages are flagged
for update and only one is actually upgraded. The warning is emitted
only for the unchanged one. The test fails without the change.
- `test/package-manager.test.ts` passes (129 tests), and `npm run check`
passes.
- Ran the built CLI with an `npmCommand` stub whose `view` reports
`1.0.1` and whose `install` changes nothing:
  ```
  Updating npm:demo-pkg...
npm:demo-pkg is still 1.0.0 (registry reports 1.0.1); npm registry
metadata may be stale, try again in a few minutes
  Updated packages
  ```

Fixes earendil-works#11536

Requested by @davidbrai

Co-authored-by: triagent <triagent@localhost>
…ges (earendil-works#11577)

When an image needs resizing, `resizeImageInProcess` builds a list of
encodings: PNG first, then JPEG at decreasing qualities. It returned the
first one under `maxBytes`. Since PNG was always first, it was used
whenever it fit, even when it was many times larger than the JPEG. The
doc comment says "Try both PNG and JPEG formats, pick the smaller one",
and the code worked that way before v0.62.0. Because of this, photos
were sent as multi-MB PNGs, which made requests large enough to fail or
time out.

This change compares PNG with JPEG at the configured quality at each
size step and tries the smaller one first. It tries the other one next,
and the lower JPEG qualities only after that. Screenshots and other
images where PNG is smaller still go out as PNG.

**Testing**
- Added a regression test to `test/image-processing.test.ts`: a
generated photo-like 1200×800 JPEG resized to 1000 px must come back as
`image/jpeg`. The test fails without the fix (PNG is returned) and
passes with it.
- `node ../../node_modules/vitest/dist/cli.js --run
test/image-processing.test.ts test/image-resize-callers.test.ts`: all
pass.
- `npm run check`: no issues in the changed files. It does report type
errors, all in `packages/ai/test` (model ID unions), which this change
does not touch.
- Reproduction with a photo-like 2400×1620 JPEG (460 KB base64):
  - before: `image/png`, 4,008,480 bytes base64, at 2000×1350
  - after: `image/jpeg`, 255,316 bytes base64, at 2000×1350

Fixes earendil-works#11551

Requested by @davidbrai

Co-authored-by: triagent <triagent@localhost>
Some Apple Terminal setups send ESC+CR for Shift+Enter, which parsed as
alt+enter and queued a follow-up. The native Shift fallback now covers
ESC+CR as well as plain CR.

fixes earendil-works#11559
…st-requiring resources

A saved Do not trust decision was ignored when the project had no
trust-requiring resources, so ctx.isProjectTrusted() returned true.
The saved decision now takes precedence over auto-trust.

closes earendil-works#10947
…ewer version exists

Temporary -e npm: sources were installed once and reused forever. Like
temporary git sources, check npm on load (unless offline) and reinstall
with name@latest or the configured spec when a newer version exists,
keeping the cached install if the refresh fails.
A temporary -e git source with a ref was treated as pinned and never refreshed,
so git:host/repo@main stayed on its first checkout. Refresh refs too, skipping
only a commit-hash ref that is already checked out.
…dil-works#11588)

Fixes earendil-works#10488

## Problem

On Windows, `c:\Users\me` and `C:\Users\me` are the same directory, but
pi compared paths as exact strings (`===`, `startsWith`, map and set
keys). `realpathSync` keeps the input casing, so canonicalizing does not
help.

The reported case, with cwd `c:\Users\me\project` and home
`C:\Users\me`:

1. Ancestor discovery finds `c:\Users\me\.agents\skills`. The exclusion
of the user dir `C:\Users\me\.agents\skills` does not match, so global
skills load as project skills.
2. The same dir loads again as user skills. Dedup does not match either,
so every skill is reported as a name collision.
3. `hasTrustRequiringProjectResources` treats `~/.agents/skills` as a
project resource that requires trust.

The same pattern affected other places:

- Saved trust decisions in `trust.json` were not found when cwd casing
differed, so pi asked for trust again.
- With a custom session dir, `--continue`, resume by id, and the session
list hid sessions recorded with different cwd casing.
- Git worktree `AGENTS.md` shadowing failed when cwd casing differed
from git's paths.
- Local package matching (`pi remove c:\pkg` vs configured `C:\pkg`) and
package identity across global and project settings.
- Extension preload matching, skill scope classification, resource
source info, the config and session selector keys, and the session
import self-check.

Separately, home-to-`~` shortening used an unanchored `startsWith(home)`
in four places, so `/home/me2/x` was shown as `~2/x`.

## Fix

New helpers in `utils/paths.ts`, all based on one comparison key:

- `getPathComparisonKey(path)`: the resolved path, lowercased on
Windows. It does not touch the filesystem.
- `pathsEqual(a, b)`
- `isPathWithin(parent, child)`: true for equal paths or paths inside
`parent`, matching whole path segments.
- `shortenHomePath(path, home?)`: replaces a leading home dir with `~`.

The helpers pick `path.win32` or `path.posix` from `process.platform` at
call time, so tests can exercise real Windows paths (drive letters,
UNC).

`getResourcePathKey(path)` in `source-info.ts` is the dedup key for
resources. Synthetic identifiers (`builtin:<name>`, `<inline:...>`) stay
unchanged, because they are not filesystem paths and their lookup is
case-sensitive. File paths go through realpath plus the comparison key.

The helpers are used only for comparison; stored and displayed paths are
unchanged. Trust store lookups and `set` match keys case-insensitively
on Windows.

Unchanged on purpose:

- The package install-root guards (`resolveManagedPath`,
`pruneEmptyGitParents`, the `node_modules` dependency check) keep exact
string checks. Both sides come from the same root string, and exact is
the stricter option.
- Checks based on `path.relative()` (`getCwdRelativePath`, the changelog
containment check) were already correct: Node's `path.win32.relative`
ignores case.

NTFS can enable per-directory case sensitivity, but it is rarely used
outside WSL interop, so it is ignored (see the comment on
`getPathComparisonKey`).

## Tests

- `test/paths.test.ts`: Windows drive-letter, UNC and root cases, POSIX
case sensitivity, segment-boundary containment, and home shortening,
including root and UNC homes.
- `test/source-info.test.ts`: file paths fold case on Windows; builtin
and inline identifiers keep their case.
- `test/trust-manager.test.ts`: the `.agents/skills` exclusion with a
lowercase `HOME`, plus saved trust decisions matched and replaced across
casing (simulated win32).

An end-to-end test of the reported loader scenario is not feasible on
Linux: the loader resolves paths with POSIX `node:path`, and the
filesystem is case-sensitive.

`npm run check` passes. The coding-agent suite passes, except for 13
failures that also occur without this change: clipboard tool detection
in this environment, and one test that needs built `dist/` output.
…through at line end (earendil-works#11591)

Fixes earendil-works#10925.

## Problem

`splitIntoTokensWithAnsi()` attaches pending escape codes to the next
visible character. The code that closes a styled word therefore lands on
the following space: `**commodo** consequat` tokenizes as
`\x1b[1mcommodo`, `\x1b[22m `, `consequat`.

`wrapSingleLine()` detected whitespace with `token.trim() === ""`, which
is false for `"\x1b[22m "`. When the styled word ended exactly at the
wrap width, that space was treated as a word and moved to the next line:

```
before: ["aaaa \x1b[1mbold", "\x1b[1m\x1b[22m next", "word"]   (width 9)
after:  ["aaaa \x1b[1mbold\x1b[22m", "next word"]
```

Because the closing code moved to the next line, the style also stayed
open into the right padding of the wrapped line. Separately,
`getLineEndReset()` closed only underline at line end, so strikethrough
that spans a wrap continued into the padding.

## Fix

- Detect whitespace tokens after stripping escape codes.
- When a whitespace token is dropped at a wrap, append its escape codes
to the wrapped line and update the tracker, so the style closes there
and is not re-opened on the next line.
- Close strikethrough (`\x1b[29m`) at line end, like underline.
`getActiveCodes()` re-opens it on the next line. Background is
unaffected.
- Whitespace wider than an empty line that carries codes (e.g.
background spaces followed by a trailing reset) keeps only the codes.
Without this, treating it as whitespace skipped the long-token path, and
the trailing reset blocked the final `trimEnd()`, leaving a line wider
than `width`.
- When a long word follows, the line before it is now trimmed before the
line-end reset is added. Otherwise the reset (now also emitted for
strikethrough) blocks the final `trimEnd()` and leaves trailing spaces.

## Tests

- `wrap-ansi.test.ts`: boundary closing code, dropped space with a color
reset, strikethrough reset and restore across a wrap, strikethrough
reset with background preserved.
- `markdown.test.ts`: padded Markdown with a background at an exact
boundary for bold and strikethrough.
- The new tests fail without the fix. With the old code, rendering the
issue text through `UserMessageComponent` (dark theme, padding 1) at
widths 40-120 produced 5 bad lines at width 78. With the fix it produces
none.
- Fuzzing over 1.2M random styled inputs with SGR and OSC 8 codes found
no line wider than `width`, and no case where the old wrapper laid out
text the same as the unstyled input but the new one does not.
- Benchmark (`wrapTextWithAnsi`, width 77, about 5-18k chars, median of
6 processes) against `main`: plain -5%, light styling -0.5%, heavy
styling +1%, long strikethrough +0%, all within noise. The whitespace
check scans character codes and stops at the first visible text instead
of allocating a stripped copy.
- `npm run check` passes. All `packages/tui` tests pass. `./test.sh`
shows no new failures; the remaining failures are environment-specific:
the clipboard tests also fail on `main` here, and the durable, env
(ssh), and chord failures are in packages that do not depend on
`pi-tui`.
…s#9329)

## Summary

Treat `TERM_PROGRAM=Orca` as Kitty-image capable in `pi-tui`'s terminal
capability detection, so image components render inline instead of
falling back to text when running inside Orca.

- Recognize Orca as supporting Kitty inline images, true color, and OSC
8 hyperlinks.
- Preserve the existing tmux and screen precedence guards.
- Gate images on Orca's capability advertisement: require both
`TERM_PROGRAM=Orca` and Orca's `ORCA_IMAGE_PROTOCOL=kitty` signal before
enabling Kitty output, so existing Orca releases that do not yet
advertise image support safely keep the text fallback.
- Add a regression test and the package/CLI changesets.

## Related

- Requested by maintainer approval in
[earendil-works#7357](earendil-works#7357)
(lgtm).
- Depends on the Orca-side capability advertisement, which has landed:
[stablyai/orca#19512](stablyai/orca#19512)
(merged 2026-09-18, `09073086a8`) exports `ORCA_IMAGE_PROTOCOL=kitty` on
the daemon, local, and relay PTY paths, and forwards it across the WSL
boundary. Supersedes
[stablyai/orca#11706](stablyai/orca#11706),
which was closed in favor of #19512.

## Testing

- `pnpm --filter @moonshot-ai/pi-tui test` — focused regression suite
passes.
- `pnpm --filter @moonshot-ai/pi-tui typecheck` — passes.
host.reload(facets, ctx): staging into a staged table with a recording self (all or
nothing), one commit (types, registries, contributions; per-name wakes; installing
holds), and each object's fate: pure swap in place, native restart after its drain,
retry on the new code, or its end by its type's removal, with a report. Old calls
finish on old handles (R15 for documents and tables), routines hand over per object,
jobs keep their versions (accepts, blocked { version }, persisted runs per R3),
registries swap providers and contributions in one batch, holds are kept,
re-subscribed or unbound with failed { waiting } (model §5), accepts maps older
consumers' state, arguments, results and keyed members.

References are the only indirection (owner, 2026-10-29): a consumer's watches and
derives attach through the reference, which passes them straight to the object
instance's own document or derive, moves them to a new instance (one frame) and keeps
its last values while unbound; MemberSource, keyed-members and the per-version
hand-over are removed. Older consumers read through the instance's shared mapping
per version. Examples 33-34, reload benchmark.
oauth.clientRegistration defaults to auto: pi's Client ID Metadata Document
when the authorization server supports it for public clients, otherwise
dynamic client registration. Registered clients stay in use. pi mcp add
gains --oauth-client-registration.

closes earendil-works#11426
A Session no longer holds its line while a commit is stored. Commits that
arrive while one storage call is in flight go out together in the next call,
each with its own seq and outcome; a job runs until it touches a key an
unpublished commit wrote, then waits for that commit's publication. Outcomes
are delivered in line order; publication is in line order, one commit per
macrotask. The scheduler's decisions run as regions that record the keys they
read and rerun after a wait, with side effects only after a clean check.

Storage.commit(batches, receive, context) replaces commit(writes, adopt,
context): prefix semantics (Durable's own checks refuse a batch and keep the
ones before it; any other failure rejects the call), receipts per stored
batch, optional publishing() scope. Memory, SQLite, JSONL, IndexedDB and
StoreStorage implement it natively; StoreStorage through the new
ChordStore.commitSeries (one store transaction per call), replacing
commitInBatch.

A normal run makes no storage reads: bounded record cache, conversation
tails, task-document index, kept context ranges. A tool slot's running status
and a generation's attempt are stored with the next commit (12 instead of 14
commits per run).

One Harness with 128 conversations over storage with a 4 ms round trip: about
5 -> 440 agent runs per second. Design: packages/durable/docs/group-commit.md.
…limit (earendil-works#11605)

After a rescan, the env daemon's file watcher sent every changed path in
a single `change` event. When a big batch of changes made that event
larger than the 16 MiB frame limit, `write_frame` rejected it. The
daemon's only output writer then shut down. The daemon process kept
running but sent no more replies or pings. Requests hung until the
client's 30 s silence limit dropped the connection and reconnected.

Now the watcher builds the event with `change_event`. If the serialized
path list would not fit in a frame (`MAX_PAYLOAD`), it sends the
existing `{ kind: "change", overflow: true }` event in its place. The
client already maps that to an `overflow` file change, so the wire
format does not change and the connection keeps working.

Testing:
- Added Rust unit tests in `watch.rs`. One checks that a small path list
is sent as `paths`. The other checks that a list larger than
`MAX_PAYLOAD` becomes `overflow`.
- `cargo fmt --check`, `cargo clippy --locked --all-targets -- -D
warnings` and `cargo test --locked` pass in `packages/env/daemon`.
- The `packages/env` vitest suite passes.
- Reproduced with the real daemon: I watched a directory and renamed in
a directory of 6,000 files with long paths, about 20 MB of path JSON.
Before the change, `fileInfo` requests timed out from the flush until
the client reconnected about 30 s later. After the change, the watcher
reports `overflow` right away and `fileInfo` keeps working. With 3,000
files, which fits in a frame, the paths are still reported as before.

Fixes earendil-works#10516

Requested by @badlogic

Co-authored-by: triagent <triagent@localhost>
…ndil-works#11607)

`sanitizeOutput()` in `@earendil-works/chord/output` deleted the ESC
byte (0x1b) along with the other control characters, but it left the
rest of each escape sequence in place. Colored tool output such as
`\x1b[31mERROR\x1b[0m` came back from `OutputBuffer.snapshot()` as
`[31mERROR[0m`. Because the ESC byte was already gone, later ANSI
stripping could no longer recognize the leftover fragments.

`sanitizeOutput()` now removes whole 7-bit escape sequences before it
removes single control characters. It handles:
- CSI sequences (colors, cursor and erase commands)
- string sequences (OSC, DCS, APC, PM, SOS) terminated by BEL or `ESC \`
- short `ESC x` forms such as `ESC ( B` or `ESC 7`

`snapshot()` sanitizes the concatenated retained text, so a sequence
split across pushed chunks is also removed.

**Testing**
- Added a regression test in `packages/chord/test/output.test.ts`. It
covers SGR colors, cursor and erase CSI, charset and short escapes, OSC
8 hyperlinks and an OSC title, plus an `OutputBuffer` fed a color
sequence split across three chunks. The test fails without the change
and passes with it.
- `test/output.test.ts` and `test/output-skip.test.ts` pass.
- Biome is clean. The only `tsc` errors in `npm run check` are in
`packages/ai` tests and are unrelated to this change.
- The durable package test results are the same with and without the
change: 71 test files fail on both, and every individual test that runs
passes.
- Reproduction from the issue: before the change both lines printed
`"[31mERROR[0m\n"`; after it, both print `"ERROR\n"`.

Fixes earendil-works#11604

Requested by @badlogic

Co-authored-by: triagent <triagent@localhost>
…endil-works#11606)

In pi-durable, a Code Mode script that called the built-in `read` on an
image got `""` back, so `image(await tools.read({ path: "pixel.png" }))`
threw a TypeError. `read` declared no `structuredOutputSchema`. Code
Mode resolves a schema-less tool with only its text, and drops images on
purpose. `read`'s output for an image is a single image block with no
text, so the script got an empty string. The coding agent fixed the same
problem for earendil-works#10251.

With this change, `read` declares a `structuredOutputSchema`: a string,
or an image block `{ type: "image", data, mimeType }`. It returns the
matching `structuredOutput`:
- for a text file, its text, so scripts get the same value as before;
- for an image, its `ImageContent`, which `image()` accepts.

The "Read image file" note stays an info diagnostic, as before. The
README section on structured output and the changelog are updated.

**Testing**
- New test in `test/codemode.test.ts`. A script reads a real PNG and a
text file through `tools.read()`, checks the block's type, MIME type,
and data, and passes the block to `image()`. It checks that the script
succeeds, the image is saved and shown, and the text read still returns
its text. The test fails without the change: the script errors because
`image()` gets `""`.
- `test/tools-read-differential.test.ts`: the reference implementation
now includes the new `structuredOutput` field.
- `npm run check`.
- `test/codemode.test.ts`, `codemode-describe`, `tools`, `tools-images`,
`tools-read-differential`, `harness-tools`, `harness-hook-reads`,
`harness-read-only`, and `spec-usage` all pass.

Fixes earendil-works#11015

Requested by @badlogic

Co-authored-by: triagent <triagent@localhost>
Report unknown options as typed and suggest a configured server for a
mistyped name.
The owner's audit decisions (decisions.md, "Simplification audit"): cut the reload-only
installing state (staging is synchronous) and the static registry cycle edges (the
runtime guard stays); retry propagation by a visited set and a hop budget (retry.hops)
instead of operation ids and through lists; a pure swap sends holds a fresh bound; one
rule after a live failure (every member followed until the admitted commits published);
parked_long for any long park, naming the first blocker in declaration order with its
state; one seal token per attempt. Keys follow the old instance during a restart's drain
like members. src/next: about 255 code lines fewer.
Port the adapter to Storage.commit(batches, receive, context). A group of
batches goes to a new commit_batches function in one round trip: it locks
the session row and checks the owner once, then runs commit_batch per batch
in a subtransaction, so a refusal by one of the adapter's checks keeps the
batches before it and ends the call with the refusal, while any other error
rolls the call back and rejects it. A batch with a document.copy starts a
new call once the batches before it are durable, since the copy's source is
read while preparing; every other group is one round trip.

Schema version 3 also admits the sleeping task status added in 1.4.0.

A round-trip test pins one query for a group of plain batches and three for
a group whose last batch copies a document the earlier ones wrote.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.