@oh-my-pi/pi-coding-agent 17.1.2 → 17.1.4
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 +63 -0
- package/dist/cli.js +3806 -3798
- package/dist/types/cli/__tests__/auth-gateway-catalog.test.d.ts +1 -0
- package/dist/types/cli/auth-gateway-cli.d.ts +8 -0
- package/dist/types/cli/update-cli.d.ts +41 -0
- package/dist/types/cli/usage-cli.d.ts +4 -2
- package/dist/types/commands/update.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +19 -0
- package/dist/types/config/settings-schema.d.ts +30 -7
- package/dist/types/cursor.d.ts +47 -1
- package/dist/types/eval/js/process-entry.d.ts +2 -1
- package/dist/types/eval/js/worker-core.d.ts +4 -1
- package/dist/types/extensibility/extensions/runner.d.ts +1 -0
- package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +4 -0
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +4 -0
- package/dist/types/extensibility/legacy-pi-tui-shim.d.ts +15 -7
- package/dist/types/extensibility/shared-events.d.ts +2 -0
- package/dist/types/modes/components/assistant-message.d.ts +9 -0
- package/dist/types/modes/components/custom-editor.d.ts +12 -8
- package/dist/types/plan-mode/approved-plan.d.ts +3 -2
- package/dist/types/secrets/obfuscator.d.ts +7 -31
- package/dist/types/session/agent-session.d.ts +19 -4
- package/dist/types/session/prewalk.d.ts +2 -1
- package/dist/types/session/session-advisors.d.ts +8 -0
- package/dist/types/session/session-metadata.d.ts +29 -0
- package/dist/types/session/turn-recovery.d.ts +5 -5
- package/dist/types/stt/sherpa-runtime.d.ts +38 -0
- package/dist/types/thinking.d.ts +24 -0
- package/dist/types/tools/auto-generated-guard.d.ts +5 -2
- package/dist/types/tools/bash.d.ts +5 -4
- package/dist/types/tools/computer/exposure.d.ts +8 -0
- package/dist/types/tools/computer/supervisor.d.ts +1 -1
- package/dist/types/tools/computer/worker-entry.d.ts +1 -1
- package/dist/types/tools/computer/worker.d.ts +1 -1
- package/dist/types/tools/computer.d.ts +6 -0
- package/dist/types/tools/memory-render.d.ts +1 -4
- package/dist/types/tools/shell-tokenize.d.ts +14 -0
- package/dist/types/tools/todo.d.ts +7 -1
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +80 -10
- package/src/advisor/runtime.ts +27 -1
- package/src/cli/__tests__/auth-gateway-catalog.test.ts +111 -0
- package/src/cli/auth-gateway-cli.ts +63 -16
- package/src/cli/config-cli.ts +25 -7
- package/src/cli/update-cli.ts +229 -29
- package/src/cli/usage-cli.ts +144 -15
- package/src/cli.ts +21 -15
- package/src/commands/update.ts +6 -0
- package/src/config/__tests__/model-registry.test.ts +42 -7
- package/src/config/model-registry.ts +67 -4
- package/src/config/settings-schema.ts +39 -8
- package/src/config/settings.ts +24 -2
- package/src/cursor.ts +153 -0
- package/src/dap/session.ts +70 -11
- package/src/edit/hashline/filesystem.ts +1 -1
- package/src/edit/modes/patch.ts +1 -1
- package/src/eval/__tests__/js-context-manager.test.ts +9 -1
- package/src/eval/__tests__/process-entry-import.test.ts +111 -1
- package/src/eval/js/process-entry.ts +9 -5
- package/src/eval/js/worker-core.ts +6 -2
- package/src/exec/bash-executor.ts +4 -2
- package/src/extensibility/extensions/runner.ts +23 -8
- package/src/extensibility/legacy-pi-ai-shim.ts +9 -0
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +14 -2
- package/src/extensibility/legacy-pi-tui-shim.ts +33 -0
- package/src/extensibility/shared-events.ts +2 -0
- package/src/main.ts +22 -1
- package/src/modes/acp/acp-agent.ts +6 -0
- package/src/modes/components/assistant-message.ts +88 -11
- package/src/modes/components/chat-transcript-builder.ts +2 -0
- package/src/modes/components/custom-editor.test.ts +170 -0
- package/src/modes/components/custom-editor.ts +79 -29
- package/src/modes/components/settings-defs.ts +5 -1
- package/src/modes/controllers/event-controller.ts +96 -4
- package/src/modes/controllers/selector-controller.ts +14 -0
- package/src/modes/interactive-mode.ts +34 -8
- package/src/modes/utils/context-usage.ts +19 -2
- package/src/modes/utils/interactive-context-helpers.ts +1 -0
- package/src/plan-mode/approved-plan.ts +18 -10
- package/src/prompts/system/custom-system-prompt.md +1 -1
- package/src/prompts/system/system-prompt.md +10 -1
- package/src/prompts/tools/ast-edit.md +1 -1
- package/src/sdk.ts +4 -0
- package/src/secrets/obfuscator.ts +36 -126
- package/src/session/agent-session.ts +69 -60
- package/src/session/prewalk.ts +8 -3
- package/src/session/session-advisors.ts +67 -3
- package/src/session/session-metadata.ts +53 -0
- package/src/session/session-provider-boundary.ts +0 -1
- package/src/session/session-tools.ts +35 -1
- package/src/session/stream-guards.ts +1 -1
- package/src/session/turn-recovery.ts +36 -19
- package/src/slash-commands/builtin-registry.ts +49 -7
- package/src/stt/asr-worker.ts +2 -37
- package/src/stt/sherpa-runtime.ts +71 -0
- package/src/task/executor.ts +6 -5
- package/src/thinking.ts +39 -0
- package/src/tools/auto-generated-guard.ts +18 -5
- package/src/tools/bash.ts +43 -15
- package/src/tools/computer/exposure.ts +38 -0
- package/src/tools/computer/supervisor.ts +61 -13
- package/src/tools/computer/worker-entry.ts +28 -19
- package/src/tools/computer/worker.ts +3 -7
- package/src/tools/computer.ts +65 -10
- package/src/tools/gh-cache-invalidation.ts +2 -82
- package/src/tools/memory-render.ts +11 -2
- package/src/tools/render-utils.ts +8 -3
- package/src/tools/shell-tokenize.ts +83 -0
- package/src/tools/todo.ts +44 -17
- package/src/tools/write.ts +1 -1
|
@@ -71,7 +71,7 @@ import type {
|
|
|
71
71
|
ToolResultMessage,
|
|
72
72
|
UsageReport,
|
|
73
73
|
} from "@oh-my-pi/pi-ai";
|
|
74
|
-
import {
|
|
74
|
+
import { type Effort, streamSimple } from "@oh-my-pi/pi-ai";
|
|
75
75
|
import * as AIError from "@oh-my-pi/pi-ai/error";
|
|
76
76
|
import { resetOpenAICodexHistoryAfterCompaction } from "@oh-my-pi/pi-ai/providers/openai-codex-responses";
|
|
77
77
|
import { toolWireSchema } from "@oh-my-pi/pi-ai/utils/schema";
|
|
@@ -81,7 +81,6 @@ import {
|
|
|
81
81
|
escapeXmlText,
|
|
82
82
|
formatDuration,
|
|
83
83
|
getAgentDbPath,
|
|
84
|
-
getInstallId,
|
|
85
84
|
isBunTestRuntime,
|
|
86
85
|
isEnoent,
|
|
87
86
|
isInteractiveHost,
|
|
@@ -229,7 +228,6 @@ import {
|
|
|
229
228
|
type AsyncResultEntry,
|
|
230
229
|
buildAsyncResultBatchMessage,
|
|
231
230
|
} from "./async-job-delivery";
|
|
232
|
-
import type { AuthStorage } from "./auth-storage";
|
|
233
231
|
import { BashRunner, type BashRunnerHost } from "./bash-runner";
|
|
234
232
|
import {
|
|
235
233
|
checkpointStartedAtFromEntry,
|
|
@@ -304,6 +302,7 @@ import {
|
|
|
304
302
|
} from "./session-maintenance";
|
|
305
303
|
import { cleanupEmptyMoveSession, type SessionManager } from "./session-manager";
|
|
306
304
|
import { SessionMemory, type SessionMemoryHost } from "./session-memory";
|
|
305
|
+
import { buildSessionMetadata } from "./session-metadata";
|
|
307
306
|
import { SessionProviderBoundary, type SessionProviderBoundaryHost } from "./session-provider-boundary";
|
|
308
307
|
import { SessionStatsTracker, type SessionStatsTrackerHost } from "./session-stats";
|
|
309
308
|
import { SessionTools, type SessionToolsHost } from "./session-tools";
|
|
@@ -330,53 +329,6 @@ const PLAN_MODE_REMINDER_MAX = 3;
|
|
|
330
329
|
// Constants
|
|
331
330
|
// ============================================================================
|
|
332
331
|
|
|
333
|
-
/** Standard thinking levels */
|
|
334
|
-
|
|
335
|
-
/**
|
|
336
|
-
* Build the per-request `metadata` payload for the Anthropic provider, shaped
|
|
337
|
-
* like real Claude Code's `getAPIMetadata` output (`{ session_id, account_uuid,
|
|
338
|
-
* device_id }`) so the backend buckets requests under one session and attributes
|
|
339
|
-
* them to the authenticated OAuth account when available. Resolved at request
|
|
340
|
-
* time so token refreshes and login/logout transitions don't strand a stale
|
|
341
|
-
* account UUID in memory. `account_uuid` and `device_id` are omitted for
|
|
342
|
-
* non-Anthropic providers to avoid leaking the user's Claude identity to
|
|
343
|
-
* third-party APIs (including Anthropic-format-compatible proxies such as
|
|
344
|
-
* cloudflare-ai-gateway or gitlab-duo).
|
|
345
|
-
*
|
|
346
|
-
* `provider` is the target provider string (e.g. `"anthropic"`) and gates the
|
|
347
|
-
* `account_uuid` and `device_id` lookups — only `"anthropic"` requests carry them.
|
|
348
|
-
*
|
|
349
|
-
* `sessionId` is forwarded to the auth-storage session-sticky lookup so that
|
|
350
|
-
* multi-credential setups attribute to the same OAuth account used for the
|
|
351
|
-
* actual API request rather than always picking the first credential.
|
|
352
|
-
*
|
|
353
|
-
* `authStorage` is treated as optional so test fixtures that stub `modelRegistry`
|
|
354
|
-
* without a real storage layer still work; the resolver simply skips the lookup
|
|
355
|
-
* and emits `{ session_id }` alone, matching the no-OAuth-credential path.
|
|
356
|
-
*/
|
|
357
|
-
function buildSessionMetadata(
|
|
358
|
-
sessionId: string,
|
|
359
|
-
provider: string,
|
|
360
|
-
authStorage: AuthStorage | undefined,
|
|
361
|
-
): Record<string, unknown> {
|
|
362
|
-
const userId: Record<string, string> = { session_id: sessionId };
|
|
363
|
-
// Only look up account_uuid when the request is going to Anthropic. Injecting
|
|
364
|
-
// a Claude OAuth account_uuid into requests bound for other providers (including
|
|
365
|
-
// Anthropic-format-compatible proxies like cloudflare-ai-gateway or gitlab-duo)
|
|
366
|
-
// would leak the user's Anthropic identity to unrelated third-party APIs.
|
|
367
|
-
if (provider === "anthropic") {
|
|
368
|
-
const accountUuid = authStorage?.getOAuthAccountId("anthropic", sessionId);
|
|
369
|
-
if (typeof accountUuid === "string" && accountUuid.length > 0) {
|
|
370
|
-
userId.account_uuid = accountUuid;
|
|
371
|
-
// Claude Code's `device_id` is a stable 64-hex account-scoped install
|
|
372
|
-
// identifier. Include both omp's persistent install id and the Claude
|
|
373
|
-
// account UUID so two accounts on the same install do not share a device.
|
|
374
|
-
userId.device_id = deriveClaudeDeviceId(getInstallId(), accountUuid);
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
return { user_id: JSON.stringify(userId) };
|
|
378
|
-
}
|
|
379
|
-
|
|
380
332
|
const noOpUIContext: ExtensionUIContext = {
|
|
381
333
|
select: async (_title, _options, _dialogOptions) => undefined,
|
|
382
334
|
confirm: async (_title, _message, _dialogOptions) => false,
|
|
@@ -901,6 +853,7 @@ export class AgentSession {
|
|
|
901
853
|
agent: this.agent,
|
|
902
854
|
sessionManager: this.sessionManager,
|
|
903
855
|
model: () => this.model,
|
|
856
|
+
configuredThinkingLevel: () => this.configuredThinkingLevel(),
|
|
904
857
|
emitNotice: (level, message, source) => this.emitNotice(level, message, source),
|
|
905
858
|
setModelTemporary: (model, thinkingLevel, options) => this.setModelTemporary(model, thinkingLevel, options),
|
|
906
859
|
setActiveToolsByName: names => this.setActiveToolsByName(names),
|
|
@@ -2249,7 +2202,7 @@ export class AgentSession {
|
|
|
2249
2202
|
// Deobfuscate assistant message content for display emission — the LLM echoes back
|
|
2250
2203
|
// obfuscated placeholders, but listeners (TUI, extensions, exporters) must see real
|
|
2251
2204
|
// values. The original event.message stays obfuscated so the persistence path below
|
|
2252
|
-
// writes
|
|
2205
|
+
// writes `$$HASH$$` tokens to the session file; convertToLlm re-obfuscates outbound
|
|
2253
2206
|
// traffic on the next turn. Walks text, thinking, and toolCall arguments/intent.
|
|
2254
2207
|
let displayEvent: AgentEvent = event;
|
|
2255
2208
|
const obfuscator = this.#obfuscator;
|
|
@@ -2632,8 +2585,14 @@ export class AgentSession {
|
|
|
2632
2585
|
return;
|
|
2633
2586
|
}
|
|
2634
2587
|
|
|
2635
|
-
|
|
2636
|
-
|
|
2588
|
+
const resolvedInterruptedToolTurn = this.#recovery.classifyResolvedInterruptedToolTurn(msg);
|
|
2589
|
+
if (this.#recovery.isRetryableReasonlessAbort(msg) || resolvedInterruptedToolTurn === "reasonless-abort") {
|
|
2590
|
+
const didRetry = await this.#recovery.handleRetryableError(
|
|
2591
|
+
msg,
|
|
2592
|
+
resolvedInterruptedToolTurn === "reasonless-abort"
|
|
2593
|
+
? { allowModelFallback: false, preserveFailedTurn: true }
|
|
2594
|
+
: { allowModelFallback: false },
|
|
2595
|
+
);
|
|
2637
2596
|
if (didRetry) {
|
|
2638
2597
|
await emitAgentEndNotification({ willContinue: true });
|
|
2639
2598
|
return;
|
|
@@ -2659,7 +2618,7 @@ export class AgentSession {
|
|
|
2659
2618
|
return;
|
|
2660
2619
|
}
|
|
2661
2620
|
}
|
|
2662
|
-
const resumeResolvedStreamStall =
|
|
2621
|
+
const resumeResolvedStreamStall = resolvedInterruptedToolTurn === "stream-stall";
|
|
2663
2622
|
if (resumeResolvedStreamStall || this.#recovery.isRetryableError(msg)) {
|
|
2664
2623
|
const didRetry = await this.#recovery.handleRetryableError(
|
|
2665
2624
|
msg,
|
|
@@ -3078,6 +3037,7 @@ export class AgentSession {
|
|
|
3078
3037
|
session_id: this.sessionId,
|
|
3079
3038
|
session_file: this.sessionFile,
|
|
3080
3039
|
stop_hook_active: this.#sessionStopHookActive,
|
|
3040
|
+
signal: this.#postPromptTasksAbortController.signal,
|
|
3081
3041
|
});
|
|
3082
3042
|
if (this.#promptGeneration !== generation || this.#abortInProgress || this.#isDisposed) {
|
|
3083
3043
|
this.#resetSessionStopContinuationState();
|
|
@@ -3336,6 +3296,12 @@ export class AgentSession {
|
|
|
3336
3296
|
this.agent.setMetadataResolver((provider: string) =>
|
|
3337
3297
|
buildSessionMetadata(sid, provider, this.#modelRegistry.authStorage),
|
|
3338
3298
|
);
|
|
3299
|
+
// Keep every live advisor's provider identity in lockstep with the primary's
|
|
3300
|
+
// across every session-boundary transition — including branch paths that
|
|
3301
|
+
// skip conversation restore — so advisors never emit the previous
|
|
3302
|
+
// conversation's session id/metadata (issue #6625). Guarded because this
|
|
3303
|
+
// runs once during construction before the advisor controller exists.
|
|
3304
|
+
if (this.#advisors) this.#advisors.refreshProviderIdentity();
|
|
3339
3305
|
}
|
|
3340
3306
|
|
|
3341
3307
|
/** Run one abortable auto-learn capture outside the primary agent loop. */
|
|
@@ -4097,10 +4063,7 @@ export class AgentSession {
|
|
|
4097
4063
|
* Transcript for TUI display. Full history is kept for export/resume-style
|
|
4098
4064
|
* callers; live chat can collapse compacted history to keep the hot render
|
|
4099
4065
|
* surface bounded. Display-only — NEVER feed the result to
|
|
4100
|
-
* `agent.replaceMessages` or a provider.
|
|
4101
|
-
* it opts into legacy index-derived alias restoration so pre-keyed sessions
|
|
4102
|
-
* still render their secrets; the agent-feeding paths
|
|
4103
|
-
* (`buildDisplaySessionContext`) keep the keyed-only default.
|
|
4066
|
+
* `agent.replaceMessages` or a provider.
|
|
4104
4067
|
*/
|
|
4105
4068
|
buildTranscriptSessionContext(
|
|
4106
4069
|
options?: Pick<BuildSessionContextOptions, "collapseCompactedHistory" | "keepDanglingToolCalls">,
|
|
@@ -7373,6 +7336,14 @@ export class AgentSession {
|
|
|
7373
7336
|
* (issue #5642).
|
|
7374
7337
|
*/
|
|
7375
7338
|
reopenAsk?: { toolCallId: string; questions: AskToolInput["questions"] };
|
|
7339
|
+
/**
|
|
7340
|
+
* `true` when this call committed a new sibling answer for an `ask`
|
|
7341
|
+
* re-answer (`reanswerAskResult` was applied). The interactive caller
|
|
7342
|
+
* resumes the agent via {@link resumeAfterAskReanswer} *after* rebuilding
|
|
7343
|
+
* its transcript, so the resumed turn never renders against the stale
|
|
7344
|
+
* pre-rebuild UI (issue #6483).
|
|
7345
|
+
*/
|
|
7346
|
+
askReanswerCommitted?: boolean;
|
|
7376
7347
|
}> {
|
|
7377
7348
|
await this.#bash.flushPending();
|
|
7378
7349
|
const oldLeafId = this.sessionManager.getLeafId();
|
|
@@ -7523,6 +7494,10 @@ export class AgentSession {
|
|
|
7523
7494
|
// Determine the new leaf position based on target type
|
|
7524
7495
|
let newLeafId: string | null;
|
|
7525
7496
|
let editorText: string | undefined;
|
|
7497
|
+
// Set when the second-pass `ask` re-answer branch below actually commits a
|
|
7498
|
+
// new sibling answer — the trigger for resuming the agent afterwards so the
|
|
7499
|
+
// model consumes it, mirroring a live `ask` completion (issue #6483).
|
|
7500
|
+
let isAskReanswerCompletion = false;
|
|
7526
7501
|
|
|
7527
7502
|
if (targetEntry.type === "message" && targetEntry.message.role === "user") {
|
|
7528
7503
|
// User message: leaf = parent (null if root), text goes to editor
|
|
@@ -7559,6 +7534,7 @@ export class AgentSession {
|
|
|
7559
7534
|
timestamp: Date.now(),
|
|
7560
7535
|
};
|
|
7561
7536
|
newLeafId = this.sessionManager.appendMessageToBranch(toolResultMessage, targetEntry.parentId);
|
|
7537
|
+
isAskReanswerCompletion = true;
|
|
7562
7538
|
} else {
|
|
7563
7539
|
// Non-user message (or a user-invoked skill-prompt injection): land the
|
|
7564
7540
|
// leaf on the selected node so it stays on the active branch. Skill
|
|
@@ -7604,6 +7580,14 @@ export class AgentSession {
|
|
|
7604
7580
|
|
|
7605
7581
|
this.#branchSummaryAbortController = undefined;
|
|
7606
7582
|
|
|
7583
|
+
// Report a committed `ask` re-answer so the interactive caller can resume
|
|
7584
|
+
// the agent via `resumeAfterAskReanswer()` *after* rebuilding its
|
|
7585
|
+
// transcript. Scheduling the continue here instead would start a fresh
|
|
7586
|
+
// streaming turn whose `agent_start`/`turn_start` events could render
|
|
7587
|
+
// against the stale pre-rebuild UI and then be clobbered by the caller's
|
|
7588
|
+
// `renderInitialMessages(...)` (issue #6483). Plain leaf moves and the
|
|
7589
|
+
// read-only `reopenAsk` probe leave the flag unset.
|
|
7590
|
+
|
|
7607
7591
|
// Emit session_tree event; only handlers can mutate session entries, so skip
|
|
7608
7592
|
// the emit and the context rebuild when no handlers are registered (mirrors
|
|
7609
7593
|
// the session_before_tree guard above).
|
|
@@ -7616,9 +7600,34 @@ export class AgentSession {
|
|
|
7616
7600
|
fromExtension: summaryText ? fromExtension : undefined,
|
|
7617
7601
|
});
|
|
7618
7602
|
const rawContext = this.sessionManager.buildSessionContext();
|
|
7619
|
-
return {
|
|
7603
|
+
return {
|
|
7604
|
+
editorText,
|
|
7605
|
+
cancelled: false,
|
|
7606
|
+
summaryEntry,
|
|
7607
|
+
sessionContext: rawContext,
|
|
7608
|
+
askReanswerCommitted: isAskReanswerCompletion,
|
|
7609
|
+
};
|
|
7620
7610
|
}
|
|
7621
|
-
return {
|
|
7611
|
+
return {
|
|
7612
|
+
editorText,
|
|
7613
|
+
cancelled: false,
|
|
7614
|
+
summaryEntry,
|
|
7615
|
+
sessionContext: stateContext,
|
|
7616
|
+
askReanswerCommitted: isAskReanswerCompletion,
|
|
7617
|
+
};
|
|
7618
|
+
}
|
|
7619
|
+
|
|
7620
|
+
/**
|
|
7621
|
+
* Resume the agent after the interactive `/tree` caller has committed an
|
|
7622
|
+
* `ask` re-answer (`navigateTree` returned `askReanswerCommitted`) and
|
|
7623
|
+
* rebuilt its transcript. Mirrors how a live `ask` completion drives a
|
|
7624
|
+
* follow-up turn, but is deferred to the caller so the resumed turn renders
|
|
7625
|
+
* against the rebuilt UI rather than the stale pre-navigation transcript
|
|
7626
|
+
* (issue #6483). The scheduled continue honors the same disposed/compacting
|
|
7627
|
+
* guards as every other post-prompt continuation.
|
|
7628
|
+
*/
|
|
7629
|
+
resumeAfterAskReanswer(): void {
|
|
7630
|
+
this.#scheduleAgentContinue();
|
|
7622
7631
|
}
|
|
7623
7632
|
|
|
7624
7633
|
/**
|
package/src/session/prewalk.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { Agent, AgentMessage, AgentToolResult, AgentTurnEndContext } from "@oh-my-pi/pi-agent-core";
|
|
2
2
|
import { invalidateMessageCache } from "@oh-my-pi/pi-agent-core/compaction";
|
|
3
3
|
import type { Model } from "@oh-my-pi/pi-ai";
|
|
4
|
-
import { modelsAreEqual } from "@oh-my-pi/pi-catalog/models";
|
|
5
4
|
import { prompt } from "@oh-my-pi/pi-utils";
|
|
6
5
|
import type { LocalProtocolOptions } from "../internal-urls";
|
|
7
6
|
import { resolveApprovedPlan } from "../plan-mode/approved-plan";
|
|
@@ -11,7 +10,7 @@ import planYoloHandoffPrompt from "../prompts/system/plan-yolo-handoff.md" with
|
|
|
11
10
|
import prewalkChecklistPrompt from "../prompts/system/prewalk-checklist.md" with { type: "text" };
|
|
12
11
|
import prewalkContinuePrompt from "../prompts/system/prewalk-continue.md" with { type: "text" };
|
|
13
12
|
import prewalkPlanPrompt from "../prompts/system/prewalk-plan.md" with { type: "text" };
|
|
14
|
-
import type
|
|
13
|
+
import { type ConfiguredThinkingLevel, prewalkWouldBeNoop } from "../thinking";
|
|
15
14
|
import type { PlanProposalHandler } from "../tools/resolve";
|
|
16
15
|
import { ToolError } from "../tools/tool-errors";
|
|
17
16
|
import type { PlanYolo, Prewalk } from "./agent-session-types";
|
|
@@ -31,6 +30,7 @@ export interface PrewalkCoordinatorHost {
|
|
|
31
30
|
agent: Agent;
|
|
32
31
|
sessionManager: SessionManager;
|
|
33
32
|
model(): Model | undefined;
|
|
33
|
+
configuredThinkingLevel(): ConfiguredThinkingLevel | undefined;
|
|
34
34
|
emitNotice(level: "info" | "warning" | "error", message: string, source?: string): void;
|
|
35
35
|
setModelTemporary(
|
|
36
36
|
model: Model,
|
|
@@ -126,8 +126,13 @@ export class PrewalkCoordinator {
|
|
|
126
126
|
this.#scrubPlanNudge(liveMessages);
|
|
127
127
|
const target = prewalk.target;
|
|
128
128
|
const currentModel = this.#host.model();
|
|
129
|
-
if (currentModel
|
|
129
|
+
if (prewalkWouldBeNoop(currentModel, this.#host.configuredThinkingLevel(), target, prewalk.thinkingLevel)) {
|
|
130
130
|
this.#prewalk = undefined;
|
|
131
|
+
this.#host.emitNotice(
|
|
132
|
+
"info",
|
|
133
|
+
`Prewalk: target ${target.provider}/${target.id} already matches the active model and thinking level; nothing to switch.`,
|
|
134
|
+
"prewalk",
|
|
135
|
+
);
|
|
131
136
|
return;
|
|
132
137
|
}
|
|
133
138
|
await this.#host.setModelTemporary(target, prewalk.thinkingLevel, { ephemeral: true });
|
|
@@ -30,7 +30,7 @@ import type {
|
|
|
30
30
|
ServiceTier,
|
|
31
31
|
SimpleStreamOptions,
|
|
32
32
|
} from "@oh-my-pi/pi-ai";
|
|
33
|
-
import { isUsageLimitOutcome, resolveModelServiceTier } from "@oh-my-pi/pi-ai";
|
|
33
|
+
import { isUsageLimitOutcome, resolveModelServiceTier, streamSimple } from "@oh-my-pi/pi-ai";
|
|
34
34
|
import * as AIError from "@oh-my-pi/pi-ai/error";
|
|
35
35
|
import { modelsAreEqual } from "@oh-my-pi/pi-catalog/models";
|
|
36
36
|
import { extractHttpStatusFromError, extractRetryHint, logger } from "@oh-my-pi/pi-utils";
|
|
@@ -93,8 +93,10 @@ import { formatSessionDumpText } from "./session-dump-format";
|
|
|
93
93
|
import type { CompactionEntry, SessionEntry } from "./session-entries";
|
|
94
94
|
import { formatSessionHistoryMarkdown } from "./session-history-format";
|
|
95
95
|
import type { SessionManager } from "./session-manager";
|
|
96
|
+
import { buildSessionMetadata } from "./session-metadata";
|
|
96
97
|
import type { YieldQueue } from "./yield-queue";
|
|
97
98
|
|
|
99
|
+
const ADVISOR_CODEX_SSE_MAX_ATTEMPTS = 1;
|
|
98
100
|
/** Advisor statistics for the advisor status command. */
|
|
99
101
|
export interface AdvisorStats {
|
|
100
102
|
configured: boolean;
|
|
@@ -328,6 +330,17 @@ export class SessionAdvisors {
|
|
|
328
330
|
this.#resetAdvisorSessionState();
|
|
329
331
|
}
|
|
330
332
|
|
|
333
|
+
/**
|
|
334
|
+
* Rebind every live advisor to the active primary conversation's provider
|
|
335
|
+
* identity (session id, prompt-cache key, credential + metadata resolvers,
|
|
336
|
+
* telemetry). Invoked on every provider-session change — including branch
|
|
337
|
+
* paths that skip conversation restore — so advisors never keep emitting the
|
|
338
|
+
* previous conversation's session id/metadata (issue #6625).
|
|
339
|
+
*/
|
|
340
|
+
refreshProviderIdentity(): void {
|
|
341
|
+
for (const advisor of this.#advisors) this.#refreshAdvisorProviderIdentity(advisor);
|
|
342
|
+
}
|
|
343
|
+
|
|
331
344
|
/** Re-primes advisor transcript views after an in-conversation history rewrite. */
|
|
332
345
|
resetAllRuntimes(): void {
|
|
333
346
|
this.#resetAllAdvisorRuntimes();
|
|
@@ -391,6 +404,38 @@ export class SessionAdvisors {
|
|
|
391
404
|
this.#advisorInterruptImmuneTurnStart = this.#advisorPrimaryTurnsCompleted + 1;
|
|
392
405
|
}
|
|
393
406
|
|
|
407
|
+
/** Rebind one advisor to the active primary conversation's provider identity. */
|
|
408
|
+
#refreshAdvisorProviderIdentity(advisor: ActiveAdvisor): void {
|
|
409
|
+
const primaryProviderSessionId = this.#host.sessionId();
|
|
410
|
+
const providerSessionId = getOrCreateAdvisorProviderSessionId(
|
|
411
|
+
this.#advisorProviderSessionIds,
|
|
412
|
+
primaryProviderSessionId,
|
|
413
|
+
advisor.slug,
|
|
414
|
+
);
|
|
415
|
+
advisor.providerSessionId = providerSessionId;
|
|
416
|
+
advisor.agent.sessionId = providerSessionId;
|
|
417
|
+
advisor.agent.promptCacheKey = this.#host.agent.promptCacheKey ?? providerSessionId;
|
|
418
|
+
advisor.agent.getApiKey = requestModel => this.#host.modelRegistry.resolver(requestModel, providerSessionId);
|
|
419
|
+
advisor.agent.setMetadataResolver(
|
|
420
|
+
providerSessionId
|
|
421
|
+
? provider => buildSessionMetadata(providerSessionId, provider, this.#host.modelRegistry.authStorage)
|
|
422
|
+
: undefined,
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
const telemetry = advisor.agent.telemetry;
|
|
426
|
+
if (telemetry?.agent) {
|
|
427
|
+
advisor.agent.setTelemetry({
|
|
428
|
+
...telemetry,
|
|
429
|
+
agent: {
|
|
430
|
+
...telemetry.agent,
|
|
431
|
+
id: advisor.slug
|
|
432
|
+
? `${primaryProviderSessionId}-advisor-${advisor.slug}`
|
|
433
|
+
: `${primaryProviderSessionId}-advisor`,
|
|
434
|
+
},
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
394
439
|
/**
|
|
395
440
|
* Re-prime the advisor across a conversation boundary: `/new`, `/branch`,
|
|
396
441
|
* `/btw`, `/tree`, and session switch/resume. Beyond {@link AdvisorRuntime.reset}
|
|
@@ -644,6 +689,15 @@ export class SessionAdvisors {
|
|
|
644
689
|
tools: advisorToolMap,
|
|
645
690
|
allowNativeDelete: advisorCanMutateFiles,
|
|
646
691
|
});
|
|
692
|
+
const baseAdvisorStreamFn = this.#advisorStreamFn ?? streamSimple;
|
|
693
|
+
const advisorStreamFn: StreamFn = (requestModel, context, options) =>
|
|
694
|
+
baseAdvisorStreamFn(
|
|
695
|
+
requestModel,
|
|
696
|
+
context,
|
|
697
|
+
requestModel.api === "openai-codex-responses"
|
|
698
|
+
? { ...options, codexSseMaxAttempts: ADVISOR_CODEX_SSE_MAX_ATTEMPTS }
|
|
699
|
+
: options,
|
|
700
|
+
);
|
|
647
701
|
const advisorAgent = new Agent({
|
|
648
702
|
initialState: {
|
|
649
703
|
systemPrompt,
|
|
@@ -659,7 +713,7 @@ export class SessionAdvisors {
|
|
|
659
713
|
cwdResolver: () => this.#host.sessionManager.getCwd(),
|
|
660
714
|
preferWebsockets: this.#host.preferWebsockets,
|
|
661
715
|
getApiKey: requestModel => this.#host.modelRegistry.resolver(requestModel, advisorProviderSessionId),
|
|
662
|
-
streamFn:
|
|
716
|
+
streamFn: advisorStreamFn,
|
|
663
717
|
onPayload: this.#host.onPayload,
|
|
664
718
|
onResponse: this.#host.onResponse,
|
|
665
719
|
onSseEvent: this.#host.onSseEvent,
|
|
@@ -772,6 +826,7 @@ export class SessionAdvisors {
|
|
|
772
826
|
retryFallbackPendingSuccess: false,
|
|
773
827
|
signature,
|
|
774
828
|
};
|
|
829
|
+
this.#refreshAdvisorProviderIdentity(advisorRef);
|
|
775
830
|
this.#attachAdvisorRecorderFeed(advisorRef);
|
|
776
831
|
if (seedToCurrent) runtime.seedTo(this.#host.agent.state.messages.length);
|
|
777
832
|
this.#advisorStatuses.set(slug, { name: advisorName, status: "running" });
|
|
@@ -1247,7 +1302,15 @@ export class SessionAdvisors {
|
|
|
1247
1302
|
for (const candidate of candidates) {
|
|
1248
1303
|
const apiKey = await this.#host.modelRegistry.getApiKey(candidate, advisorProviderSessionId);
|
|
1249
1304
|
if (!apiKey) continue;
|
|
1250
|
-
|
|
1305
|
+
// The advisor overflow-compaction one-shot bypasses the advisor `Agent`,
|
|
1306
|
+
// so its installed metadata resolver never runs. Emit the same
|
|
1307
|
+
// `metadata.user_id` identity here (resolved per candidate provider,
|
|
1308
|
+
// after the session-sticky credential is selected) so summarization
|
|
1309
|
+
// requests carry the advisor session id like every other advisor call
|
|
1310
|
+
// (issue #6625).
|
|
1311
|
+
const advisorMetadata = advisorProviderSessionId
|
|
1312
|
+
? buildSessionMetadata(advisorProviderSessionId, candidate.provider, this.#host.modelRegistry.authStorage)
|
|
1313
|
+
: undefined;
|
|
1251
1314
|
try {
|
|
1252
1315
|
compactResult = await compact(
|
|
1253
1316
|
preparation,
|
|
@@ -1262,6 +1325,7 @@ export class SessionAdvisors {
|
|
|
1262
1325
|
tools: agent.state.tools,
|
|
1263
1326
|
sessionId: advisorProviderSessionId,
|
|
1264
1327
|
promptCacheKey: advisorProviderSessionId,
|
|
1328
|
+
metadata: advisorMetadata,
|
|
1265
1329
|
providerSessionState: this.#host.providerSessionState,
|
|
1266
1330
|
codexCompaction,
|
|
1267
1331
|
},
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { deriveClaudeDeviceId } from "@oh-my-pi/pi-ai";
|
|
2
|
+
import { getInstallId } from "@oh-my-pi/pi-utils";
|
|
3
|
+
import type { AuthStorage } from "./auth-storage";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Build the per-request `metadata` payload for the Anthropic provider, shaped
|
|
7
|
+
* like real Claude Code's `getAPIMetadata` output (`{ session_id, account_uuid,
|
|
8
|
+
* device_id }`) so the backend buckets requests under one session and attributes
|
|
9
|
+
* them to the authenticated OAuth account when available. Resolved at request
|
|
10
|
+
* time so token refreshes and login/logout transitions don't strand a stale
|
|
11
|
+
* account UUID in memory. `account_uuid` and `device_id` are omitted for
|
|
12
|
+
* non-Anthropic providers to avoid leaking the user's Claude identity to
|
|
13
|
+
* third-party APIs (including Anthropic-format-compatible proxies such as
|
|
14
|
+
* cloudflare-ai-gateway or gitlab-duo).
|
|
15
|
+
*
|
|
16
|
+
* Installed via `Agent#setMetadataResolver` on the main `AgentSession`, each
|
|
17
|
+
* subagent session, and the separately constructed advisor `Agent` — each with
|
|
18
|
+
* its own provider session id — so Main, subagent, and Advisor requests each
|
|
19
|
+
* expose a distinct, stable provider-facing session identity.
|
|
20
|
+
*
|
|
21
|
+
* `provider` is the target provider string (e.g. `"anthropic"`) and gates the
|
|
22
|
+
* `account_uuid` and `device_id` lookups — only `"anthropic"` requests carry them.
|
|
23
|
+
*
|
|
24
|
+
* `sessionId` is forwarded to the auth-storage session-sticky lookup so that
|
|
25
|
+
* multi-credential setups attribute to the same OAuth account used for the
|
|
26
|
+
* actual API request rather than always picking the first credential.
|
|
27
|
+
*
|
|
28
|
+
* `authStorage` is treated as optional so test fixtures that stub `modelRegistry`
|
|
29
|
+
* without a real storage layer still work; the resolver simply skips the lookup
|
|
30
|
+
* and emits `{ session_id }` alone, matching the no-OAuth-credential path.
|
|
31
|
+
*/
|
|
32
|
+
export function buildSessionMetadata(
|
|
33
|
+
sessionId: string,
|
|
34
|
+
provider: string,
|
|
35
|
+
authStorage: AuthStorage | undefined,
|
|
36
|
+
): Record<string, unknown> {
|
|
37
|
+
const userId: Record<string, string> = { session_id: sessionId };
|
|
38
|
+
// Only look up account_uuid when the request is going to Anthropic. Injecting
|
|
39
|
+
// a Claude OAuth account_uuid into requests bound for other providers (including
|
|
40
|
+
// Anthropic-format-compatible proxies like cloudflare-ai-gateway or gitlab-duo)
|
|
41
|
+
// would leak the user's Anthropic identity to unrelated third-party APIs.
|
|
42
|
+
if (provider === "anthropic") {
|
|
43
|
+
const accountUuid = authStorage?.getOAuthAccountId("anthropic", sessionId);
|
|
44
|
+
if (typeof accountUuid === "string" && accountUuid.length > 0) {
|
|
45
|
+
userId.account_uuid = accountUuid;
|
|
46
|
+
// Claude Code's `device_id` is a stable 64-hex account-scoped install
|
|
47
|
+
// identifier. Include both omp's persistent install id and the Claude
|
|
48
|
+
// account UUID so two accounts on the same install do not share a device.
|
|
49
|
+
userId.device_id = deriveClaudeDeviceId(getInstallId(), accountUuid);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return { user_id: JSON.stringify(userId) };
|
|
53
|
+
}
|
|
@@ -17,6 +17,7 @@ import type { MemoryBackendStartOptions } from "../memory-backend/types";
|
|
|
17
17
|
import xdevMountNoticePrompt from "../prompts/system/xdev-mount-notice.md" with { type: "text" };
|
|
18
18
|
import { usesCodexTaskPrompt } from "../task/prompt-policy";
|
|
19
19
|
import { isMCPToolName, normalizeToolNames } from "../tools/builtin-names";
|
|
20
|
+
import { computerExposureMode } from "../tools/computer/exposure";
|
|
20
21
|
import { wrapToolWithMetaNotice } from "../tools/output-meta";
|
|
21
22
|
import { ToolAbortError, ToolError } from "../tools/tool-errors";
|
|
22
23
|
import { isMountableUnderXdev, type XdevRegistry } from "../tools/xdev";
|
|
@@ -294,6 +295,16 @@ export class SessionTools {
|
|
|
294
295
|
return usesCodexTaskPrompt(model) ? "task-policy:gpt-5.6" : "task-policy:default";
|
|
295
296
|
}
|
|
296
297
|
|
|
298
|
+
#logComputerState(message: string, enabled: boolean): void {
|
|
299
|
+
const model = this.#host.model();
|
|
300
|
+
logger.debug(message, {
|
|
301
|
+
enabled,
|
|
302
|
+
active: this.getEnabledToolNames().includes("computer"),
|
|
303
|
+
model: model ? formatModelString(model) : undefined,
|
|
304
|
+
exposure: computerExposureMode(model),
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
297
308
|
/** Rebuilds model-dependent tool prompts after a model change. */
|
|
298
309
|
async syncAfterModelChange(previousEditMode: EditMode): Promise<void> {
|
|
299
310
|
const currentEditMode = this.resolveActiveEditMode();
|
|
@@ -303,6 +314,20 @@ export class SessionTools {
|
|
|
303
314
|
if (editModeChanged || modelChanged) {
|
|
304
315
|
await this.refreshBaseSystemPrompt();
|
|
305
316
|
}
|
|
317
|
+
const computerExpected = this.#host.settings.get("computer.enabled");
|
|
318
|
+
const computerActive = this.getEnabledToolNames().includes("computer");
|
|
319
|
+
if (computerExpected && !computerActive) {
|
|
320
|
+
const model = this.#host.model();
|
|
321
|
+
const modelName = model ? formatModelString(model) : "the current model";
|
|
322
|
+
logger.warn("Enabled computer tool missing after model change", { model: modelName });
|
|
323
|
+
this.#host.emitNotice(
|
|
324
|
+
"warning",
|
|
325
|
+
`Computer use remains enabled, but the computer tool is unavailable to ${modelName}.`,
|
|
326
|
+
"computer",
|
|
327
|
+
);
|
|
328
|
+
} else if (computerExpected) {
|
|
329
|
+
this.#logComputerState("Computer tool retained after model change", true);
|
|
330
|
+
}
|
|
306
331
|
}
|
|
307
332
|
|
|
308
333
|
/** Enabled MCP tools in their current presentation partition. */
|
|
@@ -713,16 +738,24 @@ export class SessionTools {
|
|
|
713
738
|
* tool (e.g. restricted child sessions have no factory).
|
|
714
739
|
*/
|
|
715
740
|
async setComputerToolEnabled(enabled: boolean): Promise<boolean> {
|
|
741
|
+
const logState = (): void => this.#logComputerState("Computer tool state changed", enabled);
|
|
716
742
|
const active = this.getEnabledToolNames();
|
|
717
743
|
if (!enabled) {
|
|
718
744
|
if (active.includes("computer")) {
|
|
719
745
|
await this.applyActiveToolsByName(active.filter(name => name !== "computer"));
|
|
720
746
|
}
|
|
747
|
+
logState();
|
|
721
748
|
return true;
|
|
722
749
|
}
|
|
723
750
|
if (!this.#toolRegistry.has("computer")) {
|
|
724
751
|
const tool = await this.#createComputerTool?.();
|
|
725
|
-
if (tool?.name !== "computer")
|
|
752
|
+
if (tool?.name !== "computer") {
|
|
753
|
+
const model = this.#host.model();
|
|
754
|
+
logger.warn("Computer tool could not be created", {
|
|
755
|
+
model: model ? formatModelString(model) : undefined,
|
|
756
|
+
});
|
|
757
|
+
return false;
|
|
758
|
+
}
|
|
726
759
|
const wrapped = this.#wrapRuntimeTool(tool);
|
|
727
760
|
this.#toolRegistry.set(wrapped.name, wrapped);
|
|
728
761
|
this.#builtInToolNames.add(wrapped.name);
|
|
@@ -730,6 +763,7 @@ export class SessionTools {
|
|
|
730
763
|
if (!active.includes("computer")) {
|
|
731
764
|
await this.applyActiveToolsByName([...active, "computer"]);
|
|
732
765
|
}
|
|
766
|
+
logState();
|
|
733
767
|
return true;
|
|
734
768
|
}
|
|
735
769
|
|
|
@@ -185,7 +185,7 @@ export class StreamingEditGuard {
|
|
|
185
185
|
#abortForAutoGeneratedPath(toolCall: ToolCall, filePath: string, resolvedPath: string): void {
|
|
186
186
|
if (this.#lastToolCallId === toolCall.id) return;
|
|
187
187
|
this.#lastToolCallId = toolCall.id;
|
|
188
|
-
void assertEditableFile(resolvedPath, filePath).catch(error => {
|
|
188
|
+
void assertEditableFile(resolvedPath, filePath, this.#host.settings).catch(error => {
|
|
189
189
|
if (!(error instanceof ToolError) || this.#lastToolCallId !== toolCall.id) return;
|
|
190
190
|
if (!this.#abortTriggered) {
|
|
191
191
|
this.#abortTriggered = true;
|
|
@@ -774,10 +774,6 @@ export class TurnRecovery {
|
|
|
774
774
|
return id;
|
|
775
775
|
}
|
|
776
776
|
|
|
777
|
-
#isGenericAbortSentinel(message: AssistantMessage): boolean {
|
|
778
|
-
return message.errorMessage === "Request was aborted" || message.errorMessage === "Request was aborted.";
|
|
779
|
-
}
|
|
780
|
-
|
|
781
777
|
/**
|
|
782
778
|
* Retry an empty, reason-less provider abort: a turn with no content that
|
|
783
779
|
* carries the generic sentinel (bare `abort()`), whether the provider
|
|
@@ -806,7 +802,9 @@ export class TurnRecovery {
|
|
|
806
802
|
|
|
807
803
|
const id = this.#classifyRetryMessage(message);
|
|
808
804
|
if (message.stopReason === "aborted" && AIError.is(id, AIError.Flag.Abort)) return true;
|
|
809
|
-
if (
|
|
805
|
+
if (message.errorMessage !== "Request was aborted" && message.errorMessage !== "Request was aborted.") {
|
|
806
|
+
return false;
|
|
807
|
+
}
|
|
810
808
|
|
|
811
809
|
message.errorId = AIError.create(AIError.Flag.Abort);
|
|
812
810
|
return true;
|
|
@@ -830,30 +828,48 @@ export class TurnRecovery {
|
|
|
830
828
|
}
|
|
831
829
|
|
|
832
830
|
/**
|
|
833
|
-
*
|
|
834
|
-
*
|
|
835
|
-
*
|
|
836
|
-
*
|
|
831
|
+
* Classify a reasonless abort or stream stall whose emitted tool calls all
|
|
832
|
+
* have results. The failed assistant/tool-result pair stays in context so
|
|
833
|
+
* continuation cannot replay completed side effects; synthetic results tell
|
|
834
|
+
* the next turn that an unexecuted call must be reissued.
|
|
837
835
|
*/
|
|
838
|
-
|
|
839
|
-
if (message.stopReason !== "error" || !message.errorMessage?.toLowerCase().includes("stream stall")) {
|
|
840
|
-
return false;
|
|
841
|
-
}
|
|
836
|
+
classifyResolvedInterruptedToolTurn(message: AssistantMessage): "reasonless-abort" | "stream-stall" | undefined {
|
|
842
837
|
const id = this.#classifyRetryMessage(message);
|
|
843
|
-
|
|
844
|
-
|
|
838
|
+
const genericAbort =
|
|
839
|
+
message.errorMessage === "Request was aborted" || message.errorMessage === "Request was aborted.";
|
|
840
|
+
const reasonlessAbort =
|
|
841
|
+
(message.stopReason === "aborted" || message.stopReason === "error") &&
|
|
842
|
+
!this.#host.abortInProgress() &&
|
|
843
|
+
!this.#host.isDisposed() &&
|
|
844
|
+
!this.#host.streamingEditAbortTriggered() &&
|
|
845
|
+
((message.stopReason === "aborted" && AIError.is(id, AIError.Flag.Abort)) || genericAbort);
|
|
846
|
+
const streamStall =
|
|
847
|
+
message.stopReason === "error" &&
|
|
848
|
+
message.errorMessage?.toLowerCase().includes("stream stall") === true &&
|
|
849
|
+
AIError.retriable(id);
|
|
850
|
+
if (!reasonlessAbort && !streamStall) return undefined;
|
|
851
|
+
if (reasonlessAbort && genericAbort) message.errorId = AIError.create(AIError.Flag.Abort);
|
|
852
|
+
|
|
853
|
+
// The Cursor server-execution marker gate applies only to the stream-stall
|
|
854
|
+
// path: an unmarked/unresolved Cursor block there means the server has not
|
|
855
|
+
// finished executing, so resuming would race it. A reasonless abort instead
|
|
856
|
+
// ends the turn and the agent loop pairs every un-run call (Cursor's unmarked
|
|
857
|
+
// `todo`/MCP blocks included) with a synthetic `executed: false` result, so
|
|
858
|
+
// the tool-result reconciliation below is the safety gate and the marker is
|
|
859
|
+
// irrelevant.
|
|
845
860
|
const resolvedToolCallIds: string[] = [];
|
|
846
861
|
for (const block of message.content) {
|
|
847
862
|
if (block.type !== "toolCall") continue;
|
|
848
863
|
if (
|
|
864
|
+
streamStall &&
|
|
849
865
|
message.provider === "cursor" &&
|
|
850
866
|
(!(kCursorExecResolved in block) || block[kCursorExecResolved] !== true)
|
|
851
867
|
) {
|
|
852
|
-
return
|
|
868
|
+
return undefined;
|
|
853
869
|
}
|
|
854
870
|
resolvedToolCallIds.push(block.id);
|
|
855
871
|
}
|
|
856
|
-
if (resolvedToolCallIds.length === 0) return
|
|
872
|
+
if (resolvedToolCallIds.length === 0) return undefined;
|
|
857
873
|
|
|
858
874
|
const messages = this.#host.agent.state.messages;
|
|
859
875
|
let assistantIndex = -1;
|
|
@@ -864,14 +880,15 @@ export class TurnRecovery {
|
|
|
864
880
|
break;
|
|
865
881
|
}
|
|
866
882
|
}
|
|
867
|
-
if (assistantIndex < 0) return
|
|
883
|
+
if (assistantIndex < 0) return undefined;
|
|
868
884
|
|
|
869
885
|
const unresolvedToolCallIds = new Set(resolvedToolCallIds);
|
|
870
886
|
for (let i = assistantIndex + 1; i < messages.length; i++) {
|
|
871
887
|
const candidate = messages[i];
|
|
872
888
|
if (candidate.role === "toolResult") unresolvedToolCallIds.delete(candidate.toolCallId);
|
|
873
889
|
}
|
|
874
|
-
|
|
890
|
+
if (unresolvedToolCallIds.size > 0) return undefined;
|
|
891
|
+
return reasonlessAbort ? "reasonless-abort" : "stream-stall";
|
|
875
892
|
}
|
|
876
893
|
/**
|
|
877
894
|
* Retried turns remove the failed assistant message from active context.
|