- 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.