Commit graph

146 commits

Author SHA1 Message Date
Armin Ronacher
06a4669355 Release v1.2.0 2026-10-13 00:46:55 +02:00
Mario Zechner
55df12bf62 docs(durable,env): changelog entries for listFiles 2026-10-12 15:22:48 +00:00
Mario Zechner
abfe62928b feat(durable): optional ExecutionEnv.listFiles for tree listings
An environment may list the files, directories and symbolic links below a
directory: relative / paths, depth first in name order, without .git and
what git ignores in work trees, bounded with a truncated flag. Callers fall
back to their own walk when it is missing or fails with not_supported.
The env conformance suite checks it with the new listFiles option.
2026-10-12 15:22:14 +00:00
Mario Zechner
6db4070f17 docs: describe only the current Chord, Durable, and coding-agent code
Chord: README rewritten for the new runtime; notes.md and requests.md removed, their open limits and
decisions folded into spec.md section 12; guide and spec status lines updated. Durable: drops the
pico-v5 handoff and delta findings, renames the Pico spec, and uses current Chord terms. Delta README,
issue categories, and PI_RADIUS_GATEWAY updated.
2026-10-12 10:47:09 +00:00
Mario Zechner
0104fef1e9 feat(chord): replace the facet and service runtime with the chord2 host runtime
Moves chord2 (contracts, objects, cells, roots, plugins, hot reload, memory/IPC/WebSocket links)
from p2 into packages/chord, with its tests (now on vitest) and docs. The root export is the new
runtime plus the shared base (Context, JsonValue, JsonRepresentation, Draft, copyJson, isJsonValue,
replicatedState and its types). ./node now holds the host manager, child control, process locks,
and nodeLoader. ./context and ./delta are unchanged; ./bundler is removed.

replicatedState stays a standalone module with the same exports. It no longer exposes the exact-op
subscription the removed service provider used, so listener failures of mutable states are always
reported asynchronously. The package depends on typebox instead of esbuild. The wire format stays JSON.
2026-10-12 10:41:59 +00:00
Mario Zechner
2ee5c1e56d docs(durable): drop the Chord facet usage guide and its test
The guide and chord-guide.test.ts used Chord's facet and service runtime, which is being replaced.
2026-10-12 10:41:59 +00:00
Mario Zechner
eb2fce43d2 fix(durable): reject waits that close a wait cycle
A wait is rejected when it closes a cycle in the wait-for graph: waits of
tasks that are not abort-marked, and holds of live ordinary owned work. The
rejection names the cycle. Replaces the owner-chain rule, so a background task
may wait on its conversation's owner. Approach and tests from #10533 by
@odysseus0.

fixes #10411
2026-10-12 10:15:26 +00:00
Mario Zechner
77332f7a34 feat(durable): deferred tools and the tool_search extension
- ToolRegistration.deferred: not offered to the model until modelTools names it
- modelTools gains { remove?, add? }; control.addTools writes names to add, so
  loaded tools follow the others in load order and a load only appends
- Agent.deferred lists deferred tools not loaded yet
- @earendil-works/pi-durable/tool-search: the coding agent's tool_search (BM25)
- the codemode section leaves deferred tools out
2026-10-12 08:05:26 +00:00
Armin Ronacher
f11e50f72e fix(coding-agent): make codemode output joining linear
joinAdjacentText checked endsWith on the growing joined string, which
makes V8 flatten the concatenation on every item. Collect each text run
in an array, check the separator against the last non-empty part, and
join once. Same fix in the durable code mode copy.

closes #10678
2026-10-12 10:03:56 +02:00
Armin Ronacher
c661bc574f fix(durable): drop shared MCP types from the codemode section
pi-codemode no longer exports MCP_TYPESCRIPT_PREAMBLE or mcpStructuredContentSchema(); output schemas render as their own types, as in the coding agent.
2026-10-12 09:21:04 +02:00
Mario Zechner
8bb180bec1 feat(durable): code mode listing, tool search, output, models.*, grammar; tool error rule
- structuredOutput is always the tool's own value; a schema tool's error text
  without one becomes a tool_error diagnostic on nested results
- code mode: a schema tool's error result with its value resolves (bash nonzero
  exit), schema-less tools resolve with their text, as in the coding agent
- codemode prompt section with tool declarations by namespace within an inline
  budget; searchTools/describeTool/describeNamespace from the start data
- result layout of the coding agent: header, text markers, console block,
  truncation within max_output_tokens and the tool's output limits, spill and
  image files in a recorded output directory (a rerun overwrites them)
