agent-session.ts:_emitModelSelectnow syncs_systemPromptOverridewith the prompt an extension installs, clearing it when the handler returnsnull. The continuation snapshot andsetActiveToolsByNameboth read that field, so a mid-turn model switch no longer reverts to the previous model's prompt on the next tool continuation or tool-set reconciliation.agent-session.tsfollow-up: the_systemPromptOverrideupdate runs before the no-visible-change return in_emitModelSelect. A handler returningnullresets to the base prompt, which can equal the currently visible string (a conditionalbefore_agent_startmodifier records the base itself as the override). Updating after the early return left that stale override alive, and the nextsetActiveToolsByNamerebuild read_systemPromptOverride ?? _baseSystemPromptand pinned the pre-reconciliation prompt - the reduced tool set never reached the model.system-prompt.ts:buildSystemPrompttestscustomPrompt !== undefinedinstead of truthiness, so an explicit empty replacement is honored instead of silently building the default identity. This matches the nullish precedenceAgentSessionalready uses.
- Review found split prompt state:
model_selectwrote onlyagent.state.systemPrompt, while continuations reconstructed from_systemPromptOverride ?? _baseSystemPrompt. A fallback-selected model could therefore change identity mid-turn, including away from a worker role contract. - The two prompt builders disagreed on
"": the session path selected it, the generic builder discarded it. Delegated worker roles depend on explicit replacement being authoritative in both.
- MEDIUM:
agent-session.ts_emitModelSelectbody. - LOW:
system-prompt.tscustom-prompt branch guard.
Resume queued messages after non-auto compaction; retain admission-rejected custom messages (2026-08-03)
agent-session.tsgained_resumeQueuedMessagesAfterCompaction(), mirroring_runAutoCompaction's accepted-path recovery, and calls it on the success paths ofapplyCompaction(), the extensioncompactcontext action, and manualcompact().sendCustomMessage()'s non-streamingtriggerTurnpath now retains the message in the matching agent-level queue (followUp/steer) before rethrowing when provider admission (_enforceCompactionBeforeProvider/_enforceFinalProviderAdmission) rejects, mirroringsendUserMessage's documented retention contract.
- A custom
triggerTurnmessage sent while a non-auto compaction owned the session (extension feedback stage viabeginCompaction, extensioncompactaction, manual/compact) was parked in the agent-level queues without a turn and nothing resumed it afterwards; an admission rejection dropped the message entirely because the fire-and-forget extensionsendMessageaction swallows the rejection. Hidden goal continuations were the primary victim: their single-flight latch clears only onagent_start, so the goal silently idled at "Pursuing goal (...)" until manual user input.
- Both fixes depend on internal compaction lifecycle ownership, agent-level queue state, and the private continuation scheduler; no extension hook can observe or reschedule them.
- MEDIUM:
agent-session.tsaroundcompact()/applyCompaction()/ the extensioncompactaction finally blocks,sendCustomMessage()'s triggerTurn branch, and_scheduleContinuationAfterCurrentEvent().
agent-session.tsretry orchestration classifies 429-class errors into three tiers using the structured hint frompackages/ai: no-hint 429 falls back immediately with zero same-model retries; tier 1 (hint ≤hintedWaitCapMs, default 300 000 ms) performs in-turn half/full probes vianextInTurnDelayMswith cumulative-cap demotion; tier 2 (hintedWaitCapMs< hint <probeBackMaxMs) falls back and schedules at most twoProbeBackSchedulerprobes at half/deadline, clearing cooldown on success somaybeRestorePrimaryreverts next turn; tier 3 (hint ≥probeBackMaxMs) falls back only with a remaining-hint cooldown.retry-fallback/probe-scheduler.ts(new) owns the tier 2 probe schedule.retry-fallback/controller.tsandretry-fallback/cooldown.tscarry the tier decision and cooldown state.retry-fallback/settings.tsaddsresolveHintPolicySettingswithhintedWaitCapMsandprobeBackMaxMs(defaults 300 000 / 3 600 000 ms).- New session events
retry_probe_scheduledandretry_probe_resultsurface probe lifecycle to the client. retry-fallback-long-delay.test.tshas two intentionally updated assertions: tier routing replaces the legacy over-budget gate for 429-class errors, so the expected retry/fallback behavior changes accordingly.
- A blind exponential backoff on 429 wastes a turn when the provider says “retry now,” and retries immediately when the provider says “wait an hour.” Structured hints let the agent respect the provider's guidance instead of guessing.
- The tier decision must intercept the retry sleep and fallback switch inside agent-session's orchestration loop, between the provider error and the retry/fallback decision. The extension API exposes no hook at that point — extensions see only post-decision error strings.
- HIGH:
agent-session.tsretry orchestration (approximately lines 5380–5705). - MEDIUM:
retry-fallback/*(controller, cooldown, settings, probe-scheduler). - LOW:
settings-manager.tsfor the newhintedWaitCapMs/probeBackMaxMssettings.
- Eval bridge requests no longer deadlock the session when completion and bridge delivery race.
- A blocked bridge stalls the entire agent turn and leaves no safe continuation path.
- The fix depends on internal agent-session bridge ordering and completion ownership.
- Agent-session eval bridge handlers, pending request state, and completion/error cleanup.
agent-session.tsnow remembers every sensitive provider/model identity that already displayed the high-reasoning warning during the current session.- Moving between
xhigh,max, lower reasoning levels, or another model no longer re-arms the warning for an identity the user already saw. - A different sensitive provider/model identity still receives its own first warning.
- The previous single last-key value was cleared whenever the active state was not warnable and included the reasoning level in its key. Cycling reasoning levels or switching away and back therefore appended the same large warning box repeatedly.
- LOW:
agent-session.tswarning-dedup state and_emitHighReasoningWarningIfNeeded().
agent-session.ts: manual model selection and favorite-model cycling now apply model-specific overrides and capability clamps as session-effective levels without replacingdefaultThinkingLevel.- Model switches without an explicit favorite tier restore the remembered
defaultThinkingLevelbefore clamping it to the selected model.
- Switching from a max-capable model to a basic reasoning model persisted the
clamped
hightier, so switching back no longer restored the user's last selectedmaxtier. Explicit favorite tiers could likewise replace the global preference even though they are model-specific overrides.
- LOW:
agent-session.tsaround_switchActiveModel(),_cycleFavoriteModel(), and_getThinkingLevelForModelSwitch().
thinking-levels.tsno longer re-implements thexhigh/maxmodel-id lists.supportsXhigh,supportsMax, andgetSupportedThinkingLevelsnow wrap the canonical@earendil-works/pi-aihelpers and only keep the coding-agent'sThinkingLevelvocabulary plus the non-empty["off"]fallback.- The local
ModelWithThinkingLevelMapcast is gone:Model.thinkingLevelMapis already part of the publicpi-aimodel type.
- Tier rules belong to
packages/ai; delegating removes the coding-agent's duplicate model-id lists and precedence logic so future capability changes have one implementation. Generated catalog models retain their explicit maps, so behavior for real catalog models is intentionally unchanged.
- Tier detection feeds session thinking-level clamping and the model/RPC surfaces inside core; it is not reachable from an extension.
model-registry.tsnow exposes a selected model's configuredserviceTiersynchronously, alongside the existinggetUpstreamModelId()lookup.- The builtin
/fastcommand uses both values to accept only catalog siblings that send the same upstream model withservice_tier: "priority".
- A
-fastsuffix alone is not proof that a model supports priority processing. The command must validate the request metadata already resolved by the model registry before switching the session.
- Compatibility request metadata is composed inside
ModelRuntime; extensions can inspect the registry but could not synchronously read its resolved per-model service tier.
- LOW: the request-metadata accessors in
model-registry.ts.
settings-manager.tsFileSettingsStorage.withLock: when the settings file does not exist yet, the merge callback used to run with no lock held and the write-time lock then overwrote whatever a concurrent process had created. The write path now re-checks existence after acquiring the lock and re-runs the merge callback against the winner's content before writing. The existing-file path additionally re-verifies existence after the lock before reading.- Pure read paths are unchanged: a read on a missing file still creates no directory and no lock artifacts, so loading settings in an arbitrary cwd still cannot spray
.senpi/directories.
- Two processes racing the first write of a fresh
settings.jsonsilently lost one side's fields (existsSyncgated the lock, so the merge ran unlocked). Deterministic regression:test/settings-storage-lock.test.tsinjects a concurrent first-write at lock acquisition and asserts the merge preserves it.
settings-manager.ts: project settings now resolve from the nearest ancestor containing a real.senpidirectory, rather than only from the exact cwd. If none exists, the legacy<cwd>/.senpi/settings.jsonpath remains the write/read target.- Global settings remain loaded before project settings, so project values continue to override the selected agent-directory settings layer.
- Invoking senpi below a project root silently skipped that root's
.senpi/settings.json.
- LOW:
getSettingsPath()insettings-manager.ts.
messages.tsnow excludes consumedgoal-continuationcustom messages by position instead of by type: everyrole === "custom" && customType === GOAL_CONTINUATION_MESSAGE_TYPEentry is dropped except the last one. The same keep-latest rule is applied in bothfilterContextExcludedMessagesandconvertToLlm, so token estimation and provider payload assembly stay in sync.isContextExcludedCustomMessageremainsfalsefor this custom type; the live triggering message still needs to be visible to per-entry consumers such as compaction and branch summarization.
- Goal continuation messages accumulate across long sessions, and stale consumed entries must stay out of the next provider request without hiding the active trigger or letting the estimator disagree with the payload.
- LOW in
messages.tsaround the shared keep-latest helper,filterContextExcludedMessages, andconvertToLlm. - NONE in the per-entry custom-message predicate semantics.
extensions/types.tsnow exposes an optionalwillRetry?: booleanonAgentEndEvent, mirroring the agent-session end event so builtin extensions can tell a terminal provider error from a retryable one.- The field is additive only; existing extension consumers that ignore it continue to behave the same.
- The goal builtin needs to block on terminal provider errors only after retries are exhausted. Without the retry signal, a terminal error could be misclassified while a fallback retry was still in flight.
- LOW in
extensions/types.tsand the runner plumbing that forwards agent-session end events to builtin extensions.
- New builtin extension
core/extensions/builtin/claude-agent-sdk/: routes LLM calls through the official Claude Agent SDK (spawns the real Claude Code engine) while senpi executes all tools (Claude Code tool use is denied; custom tools are exposed in-process asmcp__custom-tools__*). - Auth:
/login claude-agent-sdkruns the existing Anthropic PKCE flow and stores multi-account slots inside the provider credential (top-level fields are non-expiring sentinels; real refresh is per-slot under the store lock). Import of an existinganthropicOAuth credential andCLAUDE_CODE_OAUTH_TOKEN(_N)env accounts supported. - HRW session affinity (rendezvous hashing) pins each session to one account to preserve prompt
cache; mandatory failover on rate_limit/overloaded/auth errors only, stream-safe (no transparent
retry after the first visible delta) with an AgentSession
senpi:no-turn-retry:marker suppressing whole-turn replay of post-delta failures. - Surfaces:
/claude-accountcommand,--claude-accountflag, RPCget_provider_accounts/account_pin/account_removeplusauth_accounts_changed/account_failoverevents, and actionable auth guidance.AuthStoragelearned to enumerate extension-registered OAuth providers (registerOAuthProviderbridge), synced fromModelRuntime.registerProvider. - Dependency:
@anthropic-ai/claude-agent-sdkpinned0.3.220;@anthropic-ai/sdkstays0.91.1via a root override (the>=0.93.0peer range breaks the browser build through node-builtin imports in new credential modules).
session-title-generator.ts:generateSessionTitle()accepts an optionalretry: RetryPolicyand wraps the title call inretryAssistantCall, mirroringcompleteSummarization(). A transient provider error (e.g. an Anthropic 529overloaded_errorstream event) no longer fails title generation on the first attempt. Final failures throwhumanizeProviderError(...)output — a short human-readable line such asOverloaded (overloaded_error, request req_...)— instead of the raw provider JSON body.session-title-generator.ts: newsessionTitleRetryPolicy()narrows the user'ssettings.retryfor this cosmetic background call —enabledis preserved,maxRetriescapped at 1 andbaseDelayMsat 2000ms, and a smaller configured budget is never inflated. The full agent-turn budget would keep hitting an already-overloaded provider for ~14s while the user's real turn competes for the same capacity; a title that still fails is regenerated at the next turn end anyway.agent-session.ts:_generateSessionTitle()passessessionTitleRetryPolicy(settingsManager.getRetrySettings()). The runtime-emitted extension-error sites now use the sharedRUNTIME_EXTENSION_PATHsentinel constant.
- A single transient 529 during background title generation surfaced as
Extension "<runtime>" error: {raw json}in the TUI and left the session untitled until the next turn end.
- LOW:
session-title-generator.tsaroundgenerateSessionTitle(). - LOW:
agent-session.ts_generateSessionTitle()and theemitErrorcall sites.
agent-session.ts:/skill:<name>now accepts a leading whitespace-separated run of loaded skills, expanding each unique skill in written order before appending the remaining prompt text. Repeated skills expand only once, unknown skills stop the run and remain literal, and slash text outside that leading run is never interpreted as a skill command.- Explicit expansion is capped at
MAX_SKILL_EXPANSIONS_PER_PROMPT(5). Commands beyond the cap remain literal and emit an existingskill_expansionerror-channel notification, preventing a composed prompt from growing context without bound. - The shared expansion seam is called by
prompt(),steer(), andfollowUp(), so queued and non-TUI/RPC prompt paths receive identical behavior.
Skill commands are resource-loader entries rather than extension commands, and their substitution happens in the private AgentSession prompt and queue boundary before the outbound user message is assembled.
- LOW:
agent-session.ts_expandSkillCommand()if upstream revises skill-command parsing.
messages.ts: added a transport-only 24 MiB inline image budget. Provider-bound conversion keeps the newest image block, counts it against the budget, and replaces images older than the hard recency cutoff with a re-read placeholder while preserving all text and leaving the persisted session untouched.sdk.ts: routes the main agent loop through the shared transport conversion while preserving the dynamicimages.blockImageskill switch and its existing placeholder/deduplication behavior.test/suite/harness.ts: uses the same transport conversion and accepts a small injectable image budget for deterministic first-request integration coverage.
- Inline images must be bounded after session messages are converted but before every main-loop provider request, including resumed sessions and provider fallbacks. That conversion boundary is owned by the core Agent wiring.
- MEDIUM:
sdk.tsaround the AgentconvertToLlmwiring. - LOW: the transport helpers at the end of
messages.tsand the Agent construction intest/suite/harness.ts.
src/core/thinking-levels.ts:supportsXhighnow recognizesgpt-5.6,opus-5,sonnet-5andfable-5;supportsMaxrecognizesopus-5,sonnet-5andfable-5. These lists are the fallback for models with nothinkingLevelMap(custommodels.jsonentries and third-party gateways), so those models previously could not reach thexhigh/maxtiers in the level cycler even though their provider accepts them. Bundled catalog models are unaffected because an explicit map wins.- This file is the coding-agent copy of the tier predicates;
packages/ai/src/models.tsowns thepi-aicopy and was updated in lockstep.
offalso became selectable for Claude Fable 5 in this change set:packages/ainow encodes "cannot sendthinking.type: disabled" as a compat fact rather thanthinkingLevelMap.off: null, and the Messages provider pins the cheapest effort for an off turn. The selector needed no change for that - removing thenullwas enough.
agent-session.tsnow holds a monotonic compaction lifecycle coordinator that snapshots the active model and controller at operation start, rejects stale completion/feedback, and retains the terminal result until another operation begins. Feedback-only aborts publish one terminal event, and accepted completions publish their terminal event beforesession_compacthandlers can begin a fresh operation.- Owned automatic compaction attempts publish balanced start/end events when execution cannot begin. Ownership is
rechecked after start: a synchronous listener that supersedes the controller with a new operation silences the stale
terminal event (the new owner publishes its own lifecycle), while a listener that aborts the same controller still
receives an
abortedterminal event so UI state opened oncompaction_startis always closed. - Durable append now rejects a generation whose message revision or agent-message snapshot changed during preparation
or summary generation (
stale-revision), preserving intervening context without duplicate replay. - Required compaction uses one provider-admission gate for normal prompts, extension-triggered turns, and every next
turn. Provider-confirmed overflow remains fail-closed even when the local token estimate is below the configured
threshold;
agent_endsynchronously transfers both silent-overflow and threshold-compaction continuation ownership toAgentSessionbefore agent-core can drain native queues, and failed recovery restores the overflow context so later prompts cannot bypass the same requirement. - Next-turn snapshots reapply the live active tools and effective per-run system prompt after asynchronous preparation, so a tool removed during the turn is neither advertised nor executable by the following provider request.
- Required ownership now suppresses only agent-core's post-
agent_endqueue drain, not the run abort signal. Deferred extension dispatch retains the real source signal, so compaction ownership does not masquerade as user cancellation. - Retry and fallback admission resolve required compaction first; rejected recovery retains native queues without dispatching a provider retry. Active-tool changes advance the context revision and abort active core compaction so summaries prepared against a prior tool set cannot apply.
- Fallback apply/revert transitions emit typed model-selection events, rebuild model-scoped tools and prompts, abort compaction prepared for the prior model, and re-run required compaction against the selected model's context window before retrying.
- Message objects are associated with their persisted session-entry order. Compaction-boundary checks use that order
(and treat pending
message_endpersistence as post-boundary) instead of relying only on payload timestamps. - Session reload materialization restores those message-to-entry associations, so older payload timestamps cannot bypass post-compaction admission after reopening a session.
- When a late queue triggers compaction after a host
prepareNextTurnWithContextcallback, the callback is replayed once against the compacted context so its message filtering/injection contract reaches the provider request. - Every compaction execution receives its route-owned controller explicitly. Auto compaction cannot promote unrelated extension feedback, and superseded feedback controller references are released even when their stale terminal callback never arrives.
- Post-retry and post-compaction usage exemptions suppress only stale threshold accounting. Provider-confirmed overflow always retains queue ownership and runs fail-closed recovery.
- Extension-originated provider turns now wait behind active session work and manual compaction.
clearQueue()clears both native and post-compaction deferred ownership layers, preventing canceled steer/follow-up input from resurfacing. - Provider admission is checked again after assembling
nextTurnandbefore_agent_startcustom messages. Rejected compaction restores one-shot additions transactionally; accepted compaction rebuilds and rechecks the final visible request before the provider is called. - Request-local context provenance is attached non-enumerably to message identities and removed from persisted/session JSON. Remote replay uses it to prove the exact checkpoint boundary after filtering, injection, or reordering.
- Trigger-turn custom messages serialize behind manual/extension compaction before they are appended or sent.
Scheduled continuation revalidates the canonical context against any model selected by
session_compact, retaining queues when the smaller model requires rejected re-compaction. - Manual and extension compaction claim a synchronous pending-admission barrier before their first await, closing the same-tick window where a trigger-turn custom message could overtake startup. Retry continuation failures that occur before provider dispatch now settle retry/idle state and retain queues instead of hanging the session.
- Fire-and-forget
session_startmessages defer past replacement-session work without being discarded as stale.
- Model selection, durable session append, provider-overflow recovery, controller ownership, and prompt admission are
private
AgentSessionlifecycle boundaries.
- HIGH:
agent-session.tscompaction execution, pre-prompt recovery, abort handling, and extension context bindings.
agent-session.ts(prompt()): a submission with astreamingBehaviorwhile a run is active and not compacting now queues immediately instead of awaiting_waitForSettledSessionWork(). Scheduled queued-message continuations (goal chains, queued follow-ups) hold theSessionWorkBarrierfor the entire remaining run, so the old gate trapped typed input insideprompt()— invisible, unqueued, and undelivered — until the whole chain settled or the user pressed Esc.- If the run ends while the bypassed input is being expanded,
prompt()re-serializes with remaining session work and re-queues when a scheduled continuation started a new run in the meantime.
- The trap sits between core-owned
prompt()serialization and the core continuation scheduler; both are privateAgentSessionlifecycle boundaries.
- MEDIUM:
agent-session.tsprompt()entry serialization and the streaming queue dispatch branch.
session-manager.ts: added a monotonicmutationCountbumped by every mutator (_appendEntry,branch(),resetLeaf(),setSessionFile,newSession,createBranchedSession).getEntries()is memoized onmutationCount, no-arggetBranch()on(leafId, mutationCount)(explicitfromIdbypasses), andgetSessionName()is O(1) via a cached value maintained onappendSessionInfo/_buildIndex(empty name still clears the title).getEntries()now returns a shared cached array callers must not mutate.
- The mutation surface and resident-store materialization are private to
SessionManager; external wrappers cannot observe every invalidation point.
- LOW: private fields and the listed getters; upstream rarely touches
SessionManagerinternals.
settings-manager.ts: added persistedsmoothStreamingandsmoothStreamingFpssettings. Smoothing defaults on, FPS defaults to 60, and reads clamp the configured value to 30–120.
- The built-in interactive renderer must read the setting before extensions load and while it owns an active stream.
- LOW:
Settingsfields and accessors near the existing thinking-visibility setting.
provider-composer.ts: modelinputarrays (config input, models.json override, custom model definition) widened to("text" | "image" | "video")[], tracking the pi-aiModel.inputunion. Enables the kimi-codingk3video input capability and models.json overrides declaring video.remote-catalog-provider.ts:mergeModelsnow unionsinputmodalities (canonical text/image/video order) when a pi.dev overlay entry replaces a builtin model. The overlay refreshes costs/limits but a stale remote entry must not silently drop a fork-declared capability — the cached kimi-codingk3entry inmodels-store.jsonotherwise strips"video"and deactivates theread_videotool.
- The modality union is a core type shared with pi-ai; extensions consume it but cannot widen it.
- LOW:
provider-composer.tsmodel field lists.
src/core/agent-session.ts:_modelSelectionChangesContextnow also fires onapichanges with identical provider, id, and context window, so wire-protocol-only model changes trigger full toolset/prompt synchronization.src/core/extensions/runner.ts:emitModelSelectre-reads livesystemPromptOptionsper handler so an earlier handler that swaps the active toolset (gpt-apply-patch) lets later handlers (prompt-preset) rebuild the system prompt from the post-swap tools in the same emission.
- The stale-snapshot defect lives in the core emission path; extensions only consume the
combined
model_selectresult.
provider-composer.ts: composed providerstream()andstreamSimple()now apply the text tool-call middleware when a model hascompat.toolCallFormatand active tools. Custommodels.jsonproviders previously dispatched directly to their base or API provider, silently bypassing this compatibility behavior.
- Provider composition owns the final base-provider/API-provider stream dispatch before extensions receive model output, so extensions cannot insert the required context transformation and streaming parser on both paths.
- LOW:
provider-composer.tssharedstreamWith()dispatch and its@earendil-works/pi-ai/compatimports.
model-config.ts(AnthropicMessagesCompatSchema): added optional booleansupportsWebSearch, mirroringsupportsWebSearchPreviewinOpenAIResponsesCompatSchema. This is the models.json opt-in for Anthropic-compatible endpoints that genuinely support server-side web search (seepackages/ai/src/changes.md2026-07-16); without the schema entry the flag would fail models.json validation.
- models.json validation happens in core
model-config.tsbefore any extension sees the model entry.
- LOW:
model-config.tscompat schemas if upstream adds more compat flags.
skills.ts(formatSkillsForPrompt): the load trigger changed from "when the task matches its description" to "whenever its description even loosely matches the task - loading an irrelevant skill costs little; missing a relevant one degrades the work" (ported from omo Hephaestus).skills.test.tspins "even loosely matches".
formatSkillsForPromptis core-owned and rendered into every system prompt. Strict-match framing under-loads skills on compression-biased models (GPT-5.6); stating the cost asymmetry is the decision-rule form the 5.6 guide prescribes for judgment calls.
- LOW:
skills.tsintro lines if upstream rewords the skills preamble.
agent-session.ts: accepted auto-compaction now releases only its own abort-controller identity before awaiting the recovery continuation, while the session-work barrier remains active until recovery settles.- Final cleanup is identity-guarded so an older compaction cannot clear a newer controller installed during recovery.
- Interactive input classification reads core-owned
AgentSession.isCompacting, and fresh-prompt serialization depends on the private session-work barrier. Extensions cannot split those two lifecycle boundaries safely.
- MEDIUM:
agent-session.tsaround_runAutoCompaction()accepted-result handling and final controller cleanup.
agent-session.ts: deferred post-compaction and queued-message continuations until the current serializedagent_endevent promise resolves, while registering the detached continuation inSessionWorkBarrier.- Overflow retry, threshold/pending-message delivery, and normal queued
agent_endcontinuation use the same scheduler.
- Awaiting
agent.continue()inside the activeagent_endqueue item deadlocked tool-bearing continuations because pre-tool hooks wait for the current agent-event queue to finish persisting.
AgentSessionowns the event queue, tool-hook barrier, settlement state, and continuation launch boundary.
- MEDIUM:
agent-session.tsaroundagent_endqueued continuation handling and_runAutoCompaction()recovery. - LOW:
_continueAgentAfterCurrentRun()and the session-work barrier integration.
resource-loader.ts: project-trust reloads now carry forward only preloaded factory-origin extensions - builtins, bundled codemode package entries, and inline factories - ahead of file-based extensions.- Shadowed or disabled file extensions from the pre-trust pass remain excluded from the trusted final set instead of being restored by the factory carry-over.
- Added regression coverage that verifies trusted reloads preserve plain-reload membership and builtin-first order, including
todowrite, codemode'sevaltool, and a shadowedpi-todotoolspackage.
- Project trust uses a core-owned two-phase resource load. Only the resource loader can retain the factory instances and side effects from the untrusted bootstrap pass while rebuilding the final trusted extension order.
- LOW:
resource-loader.tsaround trusted final extension-set composition.
model-registry.ts: exposed configuredupstreamModelIdmetadata synchronously so session-control code can compare selected aliases with provider-reported wire model ids without resolving credentials.agent-session.ts: overflow recovery now treats a context-window error from the configured upstream model id as the same current-model source, preserving the existing stale/unrelated model guard.
- Provider context-overflow recovery happens inside the core session compaction gate before extensions can safely decide whether to retry the active turn.
- MEDIUM:
agent-session.tsaround_checkCompaction()overflow eligibility. - LOW:
model-registry.tsaround model request metadata accessors.
resource-loader.ts: addedcodemodeas a builtin-adjacent bundled extension loaded from the@code-yeongyu/senpi-codemodepackage manifest.- The bundled extension is enabled by default, respects
enabledBuiltinExtensionsanddisabledBuiltinExtensions, is unaffected by--no-extensions, and is still removable from the active tool set through--exclude-tools eval. - Resolution failures, including compiled Bun binary package-resolution gaps, are reported as extension diagnostics and startup continues without
eval.
- The extension package is shipped with the CLI and must be active before user extension discovery and project-trust resolution. User-installed extension paths cannot model that trusted default-on load order.
- HIGH:
resource-loader.tsaround builtin extension loading, package shadowing, and active builtin id filtering.
agent-session.ts: added the core implementation forpi.executeTool(), including active-tool resolution, shared agent-loop argument preflight, syntheticcodemode-*tool call ids, hook block handling, and post-result rewrites.- Extracted the existing
beforeToolCallandafterToolCallhook bodies into shared helpers used by both normal agent-loop dispatch andexecuteTool().
- Extensions can observe and register tools, but only the session owns the active wrapped tool instances, the agent-event queue, and the hook/permission pipeline needed to execute subcalls with the same semantics as model tool calls.
- HIGH:
agent-session.tsaround_installAgentToolHooks(),getActiveToolNames(), and extensionbindCore()wiring.
auth-providers.ts(fork-only): shared auth-provider list module — the single source of truth across the classic TUI/loginflow and the RPC auth commands.agent-session.ts: emitsauth_login_url/auth_login_endasAgentSessionEvents so interactive OAuth round-trips can complete out-of-band of a single RPC request; reusesAuthStorage.logincallbacks unchanged.
- The neo Go TUI logs in over RPC (see
modes/rpc/changes.md); login completion cannot fit inside the 30s RPC request timeout, so the terminal result must arrive as session events.
- Auth storage, login callbacks, and session event emission are core session services.
- MEDIUM:
agent-session.tsaround session event union and emission sites. - LOW:
auth-providers.ts(fork-only file).
sdk.ts: the agent's stream idle timeout now defaults tohttpIdleTimeoutMs(300s default) instead of being off unlessretry.provider.timeoutMswas set.settings-manager.ts:httpIdleTimeoutMsparticipates in the default resolution;0disables, andretry.provider.timeoutMsstill overrides.
- Sessions went stale forever when the network dropped and reconnected mid-stream: the dead connection never errors.
Node runs were eventually rescued by the undici dispatcher body timeout, but the Bun binary has no such protection
and hung indefinitely. With the guard on by default, a silently dead connection fails with a retryable idle-timeout
error and auto-retry recovers the turn (abort-side fix in
packages/agent/src/changes.md2026-07-06).
- Stream option defaults are resolved in core SDK/settings plumbing before extensions see a request.
- LOW:
sdk.tsstream-option assembly;settings-manager.tsretry/timeout resolution.
hidden-stdout-log.ts(fork-only): hidden external stdout writes are redacted and appended to the debug log.output-guard.ts/sensitive-output.ts: stderr writes are likewise hidden and redacted while a TUI owns the terminal, matching the interactive stderr guard.- Wiring: interactive mode, startup dialogs, and the config selector (see
modes/interactive/changes.mdandcli/changes.md); the TUI-side hook isProcessTerminal.onExternalStdoutWrite(packages/tui/src/changes.md2026-07-04).
- A stray
console.logfrom a library or extension corrupted the trust dialog and permanently desynchronized differential rendering.
- Redaction and debug-log routing for hidden writes are core services shared by every TUI surface.
- LOW:
hidden-stdout-log.ts,output-guard.ts,sensitive-output.ts(fork-heavy files).
bash-executor.ts: when bash output is truncated for the model context, the truncated contents are still persisted so the session record keeps the full output.
- Truncation previously dropped the overflow entirely; transcripts and session replays lost output that the user's terminal had shown.
- Output truncation happens inside the built-in bash executor before tool results reach extension hooks.
- LOW:
bash-executor.tstruncation/persistence path.
model-resolver.ts: available-model lookups are properly awaited instead of racing an unresolved promise.
- The fork's model-resolution flow could observe an empty model list mid-startup.
- Startup model resolution runs before extensions load.
- LOW:
model-resolver.tsasync lookup call sites.
project-trust.ts:AppModegained"app-server"so project-trust resolution covers the fork's app-server mode (mode itself lives inmodes/app-server/, dispatch insrc/changes.md2026-07-02).
- App-server sessions must honor the same project-trust gating as interactive/rpc modes.
- Trust gating is evaluated in core before a mode starts.
- LOW:
project-trust.tsAppModeunion.
auth-storage.ts: accepted upstream persistence-failure surfacing so/logindoes not report success whenauth.jsoncould not be saved.agent-session.ts: accepted upstream split-turn serialization and kept fork prompt/compaction settlement behavior.session-manager.ts: accepted upstream context-building helper splits while preserving fork compaction detail propagation throughcreateCompactionSummaryMessage(entry.details).model-resolver.ts: accepted upstream structured model-resolution diagnostics and public helper behavior while preserving the fork's optional warning callback.
- These upstream fixes improve observable login errors, prevent overlapping summary generations, and expose consistent model diagnostics without dropping fork-only compaction metadata or warning behavior.
- Auth persistence, session context reconstruction, prompt/compaction scheduling, and model scope resolution are core session services that run before extensions can replace them.
- HIGH:
agent-session.tsaround prompt execution, compaction settlement, and split-turn continuation. - MEDIUM:
session-manager.tsaroundsessionEntryToContextMessages()and compaction-entry reconstruction. - MEDIUM:
model-resolver.tsaroundresolveModelScope()and diagnostics helpers. - LOW:
auth-storage.tsaround save failure propagation.
src/core/session-manager.ts: large in-memory session strings are retained through a resident store while public readers, LLM context construction, branching, forking, and JSONL persistence materialize the original content.src/core/session-resident-store.ts: centralizes resident string references and store statistics for session payloads.
- Long sessions can retain repeated large message payloads in every session tree/index view. Keeping large resident strings behind lightweight refs lowers steady-state session memory pressure without changing persisted sessions.
- MEDIUM:
SessionManagerappend, reload, branch, and persistence paths. - LOW: tests under
test/session-manager/that assert exact in-memory entry identity.
src/core/agent-session.ts: normal user prompts now wait for pending session event processing and in-flight compaction work before starting a fresh provider request.src/core/agent-session.ts: overflow retry and user-visible queued follow-up/steering recovery now await the post-compaction continuation instead of scheduling an unobserved delayedcontinue().src/core/agent-session.ts: agent-level custom-only queues also use the awaited post-compaction continuation path.src/core/session-work-barrier.ts: centralizes nested session-work barriers used by compaction settlement.
Agentcan become idle beforeAgentSessionfinishesagent_endcompaction work. A prompt submitted in that window could race ahead of the compaction boundary or overflow recovery, making queued messages appear out of order or miss the compacted context.
- Extensions can provide compaction results, but only
AgentSessioncan serialize fresh prompts against session event processing, compaction mutation, and retry/queue continuation.
- MEDIUM:
AgentSession.prompt()around the pre-prompt settlement and post-prompt wait. - MEDIUM:
_executeCompaction()and_runAutoCompaction()around compaction lifecycle and continuation handling.
src/core/agent-session.ts:abort()anddispose()now cancel in-flight manual/auto compaction and branch summarization controllers along with retry/agent cleanup.src/core/agent-session.ts:setModel()and favorite model cycling invalidate compaction state and bump the message revision whenever the selected model identity or context window changes.src/core/agent-session.ts:model_selectnow emits for same provider/model-id selections that change the effective context window, so extensions can drop stale model-bound work.
- An aborted over-context turn could leave a compaction request alive. If the user then switched to a larger-context model, stale compaction could finish beside the next normal assistant response and surface duplicate Working/status state.
- Extensions can observe model and compaction events, but the session owns the abort controllers and the monotonic message revision that guards precomputed compaction snapshots.
- MEDIUM:
AgentSession.abort(),setModel(), and_cycleFavoriteModel()lifecycle paths. - LOW:
AgentSession.dispose()cleanup path and_emitModelSelect()early-return logic.
src/core/extensions/runner.ts:tool_callandtool_resulthandlers now emit internal start/end lifecycle observations withPreToolUse/PostToolUselabels, bounded status messages, elapsed-time anchors, and completed, blocked, or failed end statuses.src/core/agent-session.ts: the session relays those internal observations to mode listeners astool_hook_statusevents without exposing a new extension author API.
- The interactive TUI needs to show when extension hook work is happening, including permission-rule matching and post-tool result processing, instead of leaving users with only a generic Working indicator.
- Extensions can show their own UI, but only the runner knows when each individual hook handler starts, ends, blocks, or fails. The session must relay that host-owned lifecycle to the TUI.
- MEDIUM:
extensions/runner.tsaroundemitToolCall()andemitToolResult(). - LOW:
agent-session.tsaround_applyExtensionBindings()andAgentSessionEvent.
src/core/agent-session.ts:abort()now creates a shared user-abort settlement promise before waiting for the active agent run to become idle.src/core/agent-session.ts:prompt()waits for that user-abort promise before classifying submitted input as streaming steering/follow-up or a normal fresh prompt.
- Pressing Esc while a tool call was active started abort asynchronously. A message submitted before the old run settled
still saw
isStreaming === true, so it was queued into the aborting run and could remain stuck after abort completed.
- The stale queue classification happens inside
AgentSession.prompt()before extension commands or input handlers can reliably distinguish "streaming" from "currently aborting and about to become idle".
- MEDIUM:
AgentSession.prompt()around the streaming queue branch. - MEDIUM:
AgentSession.abort()around agent abort and idle waiting.
src/core/agent-session.ts: auto-retry now uses provider-supplied retry-after hints from assistant error messages when present, while refusing waits aboveretry.provider.maxRetryDelayMs.
- Rate-limit and overload responses can include an explicit wait period. Ignoring that hint caused senpi to retry too early with the local exponential base delay, often hitting the same provider throttle again.
- Retry scheduling is core
AgentSessionlifecycle behavior. Extensions can observe retry events, but they cannot replace the internal abortable sleep or resolve the prompt-level retry promise.
- MEDIUM:
AgentSession._handleRetryableError()and retry event emission.
messages.ts: removed the coding-agent-sideCustomAgentMessages.compactionSummarydeclaration merge entry.
@earendil-works/pi-agent-corenow declares the shared harness compaction summary message type. Keeping a second coding-agent declaration for the samecompactionSummaryslot madetsgoreject the package build because the two declarations used distinct local interface symbols.
- This is TypeScript declaration metadata for core message unions, evaluated at package build time before extensions run.
- LOW:
messages.tsaround theCustomAgentMessagesdeclaration merge block.
messages.ts:CompactionSummaryMessagecan now carry opaquedetailsfrom the accepted compaction result.session-manager.ts: reconstructed compaction summary messages preserve those details when rebuilding context from session entries.
- The OpenAI remote compact API returns provider-native retained input, counts, and route metadata that should remain visible after compaction and across context reconstruction without hard-coding provider behavior into core.
- Extensions can create the compaction result, but core owns conversion from persisted
compactionentries intoCompactionSummaryMessageobjects.
- LOW:
messages.tsaroundCompactionSummaryMessageandcreateCompactionSummaryMessage(). - LOW:
session-manager.tsaround compaction-entry reconstruction.
src/core/export-html/index.tsandsrc/core/agent-session.ts:/exportoutput paths now expand leading~before writing HTML or JSONL exports.
- A user-facing
/export ~/asdf.jsonlcould create./~/asdf.jsonlinstead of writing to the home directory.
- Export path resolution lives in the core export/session methods before extension command handlers see the final file write.
- LOW:
export-html/index.tsandAgentSession.exportToJsonl()path handling.
src/core/agent-session.ts: context-window overflow errors now trigger overflow compaction with automatic retry when the saved assistant provider differs from the current provider alias but the current context is also at the compaction limit.
- Imported or resumed sessions can contain OpenAI provider aliases from a previous run. When such a near-limit session overflows, treating the error as threshold compaction leaves the user with an empty error turn and no automatic retry.
- Overflow retry policy is core agent-loop recovery behavior; extensions can request compaction but cannot reliably remove the error turn and restart the agent turn.
- MEDIUM:
AgentSession._checkCompaction()around overflow-vs-threshold recovery.
src/core/resource-loader.ts: Extension paths are deduped by nearestpackage.jsonpackage name plus relative extension entry before loading, so the same package installed from both a git package checkout and~/.senpi/agent/extensions/loads once without dropping multi-extension packages.- Builtin extensions now precede disk-loaded extensions in the runtime array, and builtin-vs-external tool/flag name collisions no longer surface as startup errors.
- Extension flag defaults and CLI flag validation now follow that final builtin-first order, so an external duplicate flag cannot override builtin metadata by registering earlier during disk discovery.
- Users with both installed and manually cloned
code-yeongyu/pi-*extensions saw noisy duplicate tool/flag conflict errors at startup, even when the duplicates represented the same logical extension or a builtin vendored copy.
- Extension factories only run after resource discovery and conflict diagnostics. Deduping package paths and classifying builtin/external conflicts has to happen in the core resource loader before the TUI renders startup diagnostics.
- LOW:
resource-loader.tsaround extension path assembly, rebuilt flag defaults, anddetectExtensionConflicts()if upstream changes resource precedence or conflict diagnostics. - LOW:
agent-session-services.tsaround extension CLI flag validation if upstream changes extension flag parsing.
src/core/model-registry.ts: Custommodels.jsonmodel entries and built-inmodelOverridescan now carry apromptPresetstring.- The registry preserves this value as model metadata for extensions instead of interpreting preset names in core code.
- Provider-specific model IDs can be too new or too aliased for automatic prompt-preset detection. Putting
promptPresetnext to the model definition keeps the routing metadata with the model catalog entry that needs it.
- The prompt-preset extension can consume model metadata, but
models.jsonschema validation and model merging live in the core registry. Core needs to preserve the metadata before extensions see the selected model.
- LOW:
ModelDefinitionSchema,ModelOverrideSchema, andapplyModelOverride()insrc/core/model-registry.tsif upstream adds more per-model metadata fields.
- Added
src/core/thinking-levels.tsso coding-agent owns the senpi-specificxhigh/maxtier detection and supported-level expansion. - Updated
src/core/agent-session.tsandsrc/core/sdk.tsto import these helpers locally instead of from@earendil-works/pi-ai.
- The published
@code-yeongyu/senpipackage currently installs the registry@earendil-works/pi-ai@0.74.0, whose public exports do not include the fork-onlysupportsXhigh/supportsMaxhelpers. - Importing those names directly from
pi-aimakes packaged senpi fail during module loading before any CLI command runs.
- Thinking-tier availability is consumed by core session/model logic (
AgentSession, SDK helpers) during startup and model switching, before extensions can replace those imports.
- LOW:
agent-session.ts/sdk.tsimport blocks and any future upstream move of thinking-level helpers.
src/core/model-registry.ts: Custommodels.jsonmodel entries can now setupstreamModelIdand per-modelserviceTier.src/core/sdk.ts: Provider requests use the configured upstream model id while preserving the configured catalog id for model selection.
- Users need both a normal catalog entry and a priority catalog entry, such as
gpt-5.5andgpt-5.5-fast, while sending the upstream request asmodel: "gpt-5.5"withservice_tier: "priority"for only the priority entry.
- The model id is embedded by the provider payload builder before
before_provider_requesthooks run, andservice_tieris a provider-managed field. The registry has to carry the configured wire id and tier into the stream call before payload construction.
- MEDIUM:
model-registry.tsschema/request-auth metadata andsdk.tsstream option composition.
src/core/resource-loader.ts: Unchanged generated global default extension shims are now recognized by path and exact generated content, then resolved to the known in-process extension factory before the generic jiti loader runs.src/core/resource-loader.ts: User-edited or replacement files with the same default names still load through the normal extension import path.
- Clean-profile startup was spending several seconds loading deterministic generated shim files through jiti even though core already knows the matching default extension factories.
- Generated default shims are discovered and loaded by core resource bootstrap before extension code can run. Extensions cannot replace the loader's import strategy for their own files.
- LOW:
resource-loader.tsaround generated global default extension path/content checks and theloadExtensions()call.
src/core/resource-loader.ts: Default generated global extension shims now point atdistfiles when senpi itself is running fromdist, even in a linked workspace that also hassrc.
- Linked CLI startup was re-transpiling default global extension TypeScript files through jiti before the first frame.
- Generated default global extension shims are created by core resource loading before extension code runs.
- LOW:
resource-loader.tsaroundgetGlobalDefaultExtensionModulePath()and default shim generation.
src/core/model-registry.ts:models.jsoncan disable providers with top-leveldisabledProvidersor per-providerdisabled, filter provider models withwhitelist/blacklist, and replace built-in thinking-level mappings withthinkingLevelMapMode: "replace".src/core/settings-manager.tsandsrc/core/sdk.ts: addedfavoriteModelssettings support and keptenabledModelsas global model-catalog narrowing.src/core/agent-session.ts: reload refreshes the model registry, global model narrowing, and favorite models; Ctrl+P cycling only uses the configured favorite models, and available thinking levels honor model-level mapping overrides.
- The user requested opencode-style provider disable/filtering, favorite-model-only Ctrl+P cycling, and configurable replacement of reasoning variants with reload support.
- Model discovery, startup model resolution, persisted settings, and Ctrl+P cycling are core session/model-registry responsibilities. Extensions can add providers or shortcuts, but cannot reliably replace the built-in model registry, default catalog narrowing, or internal cycling semantics before the TUI starts.
- HIGH:
model-registry.tsschema/loading and model filtering. - MEDIUM:
sdk.tsstartup model narrowing resolution andagent-session.tsreload/cycle paths.
enabledModelsremains readable as global model narrowing, but Ctrl+P favorites are persisted throughfavoriteModels.
src/core/agent-session.ts: favorite models now act as a filter over the current available model list and current global narrowing before being exposed or cycled, so stale cached model objects cannot be selected after a provider/model leaves the registry.src/core/model-resolver.ts: slash-qualified glob patterns now match canonicalprovider/modelids only, preventing patterns likeopenai/*from also matching raw model ids such asopenai/gpt-*under another provider.
- Favorite cycling should only choose models that are still present in the current model catalog. This matches opencode's validity filter behavior and avoids switching to stale favorites after provider/model changes.
- Favorite model resolution and Ctrl+P cycling are core
AgentSessionbehavior, and glob pattern matching is shared by core startup/reload model resolution before extensions can safely override it.
src/core/agent-session.tsaround favorite model getters andcycleModel().src/core/model-resolver.tsaround glob pattern matching inresolveModelScope().
src/core/keybindings.ts: added configurableapp.models.toggleFavorite, defaulting toCtrl+F, for model selector favorite toggles.
- Users need the
/modeland/favorite-modelsselectors to select models normally while still being able to toggle favorite status for the highlighted row.
- Selector key handling uses the built-in keybinding registry before extension UI code can attach row-local actions, so the built-in selector action needs a first-class keybinding id.
- LOW:
keybindings.tsaround model selector keybinding definitions.
src/core/package-manager.ts:updateGit()now runs the package dependency install step even when the fetched git target already matches the local checkout.
senpi updatepreviously returned early for current git packages. If an extension checkout'snode_moduleswas damaged or incomplete, the update command reported success but left runtime imports broken.
- Git package update and dependency installation are core package-manager responsibilities that run before extension loading.
- LOW:
DefaultPackageManager.updateGit()around the post-fetch current-HEAD branch.
src/core/agent-session.ts: Appliesmodel_selectsystem prompt results immediately, emitssystem_prompt_changeonly when the active prompt string changes, and returns the change fromsetModel()/cycleModel().src/core/extensions/types.ts: Added typedsystem_prompt_changeevent and model-select prompt-change result.src/core/extensions/runner.ts: AddedemitModelSelect()to collect prompt-change results frommodel_selecthandlers.src/modes/interactive/interactive-mode.ts: Includes the changed prompt name in model-switch status messages and shows standalone prompt-change status for extension-driven switches.src/core/extensions/builtin/prompt-preset/index.ts: Resolves prompt presets duringmodel_selectso mid-session model changes update the active prompt immediately.
- The prompt-preset builtin only changed the effective prompt at the next
before_agent_start. The user requested mid-session model changes to switch the system prompt immediately, emit api.onevent, and show the TUI notice only when the prompt actually changes.
- The existing extension event runner ignored
model_selectreturn values and had no core-owned typed event for active system prompt changes. TUI status also needs core session feedback fromsetModel()/cycleModel().
- HIGH:
agent-session.tsaround model switching and event emission. - HIGH:
extensions/types.tsandextensions/runner.tsaround model events. - MEDIUM:
interactive-mode.tsmodel status rendering.
- Keep
system_prompt_changegated by actual string inequality. Same-preset model switches must not spam the event or TUI.
src/core/agent-session.ts: Added in-memory monotonic message revision counter. AddedgetMessageRevision()andapplyCompaction(precomputed, { reason, expectedRevision })for compare-and-apply speculative compaction.src/core/agent-session.ts: Extended_executeCompaction()to accept a precomputedCompactionResult.src/core/extensions/types.ts: AddedApplyCompactionOptions,ApplyCompactionResult,ExtensionContext.getMessageRevision(),ExtensionContext.applyCompaction().src/core/extensions/runner.ts: Wired new context actions throughbindCore()andcreateContext().src/modes/interactive/interactive-mode.ts: Added same methods to inline shortcutExtensionContextliteral.
- Speculative/v2 compaction needs a stable compare-and-apply seam: extensions can prepare a compaction summary against revision N and only apply it if no context-affecting message mutation has happened since.
getMessageRevision()is intentionally monotonic and in-memory only; it is a staleness guard, not persisted session data.applyCompaction()returns explicitok,stale, orrejectedoutcomes so extensions can avoid racing the live session.
Extensions can observe hooks and return summaries during a core-driven compaction, but they cannot append a compaction entry, rebuild agent context, emit core compaction events, or atomically guard against stale session context without a typed core API.
- HIGH:
agent-session.tsaround message revision andapplyCompaction()implementation. - HIGH:
extensions/types.tsandextensions/runner.tsaroundExtensionContext/ExtensionContextActionsdefinitions. - MEDIUM:
interactive-mode.tsshortcut context literals must retain parity withExtensionRunner.createContext().
If upstream adds new ExtensionContext methods or changes AgentSession message mutation logic, preserve the monotonic revision counter and the applyCompaction() compare-and-apply semantics. The revision guard must remain in-memory and advance on every context-affecting mutation. Do not let upstream's ExtensionContext additions shadow the new methods.
src/core/agent-session.ts: Added core-owned begin/end helpers for extension-driven compaction feedback and wired them intoExtensionContext.src/core/agent-session.ts:applyCompaction()now reuses an already-open compaction abort controller so an extension can showcompaction_startbefore it has a precomputed summary without emitting duplicate start events.src/core/extensions/types.tsandsrc/core/extensions/runner.ts: Added optionalbeginCompaction()andendCompaction()context methods.
- The fork's speculative/blocking compaction extension can spend time generating or awaiting a summary before
applyCompaction()is called. - Without a core-owned feedback scope, the TUI has no compaction loader, Esc cancellation signal, or
isCompactinginput queueing during that wait.
Extensions can call UI methods, but they cannot set AgentSession.isCompacting, own the session abort controller, or emit canonical compaction_start/compaction_end events without a core context action.
- HIGH:
agent-session.tsaroundapplyCompaction(), compaction abort controllers, and extension context binding. - HIGH:
extensions/types.tsandextensions/runner.tsaroundExtensionContext/ExtensionContextActions.
If upstream adds a native progress or cancellation API for compaction, map the builtin compaction extension to that API while preserving the invariant that visible feedback starts before extension summary generation begins and ends exactly once.
src/core/agent-session.ts: Consolidated manual, threshold, overflow, pre-prompt, and extension-triggered compaction routes into a single private_executeCompaction()pipeline.- The unified pipeline covers: preparation, extension hook execution (
session_before_compact), summary generation, pre-append token simulation, session append, context rebuild, and completion event emission (session_compact). - Route-specific metadata (reason, custom instructions, thinking/max-token behavior), error handling, retry handling, token estimation before append, and abort handling now flow through one seam.
- The user identified 9 route inconsistencies caused by duplicated compaction code paths across manual
/compact, threshold-triggered, overflow-recovery, pre-prompt, and extension-triggered compaction. - Without unification, each route handled metadata, error recovery, token estimation, and event emission differently, causing observable behavioral differences for extensions consuming compaction events.
The duplicated route control flow lives inside AgentSession. Extensions can customize compaction content via session_before_compact hooks, but they cannot unify internal caller behavior, append semantics, context rebuilds, or core event ordering from outside the session.
- HIGH:
agent-session.tsis the highest-churn upstream file. Rebase conflict resolution must preserve the_executeCompaction()pipeline and keep branch summarization outside this helper.
If upstream modifies any compaction route (manual, threshold, overflow, pre-prompt), resolve conflicts by routing the modified logic through _executeCompaction() rather than restoring inline duplication. Preserve the 6-route coverage: manual, threshold, overflow-recovery, pre-prompt, extension-triggered, and branch summarization (which routes through the hook but remains a separate caller). Keep the pre-append token simulation step to prevent post-compaction overflow.
- Changed
src/core/extensions/builtin/index.tsandsrc/core/resource-loader.tsso builtin extensions keep stable synthetic ids like<builtin:todowrite>instead of being loaded as numbered inline factories. - This was changed in core because the startup Extensions list is sourced from extension metadata produced by
DefaultResourceLoader; the extension API cannot rename builtin factory identities after load. - Expected merge-conflict zone on upstream sync: builtin extension registration in
src/core/extensions/builtin/index.tsand builtin factory loading insrc/core/resource-loader.ts.
- Changed
src/core/extensions/builtin/index.tsandsrc/core/resource-loader.tssodiff,files,prompt-url-widget, andtpsare no longer registered as builtin factories. DefaultResourceLoadernow seeds generated shim files for those four defaults into the real globalagentDir/extensions/directory, so they load through normal global extension discovery instead of builtin registration.DefaultResourceLoadernow rewrites previously generated shim files when their absolute builtin module paths become stale after the checkout/package directory moves or is renamed.- This had to be done in core because builtin-vs-global extension ownership is determined during resource bootstrap, before any extension code runs.
- Expected merge-conflict zone on upstream sync: builtin extension registration and early resource bootstrap in
src/core/resource-loader.ts.
- Changed
src/core/settings-manager.tsandsrc/core/resource-loader.tssosettings.jsoncan disable selected builtin extensions withdisabledBuiltinExtensions. DefaultResourceLoadernow skips builtin factories whose ids are listed in settings.- This had to be done in core because builtin extensions are instantiated during early resource bootstrap, before project extensions can intercept or unregister them.
- Expected merge-conflict zone on upstream sync: settings schema/getters in
src/core/settings-manager.tsand builtin factory loading insrc/core/resource-loader.ts.
- Changed
src/core/settings-manager.tssogetSteeringMode()now defaults to"all"instead of"one-at-a-time"when no explicit setting is present. - Added
test/settings-manager.test.tscoverage to lock the new default behavior. - This was changed in core because the default steering mode is injected into
Agentduring session creation viaSettingsManager, so an extension cannot change the built-in default before the session runtime is constructed. - Expected merge-conflict zone on upstream sync:
src/core/settings-manager.tsdefault getter behavior.
- Changed
src/core/settings-manager.ts,src/core/extensions/builtin/index.ts, and addedsrc/core/extensions/builtin/service-tier.tssosettings.jsoncan setopenai.serviceTierand automatically injectservice_tierinto OpenAI Responses payloads. - Added test coverage in
test/suite/service-tier-extension.test.ts,test/suite/service-tier-settings.test.ts, and updated builtin extension registration coverage intest/resource-loader.test.ts. - This was changed in core because builtin extension registration and settings schema/getter wiring happen before extension code can discover a new builtin id or read typed settings from the existing settings manager.
- Expected merge-conflict zone on upstream sync: builtin extension registration in
src/core/extensions/builtin/index.tsand settings schema/getter additions insrc/core/settings-manager.ts.
- Changed
src/core/extensions/builtin/index.ts,src/core/resource-loader.ts, andsrc/core/settings-manager.tsso builtin extensions can be allowlisted withenabledBuiltinExtensionswhile preservingdisabledBuiltinExtensionsas an override. - Added
src/core/extensions/builtin/webfetch/as a builtin extension synced from../pi-extensions/pi-webfetch, and movedbash-timeoutandopenai-api-parallel-tool-callsto synced../pi-extensionslayouts. - Added
scripts/sync-builtin-extensions.mjs, wired into the package build, so local builds refresh the vendored builtin snapshots fromSENPI_BUILTIN_EXTENSIONS_SOURCEor../pi-extensionswhen that source checkout exists.external-versions.jsonrecords the source package names and versions included in the snapshot. - This had to be done in core because builtin extension registration and builtin settings filtering happen before any user extension can affect resource discovery.
- Expected merge-conflict zone on upstream sync: builtin extension registration in
src/core/extensions/builtin/index.ts, builtin factory filtering insrc/core/resource-loader.ts, and settings schema/getters insrc/core/settings-manager.ts.
- Widened the
"max"thinking level through the coding agent surface: CLI--thinking max,/settingsselector, Shift+Tab cycle,settings.jsondefaultThinkingLevel, thinking border color mapping. - Extended
packages/coding-agent/src/core/model-registry.tssomodels.json(andpi.registerProvider()) acceptsextraBodyat both provider and per-model level.getApiKeyAndHeadersnow resolvesextraBody, andsdk.tsmerges provider/model extraBody with any call-siteextraBodybefore invokingstreamSimple. - This had to be done in core because
ThinkingLevelis exported from@mariozechner/pi-agent-coreand every UI/CLI/settings surface needed to be widened, and becausegetApiKeyAndHeaders+ stream option composition live in coreModelRegistry/sdk.ts. - Expected merge-conflict zone on upstream sync:
model-registry.tsschemas +getApiKeyAndHeaders,sdk.tsstream option composition,cli/args.tsvalidator,settings-manager.tsthinking level type,agent-session.tsthinking cycle list, interactive TUI thinking selector and border color map.
agent-session.ts: accepts a session-onlyPromptOptions.thinkingLevel, rejects queued prompts carrying it before queue mutation, and emitsthinking_level_changedwhen retry fallback applies an ephemeral level.
- Prompt preflight, session-only level application, fallback model switching, and session event emission are private core lifecycle boundaries.
- HIGH:
agent-session.tsprompt serialization and fallback model-switch logic.