@oh-my-pi/pi-coding-agent 16.3.14 → 16.4.0

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.
Files changed (76) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/cli.js +3053 -3158
  3. package/dist/types/advisor/config.d.ts +7 -0
  4. package/dist/types/cli/args.d.ts +1 -0
  5. package/dist/types/config/model-resolver.d.ts +1 -1
  6. package/dist/types/config/models-config-schema.d.ts +28 -15
  7. package/dist/types/config/models-config.d.ts +6 -0
  8. package/dist/types/config/settings-schema.d.ts +7 -2
  9. package/dist/types/dap/config.d.ts +13 -1
  10. package/dist/types/extensibility/extensions/types.d.ts +6 -0
  11. package/dist/types/lsp/config.d.ts +7 -1
  12. package/dist/types/main.d.ts +2 -0
  13. package/dist/types/mcp/transports/stdio.d.ts +8 -3
  14. package/dist/types/modes/components/hook-selector.d.ts +2 -0
  15. package/dist/types/modes/theme/defaults/index.d.ts +2 -0
  16. package/dist/types/modes/theme/theme.d.ts +3 -2
  17. package/dist/types/sdk.d.ts +2 -0
  18. package/dist/types/session/agent-session.d.ts +5 -3
  19. package/dist/types/session/session-context.test.d.ts +1 -0
  20. package/dist/types/session/session-entries.d.ts +4 -0
  21. package/dist/types/thinking.d.ts +6 -3
  22. package/dist/types/utils/file-display-mode.d.ts +1 -1
  23. package/package.json +12 -12
  24. package/src/advisor/__tests__/config.test.ts +84 -0
  25. package/src/advisor/config.ts +28 -0
  26. package/src/cli/args.ts +1 -0
  27. package/src/cli/flag-tables.ts +3 -0
  28. package/src/config/model-registry.ts +1 -1
  29. package/src/config/model-resolver.ts +14 -12
  30. package/src/config/models-config-schema.ts +3 -2
  31. package/src/config/settings-schema.ts +5 -2
  32. package/src/dap/config.ts +136 -39
  33. package/src/dap/defaults.json +1 -1
  34. package/src/eval/js/shared/runtime.ts +20 -4
  35. package/src/eval/js/worker-core.ts +9 -2
  36. package/src/exec/bash-executor.ts +8 -1
  37. package/src/extensibility/extensions/types.ts +6 -0
  38. package/src/internal-urls/docs-index.generated.txt +1 -1
  39. package/src/lsp/config.ts +27 -13
  40. package/src/lsp/index.ts +114 -29
  41. package/src/main.ts +21 -1
  42. package/src/mcp/transports/stdio.test.ts +12 -0
  43. package/src/mcp/transports/stdio.ts +14 -8
  44. package/src/modes/components/hook-selector.ts +9 -2
  45. package/src/modes/controllers/extension-ui-controller.ts +3 -0
  46. package/src/modes/controllers/input-controller.ts +4 -4
  47. package/src/modes/controllers/selector-controller.ts +1 -1
  48. package/src/modes/theme/defaults/dark-poimandres.json +6 -5
  49. package/src/modes/theme/defaults/light-poimandres.json +6 -5
  50. package/src/modes/theme/theme-schema.json +6 -2
  51. package/src/modes/theme/theme.ts +24 -18
  52. package/src/prompts/agents/init.md +1 -1
  53. package/src/prompts/agents/plan.md +2 -2
  54. package/src/prompts/agents/reviewer.md +1 -1
  55. package/src/prompts/agents/{explore.md → scout.md} +3 -2
  56. package/src/prompts/system/plan-mode-active.md +2 -2
  57. package/src/prompts/system/system-prompt.md +5 -3
  58. package/src/prompts/tools/ast-grep.md +1 -1
  59. package/src/prompts/tools/debug.md +4 -4
  60. package/src/prompts/tools/grep.md +1 -1
  61. package/src/prompts/tools/task.md +2 -2
  62. package/src/sdk.ts +68 -45
  63. package/src/session/agent-session.ts +215 -64
  64. package/src/session/session-context.test.ts +83 -0
  65. package/src/session/session-context.ts +1 -0
  66. package/src/session/session-entries.ts +4 -0
  67. package/src/session/session-manager.ts +9 -1
  68. package/src/system-prompt.test.ts +20 -11
  69. package/src/task/agents.ts +2 -5
  70. package/src/task/executor.ts +111 -74
  71. package/src/thinking.ts +16 -7
  72. package/src/tools/ask.ts +51 -13
  73. package/src/tools/browser/cmux/cmux-tab.ts +21 -12
  74. package/src/tools/debug.ts +41 -11
  75. package/src/utils/file-display-mode.ts +1 -1
  76. package/src/prompts/agents/tester.md +0 -111
@@ -38,6 +38,7 @@ import {
38
38
  createToolScopedAbortReason,
39
39
  resolveTelemetry,
40
40
  type StreamFn,
41
+ TERMINAL_TOOL_RESULT_ABORT_REASON,
41
42
  ThinkingLevel,
42
43
  type ToolChoiceDirective,
43
44
  } from "@oh-my-pi/pi-agent-core";
