Source lock. This chapter describes pi-subagents 0.18.0 at
3f9d35cd078d18a141eb5a6d8f4fc5010d756280against Pi 0.84.2 at914cf1472e715297caa30db4b9535d534a9eb718. All source links are commit-pinned.
An agent run can leave several artifacts with similar-looking JSON, yet no single component owns “persistence.” Pi owns resumable conversation history. pi-subagents owns an optional streaming transcript, settings, memory instructions, schedules, and worktree cleanup. This chapter’s observable goal is to produce an evidence-backed diagnosis by naming each artifact, its owner, the event that writes it, and the proof boundary behind the claim.
1. Evidence has levels
Evidence strength is claim-specific. Use the strongest level that actually crosses the boundary in question.
| Level | What it establishes | What it does not establish |
|---|---|---|
| Contract | Public types, docs, or comments state intended behavior. | That the implementation honors the contract. |
| Source | The current implementation has a concrete owner and path. | That callers reach it or dependencies behave as assumed. |
| Mock wiring | A caller passes the right value and reacts correctly at a seam. | The real collaborator, filesystem, Pi session, or provider. |
| Real-Pi faux e2e | Real Pi sessions, extension wiring, and agent turns run with deterministic model responses. | Provider authentication, network streaming, or a real model’s choices. |
| Live provider | The installed provider/auth/network path works in the tested environment. | Determinism or every failure partition. Unverified for this chapter. |
Do not rank only by filenames. schedule-e2e.test.ts uses real timers and the real on-disk store, but a mocked AgentManager. The error-status and worktree suites boot real Pi and the real extension on a faux provider. Faux is not automatically weak: an empty provider-error turn is easier to prove with a scripted response than with a live model that cannot be ordered to fail in that exact shape.
2. Persistence matrix: six records, six owners
| Artifact | Owner and scope | Write/restore semantics | Guarantee boundary |
|---|---|---|---|
| Pi child session JSONL | Pi SessionManager; normally Pi’s session directory, optionally session_dir | Top-level agents persist by default; persist_session frontmatter overrides. Nested children default to memory. Resume opens the existing file. Pi appends messages on message_end; a new file is deferred until an assistant message exists. | Canonical resumable conversation tree, including message stop reasons. A path may be allocated before the file exists. |
.output streaming JSONL | pi-subagents output-file.ts; OS temp root keyed by uid, cwd, parent session id, and agent id | Agent-tool and nested-tool fresh spawns attach the writer; detached top-level resumes append. Scheduler, RPC, direct-manager spawns, foreground resumes, and nested resumes do not attach it at this commit. When attached, the initial prompt is written, then new messages flush at turn_end, before compaction, and at cleanup. | Human/tool-facing chronological projection, not Pi’s resumable session format and not an authoritative transactional log. Writes after the initial entry are best effort. |
| Operational settings | pi-subagents settings.ts; global <agentDir>/subagents.json plus project <cwd>/.pi/subagents.json | Each file is sanitized, then a shallow top-level merge gives project fields precedence. UI saves overwrite a project snapshot; code never writes global settings. Save failure leaves the live setting applied and reports “session only.” | Configuration state, not run history. The project write is not temp-plus-rename and has no inter-process lock. |
| Memory prompt and files | pi-subagents prompt builder plus the agent’s file tools | MEMORY.md contributes at most 200 lines to the system prompt. Writable agents get a directory and memory-tool guidance; read-only agents do not create it. Scope is user, project, or local. | A filesystem convention and prompt affordance. The extension does not automatically decide, validate, or commit memories for the model. |
| Schedule store | pi-subagents ScheduleStore; <cwd>/.pi/subagent-schedules/<sessionId>.json | Mutations take a PID lock, reload, write a temp file, and rename. Resuming the same Pi session ID selects its prior store; /new uses a different ID and file. In-process timers are armed at session_start and stopped on switch/shutdown. | File-backed definitions and last-known status can reload only when the same Pi session ID is restored and the JSON remains intact. It is not a daemon and makes no promise about corrupt/power-loss recovery, missed-run replay, sleep, or exactly-once execution. |
| Worktree branch | Git plus pi-subagents worktree.ts | Agent runs in a detached worktree at HEAD. On successful cleanup, all dirty non-ignored changes in that worktree—including memory files—are staged and committed; if the child already committed, cleanup need not add another commit. A pi-agent-* branch is created at the resulting HEAD, then the worktree is removed. | A recoverable branch only after successful branch creation. Cleanup can include changes beyond the immediate task; the temporary directory itself is not durable, and the main checkout’s uncommitted/staged changes are not included in the starting HEAD. |
There is also a smaller parent-session trace: on top-level completion, the extension calls pi.appendEntry("subagents:record", …) with status, result, and error. That is useful for history reconstruction, but it is not the child’s resumable session and does not replace any row above.
Recall
Which artifact must exist to reopen an evicted agent’s conversation: the .output file or the Pi child session file?
Answer
The Pi child session file. AgentManager captures session.sessionManager.getSessionFile() in an in-memory tombstone map, capped at 100 entries and cleared at parent-session boundaries. That permits post-eviction reopen only inside the same process and ownership lifetime. After process restart, pi-subagents does not rebuild handle-to-session-file mappings from subagents:record entries or child JSONL files. An in-memory child session returns undefined and cannot be reopened after eviction; the .output file is an optional display transcript with different schema and semantics.
3. Follow the write event, not the filename
Pi’s AgentSession receives agent-core events. On message_end, it calls SessionManager.appendMessage; the manager maintains an append-only tree whose entries carry id and parentId. SessionManager.create is persistent, open loads an existing conversation, and inMemory retains the same logical API without a file. pi-subagents chooses among those constructors; it does not implement Pi’s session codec.
The .output writer instead subscribes as an observer. Its counter tracks the portion of session.messages already projected. Compaction makes this coupling visible: Pi can replace the message array with a shorter summary. The writer flushes before compaction and re-anchors after a successful compaction_end; a microtask accounts for Pi trimming an overflow error after emitting that event. The real-Pi faux test proves manual-compaction array replacement and continued streaming at the pinned dependency; a fake-session unit test simulates the narrower post-event overflow trim. Both are compatibility evidence, not a public guarantee that upstream event order will never change.
Settings show a third write model. Malformed JSON produces a warning and contributes {}; invalid individual fields are silently omitted by the sanitizer. Project values override global values field by field. saveSettings then writes the supplied project object directly. Therefore “the toggle changed now” and “the toggle will survive restart” are separate outcomes, represented by the persisted flag and toast.
Memory is different again: the extension reads an index and injects instructions. A writable memory block eagerly creates the directory and tells the agent to use files; a read-only block only exposes existing MEMORY.md. Symlink checks and safe-name validation constrain path handling, but memory truth still depends on what the model writes.
Predict
Project settings contain outputTranscript: false and rememberAgents: true. Agent auditor.md declares output_transcript: true, persist_session: false, memory: project, and has read, write, and edit. One Agent call registers it with schedule; a later timer fire runs it with isolation: worktree. What can each of those two moments write?
Answer
- The scheduling call writes the job definition to the schedule-store file keyed by the parent Pi session ID. It does not immediately spawn a child, create child memory, open a worktree, or write a child session.
- When the timer fires, no
.outputfile is created for that scheduled run at this pinned commit.output_transcript: truedoes not bridge the missing scheduler attachment. - No persistent Pi child session is expected because
persist_session: falseoverridesrememberAgents; the scheduled child session is in memory. - Project memory files can be written under
.pi/agent-memory/auditor/because memory is enabled with write-capable tools. For this worktree run, project memory resolves in the copied tree and reaches the resulting branch only if it is non-ignored and cleanup successfully preserves the worktreeHEAD. - Agent edits can reach a
pi-agent-*branch only through successful cleanup. These switches are independent; disabling or bypassing the transcript is never a “no disk writes” mode.
4. Terminal failure is a classification pipeline
A resolved promise is not synonymous with success. Pi may resolve a turn whose final assistant message has stopReason: "error". For a fresh spawn, pi-subagents inspects the final assistant message created during the current invocation, bounded by the message count captured before the prompt. Settlement priority is: an existing stopped state, hard abort, final-turn failure, soft-limit steered, then clean completion. Foreground resume has the narrower semantics described in Chapter 5.
| Observation | Manager terminal status |
|---|---|
| Existing external stop of running/queued record | stopped; it wins over late settlement |
| Hard turn-cap abort flag | aborted |
Final assistant stopReason: "error" | error, using errorMessage or a fallback |
Final stopReason: "length" with no text | error |
Final stopReason: "length" with text | completed with a truncated but useful answer |
| Soft limit requested wrap-up and run finished | steered |
| Immediate startup throw | Agent tool throws; the provisional record is deleted |
| Async run rejection, or a queued spawn that throws when drained | error |
| Clean stop, including an empty clean final message | completed |
Text and status are deliberately orthogonal. An errored turn may retain partial text, which the UI labels as partial output rather than laundering it into a clean answer. On resume, the starting message index prevents a failed empty turn from returning the previous successful answer. Startup failures are different: the tool execution throws so Pi marks the tool call failed; returning a diagnostic would falsely resemble a subagent result.
Scheduler status is a projection of this record state. After a spawned record settles, completed and steered become schedule success; error, aborted, and stopped become schedule error. That mapping does not make scheduling exactly once: a process can stop between a side effect and the store update, and overlapping interval fires bypass the normal concurrency queue.
5. Source-trace capstone: from symptom to owner
Suppose the parent model sees:
Agent failed: invalid request: provider rejected the prompt
Partial output before the failure:
EARLIER-PARTIAL-TEXT
Trace it backwards:
- Presentation owners —
index.tsandstatus-note.ts: the Agent tool chooses the error branch and printsAgent failed:;partialOutputSuffixsupplies the explicitly labeled salvage text. - Lifecycle owner —
AgentManager: a resolvedRunResult.failurebecomes statuserror;responseTextremains on the record. A hard abort has higher precedence, while an externalstoppedstate is not overwritten. - Classification owner —
agent-runner.ts:finalTurnErrorreads only the final assistant message in the current invocation. Earlier text is recoverable context, not evidence that the final turn succeeded. - Protocol owner — Pi
AgentSession: the assistant message and its stop reason arrive through real session events and are appended to Pi session history onmessage_end. - Independent observer —
.output: if enabled, streaming may also contain the messages, but it neither decides terminal status nor makes the conversation resumable. - Proof: the faux full-stack regression boots a real Pi loader, real extension, real runner, and real child session, then scripts the otherwise hard-to-induce empty error. It proves this pipeline without claiming live-provider verification.
Practice
A bug report says, “The transcript says the agent answered, but /agents says error.” Write a three-part investigation plan.
Answer
- Inspect the final assistant message for this invocation in the Pi session: its
stopReasonanderrorMessageown success classification. Do not infer status from non-empty earlier text. - Trace
finalTurnError → RunResult.failure → AgentManager.status → index.tsand verify partial text remains labeled. Use the real-Pi faux error regression for the exact failure shape. - Inspect
.outputseparately for projection gaps or duplication. Its presence proves best-effort observation, not successful settlement. If the report depends on provider streaming or authentication, add a live-provider reproduction and mark it unverified until run.
6. Brittle upstream bridges
Three seams deserve explicit compatibility alarms:
- Pi changed
createAgentSessionfrommodelRegistrytomodelRuntime, whileExtensionContextstill exposes the registry façade. pi-subagents reads(ctx.modelRegistry as …).runtimeand passes both fields. ThismodelRegistry.runtimebridge is intentionally type-cast and brittle; a mock-wiring test proves propagation, not that every supported Pi build exposes the field. - Transcript compaction recovery depends on Pi’s message-array replacement and overflow-trim event ordering. A real-Pi faux test guards array replacement under manual compaction; the overflow trim after
compaction_endis simulated by a fake-session unit test, so that narrower ordering claim has mock-wiring evidence only. - Child cleanup reaches
session.extensionRunnerto emitsession_shutdownbeforedispose(), with a timeout and graceful degradation for missing surfaces. This closes real lifecycle leaks but couples to upstream session internals.
Treat these as monitored dependency seams. When Pi changes, rerun the real-Pi suites and inspect upstream source before “simplifying” the adapters.
7. Explicit proof gaps
- Live provider: not run here; authentication, network backpressure, provider-specific stream errors, and billing paths remain unverified.
- Pi session durability: source and Pi tests establish append behavior, but this chapter does not prove power-loss durability, filesystem
fsync, concurrent writers to one session file, or recovery from a torn final line. .output: compaction and resume behavior are tested; disk-full, process-kill between events, concurrent writers, and best-effort write-loss recovery are not.- Settings: merge, sanitization, and save failure are tested on real temporary files; concurrent UI/process writes and crash-atomic replacement are not provided.
- Memory: paths, truncation, and symlink rejection are tested; no test proves that a model writes useful, current, non-conflicting memory.
- Scheduler: tests cover stale PID-lock recovery, sequential lock release, temp-file replacement, real timers, and mocked-manager status projection. They do not exercise simultaneous writers or crash atomicity. There is no real-Pi scheduler e2e here, nor proof of missed-run replay, exactly-once firing, long-duration clock behavior, sleep/wake handling, or process-independent execution.
- Worktree: a real-git/real-Pi/faux e2e proves the successful branch path. The outer cleanup catch can force-remove a corrupted worktree and return
{ hasChanges: false }, losing work without surfacing the failure. Reliability beyond the success path must not be claimed.
8. End-to-end capstone: prove the complete delegation path
Without reopening earlier answers, trace one top-level foreground Agent call and then contrast the background delivery branch. Your artifact must be a table with one row per edge:
tool registration -> parameter validation -> AgentRecord / queue decision
-> runAgent() -> createAgentSession() -> prompt() -> record settlement
-> foreground tool result OR background follow-up delivery
For every edge, record:
- the component that owns the transition;
- one commit-pinned source or test that crosses it;
- the observable state before and after it; and
- one proof gap or guarantee that the evidence does not establish.
Finish with two boundary statements: why the child is an in-process Pi AgentSession, not an OS sandbox; and why background delivery is not a second execution engine. If an edge cannot be supported at the pinned versions, label it unverified instead of filling it from memory.
Scoring rubric
- Complete trace: every listed edge is present, ordered, and distinguishes Pi runtime ownership from pi-subagents policy ownership.
- Lifecycle prediction: the trace explains queue membership, foreground/background divergence, settlement, and at least one failure outcome.
- Effective policy: the child configuration identifies the winning model, prompt/context, tools/extensions, persistence mode, and isolation semantics.
- Evidence discipline: each transition names its owner and uses pinned source or a test whose boundary actually supports the claim; gaps stay explicit.
- Security boundary: the conclusion separates capability shaping, Git worktrees, and in-process sessions from process, filesystem, network, and credential isolation.
You can now
- trace an
Agentinvocation from Pi’s extension contract through a childAgentSessionand back to its parent; - predict record, queue, control, settlement, and delivery behavior for foreground, background, and nested runs;
- resolve the effective model, prompt, context, tools, extensions, persistence, and isolation policy of a child;
- identify the owning component and strongest available evidence for each behavior; and
- state the security and reliability boundary without turning an implementation control into a sandbox guarantee.
Source index
pi-subagents, commit 3f9d35cd078d18a141eb5a6d8f4fc5010d756280
- Agent construction: memory injection, terminal classification, session choice and the runtime bridge, and invocation-bounded output
- Manager ownership: record settlement and worktree result attachment and session-file tombstones
- Transcript projection and compaction re-anchor:
src/output-file.tslines 40–144 - Partial-output labeling:
src/status-note.tslines 82–90 - Settings merge/write and persistence reporting:
src/settings.tslines 376–489 - Memory paths and prompt blocks:
src/memory.tslines 20–178 - Schedule store and timer dispatcher:
src/schedule-store.tslines 17–152,src/schedule.tslines 49–297 - Host wiring: session-bound scheduler, switch/shutdown, transcript gate, stream attachment, and user-visible failure
- Worktree success and destructive failure path:
src/worktree.tslines 60–210 - Evidence fixtures: real-Pi faux error status, real-Pi compaction, fake-session overflow ordering, real-Pi/real-git worktree, and real timers with mocked manager
Pi, commit 914cf1472e715297caa30db4b9535d534a9eb718
- Pi session manager: append-only tree and deferred first flush and create/open/in-memory constructors
- Agent event persistence on
message_end:agent-session.tslines 620–665