Applied the session-scoped tool reset after /branch and /btw create their
branched logical sessions, closing the same stale-state leak as /new,
handoff, and cross-session switches.
Added a branch transition to the staged-preview regression matrix.
Applied the session-scoped tool reset after handoff creates its replacement session.
Added regression coverage for stale staged preview directives across handoff.
Fixes#4093
Cleared queued tool-choice directives and ACP always decisions after successful logical session transitions.
Added new-session and cross-session regression coverage for staged resolves and both ACP always decisions.
Fixes#4093
- Extracted internal handlers and logic from AgentSession into dedicated runner, guard, and coordinator modules.
- Created standalone modules for bash execution, evaluation runners, IRC bridging, and prewalk coordination.
- Established dedicated session components for tracking stats, todos, streams, and retry fallback chains.
- Preserved existing session behavior while significantly reducing monolithic class size and complexity.
- The memory-backend transition await added in PR #6428 guarded with #isDisposed, which beginDispose() sets immediately; prompts issued in the disposing window were silently dropped instead of running their final turn and settling via the dispose abort.
- Guard now only drops the prompt when disposal began during the transition await.
Both compaction serializers reproduced prior assistant reasoning as text
bound for a Claude target, tripping Anthropic's reasoning_extraction
refusal and wedging Fable 5 sessions:
- context-full: serializeConversation rendered thinking verbatim inside
<thinking> tags via the anthropic dialect renderer. Now drops thinking
blocks when the summary target dialect is anthropic; other dialects
(e.g. Harmony) keep native reasoning.
- snapcompact: emitted ¶think sections baked into replayed archive
frames. Added an includeThinking serialize option (default true) and
wired the agent session to disable it for Anthropic-dialect models.
Fixes#6093
- Preserved stalled assistant turns when every emitted tool call had a result, including synthetic unexecuted results.
- Kept unresolved and non-stall tool errors replay-safe and covered terminal error agent_end delivery.
Fixes#6414
#buildPlanReferenceMessage set #planReferenceSent = true while building the
message, before it was handed to agent.prompt. Any pre-send setup bail (the
four #promptGeneration generation-bail returns) or throw (@-mention reads,
before_agent_start hooks) between build and delivery left the flag true with
nothing delivered, and the #promptWithMessage finally never cleared it — so the
retry short-circuited at `if (this.#planReferenceSent) return null` and the
approved plan was silently dropped for the rest of the session.
Move the flag commit to the delivery hand-off point in #promptWithMessage: set
it only when a plan-reference message was built AND we are about to call
#promptAgentWithIdleRetry. Every bail/throw now leaves the flag clear so the
next prompt re-injects the plan. The compaction-success resets (issue #1246)
still clear it for re-injection on the next turn.
Fixes#4094
Kept superseded summaries and preserveData durable while deriving forward transcript elision from the active compaction.
Added branch and rewind coverage for snapcompact archives and OpenAI remote replacement history.
Fixes#4090
- Serialized backend transitions across runtime state, tools, and prompts.
- Rehydrated Mnemopi listeners after clear and enqueue maintenance.
- Made memory.backend the sole post-migration local runtime gate.
Fixes#5638
parseBlobRef sliced the blob:sha256: suffix and returned it unvalidated;
get/getSync then fed it into path.join(this.dir, hash), so a crafted ref
like blob:sha256:../../../etc/passwd escaped the blob directory and read
arbitrary files into resolved image history (base64, raw UTF-8, and the ACP
sync path).
Reject any suffix that is not a canonical 64-char lowercase hex hash in
parseBlobRef, the single choke point for every resolution entry point. Reuse
the shared BLOB_HASH_RE in gc-cli instead of its duplicate HASH_RE.
Fixes#4088
#ensureDir checked #initialized then set it across an await
#scanExistingIds() gap, so two concurrent first-use save/allocatePath
callers both re-seeded #nextId=maxId+1 and allocateId() handed both the
same id — silently overwriting the first artifact (same toolType) or
making artifact:// resolution ambiguous (different toolTypes).
Memoize the initial scan as a single in-flight #initPromise so all
concurrent callers share one initialization and receive distinct ids.
Fixes#4091
After an OpenAI remote compaction, prepareCompaction decided whether to
keep the provider-native replay boundary or re-expand its originals by
asking whether *any* compaction candidate (every role model plus the
largest-context available model) shared the payload's provider. In a
multi-role setup where a role such as modelRoles.smol stays on OpenAI,
the check passed forever, so a session switched to a non-OpenAI active
model kept a placeholder-only summary and never recovered the compacted
span for the rest of the session.
Judge reusability against the active model — the one that assembles the
request context every turn — instead of the candidate set. When the
active model cannot replay the payload, re-expand the originals into a
portable local summary, matching the self-healing already present for
single-provider migrations.
Fixes#6343
Reverted branch-side edits to spawn-policy prompts/tests, settings tab
groups, mermaid cache typing, prewalk todo gating, and packages/ai test
churn back to merge-base content; trimmed their changelog entries. These
repaired stale CI against an older main and are stale or conflicting
against current main.
A completed delivery hands off from the AsyncJobManager to the session's
yield queue before the follow-up is injected (idle flush runs on a delayed
post-prompt task; mid-turn entries wait for the next step boundary). In
that window hasPendingAsyncWork() read false from manager state alone, so
a terminal yield observed there terminated the run and silently dropped
the delivered result - the stale-success class the quiescence barrier
exists to prevent. The wake predicate now also counts queued async-result
entries on the yield queue; added a session-level contract test that
pinned the window (failed before, passes after).
Seeding additionalDirectories at launch (via --add-dir or the
workspace.additionalDirectories setting) called #rewriteAtomically on a
brand-new session manager, which materialized a header-only JSONL and a
fresh breadcrumb before any assistant output. Launching and exiting with
configured roots therefore created an empty resumable session that
--continue picked over the previous conversation.
Gated all three workspace-directory mutators behind the existing
#shouldHaveSessionFile() lazy-persistence gate (one shared helper), and
made setAdditionalDirectories a no-op when the normalized list is
unchanged so resuming large sessions no longer rewrites the whole JSONL
on every startup. Roots set before the gate is crossed land in the
header with the first durable write; added a regression test.
Review follow-ups (Codex on #6362, round 5):
- #computeSnapcompactRescueMaxFrames now subtracts the kept tail AFTER the
archive (plus the existing fixed-context reserves) so the budget mirrors
what #compactionCreatedHeadroom will measure, and returns 0 when not even
one frame fits — the rescue bails instead of appending a rebuild that can
never create headroom (and would wedge prepareCompaction behind its
last-entry guard once elide fixes the real tail).
- Dead-end warnings now stamp the branch's LATEST compaction entry: the
post-pass path no longer badges the entry the rescue just superseded, and
the no-preparation path badges the rebuilt entry when the rescue appended
without creating headroom.
Claude-Session: https://claude.ai/code/session_014rh4JyWFkxgMhgFaEf8VBY
Review follow-up (Codex on #6362, round 4): rebuilding a non-tail archive
appends the replacement compaction at the leaf, so the branch tail becomes a
compaction entry that prepareCompaction's last-entry guard can never
summarize past — even after elide shrinks the oversized kept tool result
that was the real culprit. The rescue now estimates the kept tail AFTER the
latest archive and bails when it alone exceeds the recovery band, leaving
that shape to the elide/image tiers.
Claude-Session: https://claude.ai/code/session_014rh4JyWFkxgMhgFaEf8VBY
Review follow-ups (Codex on #6362):
- #rescueSnapcompactFrameOverflow now returns the CompactionResult and emits
session_compact for the rebuilt entry, so extensions see the entry that is
actually active instead of (only) the one the rescue superseded.
- The no-preparation auto_compaction_end now carries that result instead of
{result: undefined, skipped: true} when the rescue rewrote history — the
TUI rebuilds the transcript on result, so a successful rescue is no longer
presented as a benign no-op.
Claude-Session: https://claude.ai/code/session_014rh4JyWFkxgMhgFaEf8VBY
Review follow-up (Codex on #6362): the rescue's replaceMessages() rebuild
drops the transient plan-reference message, so clear #planReferenceSent
(#1246) and reset advisor runtimes / todo phases exactly like the regular
compaction append path.
Claude-Session: https://claude.ai/code/session_014rh4JyWFkxgMhgFaEf8VBY
Route loadSessionMessagesReadOnly through transcript mode (collapsed to the
latest compaction) so history:// transcripts of on-disk sessions retain
failed/aborted assistant tails that the provider-context builder now drops.
Review follow-ups (Codex on #6362):
- The !preparation frame rescue now counts as complete only when the rebuild
actually created headroom; otherwise the elide/image tiers still run and the
no-progress warning stays — a frame-count shrink alone must not suppress it
when the oversized tail is a kept message/tool result the archive rescue
cannot touch.
- #computeSnapcompactRescueMaxFrames now applies the same MAX_FRAMES_DEFAULT /
maxFramesForDataBudget caps as #computeSnapcompactMaxFrames, so a
threshold-derived count can never exceed what the rebuilt prompt can attach.
Claude-Session: https://claude.ai/code/session_014rh4JyWFkxgMhgFaEf8VBY
A branch whose last entry is a snapcompact CompactionEntry billed past the
compaction threshold (FRAME_TOKEN_ESTIMATE x frames) dead-ended on every
resume: prepareCompaction returns undefined (nothing after the entry to
summarize), and the #4786 elide/image rescue tiers only inspect
"message"/"custom_message" entries, so a type:"compaction" tail escaped both
and the "Compaction freed too little context" warning re-fired forever.
Add a dedicated first rescue tier that rebuilds the SAME archive locally (no
LLM, no network) by re-running snapcompact.compact() over the entry's
carried-forward source text at a maxFrames derived from the trigger
threshold's recovery band instead of the window-fit budget: planArchive
truncates the oldest chars to fit, so the rebuilt entry genuinely shrinks.
Persisting through appendCompaction lets the write-time
superseded-compaction elision drop the stale frame payload from the JSONL,
and the pass skips the misleading no-progress warning.
Fixes the loop reported in
https://github.com/can1357/oh-my-pi/issues/4786#issuecomment-5056055342
Claude-Session: https://claude.ai/code/session_014rh4JyWFkxgMhgFaEf8VBY
- Implemented ModelRegistry.hasProvider to return true when a provider has live models, is discoverable, or is registered at runtime.
- Replaced AgentSession's internal provider check with #isKnownProvider that delegates to the new hasProvider method, updating related fallback logic.