packages/agent/src/agent.tskeeps the fork run-loop surface on top of upstream: thebuildProviderContextre-export fromagent-loop.ts, and the config passthroughstimeoutMs,streamStartTimeoutMs,removedToolHints,resolveUnknownToolCall,abortServerSideFallback, andcursorExecHandlers.
These are fork-owned product surfaces (senpi branding, provider wire behavior, fork runtime features) that upstream does not carry; the sync must re-assert them on top of upstream's tree.
The divergence lives in core wiring, package identity, or build plumbing that executes before any extension loads, so no extension hook can express it.
- The
AgentConfig/loop-config type blocks and theagent-loop.tsimport list inpackages/agent/src/agent.ts.
packages/agent/src/agent.tsaccepts an abort reason and emits a provider-owned assistant abort for retry-watchdog cancellation.packages/agent/src/agent-loop.tspreserves an explicit abort Error instead of replacing it with genericRequest was abortedtext.packages/agent/src/assistant-terminal-state.tsstamps provider provenance where terminal stream failures are constructed.packages/agent/src/index.tsexports the typed watchdog abort reason for session hosts.
- The session watchdog must carry the real provider stall cause through low-level Agent cancellation so retry classification and terminal reporting do not lose the provider failure.
- Abort reason propagation and assistant failure-message construction occur inside the browser-safe agent lifecycle.
- LOW:
agent.tsabort API andagent-loop.tsevent-reader cancellation path.
packages/agent/src/agent-loop.ts:streamAssistantResponsecatch now treatsStreamIdleTimeoutErrorafter Cursor-resolved tools or buffered exec results as a finished turn (stopReason: "stop") instead of a terminal error.packages/agent/src/assistant-terminal-state.ts:isStreamIdleTimeoutErrorandshouldFinalizeIdleAsStopdecide when that idle is a completed turn versus a real hang.
- After Cursor-resolved tools (or buffered exec results) the parent stream can sit silent until the 300s idle timeout and die as
StreamIdleTimeoutErroreven though the child work already finished (issue #997).
- The idle reader and
streamAssistantResponsecatch live inside the agent loop; no extension hook sits between the idle timeout and the terminal assistant message it currently emits.
packages/agent/src/agent-loop.tsstreamAssistantResponsecatchpackages/agent/src/assistant-terminal-state.tsidle helpers appended aftershouldTerminateAssistantTurn
packages/agent/src/assistant-terminal-state.ts:promoteStopWithPendingToolCallsrewrites assistantstopReasonfromstoptotoolUsewhen the message still containstoolCallblocks; text-only stop stays terminal.packages/agent/src/agent-loop.ts: apply that promotion after streaming so pending (non-exec-channel) tool calls execute in the same turn and their results go back to the model. Cursor exec-resolved blocks stay filtered out of the local batch and do not re-enter the loop.
- Cursor often ends a turn as
stopwhile toolCall blocks are still present. The loop treated that as a finished turn and dropped the pending tools (issue #1010).
- Stop-reason classification lives inside the agent loop after the stream returns; no extension hook sits between stream completion and tool-batch execution.
packages/agent/src/assistant-terminal-state.tspromotion helperpackages/agent/src/agent-loop.tssuccess path afterstreamAssistantResponse
packages/agent/src/agent-loop.ts: whenconfig.cursorExecHandlersis a factory, the loop now resolves it with the outer owning-run signal (signal ?? requestAbortController.signal) instead of the per-request idle-timeout controller, and normal request completion aborts the request-scoped fallback so signal-less direct loop callers cannot leave stale handlers live.
- The bridge session (
cursor-exec-bridge-session.ts) verifies ownership by identity against the agent's live run signal. The per-request controller is a different object by construction, so every native Cursor exec frame failed the check and returnedTool execution has no active run(issues #979/#1000/#1003, regression from 31a71f0c5).
- The factory resolution happens inside the loop's provider-request assembly; no extension hook sits
between
streamAssistantResponseand the provider options it constructs.
agent-loop.tsprovider-request assembly and the requestfinallyteardown (fork-only Cursor exec channel; upstream has no cursor provider).
If the provider stream goes idle after Cursor-resolved tool calls (or buffered exec results) and there is no pending local work, the turn ends as stop instead of StreamIdleTimeoutError. A hang with no tools is still an idle error.
Conflict zone: agent-loop.ts streamAssistantResponse catch.
packages/agent/src/agent-loop.tsstays divergent from the new pin on the fork's own turn machinery: per-request stream bounds (StreamStartTimeoutError/StreamIdleTimeoutError, theinitialRequestTimeoutMs/initialRequestStreamStartTimeoutMsoverrides that apply to the first provider request only, after which the configured idle timeout resumes so a healthy reasoning gap is not bound by the short liveness probe); queued-input recovery (drainedTerminatingQueueplusrefreshTerminatingQueueDrain, which hands steering/follow-up messages back toconfig.restorePendingMessageson every terminating path instead of dropping them);streamKind: "main"stamped on the loop's own provider request so auxiliary calls stay distinguishable downstream; thinking-blockstartedAt/endedAtstamping from thethinkingTimingmap at stream-event receipt; the Cursor exec-channel bridge (handler factory resolved with the outer owning-run signal rather than the provider request's idle-timeout signal, mid-stream tool results buffered and appended,kCursorExecResolvedblocks excluded from the executable tool batch);withEmptyAssistantRecoveryaround the stream fn; and theprepareNextTurnmerge ofthinkingSelectionandabortServerSideFallback.packages/agent/src/agent.tsstays divergent on the run-ownership surface those loop features require:AgentContinuationOptions(deferQueuedMessages,timeoutMs,streamStartTimeoutMs),continueWithQueuedMessages()— queue-first continuation that re-delivers drained steering input when a compaction leaves custom context at the tail — theclearGenerationcounter andprepend()on the message queue,suppressQueuedMessageDrain()for one active run, therestorePendingMessageswiring back into the queues, and the runtime options carried onto the loop config (timeoutMs,streamStartTimeoutMs,removedToolHints,resolveUnknownToolCall,abortServerSideFallback,cursorExecHandlers).
- Upstream
59a71b235dhas no per-request stream bounds, no queued-input ownership contract, and no provider-executed-tool channel, so every one of these behaviors re-diverges on merge rather than being reconciled away. The behavioral rationale for each lives in the dated entries below (stream-start and continuation-scoped timeouts 2026-07-29, empty-assistant recovery 2026-07-30, Cursor exec-channel contract 2026-08-16 and 2026-08-18, thinking-selection provenance 2026-08-18); this entry records that the sync to the new pin leaves both files divergent for exactly those reasons.
- Stream-request construction, abort-signal ownership, the pending-message queues, and the tool-batch filter are the loop's own control flow. An extension observes turn events after the fact and cannot bound a stream that never emits, re-park input the loop already drained, or exclude a block from the batch the loop is about to execute.
- HIGH:
agent-loop.tsstreamAssistantResponserequest construction and the timeout/idle wrappers; the tool-call collection and execution block; theprepareNextTurnconfig merge. - MEDIUM:
agent.tsrunPromptMessages/continueentry points and the loop-config assembly that forwards the fork's runtime options.
packages/agent/src/types.ts:AgentLoopConfig.cursorExecHandlersalso accepts a(runSignal: AbortSignal) => CursorExecHandlersfactory.packages/agent/src/agent-loop.ts: when a factory is supplied, the loop resolves it with the outer owning-run signal. Direct loop callers without an outer signal retain the request controller as a scoped fallback, and normal request completion aborts that fallback so stale handlers cannot remain live.
- A host bridge built once per session cannot tell which run an exec frame belongs to. Handing it the owning run's signal at stream creation lets the host refuse a straggler frame from a stream whose run already ended, instead of executing it inside the replacement run.
- The plain-object form is unchanged, so existing hosts keep working.
- Only the loop knows which run owns the stream it is opening. The owning
signal exists solely inside
streamAssistantResponseat stream creation, so no extension hook can supply it to the host bridge after the fact.
agent-loop.tsexecHandlersinjection block,types.tscursorExecHandlersdeclaration.
packages/agent/src/types.ts:AgentStategainsthinkingSelection;AgentLoopTurnUpdategains a tri-statethinkingSelection(undefined leaves unchanged, null clears).packages/agent/src/agent.ts:createLoopConfigforwards the state selection alongsidereasoning.packages/agent/src/agent-loop.ts: mid-runprepareNextTurnupdates re-propagate the selection.packages/agent/src/proxy.ts: the selection joins the serializable proxy request options.
- Providers that encode reasoning on the wire (Cursor) must distinguish an explicit user choice from the
always-materialized effective level, which startup defaults to
medium.
- Loop config assembly, turn-update merging, and proxy request serialization are core agent-loop seams with no extension hook.
agent-loop.tsprepareNextTurn config merge,proxy.tsserializable option list,types.tsstate and turn-update interfaces.
packages/agent/src/agent.ts:Agent.emitExternalEvent()now accepts the originating run signal and discards bridge-generated lifecycle events when that signal no longer owns the active run.
- Cursor exec handlers can outlive an aborted provider stream. Their final
tool_execution_endevent previously reachedprocessEvents()afterfinishRun()clearedactiveRun, producing an unhandledAgent listener invoked outside active runrejection. - The ownership guard remains specific to externally injected events. Internal loop events still require an active run, and listener failures during the owning active run still propagate.
- The race occurs in the engine contract between the provider-owned Cursor exec handler and the agent run lifecycle, before an extension can intercept or recover the rejected event promise.
packages/agent/src/agent.ts: the external event entry point and active-run ownership checks.
Audit backfill (2026-08-17): the canonical four-section records added today were recorded during the repository-wide changes.md audit of divergences from the upstream pin (v0.84.2,
914cf1472e) so every audited production path assigned to this tracker carries a canonical record; they are dated by their underlying work. Legacy entries keep their original wording and detail.
- Recorded the fork divergences this tracker owns against the pinned upstream
(badlogic/pi-mono v0.84.2,
914cf1472e715297caa30db4b9535d534a9eb718) so the repository-wide changes.md audit reports them covered. The pre-backfill audit report assigned zero already-covered and thirteen uncovered production paths to this tracker; this entry is their canonical four-section record. - Audited production paths covered by this entry:
packages/agent/src/agent-loop.tspackages/agent/src/agent.tspackages/agent/src/types.tspackages/agent/src/proxy.tspackages/agent/src/stream-fn.tspackages/agent/src/harness/types.tspackages/agent/src/harness/messages.tspackages/agent/src/harness/reducer.tspackages/agent/src/harness/env/nodejs.tspackages/agent/src/harness/session/state.tspackages/agent/src/harness/compaction/branch-summarization.tspackages/agent/src/harness/compaction/compaction.tspackages/agent/src/harness/compaction/utils.ts
packages/agent/src/empty-assistant-recovery.tsandpackages/agent/src/assistant-terminal-state.tsare fork-only files absent from the pin tree, so the audit exempts them; their behavior stays recorded in the 2026-08-09 and 2026-07-27 entries.- Legacy entries predate the canonical four-heading format (their "What changed and why" style does not canonicalize), so the per-change detail for the paths above remains in those dated entries; the audit-backfill sections added today carry the canonical records for the harness reducer, session store, compaction, and stream-function surfaces.
- Root policy requires every fork-specific source change to update the nearest
changes.mdin the same verified increment, andscripts/audit-changes-md.mjsnow enforces the canonical-section contract mechanically. Without this record the gate reports every agent-core divergence as untracked.
- Tracker hygiene for fork-owned agent-core divergence. The audited surfaces themselves (loop scheduling, harness session and compaction internals, proxy wire types, stream-function plumbing) execute below the coding-agent extension runtime, as the per-change entries already document.
- NONE for this record itself (tracker prose only). The underlying per-file
zones are unchanged and stay listed in the dated entries: MEDIUM for
packages/agent/src/agent-loop.tstool-call collection and stream plumbing andpackages/agent/src/agent.tscontinuation/lifecycle queues; LOW for the harness type, reducer, session-state, Windows kill, proxy wire, and compaction content sites.
agent-loop.ts: the tool-call collection sites (loop collection and theexecuteToolCallsre-filter) skiptoolCallblocks stampedkCursorExecResolved— Cursor's server-driven protocol already executed those tools mid-stream through the exec bridge, and re-running them would duplicate side-effecting bash/write calls.streamAssistantResponsereturns{ message, providerToolResults }: whenconfig.cursorExecHandlersis set, the loop injectsexecHandlersplus a bufferingonToolResultinto the stream options; buffered results are emitted as ordinarymessage_start/message_endevents and appended to the context right after the assistant message — including on terminal error/abort paths, so resolved calls never end up unpaired.- The idle watchdog (
readNextAssistantEvent) re-arms instead of failing when the provider stream reports pending local work (AssistantMessageEventStream.hasPendingLocalWork), because a server-requested tool run legitimately emits no events while it executes. agent.ts:AgentOptions.cursorExecHandlersflows onto the loop config;emitExternalEvent()(new) lets the exec bridge injecttool_execution_start/tool_execution_endlifecycle events for tools that run inside the provider stream, outside the loop's executor.types.ts:AgentLoopConfig.cursorExecHandlers.
- Tool-call execution skipping and transcript ordering are loop-core
decisions made between the provider stream ending and
executeToolCallsstarting; no extension hook exists in that window, and atool_callblock hook can only produce error-shaped results.
- MEDIUM:
agent-loop.tsat the tool-call collection block andstreamAssistantResponse's return shape (upstream returns the bare message). - LOW:
agent.tsoptions/config plumbing (additive),types.tsadditive field.
packages/agent/src/harness/reducer.ts: the durable-log projection guards invalidateToolStartandderiveToolBatchreplaced the negated disjunction (!assistantEntry || assistantEntry.type !== "message" || ...) with optional-chain narrowing (assistantEntry?.type !== "message" || ...), so tool-start validation and tool-batch derivation keep narrowing the projected assistant entry under the repository's warning-as-error type gate.packages/agent/src/harness/session/state.ts: the fork-target guard applies the same optional-chain narrowing (entry?.type !== "message") before rejecting a non-message fork target withinvalid_fork_target.packages/agent/src/harness/types.ts:getOrUndefinedis now a generic null-to-undefined normalizer — the fork removed the dead Result-unwrapping original on 2026-06-10 and the v0.84.x sync reintroduced the name with the narrowed semantics — and the harness error classes (FileError,ExecutionError,CompactionError) declare a typedreadonly causeassigned aftersuper()socausestays typed under ES2021 library declarations.- Consolidates the 2026-08-13 "Upstream harness type cleanup" and 2026-05-11 "Harness ES2021 diagnostic compatibility" records under the canonical four-section format; runtime behavior is unchanged.
- The merged durable harness code had to pass the fork's stricter diagnostics and library level without weakening the durable projection invariants: a tool start must reference a projected assistant entry, a fork target must be a message entry, and harness errors must carry a typed cause for callers that inspect failure chains.
- These guards run inside the durable session reducer and the
SessionStateprojection, and the error contracts are exported harness primitives consumed before any coding-agent extension loads.
- LOW:
packages/agent/src/harness/reducer.tstool-start validation and tool-batch derivation guards;packages/agent/src/harness/session/state.tsfork-target validation;packages/agent/src/harness/types.tsgetOrUndefinedand the error-class cause declarations.
- Adopted, with the upstream v0.84.1/v0.84.2 syncs, the durable session store
whose JSONL publication is crash-safe:
packages/agent/src/harness/session/jsonl/storage.tsstages a complete sibling.tmpfile and atomically renames it over the destination, so a crash while populating a fork or repair leaves the published file untouched and at most an ignored temporary behind; a torn tail (an unacknowledged partial append after a crash) is repaired by atomically publishing the valid prefix. - Session names became clearable through the same durable mutation log:
setName(name: string | undefined)enqueues anamefact mutation and passingundefinedclears the name (packages/agent/src/harness/session/jsonl/storage.ts,packages/agent/src/harness/session/jsonl/codec.ts,packages/agent/src/harness/session/memory.ts,packages/agent/src/harness/session/session.ts). - The retired
jsonl-repo/memory-repolayer referenced by the 2026-05-11 UUID entry is gone; that conflict zone now maps to the store files above behind the session facade. The only fork divergence left in this tree is theSessionStateprojection guard recorded in the reducer entry.
- Crash-safe publication and torn-tail repair keep a forked or repaired session recoverable instead of half-written, and clearable names let hosts release stale labels without deleting durable history. Recording the migration keeps the tracker's legacy conflict zones honest after the store refactor.
- JSONL staging, atomic rename, torn-tail truncation, and name-fact mutations are storage-layer durability mechanics inside the harness session store, below every extension hook.
- LOW:
packages/agent/src/harness/session/jsonl/storage.tsstaged publication and torn-tail repair;packages/agent/src/harness/session/state.tsprojection guards (fork narrowing only).
- Adopted the promoted durable harness compaction API from the upstream
v0.84.x syncs: compaction runs against the durable session model with
Result-typed helpers in
packages/agent/src/harness/types.ts, compaction entries persist as session entries, and the split-turn summary-request serialization accepted earlier (2026-07-02 entry) kept its scheduling slot through the promotion (packages/agent/src/harness/compaction/compaction.ts). - The fork's surviving compaction-surface divergences on top of the promoted
API:
CompactionSummaryMessage.detailsinpackages/agent/src/harness/messages.ts(provider-native compaction route details for TUI rendering and replay, 2026-05-15 entry) and the summary-safe request-content wiring plus cut-point retention recorded in the adjacent 2026-08-13 summary-safe entry.
- The promotion moved compaction onto the same durability and error contracts as the rest of the harness; recording it keeps the tracker's compaction history continuous across the API change instead of implying the fork still patches the pre-promotion call sites.
- Compaction entry persistence, Result error contracts, and summary-request scheduling run inside the harness compaction helpers before coding-agent extensions observe a compacted session.
- LOW:
packages/agent/src/harness/messages.tsaroundCompactionSummaryMessage;packages/agent/src/harness/types.tscompaction error contracts;packages/agent/src/harness/compaction/compaction.tssummary-request scheduling and content extraction.
packages/agent/src/harness/compaction/utils.tsexportscontentTextForSummary(), which filters provider-native replay blocks from a copy before handing content to pi-ai's portablecontentText(); the provider-native blocks stay on the persisted assistant message for same-provider replay, and the persisted message is never cast or mutated.- Wired into every summarization request path:
packages/agent/src/harness/compaction/branch-summarization.ts(generateBranchSummary),packages/agent/src/harness/compaction/compaction.ts(generateSummaryWithUsage,generateTurnPrefixSummary), andserializeConversation()'s user/assistant/tool-result extraction inpackages/agent/src/harness/compaction/utils.ts. findCutPoint()inpackages/agent/src/harness/compaction/compaction.tskeeps the last valid cut point when the recent-token budget overshoots the newest eligible cut point instead of dropping the compaction (PR #40, 2026-06-15).- Consolidates the 2026-08-13 "Summary-safe branch compaction text" record under the canonical four-section format.
- Provider-native replay content must not leak into durable summaries, and a token-budget overshoot must still compact rather than leave the session over context; both decide request content before any extension sees the payload.
- The summary request content is assembled inside harness compaction helpers before coding-agent extensions can inspect or rewrite the session entry payload.
- LOW:
packages/agent/src/harness/compaction/utils.tsaroundcontentTextForSummary()andserializeConversation();packages/agent/src/harness/compaction/branch-summarization.tsingenerateBranchSummary()content extraction;packages/agent/src/harness/compaction/compaction.tscontent extraction and thefindCutPoint()overshoot branch.
- Branch summarization and compaction use
contentTextForSummary()instead of the portable-only AIcontentText()helper. - Provider-native replay blocks must be filtered while preserving the text that belongs in a durable branch summary.
- Harness compaction constructs the summary request before any coding-agent extension can inspect or rewrite the session entry payload.
- LOW:
harness/compaction/branch-summarization.ts, ingenerateBranchSummary()content extraction. - LOW:
harness/compaction/utils.ts, where the summary-safe helper is defined.
- Removed an unused compaction image type import and adopted optional-chain narrowing in reducer and session-state guards introduced by the upstream harness v2 merge.
- Runtime behavior is unchanged; the edits make the merged harness pass the repository's warning-as-error gate.
- These are internal harness compiler and lint boundaries, evaluated before any coding-agent extension loads.
- LOW: harness compaction imports, reducer assistant-entry guards, and session fork-target validation.
AgentLoopConfig.resolveUnknownToolCallis consulted before the existing unknown-tool result is emitted.- A host may return a newly activated tool, which then follows the normal argument validation, hooks, execution, and result lifecycle.
- Returning
undefinedpreserves the existingTool <name> not foundbehavior byte-for-byte.
- Unknown tool names were rejected inside the low-level agent loop before coding-agent tool hooks or extension callbacks ran.
- LOW:
types.tsnext to tool-loop callback configuration. - LOW:
agent-loop.tsunknown-tool preparation branch. - LOW:
agent.tsloop-config forwarding.
harness/env/nodejs.ts: the Windows branch of the harnesskillProcessTreemoved into the exportedkillWindowsProcessTree, which walks the ordered launcher list from the newwindowsTaskkillCandidatesexport (every existing absoluteSystem32/Sysnativetaskkill.exe, then the bare PATH-resolved name), runs each withspawnSyncunder a 5s timeout, and only degrades toprocess.kill(pid)when no launcher starts at all.spawn("taskkill", ...)resolves through PATH and reports a failed lookup asynchronously on the child'serrorevent, so the surroundingtry/catchnever observed it. Without a listener Node re-emits ENOENT as an uncaught exception, killing the host process instead of the target tree whenever PATH had lost%SystemRoot%\System32.- The kill is synchronous so a caller that tears down and exits in the same tick still terminates its children;
spawnSyncalso reports a failed lookup on its returnederrorfield instead of emitting it. The directprocess.killstays a last resort becauseTerminateProcessleaves descendants orphaned. - The same fix lands in
packages/coding-agent/src/utils/shell.ts; the two harnesses keep independent copies of this helper as they already do forgetShellEnvand bash resolution.
- The kill runs inside the Node harness's own process supervision, below every extension hook.
- LOW: the Windows branch of
killProcessTreeand thenode:child_process/node:fsimport lines inharness/env/nodejs.ts.
AgentLoopTurnUpdatecan now replaceabortServerSideFallbacktogether with the model and thinking level before the next provider request in an active run.agent-loop.tsapplies the refreshed value when rebuilding its request config after tool execution. Previously the loop snapshotted the option at run start, so a host that changed models mid-turn could send the next request with the prior model's server-fallback policy.- An explicit
falseremains authoritative because the update uses nullish fallback rather than truthiness.
- The provider options object is owned and snapshotted inside agent-core before extensions observe the next request; only the loop can replace request policy between tool turns.
- LOW:
types.tsAgentLoopTurnUpdate. - LOW:
agent-loop.tsnext-turn config replacement.
- Empty-assistant recovery now covers every model selected for text-tool-call recovery or configured with a text tool
format, expanding the previous Kimi-only gate to Claude, ANTML, Hermes, morph-XML, YAML-XML, Gemma delimiters, and
other configured text protocols. A
stopturn with no visible text and no tool call is discarded and retried once; a second invisible stop retains the existing explicitModel returned an empty response twicefailure. - Both the completed-message gate and the first-visible-event gate use pi-ai's shared Unicode visibility predicates. Unicode format-only deltas such as the U+200B block emitted by the Apitopia Kimi-K3 gateway remain buffered, so malformed thinking/tool-marker events from the discarded attempt never reach subscribers.
- The approved universal gate was narrowed after the full-suite audit: buffering all model streams suppressed ordinary thinking updates, changed provider stream-start/idle-timeout semantics, and prevented coding-agent TTSR from observing and aborting malformed reasoning streams. Plain native-protocol models therefore keep direct streaming, while every model exposed to the text-protocol failure mode receives bounded recovery.
- Healthy visible text, tool calls, and non-
stopterminal states retain their existing pass-through behavior.
- Provider stream buffering and retry happen inside agent-core before message-update events are forwarded or an assistant turn is committed; extensions cannot retract leaked attempt-one events or replace the committed turn.
- MEDIUM:
empty-assistant-recovery.tsvisibility checks and stream wrapper gate. - LOW:
agent-loop.tsat the recovery wrapper call site.
packages/agent/src/stream-fn.tsre-exportswithEmptyAssistantRecoveryfrompackages/agent/src/empty-assistant-recovery.ts, keeping the injectable stream-function seam (setDefaultStreamFn/getDefaultStreamFn) the single place a host wires streaming.packages/agent/src/agent-loop.tswraps the resolved stream function withwithEmptyAssistantRecovery(requestConfig.model, streamFunction)before each provider request, so bounded empty-assistant recovery applies to every StreamFn in effect — explicitly passed or installed as the host default — without hosts importing the wrapper from a deep path.- Landed with the Kimi empty-response retry; the 2026-07-30 and 2026-08-09 recovery entries remain the accurate behavioral history and are preserved unchanged.
- Recovery must compose with host-installed default stream functions (the browser-safe core ships no provider catalog of its own), and the re-export keeps the loop importing its stream plumbing from one module.
- The wrapper sits between the loop and the provider stream, buffering and retrying empty assistant responses before message-update events reach subscribers or a turn is committed; extensions cannot retract leaked attempt-one events or replace the committed turn.
- LOW:
packages/agent/src/stream-fn.tsre-export line;packages/agent/src/agent-loop.tsat the recovery wrapper call site.
- Kimi-family provider streams that finish with
stopbut contain neither non-empty visible text nor a tool call are discarded before turn commitment and retried once with the same request. - A successful second attempt is the only assistant turn committed and carries an
empty_assistant_response_recoverydiagnostic. A second empty response becomes a visible error instead of ending the session silently or looping indefinitely. - Error, aborted, refusal, length, and tool-call turns keep their existing behavior. The stream gate buffers only Kimi responses before their first visible text/tool signal, avoiding reasoning-stream regressions for other model families.
- Coverage: agent-loop tests pin one-shot recovery, bounded failure, terminal-state preservation, and tool execution. The real CLI mock-loop scenario proves the user-visible recovery path.
agent-loop.tsbounds the wait for the FIRST provider stream event with a new optionalAgentLoopConfig.streamStartTimeoutMs. Providers emit their first event only once the HTTP response begins, so a dead upstream that accepts a request and never answers was previously bounded only bytimeoutMs(the idle timeout, default 5 minutes): every attempt froze the session for 300s with zero events, zero usage, and nothing persisted. Observed in a donated 5h session log where the same session hung deterministically on reopen while new sessions worked. After the first event arrives the idle bound governs as before.- The failure message
Provider stream start timed out after <ms>msdeliberately contains "timed out" so the existing retryable-error classifier (isRetryableErrorMessage) retries it instead of dead-ending the session; the request-local abort controller tears the dead request down exactly like an idle timeout. agent.tsplumbsstreamStartTimeoutMsthroughAgentOptions/Agentinto the loop config.
agent-loop.tsagent.tstypes.ts../test/agent-loop-stream-start-timeout.test.ts
Agent.continue()andcontinueWithQueuedMessages()accept continuation-only options that defer queued input from the first provider request and override both stream idle and stream-start bounds for that request without mutating the agent's configured defaults. Later requests in the same run restore the configured bounds; after the first retry event, the configured idle timeout also governs inter-event gaps so healthy silent reasoning is not capped.- Queue-first recompaction recovery takes precedence over deferral: the selected queued message is the continuation input, while first-request timeout overrides still apply.
- The core run lifecycle intentionally parks queued steering and follow-up input after terminal error or abort
responses until an external retry/compaction owner or a later admitted prompt consumes it. This stop-reason policy
is distinct from
suppressQueuedMessageDrain(), which transfers one active run's post-agent_endownership. - Coding-agent retries use these controls after a silent provider stream so a doomed retry cannot consume newly queued user input and a later ordinary provider request automatically returns to the configured timeout.
agent.tstypes.tsagent-loop.ts../test/agent.test.ts../README.md
- Provider-request queue polling, event-reader timeout selection, and post-run native queue draining happen inside agent core before coding-agent extensions can safely claim or restore that work.
- MEDIUM:
agent.tscontinuation APIs/config creation and active-run lifecycle queue draining. - MEDIUM:
agent-loop.tsprovider-request timeout selection insiderunLoop().
assistant-terminal-state.tsowns terminal assistant classification, including typed classifier refusals;agent-loop.tsnow consults it before any partial tool calls are executed. Anthropic can emit a tool call and then finish the same stream with a refusal/sensitive stop; treating the message as ordinarytoolUsepreviously ran the refused call and continued on the same model.- The terminal
agent_endlets the coding-agent retry/fallback controller immediately apply its configured pinned refusal fallback.
Agentnow exposessuppressQueuedMessageDrain()for the active run. It stops only the lifecycle-owned post-agent_endsteering/follow-up drain, retaining both queues without aborting the run signal.Agentnow exposescontinueWithQueuedMessages()so compaction recovery can deliver retained steer/follow-up input when custom context leaves the transcript tail non-assistant.- The coding-agent compaction admission gate uses this ownership transfer for required recovery. Real user aborts continue to abort the active signal and retain the normal terminal semantics.
- Scheduled continuation can revalidate a model changed by
session_compact, recompact if required, and then deliver retained queues without inventing an empty continuation turn.
agent.ts../test/agent.test.ts
- Native queue draining and active-run signal ownership occur inside
Agentafter event subscribers return.
- MEDIUM:
agent.tsactive-run lifecycle and post-agent_endqueue draining.
harness/session/uuid.ts: the inlined UUIDv7 implementation uses a synchronous counter over module state. A concurrency refutation test (test/uuid-concurrency.test.ts) records that N interleaved async tasks callinguuidv7()produce unique, monotonic-per-timestamp ids — the synchronous counter makes uniqueness hold under interleaving (noawaitbetween timestamp read and counter increment). This is a recorded refutation WITH a test, not a bare assertion; it documents that the existing synchronous-counter design is correct under interleaving so future refactors do not "fix" a non-bug by adding an async lock that would change id ordering.core/agent-session-runtime.ts(CreateAgentSessionRuntimeFactory,:35,74-242,411): runtime construction now carries an immutable per-open launch profile{ permissionPreset, creationModel, initialThinkingLevel, cwd }. The profile is retained byAgentSessionRuntimeand survivesnew_session/switch_session/reload unless the command explicitly changes it. This carries per-sessioncwd, permission-preset, model selection, and thinking level with identical semantics to today's spawn flags, withoutmain.tsclosing over process-level parse.
harness/session/uuid.ts(no production change; refutation test only)../test/uuid-concurrency.test.ts(new)core/agent-session-runtime.ts
- The UUIDv7 counter and the launch-profile retention live inside
pi-agent-corebefore coding-agent extensions or mode renderers participate; the profile must be carried by the runtime the session registry constructs insiderunWithProviderScope.
- LOW:
harness/session/uuid.ts(unchanged production code; test is fork-only). - MEDIUM:
core/agent-session-runtime.tsaroundCreateAgentSessionRuntimeFactoryoptions.
- A tool call that the text tool-call middleware could only partially recover now arrives at the
agent loop carrying
incomplete: true. Previously a truncated text-protocol call could be silently dropped, leaked as raw markup, or executed from stale arguments; the loop had no way to treat a partially recovered call as a failure and ask the model to retry. prepareToolCallnow produces an immediate error outcome for any flagged call (anisErrortool result carrying a retry diagnostic such as "Re-issue the tool call"), skipping validation/hooks/execution while preserving source-order event emission in the same scheduler. The existing nativelengthstop rule is preserved for provider-native streams; only the text-middleware wrapper converts a terminallengthtotoolUsewhen tool-call activity was finalized.- The flagged error result keeps the inner loop alive (
failToolCallsFromTruncatedMessagealready returns{ terminate: false }), so the loop streams another assistant turn and the model re-issues the truncated call — the retry contract. - Flagged-call diagnostics always append
Re-issue the tool call with complete arguments.to parser-provided error messages without duplicating a final period. proxy.tstoolcall_endwire event gains an optional fulltoolCallpayload so a flagged call (which emits no argument deltas) can still be delivered to clients. The client prefers the payload and falls back to delta reconstruction; against an older server that omits it, the client degrades to the legacy delta-only path. The producing server is external; the in-repo deliverable is the wire type, the client merge, and the skew-degradation tests.
agent-loop.tsproxy.ts../test/agent-loop.test.ts,../test/proxy-events.test.ts
- Flagged-call routing into an immediate error outcome, the retry decision, and the proxy wire type
all live inside
pi-agent-corebefore coding-agent extensions or mode renderers participate.
- MEDIUM:
agent-loop.tsaroundprepareToolCallandfailToolCallsFromTruncatedMessage. - LOW:
proxy.tsaround thetoolcall_endwire event and client reconstruction.
agent-loop.tsre-polls a terminating turn's drained steering or follow-up queue after next-turn preparation, restoring it on preparation failure or abort and continuing only with work that remains queued.- This keeps queued recovery input owned by agent-core while coding-agent compaction settles, preventing a queued prompt from being dropped or dispatched from stale history.
packages/agent/src/agent-loop.tspackages/agent/test/agent.test.ts
- Queue draining, restoration, and next-turn preparation run inside the agent loop before coding-agent extensions can observe or safely requeue the consumed messages.
- MEDIUM:
packages/agent/src/agent-loop.tsaround terminating tool batches, queue polling, and next-turn preparation.
- The idle-timeout reader rejected the turn but left the underlying provider request dangling:
iterator.return()is a no-op onEventStream, so a silently dead connection (network drop + reconnect) kept its socket and stream alive forever. - The agent loop now owns a per-request
AbortController, propagates caller aborts into it through a single listener, and aborts it withStreamIdleTimeoutErrorwhen the reader times out, tearing the request down so auto-retry can recover the turn.
packages/agent/src/agent-loop.tspackages/agent/test/agent-loop.test.ts
- Stream lifetime and abort propagation live inside the agent loop's provider-request plumbing, upstream of any coding-agent extension hook.
- MEDIUM:
packages/agent/src/agent-loop.tsaround provider stream creation, idle-timeout reading, and abort-signal wiring.
- Accepted upstream harness changes for rejecting invalid/non-positive Node timeouts and serializing split-turn compaction summary requests.
- This keeps the fork aligned with upstream runtime validation and prevents single-concurrency providers from receiving overlapping compaction-summary generations.
packages/agent/src/harness/compaction/compaction.tspackages/agent/src/harness/env/nodejs.ts
- Timeout validation and harness compaction scheduling happen inside shared agent-core helpers before coding-agent extensions or mode renderers participate.
- LOW:
packages/agent/src/harness/env/nodejs.tsaround timeout parsing and validation. - LOW:
packages/agent/src/harness/compaction/compaction.tsaround summary request scheduling.
- Stopped the core agent loop immediately after a tool batch finishes under an aborted signal.
- This prevents a tool-level abort result from continuing into
prepareNextTurn, steering queue polling, follow-up queue polling, or another provider request. - This closes the remaining abort path not covered by terminal assistant stream event normalization.
packages/agent/src/agent-loop.tspackages/agent/test/agent-loop.test.ts
- The decision to poll queued steering after tool execution happens inside the core loop before extensions can safely restore UI/editor queue state.
packages/agent/src/agent-loop.tsafterturn_endemission inrunLoop().
- Preserved the fork's ES2021 diagnostic compatibility while accepting upstream's result-based harness/environment refactor.
- Kept stream option patching on
Object.prototype.hasOwnProperty.callinstead ofObject.hasOwn. - Kept harness error
causecapture without relying on two-argumentErrorconstruction.
packages/agent/src/harness/agent-harness.tspackages/agent/src/harness/types.ts
- These are exported harness primitives and internal option-merging helpers that are evaluated before coding-agent extensions can participate.
packages/agent/src/harness/agent-harness.tsaroundapplyStreamOptionsPatch().packages/agent/src/harness/types.tsaround harness error constructors.
- Added optional
detailsmetadata to the harnessCompactionSummaryMessagetype. - This keeps the shared agent-core message augmentation compatible with coding-agent compaction summaries that carry provider-native compaction route details for TUI rendering and replay.
packages/agent/src/harness/messages.ts
- This is exported type metadata in the shared harness message model. Extensions can populate compaction details, but they
cannot alter the core
CustomAgentMessagesdeclaration merge.
- LOW:
packages/agent/src/harness/messages.tsaroundCompactionSummaryMessage.
- Normalized terminal assistant stream messages in
agent-loop.tsso the event-levelreasonis authoritative fordone/errorevents. - This prevents an abort event with a stale assistant
stopReasonfrom being treated as a normal stop and draining queued steering/follow-up messages after the user interrupted the run.
packages/agent/src/agent-loop.tspackages/agent/test/agent.test.ts
- The stale-stopReason decision happens inside the core agent loop before extensions see a completed turn.
- Extensions can observe abort events after the fact, but they cannot prevent the loop from deciding to continue into queued messages.
packages/agent/src/agent-loop.tsaround terminaldone/errorstream handling.
- Updated
executeToolCallsParallel()to finalize prepared tool calls concurrently after sequential preflight. - This lets
tool_execution_endandtoolResultmessage events appear as soon as each tool finishes instead of waiting behind an earlier slow tool. - The returned
toolResultsarray still stays in assistant source order, which preserves next-turn context ordering and matches existing semantic expectations.
packages/agent/src/agent-loop.tspackages/agent/src/types.tspackages/agent/README.mdpackages/agent/test/agent-loop.test.ts
- The scheduling and final result collection logic lives in
@mariozechner/pi-agent-core, specificallyexecuteToolCallsParallel(). - Coding-agent extensions can observe and mutate tool inputs/results, but they cannot replace the agent loop's internal await/collection strategy or
toolExecutionscheduling behavior. - The existing builtin
parallel-tool-callsextension only changes provider payloads (parallel_tool_calls: true) and does not control runtime result finalization.
packages/agent/src/agent-loop.tsaroundexecuteToolCallsParallel()packages/agent/src/types.tstool execution mode docspackages/agent/README.mdtool execution behavior description
- Replaced upstream harness imports of
uuid/v7with a local UUIDv7 generator backed by Node'scrypto.randomBytes. - This keeps clean package-manager builds working without adding a new direct
uuiddependency to@earendil-works/pi-agent-core.
packages/agent/src/harness/session/uuid.ts(current location; the generator originally landed in the since-restructured session repo/storage files)
- The failing imports live inside the agent harness session storage implementation and run before any coding-agent extension can intercept them.
packages/agent/src/harness/session/uuid.ts- its importers
packages/agent/src/harness/session/{repo-utils,memory-storage,jsonl-storage}.tsaround session/entry id creation.
- Replaced
ErrorOptions/two-argumentErrorconstruction inFileErrorwith an equivalent local{ cause }option stored on the class. - Replaced
Object.hasOwnwithObject.prototype.hasOwnProperty.callin the stream option patch helper. - This keeps the upstream harness behavior intact while avoiding diagnostics in environments that type-check the package with ES2021 library declarations.
packages/agent/src/harness/types.tspackages/agent/src/harness/agent-harness.ts
- These are type-level compatibility fixes in exported harness primitives and internal option-merging code that run before coding-agent extensions are involved.
packages/agent/src/harness/types.tsaroundFileErrorconstruction.packages/agent/src/harness/agent-harness.tsaroundhasOwn().
agent-loop.tsnow stamps each streamed thinking block'sstartedAtandendedAtwith best-effort receipt timestamps. Every thinking update is restamped because thinking projection middleware may replace the block object between events; terminal completion, error/abort, reader failure, and normal stream fallthrough all close unfinished blocks before emitting the final message.
packages/agent/src/agent-loop.tspackages/agent/test/agent-loop.test.ts
- The timestamps must be attached at the agent loop's provider-event choke point, before extensions receive message updates or terminal messages.
- LOW:
packages/agent/src/agent-loop.tsstreaming event switch and terminal response paths.