- models.*: catalog globals; classify/generateImages as hidden internal tools
  (nested calls with usage), at most four at once
- Lark grammar of the script source as constrainedSampling
2026-10-12 00:06:09 +00:00
Mario Zechner
04566631c2 fix(durable): closing the code mode sandbox does not decide the script's outcome
A host that closes its CodemodeSandbox before the Harness, as on a graceful
shutdown, made the running code mode call store "Script error (aborted):
Sandbox closed" as its result, so the script never resumed. A run the sandbox
aborts without the call asking for it now stores nothing and ends with the
call's invocation; the next process replays the script.
2026-10-11 22:04:18 +00:00
Mario Zechner
127b77121e feat(durable): code mode result lists the script's tool calls
details.calls (CodemodeToolDetails): each tool call the script made, in call
order, with status ok/error/cancelled, duration, and cut arguments and error;
calls still running or not yet launched when the script ended are cancelled.
Written once with the result: live progress is pi.live.nestedTools.
bench:codemode compares a code mode script with a plain tool making the same
nested calls: 1,000 parallel calls 0.82 vs 0.78 ms/call, sequential 1.52 vs
1.10 ms/call (two more commits per call: the journal entry and the count).
2026-10-11 22:00:44 +00:00
Mario Zechner
7b597d1481 feat(durable): durable code mode (@earendil-works/pi-durable/codemode)
A codemode tool whose scripts call the conversation's tools as nested calls and
survive a restart. The script runs deterministically (pi-codemode start()); each
result is journaled before it is delivered. A rerun starts the script again from
the stored start data, delivers the journaled results in order (nested calls
reattach by key c<id>), checks every recorded call count, holds calls the
journal lacks until replay ends and checks them against earlier attempts' keys,
then continues live. A call launches only once the count of the drain that made
it is committed, so a rerun can prove which calls are new. Divergence, a changed
module version or sandbox fingerprint, and a passed deadline end the call with a
stored error; a decided outcome is returned without a VM. store()/load() keep
values per conversation, rewindable and forked as of the fork's entry.

pi-codemode: CodemodeRun.finished, set when the outcome is decided.
2026-10-11 21:31:48 +00:00
Armin Ronacher
cd8bdf7518 fix(durable): allow image conversion tests more time on CI 2026-10-11 20:25:00 +02:00
Mario Zechner
091bcc0998 fix(chord): append to arrays without walking or copying them twice
The delta tracker planned every structural array change with a walk over all
base elements; inserts and removals that keep the remaining elements in order
now get the same plan from the changed ranges alone. applyImmutable built an
appended array as a copy that the insert then grew, a second copy; an append now
builds it with one concat. Ops, values, immutability and errors are unchanged.
32,000 one-item appends, one commit each: 11.3 s -> 1.4 s
(npm run bench:document-append in packages/durable).
2026-10-11 18:04:43 +00:00
Mario Zechner
d77d2752e5 feat(durable): a schema tool's error result without structuredOutput gives programs its output
Programs calling a tool with structuredOutputSchema got isError and no text when
its error result omitted structuredOutput; they now get its bounded output,
collapsed like a schema-less tool's, so a code mode script sees the error text.
A thrown error's message stays in its tool_error diagnostic.
2026-10-11 18:04:34 +00:00
Mario Zechner
89e5c2c075 fix(durable): fail a run whose blocking compaction failed; keep compaction failures visible
A generation waiting on a blocking threshold compaction sent its request
whatever the outcome. When summarization failed, the context was still above
the blocking threshold: a session at 996K of a 1M window got an empty length
answer on every turn and nothing showed the error. The generation now fails
with model_error (Compaction failed: <error>) when its compaction failed,
faulted, was orphaned, or was aborted directly; a compaction that found
nothing to compact or was declined still lets the request go ahead.

pi.live.compactionFailure keeps the newest failure (task, reason, error)
until a compaction produces a summary. The agent events' snapshot carries it,
and compaction_end carries error for a failed compaction.
2026-10-11 15:45:13 +00:00
Mario Zechner
15e7bdc14c fix(coding-agent,durable): omit thinking from compaction summaries
Replayed thinking is often not part of the live context (for example,
radius drops it, and Anthropic drops blocks that do not match their
request), but serializeConversation included all of it. A Pi Mobile
session at ~996K of 1M context tokens produced 1.38M-token summarization
requests that failed on every attempt. Providers only return summaries of
the reasoning anyway; visible text, tool calls, and tool results record
what happened.
2026-10-11 15:45:13 +00:00
Mario Zechner
e6b9a6af5d feat(durable): inputs placed as application entries, display-only writes appended at once
- Input submissions take entry: { kind, data } (InputEntry): placed as that
  kind with that data and the content as user message, instead of pi.user.
  Kinds must be non-empty strings outside pi.*. Queued inputs carry it in
  their pi.inbox item.
