@oh-my-pi/pi-coding-agent 17.0.3 → 17.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +88 -35
- package/dist/cli.js +3540 -3521
- package/dist/types/advisor/advise-tool.d.ts +3 -0
- package/dist/types/advisor/runtime.d.ts +7 -1
- package/dist/types/async/job-manager.d.ts +2 -0
- package/dist/types/config/model-registry.d.ts +7 -0
- package/dist/types/config/model-resolver.d.ts +8 -0
- package/dist/types/config/models-config-schema.d.ts +10 -0
- package/dist/types/config/models-config.d.ts +6 -0
- package/dist/types/dap/client.d.ts +10 -0
- package/dist/types/dap/types.d.ts +6 -5
- package/dist/types/extensibility/extensions/runner.d.ts +4 -2
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +16 -0
- package/dist/types/launch/spawn-options.d.ts +10 -0
- package/dist/types/launch/spawn-options.test.d.ts +1 -0
- package/dist/types/mnemopi/state.d.ts +32 -9
- package/dist/types/modes/components/status-line/component.d.ts +2 -3
- package/dist/types/modes/components/status-line/types.d.ts +3 -1
- package/dist/types/modes/components/tool-execution.d.ts +2 -0
- package/dist/types/modes/components/transcript-container.d.ts +4 -3
- package/dist/types/modes/components/tree-selector.d.ts +6 -2
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/print-mode.d.ts +4 -0
- package/dist/types/modes/rpc/rpc-input.d.ts +5 -0
- package/dist/types/modes/rpc/rpc-mode.d.ts +1 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +19 -1
- package/dist/types/session/messages.d.ts +6 -0
- package/dist/types/task/types.d.ts +6 -6
- package/dist/types/tools/browser/aria/aria-snapshot.d.ts +17 -7
- package/dist/types/tools/browser/cmux/socket-client.d.ts +2 -0
- package/dist/types/tools/browser/tab-protocol.d.ts +2 -0
- package/dist/types/tools/browser/tab-worker.d.ts +2 -0
- package/dist/types/tools/gh.d.ts +5 -3
- package/dist/types/tools/hub/index.d.ts +1 -1
- package/dist/types/tools/hub/jobs.d.ts +1 -0
- package/dist/types/tools/hub/types.d.ts +2 -0
- package/dist/types/tools/tool-timeouts.d.ts +1 -1
- package/dist/types/utils/git.d.ts +33 -0
- package/dist/types/web/search/providers/codex.d.ts +1 -1
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +150 -0
- package/src/advisor/advise-tool.ts +4 -0
- package/src/advisor/runtime.ts +38 -14
- package/src/async/job-manager.ts +3 -0
- package/src/cli/bench-cli.ts +8 -2
- package/src/cli/dry-balance-cli.ts +1 -0
- package/src/config/model-registry.ts +89 -8
- package/src/config/model-resolver.ts +78 -14
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings.ts +3 -1
- package/src/dap/client.ts +168 -1
- package/src/dap/config.ts +51 -1
- package/src/dap/session.ts +575 -234
- package/src/dap/types.ts +6 -5
- package/src/discovery/agents.ts +2 -2
- package/src/extensibility/extensions/runner.ts +6 -4
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +26 -0
- package/src/launch/broker.ts +34 -31
- package/src/launch/client.ts +7 -1
- package/src/launch/spawn-options.test.ts +31 -0
- package/src/launch/spawn-options.ts +17 -0
- package/src/lsp/types.ts +5 -1
- package/src/main.ts +17 -4
- package/src/mcp/transports/stdio.test.ts +9 -1
- package/src/mnemopi/backend.ts +1 -1
- package/src/mnemopi/embed-worker.ts +8 -7
- package/src/mnemopi/state.ts +45 -18
- package/src/modes/components/ask-dialog.ts +137 -73
- package/src/modes/components/status-line/component.ts +2 -2
- package/src/modes/components/status-line/segments.ts +20 -3
- package/src/modes/components/status-line/types.ts +3 -1
- package/src/modes/components/tool-execution.ts +5 -0
- package/src/modes/components/transcript-container.ts +18 -111
- package/src/modes/components/tree-selector.ts +10 -4
- package/src/modes/controllers/event-controller.ts +1 -2
- package/src/modes/controllers/input-controller.ts +1 -1
- package/src/modes/controllers/selector-controller.ts +29 -8
- package/src/modes/interactive-mode.ts +40 -8
- package/src/modes/noninteractive-dispose.test.ts +12 -1
- package/src/modes/print-mode.ts +21 -9
- package/src/modes/rpc/rpc-input.ts +38 -0
- package/src/modes/rpc/rpc-mode.ts +7 -2
- package/src/modes/setup-wizard/scenes/sign-in.ts +3 -1
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/ui-helpers.ts +3 -2
- package/src/prompts/tools/browser.md +3 -2
- package/src/prompts/tools/debug.md +2 -7
- package/src/prompts/tools/github.md +6 -1
- package/src/prompts/tools/hub.md +4 -2
- package/src/prompts/tools/task-async-contract.md +1 -0
- package/src/prompts/tools/task.md +9 -2
- package/src/sdk.ts +38 -6
- package/src/session/agent-session.ts +395 -162
- package/src/session/messages.test.ts +91 -0
- package/src/session/messages.ts +248 -110
- package/src/session/session-loader.ts +31 -3
- package/src/slash-commands/builtin-registry.ts +1 -0
- package/src/stt/recorder.ts +68 -55
- package/src/task/executor.ts +59 -33
- package/src/task/index.ts +46 -9
- package/src/task/types.ts +11 -8
- package/src/task/worktree.ts +10 -0
- package/src/tools/browser/aria/aria-snapshot.ts +36 -8
- package/src/tools/browser/cmux/cmux-tab.ts +2 -1
- package/src/tools/browser/cmux/socket-client.ts +139 -3
- package/src/tools/browser/run-cancellation.ts +4 -0
- package/src/tools/browser/tab-protocol.ts +2 -0
- package/src/tools/browser/tab-supervisor.ts +21 -11
- package/src/tools/browser/tab-worker.ts +199 -33
- package/src/tools/debug.ts +3 -0
- package/src/tools/gh.ts +40 -2
- package/src/tools/hub/index.ts +4 -1
- package/src/tools/hub/jobs.ts +42 -1
- package/src/tools/hub/types.ts +2 -0
- package/src/tools/tool-timeouts.ts +1 -1
- package/src/utils/git.ts +237 -0
- package/src/web/search/index.ts +9 -5
- package/src/web/search/providers/codex.ts +195 -99
|
@@ -63,6 +63,7 @@ import {
|
|
|
63
63
|
estimateTokens,
|
|
64
64
|
generateBranchSummary,
|
|
65
65
|
generateHandoffFromContext,
|
|
66
|
+
invalidateMessageCache,
|
|
66
67
|
prepareCompaction,
|
|
67
68
|
renderHandoffPrompt,
|
|
68
69
|
resolveBudgetReserveTokens,
|
|
@@ -121,6 +122,7 @@ import {
|
|
|
121
122
|
} from "@oh-my-pi/pi-ai";
|
|
122
123
|
import * as AIError from "@oh-my-pi/pi-ai/error";
|
|
123
124
|
import { resetOpenAICodexHistoryAfterCompaction } from "@oh-my-pi/pi-ai/providers/openai-codex-responses";
|
|
125
|
+
import { kCursorExecResolved } from "@oh-my-pi/pi-ai/utils/block-symbols";
|
|
124
126
|
import { toolWireSchema } from "@oh-my-pi/pi-ai/utils/schema";
|
|
125
127
|
import { GeminiHeaderRunDetector, isGeminiThinkingModel } from "@oh-my-pi/pi-ai/utils/thinking-loop";
|
|
126
128
|
import { type RepeatedToolCallDetection, ToolCallLoopGuard } from "@oh-my-pi/pi-ai/utils/tool-call-loop-guard";
|
|
@@ -137,6 +139,7 @@ import {
|
|
|
137
139
|
getInstallId,
|
|
138
140
|
isBunTestRuntime,
|
|
139
141
|
isEnoent,
|
|
142
|
+
isInteractiveHost,
|
|
140
143
|
logger,
|
|
141
144
|
postmortem,
|
|
142
145
|
prompt,
|
|
@@ -1896,6 +1899,8 @@ export class AgentSession {
|
|
|
1896
1899
|
* suppresses advisor concern/blocker auto-resume until the user next resumes.
|
|
1897
1900
|
* Advisor advice is still recorded into the transcript, just not auto-run. */
|
|
1898
1901
|
#advisorAutoResumeSuppressed = false;
|
|
1902
|
+
/** Print-mode sessions preserve advisor notes without starting hidden primary turns. */
|
|
1903
|
+
#preserveAdvisorAdvice = false;
|
|
1899
1904
|
#advisorPrimaryTurnsCompleted = 0;
|
|
1900
1905
|
#advisorInterruptImmuneTurnStart: number | undefined;
|
|
1901
1906
|
#planModeState: PlanModeState | undefined;
|
|
@@ -2034,6 +2039,8 @@ export class AgentSession {
|
|
|
2034
2039
|
#turnIndex = 0;
|
|
2035
2040
|
#messageEndPersistenceTail: Promise<void> = Promise.resolve();
|
|
2036
2041
|
#pendingMessageEndPersistence = new Map<string, Promise<void>>();
|
|
2042
|
+
/** Async lifecycle handlers for visible advisor cards emitted outside the primary loop. */
|
|
2043
|
+
#pendingAdvisorCardEvents = new Set<Promise<void>>();
|
|
2037
2044
|
#persistedMessageKeys: { anchor: string; keys: Set<string> } | undefined;
|
|
2038
2045
|
|
|
2039
2046
|
#skills: Skill[];
|
|
@@ -2524,7 +2531,13 @@ export class AgentSession {
|
|
|
2524
2531
|
const isPlanNudge = (m: AgentMessage): boolean =>
|
|
2525
2532
|
m.role === "custom" && m.customType === PREWALK_PLAN_MESSAGE_TYPE;
|
|
2526
2533
|
for (let i = liveMessages.length - 1; i >= 0; i--) {
|
|
2527
|
-
if (isPlanNudge(liveMessages[i]))
|
|
2534
|
+
if (isPlanNudge(liveMessages[i])) {
|
|
2535
|
+
// Interior removal on the live array: drop the scrubbed message from
|
|
2536
|
+
// the convert/estimate caches so the next convert can't reuse a prefix
|
|
2537
|
+
// that still carries its fragment (the array shrinks in place).
|
|
2538
|
+
invalidateMessageCache(liveMessages[i]);
|
|
2539
|
+
liveMessages.splice(i, 1);
|
|
2540
|
+
}
|
|
2528
2541
|
}
|
|
2529
2542
|
const stateMessages = this.agent.state.messages;
|
|
2530
2543
|
const filtered = stateMessages.filter(m => !isPlanNudge(m));
|
|
@@ -3364,6 +3377,7 @@ export class AgentSession {
|
|
|
3364
3377
|
const channel = resolveAdvisorDeliveryChannel({
|
|
3365
3378
|
severity,
|
|
3366
3379
|
autoResumeSuppressed: this.#advisorAutoResumeSuppressed,
|
|
3380
|
+
preserveOnly: this.#preserveAdvisorAdvice,
|
|
3367
3381
|
// Key on the live agent-core loop, not session `isStreaming` (which also
|
|
3368
3382
|
// counts `#promptInFlightCount` during post-turn unwind). Only a running
|
|
3369
3383
|
// loop consumes a steer at its next boundary.
|
|
@@ -4163,30 +4177,58 @@ export class AgentSession {
|
|
|
4163
4177
|
return queued;
|
|
4164
4178
|
}
|
|
4165
4179
|
|
|
4180
|
+
/**
|
|
4181
|
+
* Orders subscriber fan-out across concurrent `#emitSessionEvent` calls.
|
|
4182
|
+
* Extension emits only await when the event type has handlers, so an event
|
|
4183
|
+
* with no handlers could otherwise overtake an earlier event still inside
|
|
4184
|
+
* its extension emit — an instant refusal delivered its assistant
|
|
4185
|
+
* `message_end` to the TUI before its own `message_start`, skipping the
|
|
4186
|
+
* turn-ending error render entirely.
|
|
4187
|
+
*/
|
|
4188
|
+
#subscriberEmitGate: Promise<void> = Promise.resolve();
|
|
4189
|
+
|
|
4166
4190
|
async #emitSessionEvent(event: AgentSessionEvent): Promise<void> {
|
|
4167
4191
|
if (event.type === "message_update") {
|
|
4168
4192
|
this.#emit(event);
|
|
4169
4193
|
void this.#queueExtensionEvent(event);
|
|
4170
4194
|
return;
|
|
4171
4195
|
}
|
|
4172
|
-
|
|
4173
|
-
//
|
|
4174
|
-
//
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4196
|
+
// Take a FIFO ticket before the extension emit: extension deliveries for
|
|
4197
|
+
// consecutive events still run concurrently, but subscriber fan-out waits
|
|
4198
|
+
// for every earlier event's fan-out (or deferral) to happen first.
|
|
4199
|
+
const previousGate = this.#subscriberEmitGate;
|
|
4200
|
+
const { promise: gate, resolve: releaseGate } = Promise.withResolvers<void>();
|
|
4201
|
+
this.#subscriberEmitGate = gate;
|
|
4202
|
+
try {
|
|
4203
|
+
await this.#emitExtensionEvent(event);
|
|
4204
|
+
await previousGate;
|
|
4205
|
+
// Hold the wire-level agent_end until in-flight prompts unwind. Subscribers
|
|
4206
|
+
// (rpc-mode, ACP, Cursor) treat agent_end as the "session is idle" signal;
|
|
4207
|
+
// emitting while #promptInFlightCount > 0 lets a client fire its next
|
|
4208
|
+
// `prompt` into a session that still reports isStreaming === true. Flush
|
|
4209
|
+
// happens in #endInFlight / #resetInFlight. A later agent_end (e.g. from
|
|
4210
|
+
// an auto-compaction turn that starts before the original prompt unwinds)
|
|
4211
|
+
// supersedes the pending one, which is what subscribers want — they only
|
|
4212
|
+
// care about the final settle.
|
|
4213
|
+
if (event.type === "agent_end" && this.#promptInFlightCount > 0) {
|
|
4214
|
+
this.#pendingAgentEndEmit = event;
|
|
4215
|
+
return;
|
|
4216
|
+
}
|
|
4217
|
+
this.#emit(event);
|
|
4218
|
+
} finally {
|
|
4219
|
+
releaseGate();
|
|
4184
4220
|
}
|
|
4185
|
-
this.#emit(event);
|
|
4186
4221
|
}
|
|
4187
4222
|
|
|
4188
4223
|
// Track last assistant message for auto-compaction check
|
|
4189
4224
|
#lastAssistantMessage: AssistantMessage | undefined = undefined;
|
|
4225
|
+
/**
|
|
4226
|
+
* Classifier-refusal turn pruned from active context at settle (#3591).
|
|
4227
|
+
* Retained until the next run starts so post-settle readers
|
|
4228
|
+
* ({@link getLastAssistantMessage}: print mode, task executor) still see
|
|
4229
|
+
* the terminal error instead of a silently successful-looking state.
|
|
4230
|
+
*/
|
|
4231
|
+
#prunedTerminalRefusal: AssistantMessage | undefined = undefined;
|
|
4190
4232
|
|
|
4191
4233
|
/** Internal handler for agent events - shared by subscribe and reconnect.
|
|
4192
4234
|
*
|
|
@@ -4204,7 +4246,12 @@ export class AgentSession {
|
|
|
4204
4246
|
* everything it schedules — settles. */
|
|
4205
4247
|
#handleAgentEvent = async (event: AgentEvent): Promise<void> => {
|
|
4206
4248
|
if (event.type !== "agent_end") {
|
|
4207
|
-
|
|
4249
|
+
const processing = this.#processAgentEvent(event);
|
|
4250
|
+
if ((event.type === "message_start" || event.type === "message_end") && isAdvisorCard(event.message)) {
|
|
4251
|
+
this.#pendingAdvisorCardEvents.add(processing);
|
|
4252
|
+
void processing.finally(() => this.#pendingAdvisorCardEvents.delete(processing)).catch(() => {});
|
|
4253
|
+
}
|
|
4254
|
+
return processing;
|
|
4208
4255
|
}
|
|
4209
4256
|
const { promise, resolve } = Promise.withResolvers<void>();
|
|
4210
4257
|
this.#trackPostPromptTask(promise);
|
|
@@ -4446,6 +4493,11 @@ export class AgentSession {
|
|
|
4446
4493
|
}
|
|
4447
4494
|
|
|
4448
4495
|
#processAgentEvent = async (event: AgentEvent): Promise<void> => {
|
|
4496
|
+
// A fresh run supersedes the previously settled (and pruned) refusal
|
|
4497
|
+
// turn: state-based lookups take over again.
|
|
4498
|
+
if (event.type === "agent_start") {
|
|
4499
|
+
this.#prunedTerminalRefusal = undefined;
|
|
4500
|
+
}
|
|
4449
4501
|
// Step the mid-run todo counter synchronously, BEFORE any await in this
|
|
4450
4502
|
// handler. The agent loop's next-turn `getAsideMessages` poll can run
|
|
4451
4503
|
// before queued microtasks drain, so `#takeMidRunTodoNudge` MUST see the
|
|
@@ -4464,6 +4516,19 @@ export class AgentSession {
|
|
|
4464
4516
|
} else if (!isError && MID_RUN_TODO_NUDGE_MUTATING_TOOLS[toolName]) {
|
|
4465
4517
|
this.#mutationsSinceLastTodoTouch++;
|
|
4466
4518
|
}
|
|
4519
|
+
// A tool actually ran. Clear the post-reminder suppression synchronously
|
|
4520
|
+
// too: the settle check (`#checkTodoCompletion` in agent_end maintenance)
|
|
4521
|
+
// can otherwise read the stale flag when a tool result and the terminal
|
|
4522
|
+
// stop land in the same tick, swallowing the earned re-escalation.
|
|
4523
|
+
this.#todoReminderAwaitingProgress = false;
|
|
4524
|
+
}
|
|
4525
|
+
// Track the settled assistant turn synchronously as well: agent_end
|
|
4526
|
+
// maintenance reads `#lastAssistantMessage`, and when a turn's events all
|
|
4527
|
+
// land in one tick its handler can run before this handler's post-emit
|
|
4528
|
+
// bookkeeping — leaving maintenance looking at the previous (e.g.
|
|
4529
|
+
// toolUse) assistant message and skipping settle-only work.
|
|
4530
|
+
if (event.type === "message_end" && event.message.role === "assistant") {
|
|
4531
|
+
this.#lastAssistantMessage = event.message;
|
|
4467
4532
|
}
|
|
4468
4533
|
// Plan-mode internal transition: stamp `SILENT_ABORT_MARKER` on the
|
|
4469
4534
|
// persisted message BEFORE the obfuscator's display-side copy below.
|
|
@@ -4670,9 +4735,7 @@ export class AgentSession {
|
|
|
4670
4735
|
}
|
|
4671
4736
|
// Other message types (bashExecution, compactionSummary, branchSummary) are persisted elsewhere
|
|
4672
4737
|
|
|
4673
|
-
// Track assistant message for auto-compaction (checked on agent_end)
|
|
4674
4738
|
if (event.message.role === "assistant") {
|
|
4675
|
-
this.#lastAssistantMessage = event.message;
|
|
4676
4739
|
const assistantMsg = event.message as AssistantMessage;
|
|
4677
4740
|
// Fold this turn's timing into per-model perf aggregates (drives the
|
|
4678
4741
|
// /models TPS/TTFT display). Errored turns measure nothing; aborted
|
|
@@ -4742,10 +4805,6 @@ export class AgentSession {
|
|
|
4742
4805
|
const details = isRecord(event.message.details) ? event.message.details : undefined;
|
|
4743
4806
|
const semanticResult = semanticToolResult(toolName, event.message);
|
|
4744
4807
|
const semanticDetails = isRecord(semanticResult?.details) ? semanticResult.details : undefined;
|
|
4745
|
-
// A tool actually ran. Clear the post-reminder suppression: the agent did
|
|
4746
|
-
// productive work in response to the prior nudge, so the next text-only stop
|
|
4747
|
-
// is allowed to escalate to the next reminder if todos remain incomplete.
|
|
4748
|
-
this.#todoReminderAwaitingProgress = false;
|
|
4749
4808
|
// Invalidate streaming edit cache when edit tool completes to prevent stale data
|
|
4750
4809
|
const editedPath = details ? getStringProperty(details, "path") : undefined;
|
|
4751
4810
|
if (toolName === "edit" && editedPath) {
|
|
@@ -4971,8 +5030,12 @@ export class AgentSession {
|
|
|
4971
5030
|
return;
|
|
4972
5031
|
}
|
|
4973
5032
|
}
|
|
4974
|
-
|
|
4975
|
-
|
|
5033
|
+
const resumeCursorStreamStall = this.#canResumeCursorStreamStall(msg);
|
|
5034
|
+
if (resumeCursorStreamStall || this.#isRetryableError(msg)) {
|
|
5035
|
+
const didRetry = await this.#handleRetryableError(
|
|
5036
|
+
msg,
|
|
5037
|
+
resumeCursorStreamStall ? { preserveFailedTurn: true } : undefined,
|
|
5038
|
+
);
|
|
4976
5039
|
if (didRetry) {
|
|
4977
5040
|
await emitAgentEndNotification({ willContinue: true });
|
|
4978
5041
|
return;
|
|
@@ -4991,10 +5054,14 @@ export class AgentSession {
|
|
|
4991
5054
|
}
|
|
4992
5055
|
// Classifier refusals are persisted-skipped above; also prune the trailing
|
|
4993
5056
|
// stub from active context so the next turn's prompt does not replay it.
|
|
5057
|
+
// Keep a reference for post-settle readers (print mode, task executor via
|
|
5058
|
+
// getLastAssistantMessage) — pruning made the terminal error invisible to
|
|
5059
|
+
// anything inspecting agent state after prompt() resolved.
|
|
4994
5060
|
// Fall through to the standard error tail so `session_stop` hooks (block,
|
|
4995
5061
|
// continue, telemetry) still fire — matching the pre-fix flow for
|
|
4996
5062
|
// `stopReason === "error"`.
|
|
4997
5063
|
if (this.#isClassifierRefusal(msg)) {
|
|
5064
|
+
this.#prunedTerminalRefusal = msg;
|
|
4998
5065
|
this.#removeAssistantMessageFromActiveContext(msg);
|
|
4999
5066
|
}
|
|
5000
5067
|
this.#resolveRetry();
|
|
@@ -6714,6 +6781,83 @@ export class AgentSession {
|
|
|
6714
6781
|
return this.#disposeCall;
|
|
6715
6782
|
}
|
|
6716
6783
|
|
|
6784
|
+
async #disposeOwnedAsyncJobs(): Promise<void> {
|
|
6785
|
+
this.#cancelOwnAsyncJobs();
|
|
6786
|
+
const manager = this.#ownedAsyncJobManager;
|
|
6787
|
+
if (!manager) return;
|
|
6788
|
+
|
|
6789
|
+
try {
|
|
6790
|
+
const drained = await manager.dispose({ timeoutMs: 3_000 });
|
|
6791
|
+
const deliveryState = manager.getDeliveryState();
|
|
6792
|
+
if (drained === false && deliveryState) {
|
|
6793
|
+
logger.warn("Async job completion deliveries still pending during dispose", { ...deliveryState });
|
|
6794
|
+
}
|
|
6795
|
+
} finally {
|
|
6796
|
+
if (AsyncJobManager.instance() === manager) {
|
|
6797
|
+
AsyncJobManager.setInstance(undefined);
|
|
6798
|
+
}
|
|
6799
|
+
}
|
|
6800
|
+
}
|
|
6801
|
+
|
|
6802
|
+
async #disposeEvalKernels(): Promise<void> {
|
|
6803
|
+
const settled = await this.#prepareEvalExecutionsForDispose();
|
|
6804
|
+
if (!settled) {
|
|
6805
|
+
logger.warn("Detaching retained eval-kernel ownership during dispose while eval execution is still active");
|
|
6806
|
+
}
|
|
6807
|
+
|
|
6808
|
+
const results = await Promise.allSettled([
|
|
6809
|
+
disposeKernelSessionsByOwner(this.#evalKernelOwnerId),
|
|
6810
|
+
disposeRubyKernelSessionsByOwner(this.#evalKernelOwnerId),
|
|
6811
|
+
disposeJuliaKernelSessionsByOwner(this.#evalKernelOwnerId),
|
|
6812
|
+
]);
|
|
6813
|
+
const errors: unknown[] = [];
|
|
6814
|
+
for (const result of results) {
|
|
6815
|
+
if (result.status === "rejected") errors.push(result.reason);
|
|
6816
|
+
}
|
|
6817
|
+
if (errors.length > 0) throw new AggregateError(errors, "Failed to dispose one or more eval kernels");
|
|
6818
|
+
}
|
|
6819
|
+
|
|
6820
|
+
async #releaseOwnedBrowserTabs(ownerId: string | undefined): Promise<void> {
|
|
6821
|
+
if (!ownerId) return;
|
|
6822
|
+
try {
|
|
6823
|
+
const released = await withTimeout(
|
|
6824
|
+
releaseTabsForOwner(ownerId, { kill: true }),
|
|
6825
|
+
3_000,
|
|
6826
|
+
"Timed out releasing owned browser tabs during dispose",
|
|
6827
|
+
);
|
|
6828
|
+
if (released > 0) {
|
|
6829
|
+
logger.debug("Released owned browser tabs during dispose", { ownerId, released });
|
|
6830
|
+
}
|
|
6831
|
+
} catch (error) {
|
|
6832
|
+
logger.warn("Failed to release owned browser tabs during dispose", { error: String(error) });
|
|
6833
|
+
}
|
|
6834
|
+
}
|
|
6835
|
+
|
|
6836
|
+
async #disconnectOwnedMcp(): Promise<void> {
|
|
6837
|
+
if (!this.#disconnectOwnedMcpManager) return;
|
|
6838
|
+
try {
|
|
6839
|
+
await withTimeout(
|
|
6840
|
+
this.#disconnectOwnedMcpManager(),
|
|
6841
|
+
3_000,
|
|
6842
|
+
"Timed out disconnecting owned MCP manager during dispose",
|
|
6843
|
+
);
|
|
6844
|
+
} catch (error) {
|
|
6845
|
+
logger.warn("Failed to disconnect owned MCP manager during dispose", { error: String(error) });
|
|
6846
|
+
}
|
|
6847
|
+
}
|
|
6848
|
+
|
|
6849
|
+
async #disposeMnemopi(
|
|
6850
|
+
state: MnemopiSessionState | undefined,
|
|
6851
|
+
consolidateTimeoutMs: number | undefined,
|
|
6852
|
+
): Promise<void> {
|
|
6853
|
+
try {
|
|
6854
|
+
await state?.dispose({ timeoutMs: consolidateTimeoutMs });
|
|
6855
|
+
} finally {
|
|
6856
|
+
// Consolidation may embed final memories, so terminate its worker only afterward.
|
|
6857
|
+
await shutdownMnemopiEmbedClient();
|
|
6858
|
+
}
|
|
6859
|
+
}
|
|
6860
|
+
|
|
6717
6861
|
async #doDispose(options: AgentSessionDisposeOptions = {}): Promise<void> {
|
|
6718
6862
|
this.beginDispose();
|
|
6719
6863
|
this.#recordSessionExit(options.reason ?? "dispose");
|
|
@@ -6726,124 +6870,50 @@ export class AgentSession {
|
|
|
6726
6870
|
} catch (error) {
|
|
6727
6871
|
logger.warn("Failed to emit session_shutdown event", { error: String(error) });
|
|
6728
6872
|
}
|
|
6729
|
-
|
|
6730
|
-
//
|
|
6731
|
-
// Optional-called: hosts and tests may inject partial runner facades that
|
|
6732
|
-
// implement only the dispatch surface.
|
|
6873
|
+
|
|
6874
|
+
// Stop extension timers before aborting deferred work they could enqueue.
|
|
6733
6875
|
this.#extensionRunner?.clearManagedTimers?.();
|
|
6734
6876
|
this.#fallbackExtensionTimers?.clearAll();
|
|
6735
|
-
// Abort post-prompt work so the drain below can complete. Without this, a
|
|
6736
|
-
// deferred-handoff task that has already advanced into
|
|
6737
|
-
// `await this.handoff(...) → generateHandoff(...)` keeps awaiting a live LLM stream
|
|
6738
|
-
// — Promise.allSettled() in #cancelPostPromptTasks then waits forever, freezing
|
|
6739
|
-
// /exit and Ctrl+C-double-tap. The post-prompt task's own AbortSignal does not
|
|
6740
|
-
// propagate into the inner handoff/compaction controllers, so we abort them
|
|
6741
|
-
// explicitly. agent.abort() is needed for an agent.continue() that may have
|
|
6742
|
-
// raced the deferred handoff (its streaming loop is awaited by the wrapper IIFE).
|
|
6743
|
-
//
|
|
6744
|
-
// Tool work (bash/eval/python) is NOT aborted here — those have their own
|
|
6745
|
-
// dispose paths and shared kernels are contractually allowed to survive a
|
|
6746
|
-
// session's dispose.
|
|
6747
6877
|
this.abortRetry();
|
|
6748
6878
|
this.abortCompaction();
|
|
6749
6879
|
const postPromptDrain = this.#cancelPostPromptTasks();
|
|
6750
6880
|
this.agent.abort();
|
|
6751
|
-
|
|
6752
|
-
|
|
6753
|
-
|
|
6754
|
-
|
|
6755
|
-
// the session that owns the manager goes on to dispose it (which itself
|
|
6756
|
-
// nukes any leftover jobs and pending deliveries).
|
|
6757
|
-
this.#cancelOwnAsyncJobs();
|
|
6758
|
-
const ownedAsyncManager = this.#ownedAsyncJobManager;
|
|
6759
|
-
if (ownedAsyncManager) {
|
|
6760
|
-
const drained = await ownedAsyncManager.dispose({ timeoutMs: 3_000 });
|
|
6761
|
-
const deliveryState = ownedAsyncManager.getDeliveryState();
|
|
6762
|
-
if (drained === false && deliveryState) {
|
|
6763
|
-
logger.warn("Async job completion deliveries still pending during dispose", { ...deliveryState });
|
|
6764
|
-
}
|
|
6765
|
-
if (AsyncJobManager.instance() === ownedAsyncManager) {
|
|
6766
|
-
AsyncJobManager.setInstance(undefined);
|
|
6767
|
-
}
|
|
6768
|
-
}
|
|
6769
|
-
const evalExecutionsSettled = await this.#prepareEvalExecutionsForDispose();
|
|
6770
|
-
if (!evalExecutionsSettled) {
|
|
6771
|
-
logger.warn("Detaching retained eval-kernel ownership during dispose while eval execution is still active");
|
|
6881
|
+
try {
|
|
6882
|
+
await withTimeout(postPromptDrain, 5_000, "Timed out draining post-prompt tasks during dispose");
|
|
6883
|
+
} catch (error) {
|
|
6884
|
+
logger.warn("Post-prompt tasks still draining at dispose deadline", { error: String(error) });
|
|
6772
6885
|
}
|
|
6773
|
-
await
|
|
6774
|
-
|
|
6775
|
-
|
|
6776
|
-
|
|
6777
|
-
|
|
6778
|
-
|
|
6779
|
-
|
|
6780
|
-
|
|
6781
|
-
|
|
6782
|
-
|
|
6783
|
-
|
|
6784
|
-
|
|
6785
|
-
|
|
6786
|
-
|
|
6787
|
-
|
|
6788
|
-
|
|
6789
|
-
|
|
6790
|
-
|
|
6791
|
-
|
|
6792
|
-
|
|
6793
|
-
}
|
|
6794
|
-
} catch (error) {
|
|
6795
|
-
logger.warn("Failed to release owned browser tabs during dispose", { error: String(error) });
|
|
6886
|
+
await this.#drainAutolearnCapture();
|
|
6887
|
+
|
|
6888
|
+
const hindsightState = this.getHindsightSessionState();
|
|
6889
|
+
const mnemopiState = setMnemopiSessionState(this, undefined);
|
|
6890
|
+
const advisorRecorderClosed = this.#advisorRecorderClosed;
|
|
6891
|
+
const results = await Promise.allSettled([
|
|
6892
|
+
this.#disposeOwnedAsyncJobs(),
|
|
6893
|
+
this.#disposeEvalKernels(),
|
|
6894
|
+
this.#releaseOwnedBrowserTabs(this.sessionManager.getSessionId()),
|
|
6895
|
+
shutdownTinyTitleClient(),
|
|
6896
|
+
this.#disconnectOwnedMcp(),
|
|
6897
|
+
advisorRecorderClosed,
|
|
6898
|
+
hindsightState?.flushRetainQueue() ?? Promise.resolve(),
|
|
6899
|
+
this.#disposeMnemopi(mnemopiState, options.mnemopiConsolidateTimeoutMs),
|
|
6900
|
+
]);
|
|
6901
|
+
for (const result of results) {
|
|
6902
|
+
if (result.status === "rejected") {
|
|
6903
|
+
logger.warn("Session dispose subsystem failed during parallel teardown", {
|
|
6904
|
+
error: String(result.reason),
|
|
6905
|
+
});
|
|
6796
6906
|
}
|
|
6797
6907
|
}
|
|
6798
|
-
|
|
6908
|
+
|
|
6799
6909
|
this.#releasePowerAssertion();
|
|
6800
|
-
// Clean up an empty session created by this session's /move so it doesn't accumulate.
|
|
6801
6910
|
await cleanupEmptyMoveSession(this.sessionManager, this.#movedFromEmptySessionFile);
|
|
6802
6911
|
this.#movedFromEmptySessionFile = undefined;
|
|
6912
|
+
// All teardown branches that can append session entries have settled.
|
|
6803
6913
|
await this.sessionManager.close();
|
|
6804
|
-
// beginDispose() stopped the advisor and captured its recorder close; await
|
|
6805
|
-
// it so the final advisor turn is flushed before the process may exit.
|
|
6806
|
-
await this.#advisorRecorderClosed;
|
|
6807
6914
|
this.#closeAllProviderSessions("dispose");
|
|
6808
|
-
// Disconnect the MCP manager this session OWNS so its stdio servers are
|
|
6809
|
-
// not orphaned at exit. Best-effort: a failure here must never throw out
|
|
6810
|
-
// of dispose. Only owning (top-level) sessions provide this callback;
|
|
6811
|
-
// subagents reuse a parent's manager and must not tear it down. Idempotent
|
|
6812
|
-
// with the deferred-discovery disconnect in `createAgentSession`.
|
|
6813
|
-
//
|
|
6814
|
-
// BOUNDED: an owned manager may hold an HTTP/SSE server whose session-
|
|
6815
|
-
// termination DELETE blocks up to the MCP request timeout (30s default,
|
|
6816
|
-
// unbounded when OMP_MCP_TIMEOUT_MS=0), so awaiting `disconnectAll()`
|
|
6817
|
-
// unbounded would stall /exit and print-mode shutdown on a broken remote
|
|
6818
|
-
// endpoint. Race it against a short deadline — stdio close (the subprocess
|
|
6819
|
-
// reap this targets) completes well within the bound; a slow transport
|
|
6820
|
-
// close is left to finish detached. Mirrors the bounded async-job teardown.
|
|
6821
|
-
if (this.#disconnectOwnedMcpManager) {
|
|
6822
|
-
try {
|
|
6823
|
-
await withTimeout(
|
|
6824
|
-
this.#disconnectOwnedMcpManager(),
|
|
6825
|
-
3_000,
|
|
6826
|
-
"Timed out disconnecting owned MCP manager during dispose",
|
|
6827
|
-
);
|
|
6828
|
-
} catch (error) {
|
|
6829
|
-
logger.warn("Failed to disconnect owned MCP manager during dispose", { error: String(error) });
|
|
6830
|
-
}
|
|
6831
|
-
}
|
|
6832
|
-
// Flush the retain queue BEFORE clearing the session's pointer so
|
|
6833
|
-
// `HindsightRetainQueue.#doFlush` still sees `session.getHindsightSessionState() === state`.
|
|
6834
|
-
// Reversed, the spliced batch survives just long enough to fail the
|
|
6835
|
-
// identity check and get dropped with a `session vanished` warning.
|
|
6836
|
-
const hindsightState = this.getHindsightSessionState();
|
|
6837
|
-
await hindsightState?.flushRetainQueue();
|
|
6838
6915
|
this.setHindsightSessionState(undefined);
|
|
6839
6916
|
hindsightState?.dispose();
|
|
6840
|
-
const mnemopiState = setMnemopiSessionState(this, undefined);
|
|
6841
|
-
await mnemopiState?.dispose({ timeoutMs: options.mnemopiConsolidateTimeoutMs });
|
|
6842
|
-
// Tear down the embeddings subprocess AFTER mnemopi state.dispose:
|
|
6843
|
-
// consolidate-on-dispose may still call `embed()` to store the final
|
|
6844
|
-
// memories, and that round-trips through the worker we are about to
|
|
6845
|
-
// hard-kill (issue #3031).
|
|
6846
|
-
await shutdownMnemopiEmbedClient();
|
|
6847
6917
|
this.#disconnectFromAgent();
|
|
6848
6918
|
if (this.#unsubscribeAppendOnly) {
|
|
6849
6919
|
this.#unsubscribeAppendOnly();
|
|
@@ -6944,6 +7014,53 @@ export class AgentSession {
|
|
|
6944
7014
|
await this.agent.waitForIdle();
|
|
6945
7015
|
await this.#waitForPostPromptRecovery();
|
|
6946
7016
|
}
|
|
7017
|
+
/**
|
|
7018
|
+
* Prevent advisor notes from starting hidden primary turns while a headless
|
|
7019
|
+
* caller prints and drains the final primary response.
|
|
7020
|
+
*/
|
|
7021
|
+
prepareForHeadlessAdvisorDrain(): void {
|
|
7022
|
+
this.#preserveAdvisorAdvice = true;
|
|
7023
|
+
}
|
|
7024
|
+
|
|
7025
|
+
async #waitForPendingAdvisorCardEvents(timeoutMs: number): Promise<boolean> {
|
|
7026
|
+
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
7027
|
+
while (this.#pendingAdvisorCardEvents.size > 0) {
|
|
7028
|
+
const remainingMs = deadline - Date.now();
|
|
7029
|
+
if (remainingMs <= 0) return false;
|
|
7030
|
+
const settled = Promise.allSettled([...this.#pendingAdvisorCardEvents]).then(() => true as const);
|
|
7031
|
+
const { promise: timedOut, resolve } = Promise.withResolvers<false>();
|
|
7032
|
+
const timer = setTimeout(() => resolve(false), remainingMs);
|
|
7033
|
+
try {
|
|
7034
|
+
if (!(await Promise.race([settled, timedOut]))) return false;
|
|
7035
|
+
} finally {
|
|
7036
|
+
clearTimeout(timer);
|
|
7037
|
+
}
|
|
7038
|
+
}
|
|
7039
|
+
return true;
|
|
7040
|
+
}
|
|
7041
|
+
|
|
7042
|
+
/**
|
|
7043
|
+
* Wait for active advisor reviews and their emitted card events before a
|
|
7044
|
+
* headless caller disposes the session. Returns `false` and logs work disposal
|
|
7045
|
+
* will abandon when the shared deadline expires or an advisor fails.
|
|
7046
|
+
*/
|
|
7047
|
+
async waitForAdvisorCatchup(timeoutMs: number): Promise<boolean> {
|
|
7048
|
+
const deadline = Date.now() + timeoutMs;
|
|
7049
|
+
const results = await Promise.all(this.#advisors.map(advisor => advisor.runtime.waitForCatchup(timeoutMs, 1)));
|
|
7050
|
+
const cardEventsCaughtUp = await this.#waitForPendingAdvisorCardEvents(Math.max(0, deadline - Date.now()));
|
|
7051
|
+
const abandoned = this.#advisors.filter(
|
|
7052
|
+
(advisor, index) => results[index] === false && advisor.runtime.backlog > 0,
|
|
7053
|
+
);
|
|
7054
|
+
if (abandoned.length > 0 || !cardEventsCaughtUp) {
|
|
7055
|
+
logger.warn("advisor shutdown drain incomplete; disposal will abandon reviews or cards", {
|
|
7056
|
+
timeoutMs,
|
|
7057
|
+
advisors: abandoned.map(advisor => ({ name: advisor.name, backlog: advisor.runtime.backlog })),
|
|
7058
|
+
pendingAdvisorCards: this.#pendingAdvisorCardEvents.size,
|
|
7059
|
+
});
|
|
7060
|
+
return false;
|
|
7061
|
+
}
|
|
7062
|
+
return true;
|
|
7063
|
+
}
|
|
6947
7064
|
|
|
6948
7065
|
async drainAsyncJobDeliveriesForAcp(options?: { timeoutMs?: number }): Promise<boolean> {
|
|
6949
7066
|
const manager = this.#asyncJobManager;
|
|
@@ -6962,9 +7079,14 @@ export class AgentSession {
|
|
|
6962
7079
|
}
|
|
6963
7080
|
}
|
|
6964
7081
|
|
|
6965
|
-
/**
|
|
7082
|
+
/**
|
|
7083
|
+
* Most recent settled assistant message. A classifier-refusal turn pruned
|
|
7084
|
+
* from active context at settle is still reported until the next run
|
|
7085
|
+
* starts, so terminal-outcome consumers (print mode, task executor) see
|
|
7086
|
+
* the refusal error rather than the previous turn — or nothing.
|
|
7087
|
+
*/
|
|
6966
7088
|
getLastAssistantMessage(): AssistantMessage | undefined {
|
|
6967
|
-
return this.#findLastAssistantMessage();
|
|
7089
|
+
return this.#prunedTerminalRefusal ?? this.#findLastAssistantMessage();
|
|
6968
7090
|
}
|
|
6969
7091
|
/** Current effective system prompt blocks (includes any per-turn extension modifications) */
|
|
6970
7092
|
get systemPrompt(): string[] {
|
|
@@ -7695,6 +7817,12 @@ export class AgentSession {
|
|
|
7695
7817
|
return this.#postPromptTasks.size > 0;
|
|
7696
7818
|
}
|
|
7697
7819
|
|
|
7820
|
+
/** Register post-prompt work in tests without driving a full agent turn. */
|
|
7821
|
+
trackPostPromptTaskForTests(task: Promise<unknown>): void {
|
|
7822
|
+
if (!isBunTestRuntime()) throw new Error("trackPostPromptTaskForTests is test-only");
|
|
7823
|
+
this.#trackPostPromptTask(task);
|
|
7824
|
+
}
|
|
7825
|
+
|
|
7698
7826
|
/** All messages including custom types like BashExecutionMessage */
|
|
7699
7827
|
get messages(): AgentMessage[] {
|
|
7700
7828
|
return this.agent.state.messages;
|
|
@@ -9574,6 +9702,13 @@ export class AgentSession {
|
|
|
9574
9702
|
}
|
|
9575
9703
|
|
|
9576
9704
|
#scheduleReplanTitleRefresh(): void {
|
|
9705
|
+
// Headless subagent sessions have no operator-visible title, so a todo-init
|
|
9706
|
+
// replan refresh only burns a tiny-model call whose result lands in JSONL
|
|
9707
|
+
// and is never shown (issue #5910). In an interactive host the operator can
|
|
9708
|
+
// focus a live subagent from the Agent Hub, where the status line renders
|
|
9709
|
+
// its session name — so keep the refresh there and only skip subagents when
|
|
9710
|
+
// no focusable UI exists (print/RPC/ACP/eval/SDK/CI).
|
|
9711
|
+
if (this.#agentKind === "sub" && !isInteractiveHost()) return;
|
|
9577
9712
|
if (this.#replanTitleRefreshInFlight) return;
|
|
9578
9713
|
if (!this.settings.get("title.refreshOnReplan")) return;
|
|
9579
9714
|
if (this.sessionManager.titleSource === "user") return;
|
|
@@ -11929,7 +12064,8 @@ export class AgentSession {
|
|
|
11929
12064
|
this.#emptyStopRetryCount++;
|
|
11930
12065
|
if (this.#emptyStopRetryCount > EMPTY_STOP_MAX_RETRIES) {
|
|
11931
12066
|
const attempts = this.#emptyStopRetryCount - 1;
|
|
11932
|
-
const finalError =
|
|
12067
|
+
const finalError =
|
|
12068
|
+
"Assistant returned empty stop after retry cap; try switching models or `/shake images` to remove archived frames";
|
|
11933
12069
|
logger.warn(finalError, {
|
|
11934
12070
|
attempts,
|
|
11935
12071
|
model: assistantMessage.model,
|
|
@@ -11944,11 +12080,11 @@ export class AgentSession {
|
|
|
11944
12080
|
this.#clearPendingRecoveredRetryErrors();
|
|
11945
12081
|
this.#retryAttempt = 0;
|
|
11946
12082
|
this.#resolveRetry();
|
|
11947
|
-
//
|
|
11948
|
-
//
|
|
11949
|
-
|
|
11950
|
-
|
|
11951
|
-
|
|
12083
|
+
// A zero-content turn carries no transcript value, while its provider usage
|
|
12084
|
+
// can anchor the next prompt at the full failed-request size and re-trigger
|
|
12085
|
+
// compaction at the same boundary. Remove every capped empty stop; toolUse
|
|
12086
|
+
// orphans still need this for Anthropic message-history validity.
|
|
12087
|
+
await this.#dropPersistedAssistantTurn(assistantMessage);
|
|
11952
12088
|
return false;
|
|
11953
12089
|
}
|
|
11954
12090
|
this.#discardAssistantTurn(assistantMessage);
|
|
@@ -13758,35 +13894,80 @@ export class AgentSession {
|
|
|
13758
13894
|
this.#getCompactionModelCandidates(availableModels),
|
|
13759
13895
|
this.sessionId,
|
|
13760
13896
|
);
|
|
13761
|
-
|
|
13897
|
+
let pathEntriesForCompaction = pathEntries;
|
|
13898
|
+
let preparation = prepareCompaction(pathEntriesForCompaction, compactionSettings, autoCompactionCandidates);
|
|
13762
13899
|
if (!preparation) {
|
|
13763
|
-
|
|
13764
|
-
|
|
13765
|
-
|
|
13766
|
-
|
|
13767
|
-
|
|
13768
|
-
|
|
13769
|
-
|
|
13770
|
-
|
|
13771
|
-
|
|
13772
|
-
|
|
13773
|
-
|
|
13774
|
-
|
|
13775
|
-
|
|
13776
|
-
|
|
13777
|
-
|
|
13900
|
+
// prepareCompaction found nothing to summarize because the kept region
|
|
13901
|
+
// is a single oversized recent turn — findCutPoint never cuts inside a
|
|
13902
|
+
// tool result, so a huge tool-result / fenced block tail leaves nothing
|
|
13903
|
+
// on the summarizable side and summary compaction cannot even start.
|
|
13904
|
+
// That is exactly the dead-end the elide shake rescues: it reaches
|
|
13905
|
+
// INSIDE the tail and offloads heavy content to an artifact placeholder,
|
|
13906
|
+
// shrinking the tail so findCutPoint can then move the cut and leave
|
|
13907
|
+
// older turns to summarize. Run the same tiered rescue the
|
|
13908
|
+
// post-maintenance guard uses (elide, then image drop), with progress
|
|
13909
|
+
// defined as "prepareCompaction now succeeds on the rewritten branch",
|
|
13910
|
+
// and fall through to the normal compaction body when it does (writing
|
|
13911
|
+
// a compaction entry anchors the stale billed usage so the
|
|
13912
|
+
// auto-continue re-check cannot re-trip and loop the warning — issue
|
|
13913
|
+
// #4786). `skipElide` when we already fell through from a shake
|
|
13914
|
+
// strategy pass (it tried and found nothing); skip entirely on the
|
|
13915
|
+
// idle timer (it re-checks usage on its own cadence).
|
|
13916
|
+
let rescueRewroteHistory = false;
|
|
13917
|
+
if (reason !== "idle") {
|
|
13918
|
+
await this.#rescueCompactionDeadEnd(autoCompactionSignal, {
|
|
13919
|
+
skipElide: fallbackFromShake,
|
|
13920
|
+
hasProgress: () => {
|
|
13921
|
+
// Only reached when a tier actually freed something, so the
|
|
13922
|
+
// branch has been rewritten either way.
|
|
13923
|
+
rescueRewroteHistory = true;
|
|
13924
|
+
pathEntriesForCompaction = this.sessionManager.getBranch();
|
|
13925
|
+
preparation = prepareCompaction(
|
|
13926
|
+
pathEntriesForCompaction,
|
|
13927
|
+
compactionSettings,
|
|
13928
|
+
autoCompactionCandidates,
|
|
13929
|
+
);
|
|
13930
|
+
return preparation !== undefined;
|
|
13931
|
+
},
|
|
13778
13932
|
});
|
|
13779
|
-
continuationScheduled = true;
|
|
13780
13933
|
}
|
|
13781
|
-
if (
|
|
13782
|
-
this
|
|
13783
|
-
"
|
|
13784
|
-
|
|
13785
|
-
|
|
13786
|
-
|
|
13934
|
+
if (!preparation) {
|
|
13935
|
+
await this.#emitSessionEvent({
|
|
13936
|
+
type: "auto_compaction_end",
|
|
13937
|
+
action,
|
|
13938
|
+
result: undefined,
|
|
13939
|
+
aborted: false,
|
|
13940
|
+
willRetry: false,
|
|
13941
|
+
skipped: true,
|
|
13942
|
+
});
|
|
13943
|
+
const noProgressDeadEnd = reason !== "idle";
|
|
13944
|
+
let continuationScheduled = false;
|
|
13945
|
+
if (!suppressContinuation && this.agent.hasQueuedMessages()) {
|
|
13946
|
+
this.#scheduleAgentContinue({
|
|
13947
|
+
delayMs: 100,
|
|
13948
|
+
generation,
|
|
13949
|
+
shouldContinue: () => this.agent.hasQueuedMessages(),
|
|
13950
|
+
});
|
|
13951
|
+
continuationScheduled = true;
|
|
13952
|
+
}
|
|
13953
|
+
if (noProgressDeadEnd) {
|
|
13954
|
+
this.emitNotice(
|
|
13955
|
+
"warning",
|
|
13956
|
+
compactionDeadEndWarning("shrink it (e.g. clear large tool output)"),
|
|
13957
|
+
"compaction",
|
|
13958
|
+
);
|
|
13959
|
+
}
|
|
13960
|
+
// A rescue that offloaded content but still could not produce a
|
|
13961
|
+
// preparation rewrote the branch; flag it so the overflow-recovery
|
|
13962
|
+
// rollback does not re-restore the just-failed assistant turn on top
|
|
13963
|
+
// of the elided tail.
|
|
13964
|
+
const base = continuationScheduled
|
|
13965
|
+
? COMPACTION_CHECK_CONTINUATION
|
|
13966
|
+
: noProgressDeadEnd
|
|
13967
|
+
? COMPACTION_CHECK_BLOCK_AUTOMATIC_CONTINUATION
|
|
13968
|
+
: COMPACTION_CHECK_NONE;
|
|
13969
|
+
return rescueRewroteHistory ? { ...base, historyRewritten: true } : base;
|
|
13787
13970
|
}
|
|
13788
|
-
if (continuationScheduled) return COMPACTION_CHECK_CONTINUATION;
|
|
13789
|
-
return noProgressDeadEnd ? COMPACTION_CHECK_BLOCK_AUTOMATIC_CONTINUATION : COMPACTION_CHECK_NONE;
|
|
13790
13971
|
}
|
|
13791
13972
|
|
|
13792
13973
|
let hookCompaction: CompactionResult | undefined;
|
|
@@ -13798,7 +13979,7 @@ export class AgentSession {
|
|
|
13798
13979
|
const hookResult = (await this.#extensionRunner.emit({
|
|
13799
13980
|
type: "session_before_compact",
|
|
13800
13981
|
preparation,
|
|
13801
|
-
branchEntries:
|
|
13982
|
+
branchEntries: pathEntriesForCompaction,
|
|
13802
13983
|
customInstructions: undefined,
|
|
13803
13984
|
signal: autoCompactionSignal,
|
|
13804
13985
|
})) as SessionBeforeCompactResult | undefined;
|
|
@@ -14512,6 +14693,50 @@ export class AgentSession {
|
|
|
14512
14693
|
if (this.#isClassifierRefusal(message)) return true;
|
|
14513
14694
|
return AIError.retriable(id, { replayUnsafe: this.#hasReplayUnsafeToolOutput(message) });
|
|
14514
14695
|
}
|
|
14696
|
+
|
|
14697
|
+
/**
|
|
14698
|
+
* Resume a stalled Cursor turn after every server-executed tool has produced
|
|
14699
|
+
* a result. The failed assistant/tool-result pair must stay in context: it
|
|
14700
|
+
* records completed side effects and lets the next request continue from
|
|
14701
|
+
* them instead of replaying the original turn.
|
|
14702
|
+
*/
|
|
14703
|
+
#canResumeCursorStreamStall(message: AssistantMessage): boolean {
|
|
14704
|
+
if (
|
|
14705
|
+
message.provider !== "cursor" ||
|
|
14706
|
+
message.stopReason !== "error" ||
|
|
14707
|
+
!message.errorMessage?.toLowerCase().includes("stream stall")
|
|
14708
|
+
) {
|
|
14709
|
+
return false;
|
|
14710
|
+
}
|
|
14711
|
+
const id = this.#classifyRetryMessage(message);
|
|
14712
|
+
if (!AIError.retriable(id)) return false;
|
|
14713
|
+
|
|
14714
|
+
const resolvedToolCallIds: string[] = [];
|
|
14715
|
+
for (const block of message.content) {
|
|
14716
|
+
if (block.type !== "toolCall") continue;
|
|
14717
|
+
if (!(kCursorExecResolved in block) || block[kCursorExecResolved] !== true) return false;
|
|
14718
|
+
resolvedToolCallIds.push(block.id);
|
|
14719
|
+
}
|
|
14720
|
+
if (resolvedToolCallIds.length === 0) return false;
|
|
14721
|
+
|
|
14722
|
+
const messages = this.agent.state.messages;
|
|
14723
|
+
let assistantIndex = -1;
|
|
14724
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
14725
|
+
const candidate = messages[i];
|
|
14726
|
+
if (candidate.role === "assistant" && this.#isSameAssistantMessage(candidate, message)) {
|
|
14727
|
+
assistantIndex = i;
|
|
14728
|
+
break;
|
|
14729
|
+
}
|
|
14730
|
+
}
|
|
14731
|
+
if (assistantIndex < 0) return false;
|
|
14732
|
+
|
|
14733
|
+
const unresolvedToolCallIds = new Set(resolvedToolCallIds);
|
|
14734
|
+
for (let i = assistantIndex + 1; i < messages.length; i++) {
|
|
14735
|
+
const candidate = messages[i];
|
|
14736
|
+
if (candidate.role === "toolResult") unresolvedToolCallIds.delete(candidate.toolCallId);
|
|
14737
|
+
}
|
|
14738
|
+
return unresolvedToolCallIds.size === 0;
|
|
14739
|
+
}
|
|
14515
14740
|
/**
|
|
14516
14741
|
* Retried turns remove the failed assistant message from active context.
|
|
14517
14742
|
* Text/thinking-only partials are safe to discard and replay. Retained
|
|
@@ -15089,7 +15314,12 @@ export class AgentSession {
|
|
|
15089
15314
|
*/
|
|
15090
15315
|
async #handleRetryableError(
|
|
15091
15316
|
message: AssistantMessage,
|
|
15092
|
-
options?: {
|
|
15317
|
+
options?: {
|
|
15318
|
+
allowModelFallback?: boolean;
|
|
15319
|
+
fireworksFastFallback?: boolean;
|
|
15320
|
+
hardErrorFallback?: boolean;
|
|
15321
|
+
preserveFailedTurn?: boolean;
|
|
15322
|
+
},
|
|
15093
15323
|
): Promise<boolean> {
|
|
15094
15324
|
const retrySettings = this.settings.getGroup("retry");
|
|
15095
15325
|
// The Fireworks Fast→base degrade is an intrinsic model-selection safety net,
|
|
@@ -15280,8 +15510,11 @@ export class AgentSession {
|
|
|
15280
15510
|
errorId: message.errorId,
|
|
15281
15511
|
});
|
|
15282
15512
|
|
|
15283
|
-
//
|
|
15284
|
-
|
|
15513
|
+
// Cursor exec-channel tools have already run and emitted results. Keep that
|
|
15514
|
+
// failed turn intact so continuation cannot repeat their side effects.
|
|
15515
|
+
if (!options?.preserveFailedTurn) {
|
|
15516
|
+
this.#removeAssistantMessageFromActiveContext(message, "auto-retry");
|
|
15517
|
+
}
|
|
15285
15518
|
|
|
15286
15519
|
// A thinking/response loop retried into identical context loops again. Inject a
|
|
15287
15520
|
// hidden redirect so the retried turn sees a directive to break the repeated
|