@@ -84,6 +85,7 @@ import type {
84
85
  AssistantMessageEvent,
85
86
  AssistantRetryRecovery,
86
87
  AssistantRetryRecoveryKind,
88
+ CodexCompactionContext,
87
89
  Context,
88
90
  ImageContent,
89
91
  Message,
@@ -117,6 +119,7 @@ import {
117
119
  streamSimple,
118
120
  } from "@oh-my-pi/pi-ai";
119
121
  import * as AIError from "@oh-my-pi/pi-ai/error";
122
+ import { resetOpenAICodexHistoryAfterCompaction } from "@oh-my-pi/pi-ai/providers/openai-codex-responses";
120
123
  import { toolWireSchema } from "@oh-my-pi/pi-ai/utils/schema";
121
124
  import { GeminiHeaderRunDetector, isGeminiThinkingModel } from "@oh-my-pi/pi-ai/utils/thinking-loop";
122
125
  import { type RepeatedToolCallDetection, ToolCallLoopGuard } from "@oh-my-pi/pi-ai/utils/tool-call-loop-guard";
@@ -154,6 +157,7 @@ import {
154
157
  AdvisorTranscriptRecorder,
155
158
  advisorTranscriptFilename,
156
159
  formatAdvisorBatchContent,
160
+ getOrCreateAdvisorProviderSessionId,
157
161
  isAdvisorInterruptImmuneTurnActive,
158
162
  isInterruptingSeverity,
159
163
  resolveAdvisorDeliveryChannel,
@@ -602,6 +606,20 @@ function compactionDeadEndWarning(remedies: string): string {
602
606
  );
603
607
  }
604
608
 
609
+ function createCodexCompactionContext(options: {
610
+ trigger: CodexCompactionContext["trigger"];
611
+ reason: CodexCompactionContext["reason"];
612
+ phase: CodexCompactionContext["phase"];
613
+ }): CodexCompactionContext {
614
+ return {
615
+ operationId: crypto.randomUUID(),
616
+ trigger: options.trigger,
617
+ reason: options.reason,
618
+ phase: options.phase,
619
+ strategy: "memento",
620
+ };
621
+ }
622
+
605
623
  /**
606
624
  * Per-turn prune cache window. A tool result whose all-message suffix exceeds
607
625
  * this is in the warm, already-sent prompt-cache prefix: re-writing it costs the
@@ -793,6 +811,8 @@ export interface AgentSessionConfig {
793
811
  * so that credential sticky selection is consistent with the session's streaming calls.
794
812
  */
795
813
  providerSessionId?: string;
814
+ /** Marks `agent.promptCacheKey` as fork-inherited so incompatible route changes can clear it. */
815
+ providerPromptCacheKeySource?: "explicit" | "fork";
796
816
  /**
797
817
  * Full advisor toolset, pre-built in `createAgentSession` against a distinct,
798
818
  * advisor-scoped `ToolSession` (its own `-advisor` session/agent id) so the
@@ -1061,7 +1081,7 @@ function parseRetryFallbackSelector(
1061
1081
  const trimmed = selector.trim();
1062
1082
  if (!trimmed) return undefined;
1063
1083
  const parsed = parseModelString(trimmed, {
1064
- allowMaxAlias: true,
1084
+ allowMaxSuffix: true,
1065
1085
  allowAutoAlias: true,
1066
1086
  isLiteralModelId: (provider, id) => modelLookup?.find(provider, id) !== undefined,
1067
1087
  });
@@ -1599,6 +1619,8 @@ export class AgentSession {
1599
1619
  #advisors: ActiveAdvisor[] = [];
1600
1620
  /** Configured advisor roster from WATCHDOG.yml; undefined/empty → single legacy advisor. */
1601
1621
  #advisorConfigs?: AdvisorConfig[];
1622
+ /** Provider-facing UUIDv7 identities keyed by primary provider session and advisor slug. */
1623
+ #advisorProviderSessionIds = new Map<string, string>();
1602
1624
  /** Aggregate of the most recent stop's recorder closes; awaited by dispose() and
1603
1625
  * used as the open barrier for the next build so two writers never share a file. */
1604
1626
  #advisorRecorderClosed: Promise<void> = Promise.resolve();
@@ -1697,6 +1719,7 @@ export class AgentSession {
1697
1719
  #agentKind: "main" | "sub" = "main";
1698
1720
  #providerSessionId: string | undefined;
1699
1721
  #freshProviderSessionId: string | undefined;
1722
+ #inheritedProviderPromptCacheKey: string | undefined;
1700
1723
  #isDisposed = false;
1701
1724
  // Extension system
1702
1725
  #extensionRunner: ExtensionRunner | undefined = undefined;
@@ -1846,6 +1869,7 @@ export class AgentSession {
1846
1869
  * Cleared before every new prompt turn so the next turn evaluates cleanly.
1847
1870
  */
1848
1871
  #yieldTerminationPending = false;
1872
+ #synchronouslyTerminatedYieldToolCallIds = new Set<string>();
1849
1873
  #providerSessionState = new Map<string, ProviderSessionState>();
1850
1874
  #hindsightSessionState: HindsightSessionState | undefined = undefined;
1851
1875
  readonly rawSseDebugBuffer: RawSseDebugBuffer;
@@ -2212,6 +2236,8 @@ export class AgentSession {
2212
2236
  this.#agentId = config.agentId;
2213
2237
  this.#agentKind = config.agentKind ?? "main";
2214
2238
  this.#providerSessionId = config.providerSessionId;
2239
+ this.#inheritedProviderPromptCacheKey =
2240
+ config.providerPromptCacheKeySource === "fork" ? this.agent.promptCacheKey : undefined;
2215
2241
  this.agent.setAssistantMessageEventInterceptor((message, assistantMessageEvent) => {
2216
2242
  const event: AgentEvent = {
2217
2243
  type: "message_update",
@@ -2222,8 +2248,8 @@ export class AgentSession {
2222
2248
  this.#maybeAbortStreamingEdit(event);
2223
2249
  this.#maybeInterruptGeminiHeaderRunaway(message, assistantMessageEvent);
2224
2250
  });
2225
- // Per-tool TTSR reminders are folded into the matched tool's result via this hook.
2226
- this.agent.afterToolCall = ctx => this.#ttsrAfterToolCall(ctx);
2251
+ // Tool-result hook owns synchronous post-tool actions that must affect the current loop.
2252
+ this.agent.afterToolCall = ctx => this.#afterToolCall(ctx);
2227
2253
  this.agent.providerSessionState = this.#providerSessionState;
2228
2254
  this.#syncAgentSessionId();
2229
2255
  this.#syncTodoPhasesFromBranch();
@@ -2477,18 +2503,26 @@ export class AgentSession {
2477
2503
  const names = config.tools?.length ? new Set(config.tools) : ADVISOR_DEFAULT_TOOL_NAMES;
2478
2504
  const tools = (this.#advisorTools ?? []).filter(t => names.has(t.name));
2479
2505
 
2480
- const advisorSessionId = this.#advisorSessionId(slug);
2506
+ const primaryProviderSessionId = this.sessionId;
2507
+ const advisorSessionLabel = slug
2508
+ ? `${primaryProviderSessionId}-advisor-${slug}`
2509
+ : `${primaryProviderSessionId}-advisor`;
2510
+ const advisorProviderSessionId = getOrCreateAdvisorProviderSessionId(
2511
+ this.#advisorProviderSessionIds,
2512
+ primaryProviderSessionId,
2513
+ slug,
2514
+ );
2481
2515
  const appendOnlyContext = new AppendOnlyContextManager();
2482
2516
 
2483
2517
  // Thread the primary's telemetry into the advisor loop so the advisor
2484
- // model's GenAI spans + usage/cost hooks fire stamped with the advisor's
2485
- // own identity. `conversationId` is cleared so the advisor loop falls back
2486
- // to its own session id; undefined telemetry stays undefined.
2518
+ // model's GenAI spans + usage/cost hooks fire stamped with the local advisor
2519
+ // identity. `conversationId` is cleared so provider telemetry falls back to
2520
+ // the UUIDv7 provider session id, not the local `-advisor` label.
2487
2521
  const advisorTelemetry = this.agent.telemetry
2488
2522
  ? {
2489
2523
  ...this.agent.telemetry,
2490
2524
  agent: {
2491
- id: advisorSessionId,
2525
+ id: advisorSessionLabel,
2492
2526
  name: slug ? `${MODEL_ROLES.advisor.name}: ${advisorName}` : MODEL_ROLES.advisor.name,
2493
2527
  description: formatModelString(advisorModel),
2494
2528
  },
@@ -2500,10 +2534,10 @@ export class AgentSession {
2500
2534
  // advisor's requests cache, route, and obfuscate like the main turn.
2501
2535
  // `promptCacheKey` preserves an explicitly pinned provider cache key
2502
2536
  // unchanged so tan/shared-session advisor calls read the exact shard the
2503
- // parent turn populated, while keeping only `sessionId` advisor-scoped;
2504
- // sessions without a pinned key fall back to the advisor session id for
2505
- // stable advisor-local caching (see can1357/oh-my-pi#3639).
2506
- const advisorPromptCacheKey = this.agent.promptCacheKey ?? advisorSessionId;
2537
+ // parent turn populated. Otherwise the advisor uses its provider UUIDv7 so
2538
+ // Codex request identity remains UUID-shaped while local labels keep the
2539
+ // `-advisor` suffix.
2540
+ const advisorPromptCacheKey = this.agent.promptCacheKey ?? advisorProviderSessionId;
2507
2541
  const advisorAgent = new Agent({
2508
2542
  initialState: {
2509
2543
  systemPrompt,
@@ -2512,11 +2546,11 @@ export class AgentSession {
2512
2546
  tools: [adviseTool, ...tools],
2513
2547
  },
2514
2548
  appendOnlyContext,
2515
- sessionId: advisorSessionId,
2549
+ sessionId: advisorProviderSessionId,
2516
2550
  promptCacheKey: advisorPromptCacheKey,
2517
2551
  providerSessionState: this.#providerSessionState,
2518
2552
  preferWebsockets: this.#preferWebsockets,
2519
- getApiKey: requestModel => this.#modelRegistry.resolver(requestModel, advisorSessionId),
2553
+ getApiKey: requestModel => this.#modelRegistry.resolver(requestModel, advisorProviderSessionId),
2520
2554
  streamFn: this.#advisorStreamFn,
2521
2555
  onPayload: this.#onPayload,
2522
2556
  onResponse: this.#onResponse,
@@ -2575,11 +2609,15 @@ export class AgentSession {
2575
2609
  // suspect-mark a credential on a transient advisor error).
2576
2610
  const message = error instanceof Error ? error.message : String(error);
2577
2611
  if (!isUsageLimitOutcome(extractHttpStatusFromError(error), message)) return;
2578
- await this.#modelRegistry.authStorage.markUsageLimitReached(advisorModel.provider, advisorSessionId, {
2579
- retryAfterMs: extractRetryHint(undefined, message),
2580
- baseUrl: advisorModel.baseUrl,
2581
- modelId: advisorModel.id,
2582
- });
2612
+ await this.#modelRegistry.authStorage.markUsageLimitReached(
2613
+ advisorModel.provider,
2614
+ advisorProviderSessionId,
2615
+ {
2616
+ retryAfterMs: extractRetryHint(undefined, message),
2617
+ baseUrl: advisorModel.baseUrl,
2618
+ modelId: advisorModel.id,
2619
+ },
2620
+ );
2583
2621
  },
2584
2622
  notifyFailure: error => {
2585
2623
  const message = error instanceof Error ? error.message : String(error);
@@ -2633,13 +2671,6 @@ export class AgentSession {
2633
2671
  return this.#advisors.length > 0;
2634
2672
  }
2635
2673
 
2636
- /** Provider/session id for an advisor's loop. The slug suffix MUST match the
2637
- * advisor's transcript filename so stats/telemetry attribute the same advisor. */
2638
- #advisorSessionId(slug: string): string | undefined {
2639
- if (!this.sessionId) return undefined;
2640
- return slug ? `${this.sessionId}-advisor-${slug}` : `${this.sessionId}-advisor`;
2641
- }
2642
-
2643
2674
  /**
2644
2675
  * Route one accepted advice note from `advisor` to the primary. Concern and
2645
2676
  * blocker interrupt the running agent through the steering channel; once the
@@ -2845,11 +2876,15 @@ export class AgentSession {
2845
2876
  // No compaction candidates, fallback to re-prime
2846
2877
  return true;
2847
2878
  }
2848
- const advisorSessionId = this.#advisorSessionId(advisor.slug);
2879
+ const advisorProviderSessionId = getOrCreateAdvisorProviderSessionId(
2880
+ this.#advisorProviderSessionIds,
2881
+ this.sessionId,
2882
+ advisor.slug,
2883
+ );
2849
2884
  const preparation = prepareCompaction(
2850
2885
  pathEntries,
2851
2886
  compactionSettings,
2852
- await this.#runnableCompactionCandidates(candidates, advisorSessionId),
2887
+ await this.#runnableCompactionCandidates(candidates, advisorProviderSessionId),
2853
2888
  );
2854
2889
  if (!preparation) {
2855
2890
  // Cannot prepare compaction, fallback to re-prime
@@ -2869,17 +2904,23 @@ export class AgentSession {
2869
2904
  let lastError: unknown;
2870
2905
  // Instrument the advisor's overflow-compaction one-shot like the primary
2871
2906
  // compaction path so the advisor model's maintenance call also emits spans.
2872
- const telemetry = resolveTelemetry(agent.telemetry, advisorSessionId);
2907
+ const telemetry = resolveTelemetry(agent.telemetry, advisorProviderSessionId);
2908
+
2909
+ const codexCompaction = createCodexCompactionContext({
2910
+ trigger: "auto",
2911
+ reason: "context_limit",
2912
+ phase: "pre_turn",
2913
+ });
2873
2914
 
2874
2915
  for (const candidate of candidates) {
2875
- const apiKey = await this.#modelRegistry.getApiKey(candidate, advisorSessionId);
2916
+ const apiKey = await this.#modelRegistry.getApiKey(candidate, advisorProviderSessionId);
2876
2917
  if (!apiKey) continue;
2877
2918
 
2878
2919
  try {
2879
2920
  compactResult = await compact(
2880
2921
  preparation,
2881
2922
  candidate,
2882
- this.#modelRegistry.resolver(candidate, advisorSessionId),
2923
+ this.#modelRegistry.resolver(candidate, advisorProviderSessionId),
2883
2924
  undefined,
2884
2925
  undefined,
2885
2926
  {
@@ -2887,8 +2928,10 @@ export class AgentSession {
2887
2928
  convertToLlm: messages => this.#convertToLlmForSideRequest(messages),
2888
2929
  telemetry,
2889
2930
  tools: agent.state.tools,
2890
- sessionId: advisorSessionId,
2891
- promptCacheKey: advisorSessionId,
2931
+ sessionId: advisorProviderSessionId,
2932
+ promptCacheKey: advisorProviderSessionId,
2933
+ providerSessionState: this.#providerSessionState,
2934
+ codexCompaction,
2892
2935
  },
2893
2936
  );
2894
2937
  break;
@@ -3671,9 +3714,12 @@ export class AgentSession {
3671
3714
  this.#planModeReminderAwaitingProgress = false;
3672
3715
  }
3673
3716
  }
3674
- if (event.type === "tool_execution_end" && event.toolName === "yield" && !event.isError) {
3675
- this.#lastSuccessfulYieldToolCallId = event.toolCallId;
3676
- this.#yieldTerminationPending = true;
3717
+ if (event.type === "tool_execution_end" && this.#isTerminalYieldToolResult(event)) {
3718
+ const alreadyTerminated = this.#synchronouslyTerminatedYieldToolCallIds.delete(event.toolCallId);
3719
+ if (!alreadyTerminated) {
3720
+ this.#markTerminalYieldToolCall(event.toolCallId);
3721
+ this.agent.abort(TERMINAL_TOOL_RESULT_ABORT_REASON);
3722
+ }
3677
3723
  }
3678
3724
 
3679
3725
  // TTSR: Check for pattern matches on assistant text/thinking and tool argument deltas
@@ -4418,6 +4464,21 @@ export class AgentSession {
4418
4464
  }
4419
4465
  }
4420
4466
 
4467
+ #afterToolCall(ctx: AfterToolCallContext): AfterToolCallResult | undefined {
4468
+ if (
4469
+ this.#isTerminalYieldToolResult({
4470
+ toolName: ctx.toolCall.name,
4471
+ isError: ctx.isError,
4472
+ result: ctx.result,
4473
+ })
4474
+ ) {
4475
+ this.#markTerminalYieldToolCall(ctx.toolCall.id);
4476
+ this.#synchronouslyTerminatedYieldToolCallIds.add(ctx.toolCall.id);
4477
+ this.agent.abort(TERMINAL_TOOL_RESULT_ABORT_REASON);
4478
+ }
4479
+ return this.#ttsrAfterToolCall(ctx);
4480
+ }
4481
+
4421
4482
  /** `afterToolCall` hook: fold any per-tool TTSR reminders into the result. */
4422
4483
  #ttsrAfterToolCall(ctx: AfterToolCallContext): AfterToolCallResult | undefined {
4423
4484
  const rules = this.#perToolTtsrInjections.get(ctx.toolCall.id);
@@ -5570,6 +5631,23 @@ export class AgentSession {
5570
5631
  return this.#freshProviderSessionId ?? this.#providerSessionId ?? sessionId ?? this.sessionManager.getSessionId();
5571
5632
  }
5572
5633
 
5634
+ #adoptInheritedProviderPromptCacheKey(): void {
5635
+ const key = this.sessionManager.getHeader()?.providerPromptCacheKey;
5636
+ if (!key) return;
5637
+ if (this.#inheritedProviderPromptCacheKey !== undefined || this.agent.promptCacheKey === undefined) {
5638
+ this.agent.promptCacheKey = key;
5639
+ this.#inheritedProviderPromptCacheKey = key;
5640
+ }
5641
+ }
5642
+
5643
+ #clearInheritedProviderPromptCacheKey(): void {
5644
+ const key = this.#inheritedProviderPromptCacheKey;
5645
+ this.#inheritedProviderPromptCacheKey = undefined;
5646
+ if (key !== undefined && this.agent.promptCacheKey === key) {
5647
+ this.agent.promptCacheKey = undefined;
5648
+ }
5649
+ }
5650
+
5573
5651
  /**
5574
5652
  * Set agent.sessionId from the session manager and install a dynamic
5575
5653
  * metadata resolver so every Anthropic API request carries
@@ -6375,6 +6453,9 @@ export class AgentSession {
6375
6453
  if (this.#rebuildSystemPrompt) {
6376
6454
  const signature = this.#computeAppliedToolSignature(validToolNames, tools);
6377
6455
  if (signature !== this.#lastAppliedToolSignature) {
6456
+ if (this.#lastAppliedToolSignature !== undefined) {
6457
+ this.#clearInheritedProviderPromptCacheKey();
6458
+ }
6378
6459
  const built = await this.#rebuildSystemPrompt(validToolNames, this.#toolRegistry);
6379
6460
  this.#baseSystemPrompt = built.systemPrompt;
6380
6461
  this.#baseSystemPromptBeforeMemoryPromotion = undefined;
@@ -6461,9 +6542,16 @@ export class AgentSession {
6461
6542
  if (!this.#rebuildSystemPrompt) return;
6462
6543
  const activeToolNames = this.getActiveToolNames();
6463
6544
  this.#setActiveToolNames?.(activeToolNames);
6545
+ const previousBaseSystemPrompt = this.#baseSystemPrompt;
6464
6546
  const built = await this.#rebuildSystemPrompt(activeToolNames, this.#toolRegistry);
6465
6547
  this.#baseSystemPrompt = built.systemPrompt;
6466
6548
  this.#baseSystemPromptBeforeMemoryPromotion = undefined;
6549
+ if (
6550
+ previousBaseSystemPrompt.length !== this.#baseSystemPrompt.length ||
6551
+ previousBaseSystemPrompt.some((part, index) => part !== this.#baseSystemPrompt[index])
6552
+ ) {
6553
+ this.#clearInheritedProviderPromptCacheKey();
6554
+ }
6467
6555
  this.agent.setSystemPrompt(this.#baseSystemPrompt);
6468
6556
  this.#promptModelKey = this.#currentPromptModelKey();
6469
6557
  // Refresh the cached signature so a subsequent `#applyActiveToolsByName` with
@@ -6593,8 +6681,8 @@ export class AgentSession {
6593
6681
  *
6594
6682
  * @param mcpTools The new MCP tools to register.
6595
6683
  * @param options.activateAll When true, force-activates every newly registered MCP tool
6596
- * regardless of prior selection state. Used when an ACP client provisions MCP servers
6597
- * for a session where MCP discovery is disabled.
6684
+ * regardless of prior selection state. Used when MCP discovery is disabled and tools
6685
+ * arrive after initial session activation.
6598
6686
  */
6599
6687
  async refreshMCPTools(mcpTools: CustomTool[], options?: { activateAll?: boolean }): Promise<void> {
6600
6688
  const previousSelectedMCPToolNames = this.getSelectedMCPToolNames();
@@ -6639,10 +6727,10 @@ export class AgentSession {
6639
6727
 
6640
6728
  if (options?.activateAll) {
6641
6729
  // Force-activate every newly registered MCP tool. This path is used
6642
- // when an ACP client provisions MCP servers for a session where MCP
6643
- // discovery is disabled — without it, getSelectedMCPToolNames()
6644
- // returns only already-active tools (circular deadlock: tools can
6645
- // only become active if they're already active).
6730
+ // when MCP discovery is disabled and tools arrive after initial
6731
+ // activation — without it, getSelectedMCPToolNames() returns only
6732
+ // already-active tools (circular deadlock: tools can only become
6733
+ // active if they're already active).
6646
6734
  const newMcpNames = mcpTools.map(t => t.name);
6647
6735
  const nextActive = [...new Set([...this.#getActiveNonMCPToolNames(), ...newMcpNames])];
6648
6736
  await this.#applyActiveToolsByName(nextActive, { previousSelectedMCPToolNames });
@@ -8704,6 +8792,7 @@ export class AgentSession {
8704
8792
  this.#clearCheckpointRuntimeState();
8705
8793
  this.setTodoPhases([]);
8706
8794
  this.#freshProviderSessionId = undefined;
8795
+ this.#clearInheritedProviderPromptCacheKey();
8707
8796
  this.#syncAgentSessionId();
8708
8797
  this.#rekeyHindsightMemoryForCurrentSessionId();
8709
8798
  this.#rekeyMnemopiMemoryForCurrentSessionId();
@@ -8804,6 +8893,7 @@ export class AgentSession {
8804
8893
 
8805
8894
  // Update agent session ID
8806
8895
  this.#freshProviderSessionId = undefined;
8896
+ this.#adoptInheritedProviderPromptCacheKey();
8807
8897
  this.#syncAgentSessionId();
8808
8898
  this.#rekeyHindsightMemoryForCurrentSessionId();
8809
8899
  this.#rekeyMnemopiMemoryForCurrentSessionId();
@@ -9125,6 +9215,9 @@ export class AgentSession {
9125
9215
  this.#autoThinking = true;
9126
9216
  this.#autoResolvedLevel = undefined;
9127
9217
  this.#thinkingLevel = provisional;
9218
+ if (!wasAuto) {
9219
+ this.#clearInheritedProviderPromptCacheKey();
9220
+ }
9128
9221
  this.#applyThinkingLevelToAgent(provisional);
9129
9222
  if (persist) {
9130
9223
  this.settings.set("defaultThinkingLevel", AUTO_THINKING);
@@ -9148,6 +9241,7 @@ export class AgentSession {
9148
9241
  this.#applyThinkingLevelToAgent(effectiveLevel);
9149
9242
 
9150
9243
  if (isChanging) {
9244
+ this.#clearInheritedProviderPromptCacheKey();
9151
9245
  this.sessionManager.appendThinkingLevelChange(effectiveLevel, effectiveLevel);
9152
9246
  if (persist && effectiveLevel !== undefined && effectiveLevel !== ThinkingLevel.Off) {
9153
9247
  this.settings.set("defaultThinkingLevel", effectiveLevel);
@@ -9166,7 +9260,7 @@ export class AgentSession {
9166
9260
  }
9167
9261
 
9168
9262
  /**
9169
- * Cycle to next thinking level: off → auto → minimal..xhigh → off.
9263
+ * Cycle to next thinking level: off → auto → minimal..max → off.
9170
9264
  * @returns New selector, or undefined if model doesn't support thinking
9171
9265
  */
9172
9266
  cycleThinkingLevel(): ConfiguredThinkingLevel | undefined {
@@ -9208,8 +9302,9 @@ export class AgentSession {
9208
9302
  let resolved: Effort | undefined;
9209
9303
  if (this.#magicKeywordEnabled("ultrathink") && containsUltrathink(promptText)) {
9210
9304
  // The user explicitly asked for maximum thinking; bypass the classifier
9211
- // and jump straight to the highest auto-supported level for this model.
9212
- resolved = clampAutoThinkingEffort(model, Effort.XHigh);
9305
+ // (and its xhigh auto ceiling) and jump straight to the highest
9306
+ // supported level for this model.
9307
+ resolved = clampAutoThinkingEffort(model, Effort.Max);
9213
9308
  } else {
9214
9309
  const controller = new AbortController();
9215
9310
  const timer = setTimeout(() => controller.abort(), AgentSession.#AUTO_THINKING_TIMEOUT_MS);
@@ -9751,6 +9846,7 @@ export class AgentSession {
9751
9846
  let firstKeptEntryId: string;
9752
9847
  let tokensBefore: number;
9753
9848
  let details: unknown;
9849
+ let codexCompaction: CodexCompactionContext | undefined;
9754
9850
 
9755
9851
  // Snapcompact runs locally first. The frame cap is sized from the live
9756
9852
  // model window via #computeSnapcompactMaxFrames so the post-render context
@@ -9830,6 +9926,11 @@ export class AgentSession {
9830
9926
  details = snapcompactResult.details;
9831
9927
  preserveData = { ...(compactionPrep.preserveData ?? {}), ...(snapcompactResult.preserveData ?? {}) };
9832
9928
  } else {
9929
+ codexCompaction = createCodexCompactionContext({
9930
+ trigger: "manual",
9931
+ reason: "user_requested",
9932
+ phase: "standalone_turn",
9933
+ });
9833
9934
  // Generate compaction result. Only convert known abort-shaped
9834
9935
  // rejections (AbortError raised while the abort signal is set,
9835
9936
  // or an already-typed sentinel) into `CompactionCancelledError`
@@ -9851,6 +9952,7 @@ export class AgentSession {
9851
9952
  extraContext: compactionPrep.hookContext,
9852
9953
  remoteInstructions: this.#baseSystemPrompt.join("\n\n"),
9853
9954
  convertToLlm: messages => this.#convertToLlmForSideRequest(messages),
9955
+ codexCompaction,
9854
9956
  },
9855
9957
  compactionCandidates,
9856
9958
  );
@@ -9893,7 +9995,11 @@ export class AgentSession {
9893
9995
  this.#planReferenceSent = false;
9894
9996
  this.#resetAllAdvisorRuntimes();
9895
9997
  this.#syncTodoPhasesFromBranch();
9896
- this.#closeCodexProviderSessionsForHistoryRewrite();
9998
+ if (codexCompaction) {
9999
+ this.#resetCodexProviderAfterCompaction(codexCompaction);
10000
+ } else {
10001
+ this.#closeCodexProviderSessionsForHistoryRewrite();
10002
+ }
9897
10003
 
9898
10004
  // Get the saved compaction entry for the hook
9899
10005
  const savedCompactionEntry = newEntries.find(e => e.type === "compaction" && e.summary === summary) as
@@ -10259,6 +10365,7 @@ export class AgentSession {
10259
10365
  await this.#runAutoCompaction("threshold", false, false, false, {
10260
10366
  autoContinue: false,
10261
10367
  triggerContextTokens: contextTokens,
10368
+ phase: "pre_turn",
10262
10369
  });
10263
10370
  }
10264
10371
 
@@ -10333,6 +10440,7 @@ export class AgentSession {
10333
10440
  suppressContinuation: true,
10334
10441
  suppressHandoff: true,
10335
10442
  triggerContextTokens: contextTokens,
10443
+ phase: "mid_turn",
10336
10444
  });
10337
10445
 
10338
10446
  if (signal?.aborted) return;
@@ -10579,6 +10687,7 @@ export class AgentSession {
10579
10687
  return await this.#runAutoCompaction("threshold", false, false, allowDefer, {
10580
10688
  autoContinue,
10581
10689
  triggerContextTokens: postMaintenanceContextTokens,
10690
+ phase: "pre_turn",
10582
10691
  });
10583
10692
  }
10584
10693
  logger.debug("Auto-compaction threshold satisfied but context promotion took over", {
@@ -10589,6 +10698,24 @@ export class AgentSession {
10589
10698
  }
10590
10699
  return COMPACTION_CHECK_NONE;
10591
10700
  }
10701
+ #isTerminalYieldToolResult(event: { toolName: string; isError?: boolean; result?: { details?: unknown } }): boolean {
10702
+ if (event.toolName !== "yield" || event.isError) return false;
10703
+ const details = event.result?.details;
10704
+ if (!details || typeof details !== "object") return true;
10705
+ const record = details as Record<string, unknown>;
10706
+ return !(
10707
+ record.status === "success" &&
10708
+ Array.isArray(record.type) &&
10709
+ record.type.length > 0 &&
10710
+ record.type.every(item => typeof item === "string")
10711
+ );
10712
+ }
10713
+
10714
+ #markTerminalYieldToolCall(toolCallId: string): void {
10715
+ this.#lastSuccessfulYieldToolCallId = toolCallId;
10716
+ this.#yieldTerminationPending = true;
10717
+ }
10718
+
10592
10719
  #assistantMessageHasSuccessfulYieldToolCall(assistantMessage: AssistantMessage, toolCallId: string): boolean {
10593
10720
  const lastToolCall = assistantMessage.content
10594
10721
  .slice()
@@ -10939,7 +11066,11 @@ export class AgentSession {
10939
11066
  ): Promise<CompactionCheckResult> {
10940
11067
  const compactionEntryBefore = getLatestCompactionEntry(this.sessionManager.getBranch());
10941
11068
  await this.#dropPersistedAssistantTurn(assistantMessage);
10942
- const result = await this.#runAutoCompaction(reason, true, false, allowDefer, options);
11069
+ const result = await this.#runAutoCompaction(reason, true, false, allowDefer, {
11070
+ autoContinue: options.autoContinue,
11071
+ triggerContextTokens: options.triggerContextTokens,
11072
+ phase: "mid_turn",
11073
+ });
10943
11074
  const compactionEntryAfter = getLatestCompactionEntry(this.sessionManager.getBranch());
10944
11075
  if (result.historyRewritten !== true && compactionEntryAfter === compactionEntryBefore) {
10945
11076
  this.#restoreFailedAssistantTurn(assistantMessage);
@@ -11538,6 +11669,9 @@ export class AgentSession {
11538
11669
  const currentModel = this.model;
11539
11670
  if (currentModel) {
11540
11671
  this.#closeProviderSessionsForModelSwitch(currentModel, model);
11672
+ if (!modelsAreEqual(currentModel, model)) {
11673
+ this.#clearInheritedProviderPromptCacheKey();
11674
+ }
11541
11675
  }
11542
11676
  this.agent.setModel(model);
11543
11677
 
@@ -11551,6 +11685,14 @@ export class AgentSession {
11551
11685
  this.#closeProviderSessionsForModelSwitch(currentModel, currentModel);
11552
11686
  }
11553
11687
 
11688
+ #resetCodexProviderAfterCompaction(compaction: CodexCompactionContext): void {
11689
+ resetOpenAICodexHistoryAfterCompaction({
11690
+ providerSessionState: this.#providerSessionState,
11691
+ sessionId: this.sessionId,
11692
+ compaction,
11693
+ });
11694
+ }
11695
+
11554
11696
  #resetCurrentResponsesProviderSession(reason: string): void {
11555
11697
  const currentModel = this.model;
11556
11698
  if (currentModel?.api !== "openai-responses" && currentModel?.api !== "openai-codex-responses") {
@@ -11826,7 +11968,7 @@ export class AgentSession {
11826
11968
  if (!trimmedTarget) return undefined;
11827
11969
 
11828
11970
  const parsed = parseModelString(trimmedTarget, {
11829
- allowMaxAlias: true,
11971
+ allowMaxSuffix: true,
11830
11972
  allowAutoAlias: true,
11831
11973
  isLiteralModelId: (provider, id) =>
11832
11974
  availableModels.some(model => model.provider === provider && model.id === id),
@@ -11921,18 +12063,6 @@ export class AgentSession {
11921
12063
 
11922
12064
  return candidates;
11923
12065
  }
11924
- #isCompactionAuthFailure(error: unknown): boolean {
11925
- if (!(error instanceof Error)) return false;
11926
- // Real provider 401/403 — surfaced as `.status` by the compaction layer
11927
- // (see `createSummarizationError` in packages/agent/src/compaction/compaction.ts).
11928
- // Without this branch, an expired/revoked Anthropic key would bypass the
11929
- // authenticated-fallback path and dump the raw HTTP body into the UI.
11930
- const status = (error as Error & { status?: number }).status;
11931
- if (status === 401 || status === 403) return true;
11932
- // pi-native gateway synthetic for "no credential configured" (issue #986).
11933
- // Carries no HTTP status, so the legacy message regex stays.
11934
- return /auth_unavailable|no auth available/i.test(error.message);
11935
- }
11936
12066
 
11937
12067
  #buildCompactionAuthError(): Error {
11938
12068
  const currentModel = this.model;
@@ -11982,6 +12112,7 @@ export class AgentSession {
11982
12112
  tools: this.agent.state.tools,
11983
12113
  sessionId: this.sessionId,
11984
12114
  promptCacheKey: this.sessionId,
12115
+ providerSessionState: this.#providerSessionState,
11985
12116
  // Route every summarization HTTP request through the
11986
12117
  // session's side-stream transport so the provider
11987
12118
  // concurrency cap (e.g. providers.ollama-cloud.maxConcurrency)
@@ -11997,7 +12128,7 @@ export class AgentSession {
11997
12128
  },
11998
12129
  );
11999
12130
  } catch (error) {
12000
- if (!this.#isCompactionAuthFailure(error)) {
12131
+ if (!AIError.is(AIError.classify(error, candidate.api), AIError.Flag.AuthFailed)) {
12001
12132
  throw error;
12002
12133
  }
12003
12134
  }
@@ -12320,6 +12451,7 @@ export class AgentSession {
12320
12451
  triggerContextTokens?: number;
12321
12452
  suppressContinuation?: boolean;
12322
12453
  suppressHandoff?: boolean;
12454
+ phase?: CodexCompactionContext["phase"];
12323
12455
  } = {},
12324
12456
  ): Promise<CompactionCheckResult> {
12325
12457
  const compactionSettings = this.settings.getGroup("compaction");
@@ -12362,7 +12494,7 @@ export class AgentSession {
12362
12494
  async signal => {
12363
12495
  await Promise.resolve();
12364
12496
  if (signal.aborted) return;
12365
- await this.#runAutoCompaction(reason, willRetry, true);
12497
+ await this.#runAutoCompaction(reason, willRetry, true, true, { phase: options.phase });
12366
12498
  },
12367
12499
  { generation },
12368
12500
  );
@@ -12509,6 +12641,7 @@ export class AgentSession {
12509
12641
  let hookCompaction: CompactionResult | undefined;
12510
12642
  let fromExtension = false;
12511
12643
  let preserveData: Record<string, unknown> | undefined;
12644
+ let codexCompaction: CodexCompactionContext | undefined;
12512
12645
 
12513
12646
  if (this.#extensionRunner?.hasHandlers("session_before_compact")) {
12514
12647
  const hookResult = (await this.#extensionRunner.emit({
@@ -12647,6 +12780,13 @@ export class AgentSession {
12647
12780
  const telemetry = resolveTelemetry(this.agent.telemetry, this.sessionId);
12648
12781
  let compactResult: CompactionResult | undefined;
12649
12782
  let lastError: unknown;
12783
+ codexCompaction = createCodexCompactionContext({
12784
+ trigger: "auto",
12785
+ reason: "context_limit",
12786
+ phase:
12787
+ options.phase ??
12788
+ (reason === "threshold" ? "pre_turn" : reason === "idle" ? "standalone_turn" : "mid_turn"),
12789
+ });
12650
12790
 
12651
12791
  for (let candidateIndex = 0; candidateIndex < candidates.length; candidateIndex++) {
12652
12792
  const candidate = candidates[candidateIndex];
@@ -12679,6 +12819,8 @@ export class AgentSession {
12679
12819
  tools: this.agent.state.tools,
12680
12820
  sessionId: this.sessionId,
12681
12821
  promptCacheKey: this.sessionId,
12822
+ providerSessionState: this.#providerSessionState,
12823
+ codexCompaction,
12682
12824
  },
12683
12825
  );
12684
12826
  break;
@@ -12689,7 +12831,7 @@ export class AgentSession {
12689
12831
 
12690
12832
  const message = error instanceof Error ? error.message : String(error);
12691
12833
  const id = AIError.classify(error, candidate.api);
12692
- if (this.#isCompactionAuthFailure(error)) {
12834
+ if (AIError.is(id, AIError.Flag.AuthFailed)) {
12693
12835
  lastError = this.#buildCompactionAuthError();
12694
12836
  break;
12695
12837
  }
@@ -12797,7 +12939,11 @@ export class AgentSession {
12797
12939
  this.#planReferenceSent = false;
12798
12940
  this.#resetAllAdvisorRuntimes();
12799
12941
  this.#syncTodoPhasesFromBranch();
12800
- this.#closeCodexProviderSessionsForHistoryRewrite();
12942
+ if (codexCompaction) {
12943
+ this.#resetCodexProviderAfterCompaction(codexCompaction);
12944
+ } else {
12945
+ this.#closeCodexProviderSessionsForHistoryRewrite();
12946
+ }
12801
12947
 
12802
12948
  // Get the saved compaction entry for the hook
12803
12949
  const savedCompactionEntry = newEntries.find(e => e.type === "compaction" && e.summary === summary) as
@@ -14679,6 +14825,7 @@ export class AgentSession {
14679
14825
  const previousSystemPrompt = this.agent.state.systemPrompt;
14680
14826
  const previousBaseSystemPromptBeforeMemoryPromotion = this.#baseSystemPromptBeforeMemoryPromotion;
14681
14827
  const previousFreshProviderSessionId = this.#freshProviderSessionId;
14828
+ const previousInheritedProviderPromptCacheKey = this.#inheritedProviderPromptCacheKey;
14682
14829
  const previousFallbackSelectedMCPToolNames = previousSessionFile
14683
14830
  ? this.#getSessionDefaultSelectedMCPToolNames(previousSessionFile)
14684
14831
  : undefined;
@@ -14700,6 +14847,8 @@ export class AgentSession {
14700
14847
  await this.sessionManager.setSessionFile(sessionPath);
14701
14848
  if (switchingToDifferentSession) {
14702
14849
  this.#freshProviderSessionId = undefined;
14850
+ this.#clearInheritedProviderPromptCacheKey();
14851
+ this.#adoptInheritedProviderPromptCacheKey();
14703
14852
  }
14704
14853
  this.#syncAgentSessionId();
14705
14854
  this.#rekeyHindsightMemoryForCurrentSessionId();
@@ -14853,6 +15002,7 @@ export class AgentSession {
14853
15002
  this.agent.replaceQueues(previousSteeringMessages, previousFollowUpMessages);
14854
15003
  this.#pendingNextTurnMessages = previousPendingNextTurnMessages;
14855
15004
  this.#scheduledHiddenNextTurnGeneration = previousScheduledHiddenNextTurnGeneration;
15005
+ this.#inheritedProviderPromptCacheKey = previousInheritedProviderPromptCacheKey;
14856
15006
  this.#checkpointState = previousCheckpointState;
14857
15007
  this.#pendingRewindReport = previousPendingRewindReport;
14858
15008
  this.#lastCompletedRewind = previousLastCompletedRewind;
@@ -14928,6 +15078,7 @@ export class AgentSession {
14928
15078
  this.#rehydrateCheckpointRewindState();
14929
15079
  this.#syncTodoPhasesFromBranch();
14930
15080
  this.#freshProviderSessionId = undefined;
15081
+ this.#clearInheritedProviderPromptCacheKey();
14931
15082
  this.#syncAgentSessionId();
14932
15083
  this.#rekeyHindsightMemoryForCurrentSessionId();
14933
15084
  this.#rekeyMnemopiMemoryForCurrentSessionId();