- Writes with only kind and data (no model, head, edits) are appended and
  settled done at once, busy or not; only a queued reset holds them back.
- Experimental hosts render such inputs as user messages and title by them.

closes #10407
2026-10-11 13:12:44 +00:00
Mario Zechner
cdaf6c227d feat(durable): prepare images in a worker thread in /images/node
createNodePhotonImages() now runs Photon in a worker thread, so preparing a large photo (a second or two) no longer blocks the event loop: in the test a 6-megapixel image blocked the loop over 200 ms inline and under 100 ms with the worker. The worker keeps the process alive only while it prepares an image, exits after 30 s without images to free Photon's WebAssembly memory, and a worker that cannot start falls back to the calling thread. Options: { worker: false, idleMs }.
2026-10-11 08:16:47 +00:00
Mario Zechner
f44707e7f7 feat(durable): bound the Session's document cache by use (documentRetentionMs)
The Session kept every document it loaded or created in memory for the life of the process: every conversation and
session document ever touched, built-in and third-party. Now a cached document that no watch or document state
observes leaves the cache once it was not read or written for `documentRetentionMs` (HarnessSettings and
createSession options; default ten minutes, 0 drops it right after use, Infinity keeps documents; anything that is not
a number of milliseconds means the default), and loads from Storage again on its next access. An observer pins the
incarnation it observes and releases it when that incarnation retires.

Eviction runs on the mutation line, after every commit outcome (published, no-write, rejected, failed settlement) and
from one unreferenced timer for the next expiry, so a transaction never loses a tracker it holds. Where timers cannot
be unreferenced only commits evict. Cache bookkeeping is contained: a throwing clock or setting never changes an
operation's outcome. A hot document moves in the eviction order at most once per granule (a tenth of the retention,
up to 1 s), keeping the fan-out bench at HEAD's speed.

`npm test` runs the durable suite a second time with every unattached document evicted right after use.
5,000 conversations kept 26,000 documents and about 3.8 KB each; with a 50 ms retention, after waiting, none
(bench:document-cache).
2026-10-11 08:03:35 +00:00
Mario Zechner
ea448f454a fix(durable): bound the scheduler's ownership indexes by live work
The scheduler kept every conversation edge it saw and, for conversation owners, chain loads, and owners of known
tasks, the ended tasks above them, for the life of the process. Now an owner edge or an ended task's ownership is kept
only while something is listed below it in #below: only the chains above live work are known, whole. Unneeded nodes
are queued and swept at the end of each publication, or by a sweep job on the Session line after operations that
publish nothing (rejected commits, reads), never during an operation, so a commit that creates work below a chain its
own callback loaded keeps it. Later work in an old conversation loads its chain from storage again.

Removes #conversationOwners, #pinned, #knownChildren, and #forget. A queued input triggers the withdrawal scan only
while some task has cancellation intent, and chains are loaded only for queued inputs.

10,000 ended subagent cycles under a live parent: 10,000 settled tasks and 10,001 edges before, 0 and 1 now
(bench:scheduler-memory); a later message into an old subagent conversation reads 7 records instead of 5.
2026-10-10 19:29:35 +00:00
Mario Zechner
e37cdc4206 fix(durable): drop redundant nested-call parent check; test ownedTasks() with a held child 2026-10-10 17:40:24 +00:00
Mario Zechner
b9f2b3e827 docs(durable): read only through tx inside a commit callback
Session, Harness, and runtime reads such as snapshot(), documentState(), watchDoc(), and getTask() may queue on the mutation line a commit callback holds; on a cache miss they never resolve. Documented on Session.commit, TaskRuntime.commit, and in the spec's pitfalls.
2026-10-10 17:36:39 +00:00
Mario Zechner
4ac0bd8c7b feat(durable): one document per nested call; TaskRuntime.ownedTasks()
NestedCallsDoc (one index per caller, a key per nested call, copied on every admission) and NestedResultDoc are
replaced by NestedCallDoc (`pi.tool.nested-call`): a task-scoped family of the caller keyed by the call's key,
holding { taskId, result? }. Admission creates the member; the nested call's terminal commit adds its result;
executeTool() reads it by key. No commit rewrites another call's document.

