Commit graph

7255 commits

Author SHA1 Message Date
Petr Houška
e7376afd65 docs(coding-agent): link changelog entry to #11587 2026-10-29 20:18:56 +01:00
Petr Houška
74d57a69a3 fix(coding-agent): create session directories as 0700 and session files as 0600
Session directories and files followed the umask (typically 0755/0644), so
conversations were readable by other local users whenever a parent directory
allowed it. New ones are now owner-only, like auth.json. The modes apply only
on creation; existing directories and files keep their modes. Imported
sessions no longer copy the source file's mode.
2026-10-29 20:18:35 +01:00
robopipi[bot]
e7a0df27c0
fix(coding-agent): pick the smaller of PNG and JPEG when resizing images (#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 #11551

Requested by @davidbrai

Co-authored-by: triagent <triagent@localhost>
2026-10-29 16:14:56 +01:00
robopipi[bot]
0d23d53457
fix(coding-agent): warn when an npm package update leaves the installed version unchanged (#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 #11536

Requested by @davidbrai

Co-authored-by: triagent <triagent@localhost>
2026-10-29 15:39:48 +01:00
robopipi[bot]
c719d5b08e
fix(ai): do not retry Zhipu usage limit errors (code 1308) (#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 #11537

Requested by @davidbrai

Co-authored-by: triagent <triagent@localhost>
2026-10-29 15:37:07 +01:00
robopipi[bot]
f14813e965
fix(coding-agent): label skill diagnostics as [Skill warnings] (#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 #11521

Requested by @davidbrai

Co-authored-by: triagent <triagent@localhost>
2026-10-29 14:25:50 +01:00
Mario Zechner
0c3675e647 docs(chord): slice 7b spec: reload in the kernel 2026-10-29 13:05:53 +00:00
Mario Zechner
ec7852988e docs(chord): slice 7b decisions as recommended 2026-10-29 13:02:46 +00:00
robopipi[bot]
19ce8059c3
fix(tui): stop writing OSC 133 prompt markers in regular mode (#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 #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 #11519

Requested by @davidbrai

Co-authored-by: triagent <triagent@localhost>
2026-10-29 14:00:16 +01:00
Mario Zechner
ba7bdb9afc docs(chord): slice order: 7b, 7c, 10, 11, telemetry last 2026-10-29 12:44:38 +00:00
Mario Zechner
b857fbd8a9 docs(chord): slice 7b brief: plugin dependencies 2026-10-29 12:32:09 +00:00
Mario Zechner
601461e205 docs(chord): decision: plugin dependencies bundled, external by opt-in 2026-10-29 12:31:40 +00:00
Mario Zechner
adfa8f3827 docs(chord): slice 7b brief: plugin layout, building and loading, assets, HTTP, the app kit, measurements 2026-10-29 12:15:57 +00:00
Mario Zechner
972d7017af docs(chord): slice 7b brief: reload, decided and open points 2026-10-29 11:54:38 +00:00
Mario Zechner
339847eb14 feat(chord): Chord v2 slice 7a, the dependency model
A reference is a request that outlives its provider: a missing provider or a version that does not fit leaves
it unbound, waiting for the name (failed { waiting }, a new FailureCause kind), local and remote alike. An
attempt parks until its dependencies are live, with no timer and no backoff, woken on every state change of a
dependency; its readyTimeout runs only for its own steps. Requests wait only for the host's publication (the
startup deadlock is gone); load resolves at the first outcomes of its host-lifetime objects. Getters wait through
waiting until their readyTimeout. Registry readers are waiting dependencies, woken at every usable value; a last
good value is dropped when a contribution it was built from leaves. Unbound holds count for keep-alive and are
revoked; retry passes on to failed blockers once per operation (operation id and visited list on the wire); the
dependency deadline is gone and parked_long reports a long wait on a remote attempt.
2026-10-29 11:40:57 +00:00
Mario Zechner
babe30a7e6 docs(chord): slice 7a spec: the dependency model; slice 7 split into 7a and 7b 2026-10-29 08:32:56 +00:00
Mario Zechner
39b16b2281 docs(chord): the dependency model is decided 2026-10-29 08:31:59 +00:00
Mario Zechner
a5609aae81 docs(chord): decision: no cycle probe; parked_long diagnostic and authoring rules 2026-10-29 08:31:15 +00:00
robopipi[bot]
6fed3cfb99
fix(coding-agent): let the SDK stream resolve auth for summaries (#11556)
In sessions created by `createAgentSession`, compaction, branch
summaries and bug-report summaries resolved the stored OAuth credential
in `AgentSession._getSummarizationRequestAuth`, then passed the access
token on as `options.apiKey`. The SDK's stream function calls
`modelRuntime.streamSimple`, which resolves auth again. Because it was
given an explicit `apiKey`, `resolveProviderAuth` took the API-key
override branch instead of using stored OAuth. Built-in providers accept
the token either way. A provider whose API-key path differs from its
OAuth path, such as one that refuses explicit keys, failed every summary
while normal prompts worked.

`createAgentSession` now marks its stream function with
`markRuntimeStreamFunction`. This records it in a module-level `WeakMap`
keyed by the function, mapped to its `ModelRuntime`.
`_getSummarizationRequestAuth` skips pre-resolution when the agent's
stream function is marked for the session's own runtime, so that runtime
resolves auth once, as it does for normal prompts. The check runs after
virtual-model routing. Bare `streamSimple` and custom `streamFn`s still
receive resolved credentials, so the fixes for #6324 and #7579 are
unchanged. If a caller replaces `agent.streamFunction`, the old behavior
applies.

**Testing**
- Added `test/suite/regressions/11520-sdk-summary-oauth-auth.test.ts`.
It builds an SDK session on a provider that has stored OAuth and also an
API-key method, runs a branch summary, and asserts two things: no
`getAuth` call carries an `apiKey` override, and the provider receives
the OAuth access token. It fails without the change and passes with it.
- `npm run check` passes and `./test.sh` passes. I also ran the related
tests: 6324 branch-summary auth, compaction model overrides,
auto-compaction cancellation, SDK stream options, virtual models, and
the compaction and branch-summary tests.
- Manual reproduction: an SDK script with an `openai` OAuth credential
that logs `ModelRuntime.getAuth` during `navigateTree(..., { summarize:
true })`. Before the change there were two calls, the second with
`apiKey = <oauth token>`. After the change there is one call, with no
override.

Fixes #11520

Requested by @badlogic

Co-authored-by: triagent <triagent@localhost>
2026-10-29 09:24:53 +01:00
Mario Zechner
249f24bfc5 docs(chord): getters decision: reviewer votes 2026-10-29 08:03:10 +00:00
Mario Zechner
a54f250fb4 docs(chord): decision: getters wait through waiting until their readyTimeout 2026-10-29 08:00:54 +00:00
Mario Zechner
aee933269b docs(chord): dependency model: hostAddress wording 2026-10-29 07:44:21 +00:00
Mario Zechner
373fbcc706 docs(chord): dependency model: hostAddress 2026-10-29 07:37:56 +00:00
Mario Zechner
5ec2418820 feat(chord): hostAddress instead of placement, required on remote types; only hosts with an address serve remote types
The contract field placement is hostAddress, required on every remote() (type and declaration error); the
fallback for remote types without one is gone. invalid/placement is invalid/host_address. host/placement.ts
(Placement) is host/destination.ts (Destination); here is decided by address alone, and a host without an
address refuses to serve a remote type. A hold always carries its address on the wire (the binary codec's
ADDRESS flag is gone, the text codec requires it). Tests and examples declare their host addresses.
2026-10-29 07:37:37 +00:00
Mario Zechner
d0687bce85 docs(chord): only hosts with an address serve remote types; holds always carry their address 2026-10-29 07:29:25 +00:00
Mario Zechner
943f30c909 docs(chord): design: hostAddress instead of placement 2026-10-29 07:25:19 +00:00
Mario Zechner
599127ea36 docs(chord): decision: hostAddress instead of placement 2026-10-29 07:24:20 +00:00
Mario Zechner
0ddc953102 docs(chord): dependency model: placement required on remote types 2026-10-29 07:17:49 +00:00
Mario Zechner
13dee75529 docs(chord): every remote type declares its placement 2026-10-29 07:17:22 +00:00
Mario Zechner
1b760044ec docs(chord): note: every object may contribute (for later) 2026-10-29 07:06:37 +00:00
Mario Zechner
67c63c0f7f docs(chord): dependency model: the dependency check applies R18 after live, not a rule of its own 2026-10-29 06:49:17 +00:00
Mario Zechner
e65b3b88c0 docs(chord): dependency model proposal for slice 7 and its explainer (Parked Until Resolved) 2026-10-29 00:51:57 +00:00
Mario Zechner
556860a386 docs(chord): no client providers for now 2026-10-28 23:16:28 +00:00
Christian Klotz
c466a35957
fix(coding-agent): cap pi.exec output at 256 MiB (#11546)
Closes #11360.

## Summary
- Cap combined stdout/stderr with `ExecOptions.maxBuffer`, defaulting to
the smaller of 256 MiB and the runtime string limit. Invalid explicit
limits reject before spawning.
- Terminate oversized commands and return `truncated: true`, `killed:
true`, and a nonzero exit code. Close output pipes so writing
descendants cannot keep the call pending, and discard incomplete UTF-8
suffixes rather than adding replacement characters.
- Fix SIGKILL escalation for children that ignore SIGTERM, and simplify
completion to `await` with one cleanup block and result.
- Document the API and add regressions for byte limits, shell
descendants, UTF-8 chunk boundaries, and 32-bit runtime limits.

`ExecResult.truncated` is required. Existing extensions reading
execution results continue working; typed mocks or wrappers constructing
results must add the field.

## Verification
- `npm run build` and the post-rebase `npm run build:offline`
- `npm run check`
- 39 focused execution and extension tests
- `./test.sh` passes after building the env daemon. Earlier runs hit
remote-cleanup and reftable-watcher timeouts; both passed in focused
reruns and the final full run.
2026-10-29 00:13:41 +02:00
Mario Zechner
6eb99ad65b feat(chord): Chord v2 slice 8, registries and host.root
Registries: shared tokens (defineRegistry), one provider per host (host.serve(R, { compose, accepts })),
contributions from facets and host-lifetime objects, plain values or followed Chord watchables, a table
sorted by plugin, lazy compose in the batch that changed the table, failures left out and reported with
the last good value kept; self.registry as a declared dependency, host.registry outside objects, older
readers mapped by accepts[old].value; keyedEntries and orderedEntries. Native sources: self.writable,
registry values and ref.state. self.retryOn wakes failed objects on registry values. host.root gives code
outside objects a call scope sealed at settle, abort and host.close.

Durable: registryReader(value, context); a Session close ends synchronous observers failed
("session closed"), watches still end ended.
2026-10-28 21:34:56 +00:00
Mario Zechner
edab3efa83 docs(chord): slice 8: only Chord watchables are followed as contributions 2026-10-28 21:07:42 +00:00
Mario Zechner
790a98b9ab docs(chord): design §17 compose failure and helper names as built 2026-10-28 21:07:05 +00:00
Mario Zechner
1ce44bedeb docs(chord): slice 8 S2 and S3 wording as refined 2026-10-28 21:05:01 +00:00
Petr Houška
a87b742ff4 docs(env): Windows commands' descendants survive a terminated daemon
The job object uses silent breakaway, as libuv does, so it only kills each
command's own process.
2026-10-28 21:09:58 +01:00
Petr Houška
b6d5c2ecdc fix(env): kill the daemon's commands when it is terminated
Connection.close() ends stdin and sends SIGTERM at once. The daemon did not
handle SIGTERM, so it died before its end-of-input kill_all, and running
commands were reparented to init. The daemon now blocks SIGTERM, SIGINT and
SIGHUP before starting any thread and waits for them on its own thread, which
kills every process group it started and exits. On Windows the job object
already kills commands with the daemon.
2026-10-28 21:09:58 +01:00
Mario Zechner
391e83fb17 fix(chord): Chord v2 follows its design where the audit of slices 1-6a found it did not
A failed activation releases its references after its natives closed; a dependency going live while
the dependent activates retries it at once; a call on a local object runs with the host's principal
and incarnation; retiring an unloaded document checks its stored version and fingerprint, and a cut
retirement no longer fails its successor; ref.retry() re-runs a refused hold's start on the same id
(the provider keeps refused holds), and ends released/closed on a closed host; a relayed hold waits
while the parent link is down; a link drop while a dependency is still loading is no failure.
2026-10-28 19:50:41 +00:00
robopipi[bot]
92c1da1f62
fix(env): fail exec and pty with spawn_error when a helper thread cannot start (#11540)
The pi-env daemon started an `exec`'s stdout and stderr readers and its
waiter with `thread::spawn`. `thread::spawn` panics when no thread can
be created, for example when a cgroup `TasksMax` or `RLIMIT_NPROC` limit
is reached. When that happened after the child was already running:
- only the request thread unwound, so the exec never got a reply;
- its control channel was dropped, so `cancel` and `kill` reached
nobody;
- the child kept running, even after the daemon exited.

`pty` started its reader, input writer and waiter threads the same way.

## Changes
- `exec.rs`: `spawn_reader` now uses `thread::Builder::spawn` and
returns an `io::Result`. If a reader or the waiter cannot start, `run`
sets `stop`, interrupts any readers already started, kills the process
group, reaps the child, removes it from `groups`, and returns
`spawn_error`.
- The waiter thread now gets the `Child` through a channel once it is
running. If the waiter cannot start, the child stays with the caller,
which can still reap it. Before, the failed thread's closure dropped the
child without waiting for it.
- `pty.rs`: the same handling for the reader, writer and waiter threads.
On failure the terminal is also removed from `terminals`.

## Testing
- Manual reproduction with a script that runs the daemon as `prlimit
--nproc=N -- pi-env`. It starts 30 `sleep 30` execs 200 ms apart, aborts
them all, then closes the connection. N ran from 37 to 70.
- Before: for most N, one or two execs stayed pending forever after
abort, and a `sleep 30` kept running after the daemon was closed.
- After: every exec settles, either `aborted` or `spawn_error`, and no
`sleep` is left running.
- `cargo build`, `cargo clippy --all-targets`, `cargo fmt --check` and
`cargo test` all pass. The `packages/env` vitest suite passes (78
passed), and `npm run check` passes.
- There is no automated regression test. A thread-creation failure
cannot be triggered reliably in a test: `RLIMIT_NPROC` counts all of the
user's tasks and does not apply to root, and the daemon has no
fault-injection hook.
- The Windows build was not compiled. The `exec.rs` change uses only
existing cross-platform calls, and the `pty.rs` change is Unix-only.

Fixes #11433

Requested by @petrroll

---------

Co-authored-by: triagent <triagent@localhost>
Co-authored-by: Petr Houška <houskape@gmail.com>
2026-10-28 20:44:25 +01:00
Mario Zechner
6975eb18a6 docs(chord): design §3 route context, §7 retry of a refused hold 2026-10-28 19:44:10 +00:00
Mario Zechner
e613c7487b docs(chord): design §3 placement: a parent link that is down waits 2026-10-28 19:41:25 +00:00
Mario Zechner
9017ce0813 docs(chord): slice 6b built 2026-10-28 19:27:58 +00:00
Mario Zechner
14b4c56e57 feat(chord,durable): Chord v2 slice 6b, Durable and jobs on the store
StoreStorage (Durable's storage on Chord v2's store, @earendil-works/pi-durable/storage/store) passes
Durable's storage conformance suite and runs Durable's harness tests (npm run test:store). The store
gets owner names and claims, one id allocator, Durable's Storage semantics for refused steps, history
none/latest/rewindable with reads at a point, archive revisions and applyBatch. self.follow follows
Durable's synchronous observers as Chord sources. Persisted jobs: records and state in the owner's
store, restarts as Durable's tasks, abort stored, deferred state. Chord's ./next export returns for
the adapter surface.
2026-10-28 19:18:30 +00:00
Petr Houška
b5fc6dc84f fix(coding-agent): strip private-parameter CSI sequences in stripAnsi
stripAnsi treated <, = and > after ESC [ as final bytes, so ESC[<35;59;1M
(SGR mouse report) left 35;59;1M in tool output. Accept one private prefix
byte only directly after a CSI introducer (ESC [ or 0x9B). After a bare ESC
these bytes stay final bytes, so ESC= and ESC> (keypad modes) keep the
digits of the text that follows.

closes #11377
2026-10-28 20:14:09 +01:00
Mario Zechner
4e5cd1a973 docs(chord): slice 6b resume watch cost accepted 2026-10-28 19:03:49 +00:00
Mario Zechner
33385f9572 docs(chord): attribution of the StoreStorage.close rule 2026-10-28 18:53:50 +00:00
Mario Zechner
ec2c8ed311 docs(chord): slice 6b reopen and commit numbers accepted 2026-10-28 18:51:30 +00:00