TaskRuntime.ownedTasks(context) returns copies of the live tasks a task owns directly, whichever invocation
created them, from the scheduler's index. A tool call settling aborts the live nested calls among them, which
the removed index used to list. A task memo marks a tool that made a nested call in any attempt, so settling a
leaf still skips scanning pi.live.

8000 parallel nested calls: 9.2 s -> 2.7 s, linear (bench:scheduler-fanout).
2026-10-10 15:35:29 +00:00
Mario Zechner
cb43754931 fix(durable): scheduler no longer visits every live task on each pass
Every reconcile pass walked up from every live task to the root to find cancelling owners, and owned live work,
idle scopes, chain loads, reservation, finalization, and failFast checks each scanned all live tasks. A chain of n
held tasks cost about n^3 steps: 500 took 12 s, 20,000 never finished.

The ownership semantics are unchanged; in-memory indexes, rebuilt at open, hold each pass's candidates:
- #below: the ownership tree downward around live work; owned live work, idle scopes, and conversation aborts walk
  down from one node. Ended tasks stay while walks pass through them (conversation owners, chain loads, owners of
  known tasks), so loaded chains stay loaded.
- #intent: cascades walk down from tasks with cancellation intent, not entering another such task, so the nearest
  cancelling owner still decides the reason; upward checks return at once when it is empty.
- #unloaded: only tasks whose chain is not loaded are loaded, and count as inside every scope until then.
- #finalizeChecks: finalize is a worklist in rounds from the nearest live holder of ended work.
- #runnable and #waiters: reservation and failFast candidates.
No walk recurses.

Chain of 500 held tasks: 12.3 s -> 50 ms; 20,000: 1.5 s. 8000 parallel nested calls: 18.8 s -> 9.2 s
(bench:scheduler-fanout); the rest is document copying of the nested-call index.
2026-10-10 14:56:43 +00:00
Mario Zechner
42a3497d03 feat(durable): read returns images, optional Photon image processor
read returns an image file as one image block (programs get an ImageContent) plus an info diagnostic,
instead of an unsupported_image error. Limits are the conversation model's inputLimits.images.resize,
by default 2000x2000 pixels and 4.5 MB of base64.

- Without a processor: PNG, JPEG, GIF, and WebP within the limits pass through undecoded; the byte limit
  is checked before reading the file, dimensions from the header. Larger images and BMP are error results.
- With createCodingTools({ images }): the processor orients by EXIF, converts BMP to PNG, and shrinks to
  fit. ImageProcessor.prepare is async.
- @earendil-works/pi-durable/images: createPhotonImages(wasm) on @silvia-odwyer/photon 0.3.3 (web build);
  /images/node and /images/cloudflare load the wasm. The root and ./tools entries never reach it (enforced
  in check-entry-graphs).
- An image must be read whole: one that changes while read is read again, unlike a growing text file.
- A model without image input gets a diagnostic that it sees a placeholder.
2026-10-09 19:38:02 +00:00
Mario Zechner
eba8497395 feat(durable): nested tool calls, structured output, callers; storage errors end the Session
Nested tool calls: ToolExecutionApi.executeTool() runs another tool as its own pi.tool task owned by the caller,
keyed for replay-safe reattachment. Results for programs live in the caller's NestedResultDoc until it settles;
pi.live.nestedTools shows each call with the arguments it runs with and a summary. TaskOptions.abandonOnRestart
aborts work its creator awaited only in memory before it runs again after a restart.

Structured output: structuredOutputSchema and structuredOutput, validated; a tool without a schema gives programs its
bounded output (one text as a string, one image as itself, else the list). ToolExecutionResult.content is now output.
callers and modelTools decide who may call a tool.

Storage errors: any error a Storage method throws fails the Session, which closes itself; later calls and waits reject
with SessionFailed, watches end with session_failed, and Session.closed settles. Nothing retries in the Harness.
StorageRejected is removed; StorageRequestError marks invalid reads that fail only their call. Listener and onReport
errors are contained and reported.

Also updates scripts/issues/triage.ts and the vacation experiment for the content -> output rename.
2026-10-09 17:13:34 +00:00
Armin Ronacher
75a99721d1 Add [Unreleased] section for next cycle 2026-10-07 23:33:49 +02:00
Armin Ronacher
abe508e1b8 Release v1.1.0 2026-10-07 23:33:33 +02:00
Mario Zechner
70759f48be fix: address actionable npm audit findings 2026-10-07 23:00:59 +02:00
Mario Zechner
ae92585d3b fix(durable): report settings failures when checking kept context retention
The idle check reads settings.contextRetentionMs from commit listeners,
reconciliation, and the expiry timer. A throwing settings getter escaped as an
unhandled rejection; it is now reported and the kept contexts are dropped.
2026-10-06 21:10:40 +02:00
Mario Zechner
da866ada17 feat(durable): keep each conversation's context in memory; Durable Object SQLite adapter
runtime.context() keeps each conversation's last read range and derived
view. A later read by any of the conversation's tasks with the same head
marker scans and derives only the entries committed since, so a
generation no longer rereads and re-derives the whole transcript for
each model request. Busy conversations keep the range; idle ones for
settings.contextRetentionMs (default ten minutes), checked on task
changes and by one unreferenced timer where the runtime supports it.
Kept entries are frozen; each read returns its own view arrays.

Add openDurableObjectSqliteStorage(ctx.storage) in
@earendil-works/pi-durable/storage/sqlite/cloudflare: SQLite storage on
a SQLite-backed Cloudflare Durable Object, without per-row copies.
2026-10-06 20:58:35 +02:00
Mario Zechner
92216fa15c fix(durable): lead the model context with the initial system message
A run's input is committed before the system prompt is rendered, so requests
started with the user message. Anthropic then sent the tool list at the top of
every request and each tool change invalidated the whole prompt cache. Context
derivation now moves a system message preceded only by user messages to the
front. A live e2e test gates the cache reuse across a tool change.

closes #10542
2026-10-06 20:55:52 +02:00
Mario Zechner
4dd2af42c8 feat(durable): scan conversations, entries, tasks, and submissions in either order
ScanOrder is an optional order on each scan query; cursors carry it.

closes #10546
2026-10-06 20:03:03 +02:00
Mario Zechner
36a686ee8d feat: record response, tool execution, and durable task times
pi-ai's AssistantMessageEventStream sets AssistantMessage.durationMs with a
monotonic clock for every API implementation. The agent loop and the durable
tool task set ToolResultMessage.durationMs around execute(). Durable task
records carry startedAt/endedAt stamped by the Session. The coding agent's bash
renderer shows Took from the recorded duration, also after a reload.

closes #10549
2026-10-06 19:28:54 +02:00
Mario Zechner
636703a0a4 feat(durable): take the TaskRuntime.context() cutoff as an options object
Matches Conversation.context(context, { at }). refs #10512
2026-10-06 18:12:44 +02:00
David Brailovsky
311f0e020d fix(ai): update output limit assertions for token estimate 2026-10-06 15:58:16 +00:00
Mario Zechner
76f6c06daf feat(durable): read a conversation's context as of an earlier entry
closes #10512
2026-10-06 17:22:05 +02:00
Mario Zechner
b0114ef5fa feat(durable): expose models on ToolExecutionApi and HookApi
closes #10395
2026-10-06 17:14:43 +02:00
Mario Zechner
68ccef1760 fix(durable): reuse the scanned context range within a task invocation
runtime.context() keeps the range its last read scanned for the task
invocation. A later read with the same conversation and head marker scans
only entries committed after that range's tail and re-derives the view in
memory. A generation's request no longer rereads the whole transcript after
its prepare. The range is dropped when the invocation ends or sleeps.
2026-10-06 16:59:46 +02:00
Armin Ronacher
28dcce2ba4 Add [Unreleased] section for next cycle 2026-10-05 21:23:02 +02:00
Armin Ronacher
7c10bd4337 Release v1.0.4 2026-10-05 21:22:49 +02:00
Mario Zechner
b5b4906013 docs(durable): breaking watch changes in the changelog 2026-10-05 20:39:49 +02:00
Mario Zechner
68c22123be feat(durable): powershell tool over argv exec 2026-10-05 18:37:25 +02:00
Mario Zechner
ed94330a20 feat(env): hardened SSH bootstrap and release daemon tests 2026-10-05 17:32:22 +02:00
Mario Zechner
cd60a5b990 fix(durable): watcher coverage, growing reads, output BOM and progress defaults 2026-10-05 17:32:22 +02:00
Armin Ronacher
997d31f286 Add [Unreleased] section for next cycle 2026-10-05 10:21:24 +02:00
Armin Ronacher
d78dc83d63 Release v1.0.3 2026-10-05 10:21:10 +02:00