@gajae-code/coding-agent 0.11.10 → 0.11.11

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 (111) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/dist/types/cli.d.ts +8 -1
  3. package/dist/types/config/model-profile-activation.d.ts +1 -0
  4. package/dist/types/config/model-profile-contract.d.ts +2 -0
  5. package/dist/types/config/settings-schema.d.ts +57 -2
  6. package/dist/types/config/settings.d.ts +1 -0
  7. package/dist/types/extensibility/extensions/types.d.ts +21 -0
  8. package/dist/types/gjc-runtime/linux-proc.d.ts +22 -14
  9. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +60 -0
  10. package/dist/types/gjc-runtime/state-runtime.d.ts +3 -1
  11. package/dist/types/modes/acp/acp-agent.d.ts +14 -9
  12. package/dist/types/modes/components/queue-pane.d.ts +8 -0
  13. package/dist/types/modes/components/queued-message-selector.d.ts +6 -0
  14. package/dist/types/modes/components/welcome.d.ts +1 -0
  15. package/dist/types/modes/interactive-mode.d.ts +5 -1
  16. package/dist/types/runtime/memory-domain.d.ts +9 -0
  17. package/dist/types/runtime/memory-guard-contract.d.ts +71 -0
  18. package/dist/types/runtime/memory-guard.d.ts +39 -0
  19. package/dist/types/runtime/memory-limit.d.ts +11 -0
  20. package/dist/types/runtime-mcp/manager.d.ts +8 -0
  21. package/dist/types/sdk/acp/final-text.d.ts +19 -0
  22. package/dist/types/sdk/acp/mcp.d.ts +23 -0
  23. package/dist/types/sdk/broker/lifecycle.d.ts +2 -0
  24. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
  25. package/dist/types/sdk/bus/index.d.ts +3 -1
  26. package/dist/types/sdk/bus/rate-limit-pool.d.ts +2 -0
  27. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -4
  28. package/dist/types/sdk/bus/telegram-daemon.d.ts +23 -3
  29. package/dist/types/sdk/host/control/operations.d.ts +1 -0
  30. package/dist/types/sdk/lifecycle-session.d.ts +10 -1
  31. package/dist/types/sdk/startup-capability.d.ts +6 -0
  32. package/dist/types/session/agent-session.d.ts +9 -11
  33. package/dist/types/session/fallback-chain-controller.d.ts +18 -2
  34. package/dist/types/session/internal/managed-session-scope.d.ts +27 -0
  35. package/dist/types/session/session-manager.d.ts +34 -0
  36. package/dist/types/task/executor.d.ts +6 -0
  37. package/dist/types/task/types.d.ts +36 -0
  38. package/dist/types/task/ultragoal-redteam-activation.d.ts +33 -0
  39. package/dist/types/tools/resource-gc.d.ts +19 -0
  40. package/package.json +8 -8
  41. package/scripts/generate-sdk-operation-inventory.ts +2 -0
  42. package/scripts/run-test-manifest.ts +6 -4
  43. package/src/cli/args.ts +14 -8
  44. package/src/cli/fast-help.ts +1 -1
  45. package/src/cli.ts +48 -2
  46. package/src/commands/sdk.ts +91 -7
  47. package/src/config/model-profile-activation.ts +1 -0
  48. package/src/config/model-profile-contract.ts +13 -0
  49. package/src/config/model-profiles.ts +24 -0
  50. package/src/config/settings-schema.ts +61 -2
  51. package/src/config/settings.ts +8 -0
  52. package/src/defaults/gjc/skills/ralplan/SKILL.md +21 -2
  53. package/src/defaults/gjc/skills/ultragoal/SKILL.md +1 -1
  54. package/src/extensibility/extensions/runner.ts +4 -0
  55. package/src/extensibility/extensions/types.ts +14 -0
  56. package/src/gjc-runtime/deep-interview-ambiguity.ts +16 -2
  57. package/src/gjc-runtime/deep-interview-state.ts +25 -3
  58. package/src/gjc-runtime/linux-proc.ts +70 -36
  59. package/src/gjc-runtime/ralplan-runtime.ts +252 -1
  60. package/src/gjc-runtime/state-runtime.ts +48 -11
  61. package/src/internal-urls/docs-index.generated.ts +4 -4
  62. package/src/internal-urls/local-protocol.ts +23 -4
  63. package/src/modes/acp/acp-agent.ts +554 -60
  64. package/src/modes/acp/acp-event-mapper.ts +93 -6
  65. package/src/modes/components/model-selector.ts +15 -1
  66. package/src/modes/components/notifications-settings-editor.ts +1 -1
  67. package/src/modes/components/queue-pane.ts +55 -9
  68. package/src/modes/components/queued-message-selector.ts +53 -10
  69. package/src/modes/components/tool-status-header.ts +1 -4
  70. package/src/modes/components/welcome.ts +18 -3
  71. package/src/modes/controllers/command-controller.ts +4 -0
  72. package/src/modes/controllers/extension-ui-controller.ts +4 -10
  73. package/src/modes/controllers/input-controller.ts +44 -3
  74. package/src/modes/interactive-mode.ts +65 -26
  75. package/src/modes/runtime-init.ts +1 -0
  76. package/src/prompts/agents/executor.md +1 -1
  77. package/src/runtime/memory-domain.ts +66 -0
  78. package/src/runtime/memory-guard-contract.ts +73 -0
  79. package/src/runtime/memory-guard.ts +292 -0
  80. package/src/runtime/memory-limit.ts +50 -0
  81. package/src/runtime-mcp/manager.ts +26 -6
  82. package/src/sdk/acp/adapter.ts +8 -1
  83. package/src/sdk/acp/final-text.ts +40 -0
  84. package/src/sdk/acp/mcp.ts +26 -0
  85. package/src/sdk/broker/lifecycle.ts +82 -1
  86. package/src/sdk/bus/chat-daemon-cli.ts +2 -2
  87. package/src/sdk/bus/chat-daemon-control.ts +4 -3
  88. package/src/sdk/bus/config.ts +1 -1
  89. package/src/sdk/bus/index.ts +162 -22
  90. package/src/sdk/bus/rate-limit-pool.ts +8 -0
  91. package/src/sdk/bus/telegram-daemon-contract.ts +6 -4
  92. package/src/sdk/bus/telegram-daemon.ts +531 -53
  93. package/src/sdk/host/control/dispatch.ts +2 -0
  94. package/src/sdk/host/control/operations.ts +1 -0
  95. package/src/sdk/host/host.ts +2 -2
  96. package/src/sdk/lifecycle-session.ts +19 -3
  97. package/src/sdk/protocol/operation-inventory.generated.json +47 -0
  98. package/src/sdk/protocol/operation-registry.ts +1 -0
  99. package/src/sdk/session.ts +12 -1
  100. package/src/sdk/startup-capability.ts +6 -0
  101. package/src/session/agent-session.ts +256 -142
  102. package/src/session/fallback-chain-controller.ts +47 -8
  103. package/src/session/internal/managed-session-scope.ts +40 -10
  104. package/src/session/internal/native-publish-outcome.ts +8 -3
  105. package/src/session/session-manager.ts +608 -25
  106. package/src/task/executor.ts +11 -3
  107. package/src/task/index.ts +2 -0
  108. package/src/task/types.ts +22 -11
  109. package/src/task/ultragoal-redteam-activation.ts +75 -0
  110. package/src/tools/resource-gc.ts +496 -120
  111. package/src/workflow/workflow-intent-diff.ts +4 -1
@@ -172,6 +172,7 @@ import { type AsyncJob, type AsyncJobDeliveryState, AsyncJobManager } from "../a
172
172
  import { reset as resetCapabilities } from "../capability";
173
173
  import type { Rule } from "../capability/rule";
174
174
  import type { CasReceipt } from "../config/atomic-yaml-patch";
175
+ import { activateModelProfile } from "../config/model-profile-activation";
175
176
  import {
176
177
  GJC_MODEL_ASSIGNMENT_TARGETS,
177
178
  isAuthenticated,
@@ -233,7 +234,7 @@ import type {
233
234
  TurnEndEvent,
234
235
  TurnStartEvent,
235
236
  } from "../extensibility/extensions";
236
- import type { CompactOptions, ContextUsage } from "../extensibility/extensions/types";
237
+ import type { CompactOptions, ContextUsage, ExtensionTranscriptEntry } from "../extensibility/extensions/types";
237
238
  import { ExtensionToolWrapper } from "../extensibility/extensions/wrapper";
238
239
  import {
239
240
  type LoadedSubskillActivation,
@@ -243,7 +244,13 @@ import { resolveCurrentPhaseForParent } from "../extensibility/gjc-plugins/injec
243
244
  import { readActiveSubskillsForParent, toActiveSubskillEntry } from "../extensibility/gjc-plugins/state";
244
245
  import { loadActiveSubskillTools } from "../extensibility/gjc-plugins/tools";
245
246
  import type { HookCommandContext } from "../extensibility/hooks/types";
246
- import { buildSkillPromptMessage, type Skill, type SkillWarning } from "../extensibility/skills";
247
+ import {
248
+ buildSkillPromptMessage,
249
+ getSkillSlashCommandName,
250
+ parseSkillInvocations,
251
+ type Skill,
252
+ type SkillWarning,
253
+ } from "../extensibility/skills";
247
254
  import { expandSlashCommand, type FileSlashCommand } from "../extensibility/slash-commands";
248
255
  import { assertDeepInterviewIntentManifest } from "../gjc-runtime/deep-interview-state";
249
256
  import { buildGjcRuntimeSessionEnv, consumePendingGoalModeRequest } from "../gjc-runtime/goal-mode-request";
@@ -341,6 +348,7 @@ import {
341
348
  import {
342
349
  type ConfiguredFallbackChain,
343
350
  cappedExponentialWithFullJitter,
351
+ compactionRetryDelay,
344
352
  effectiveFallbackDelay,
345
353
  FallbackChainController,
346
354
  } from "./fallback-chain-controller";
@@ -379,6 +387,7 @@ import type {
379
387
  CompactionEntry,
380
388
  DefaultModelSelectionStage,
381
389
  NewSessionOptions,
390
+ PreparedNewSession,
382
391
  RecoveryHydrationContext,
383
392
  RecoveryHydrationPromotionFence,
384
393
  SessionContext,
@@ -798,8 +807,14 @@ const KIMI_CODE_FIRST_EVENT_TIMEOUT_MESSAGES = {
798
807
 
799
808
  const ALIBABA_TOKEN_PLAN_PROVIDER = "alibaba-token-plan";
800
809
  const ALIBABA_TOKEN_PLAN_FIRST_EVENT_TIMEOUT_MESSAGES = {
801
- "openai-responses": "OpenAI responses stream timed out while waiting for the first event",
802
- "openai-completions": "OpenAI completions stream timed out while waiting for the first event",
810
+ "openai-responses": new Set([
811
+ "Provider stream timed out while waiting for the first event",
812
+ "OpenAI responses stream timed out while waiting for the first event",
813
+ ]),
814
+ "openai-completions": new Set([
815
+ "Provider stream timed out while waiting for the first event",
816
+ "OpenAI completions stream timed out while waiting for the first event",
817
+ ]),
803
818
  } as const;
804
819
 
805
820
  function hasBareDefaultRetryDisqualifyingFacts(message: AssistantMessage): boolean {
@@ -1043,6 +1058,22 @@ function createHandoffContext(document: string): string {
1043
1058
  return `<handoff-context>\n${document}\n</handoff-context>\n\nThe above is a handoff document from a previous session. Use this context to continue the work seamlessly.`;
1044
1059
  }
1045
1060
 
1061
+ async function discardPreparedNewSessionAfterFailure(
1062
+ sessionManager: SessionManager,
1063
+ prepared: PreparedNewSession,
1064
+ transitionError: unknown,
1065
+ ): Promise<unknown> {
1066
+ try {
1067
+ await sessionManager.discardPreparedNewSession(prepared);
1068
+ } catch (cleanupError) {
1069
+ throw new AggregateError(
1070
+ [transitionError, cleanupError],
1071
+ "Session transition and staged successor cleanup both failed.",
1072
+ );
1073
+ }
1074
+ return transitionError;
1075
+ }
1076
+
1046
1077
  // ============================================================================
1047
1078
  // ACP Permission Gate
1048
1079
  // ============================================================================
@@ -4008,8 +4039,21 @@ export class AgentSession {
4008
4039
  return;
4009
4040
  }
4010
4041
  const predecessorAgentEnd = this.#claimDeferredAgentEndForContinuation(predecessorAgentEndHold);
4042
+ const startsQueuedSuccessor =
4043
+ this.agent.state.messages.at(-1)?.role === "assistant" && this.agent.hasQueuedMessages();
4011
4044
  try {
4012
- await this.agent.continue(this.#managedFallbackPromptOptions());
4045
+ await this.agent.continue({
4046
+ ...this.#managedFallbackPromptOptions(),
4047
+ // Reset only after continue() has claimed the queued turn. Skipped or stale
4048
+ // continuations retain predecessor accounting, and resetAttemptBudget keeps
4049
+ // the sticky fallback cursor unchanged.
4050
+ onRunAccepted: startsQueuedSuccessor
4051
+ ? () => {
4052
+ this.#defaultFallbackChain().resetAttemptBudget();
4053
+ this.#overflowMaintenanceAttempts = 0;
4054
+ }
4055
+ : undefined,
4056
+ });
4013
4057
  } catch (error) {
4014
4058
  this.#restoreDeferredAgentEndAfterContinuationFailure(predecessorAgentEnd);
4015
4059
  throw error;
@@ -4612,13 +4656,13 @@ export class AgentSession {
4612
4656
  return resolveToCwd(normalized, this.sessionManager.getCwd());
4613
4657
  }
4614
4658
 
4615
- #localProtocolOptions(): LocalProtocolOptions {
4659
+ #localProtocolOptions(prepared?: PreparedNewSession): LocalProtocolOptions {
4616
4660
  return {
4617
- getArtifactsDir: () => this.sessionManager.getArtifactsDir(),
4661
+ getArtifactsDir: () => prepared?.artifactsDir ?? this.sessionManager.getArtifactsDir(),
4618
4662
  isManagedDestination: () => this.sessionManager.isManagedDestination(),
4619
- // Required for verified legacy local:// migration on managed sessions (#2797 / #2925).
4620
- getManagedLegacyLocalMigrationSource: () => this.sessionManager.getManagedLegacyLocalMigrationSource(),
4621
- getSessionId: () => this.sessionManager.getSessionId(),
4663
+ getManagedLegacyLocalMigrationSource: () =>
4664
+ prepared?.managedLegacyLocalMigrationSource ?? this.sessionManager.getManagedLegacyLocalMigrationSource(),
4665
+ getSessionId: () => prepared?.sessionId ?? this.sessionManager.getSessionId(),
4622
4666
  };
4623
4667
  }
4624
4668
 
@@ -6479,6 +6523,15 @@ export class AgentSession {
6479
6523
  return { ...context, messages: this.#withoutEphemeralCustomMessages(context.messages) };
6480
6524
  }
6481
6525
 
6526
+ /** Build display context from an unpublished successor without changing active session state. @internal */
6527
+ buildPreparedDisplaySessionContext(prepared: PreparedNewSession): SessionContext {
6528
+ const context = deobfuscateSessionContext(
6529
+ this.sessionManager.buildPreparedNewSessionContext(prepared),
6530
+ this.#obfuscator,
6531
+ );
6532
+ return { ...context, messages: this.#withoutEphemeralCustomMessages(context.messages) };
6533
+ }
6534
+
6482
6535
  /** Convert session messages using the same pre-LLM pipeline as the active session. */
6483
6536
  async convertMessagesToLlm(messages: AgentMessage[], signal?: AbortSignal): Promise<Message[]> {
6484
6537
  const transformedMessages = await this.#transformContext(messages, signal);
@@ -6586,9 +6639,11 @@ export class AgentSession {
6586
6639
  /** Live SDK configuration values exposed through the session query surface. */
6587
6640
  getSdkConfigItems(): Record<string, string> {
6588
6641
  const model = this.model;
6642
+ const modelPreset = this.getActiveModelProfile() ?? this.settings.get("modelProfile.default");
6589
6643
  return {
6590
6644
  mode: this.#planModeState?.enabled ? "plan" : "default",
6591
6645
  ...(model ? { model: `${model.provider}/${model.id}` } : {}),
6646
+ ...(modelPreset ? { modelPreset } : {}),
6592
6647
  thinking: this.#thinkingLevel ?? "off",
6593
6648
  steeringMode: this.steeringMode,
6594
6649
  followUpMode: this.followUpMode,
@@ -6607,6 +6662,7 @@ export class AgentSession {
6607
6662
  async invokeSkill(
6608
6663
  name: string,
6609
6664
  args = "",
6665
+ options?: Pick<PromptOptions, "onPreflightAccepted">,
6610
6666
  ): Promise<{ name: string; path: string; args?: string; lineCount?: number }> {
6611
6667
  const skillName = name.trim();
6612
6668
  if (!skillName) throw Object.assign(new Error("skill.invoke requires a skill name."), { code: "invalid_input" });
@@ -6639,7 +6695,7 @@ export class AgentSession {
6639
6695
  attribution: "user" as const,
6640
6696
  };
6641
6697
  this.#deepInterviewPreclaimedCustomInputEpochs.set(skillPromptMessage, deepInterviewUserIntentEpoch);
6642
- await this.promptCustomMessage(skillPromptMessage);
6698
+ await this.promptCustomMessage(skillPromptMessage, options);
6643
6699
  return {
6644
6700
  name: skill.name,
6645
6701
  path: skill.filePath,
@@ -6685,22 +6741,49 @@ export class AgentSession {
6685
6741
  }
6686
6742
  }
6687
6743
 
6688
- getTranscript(): Array<{ id: string; role: string; textSummary: string; ts: string; body: string }> {
6744
+ getTranscript(): ExtensionTranscriptEntry[] {
6689
6745
  return getEntriesForInternalRead(this.sessionManager).flatMap(entry => {
6690
6746
  if (entry.type !== "message") return [];
6691
- const message = entry.message as unknown as { role?: unknown; content?: unknown };
6747
+ const message = entry.message as unknown as {
6748
+ role?: unknown;
6749
+ content?: unknown;
6750
+ toolCallId?: unknown;
6751
+ toolName?: unknown;
6752
+ isError?: unknown;
6753
+ };
6754
+ const content: NonNullable<ExtensionTranscriptEntry["content"]> | undefined = Array.isArray(message.content)
6755
+ ? message.content.reduce<NonNullable<ExtensionTranscriptEntry["content"]>>((blocks, part) => {
6756
+ if (typeof part !== "object" || part === null || !("type" in part)) return blocks;
6757
+ if (part.type === "text" && "text" in part && typeof part.text === "string") {
6758
+ blocks.push({ type: "text", text: part.text });
6759
+ return blocks;
6760
+ }
6761
+ if (part.type === "thinking" && "thinking" in part && typeof part.thinking === "string") {
6762
+ blocks.push({ type: "thinking", thinking: part.thinking });
6763
+ return blocks;
6764
+ }
6765
+ if (
6766
+ part.type === "toolCall" &&
6767
+ "id" in part &&
6768
+ typeof part.id === "string" &&
6769
+ "name" in part &&
6770
+ typeof part.name === "string"
6771
+ ) {
6772
+ blocks.push({
6773
+ type: "toolCall",
6774
+ id: part.id,
6775
+ name: part.name,
6776
+ arguments: "arguments" in part ? part.arguments : {},
6777
+ });
6778
+ }
6779
+ return blocks;
6780
+ }, [])
6781
+ : undefined;
6692
6782
  const body =
6693
6783
  typeof message.content === "string"
6694
6784
  ? message.content
6695
- : Array.isArray(message.content)
6696
- ? message.content
6697
- .map(part =>
6698
- typeof part === "object" && part !== null && "text" in part && typeof part.text === "string"
6699
- ? part.text
6700
- : "",
6701
- )
6702
- .filter(Boolean)
6703
- .join("\n")
6785
+ : content
6786
+ ? content.flatMap(part => (part.type === "text" ? [part.text] : [])).join("\n")
6704
6787
  : "";
6705
6788
  return [
6706
6789
  {
@@ -6709,6 +6792,10 @@ export class AgentSession {
6709
6792
  textSummary: body.slice(0, 500),
6710
6793
  ts: entry.timestamp,
6711
6794
  body,
6795
+ ...(content ? { content } : {}),
6796
+ ...(typeof message.toolCallId === "string" ? { toolCallId: message.toolCallId } : {}),
6797
+ ...(typeof message.toolName === "string" ? { toolName: message.toolName } : {}),
6798
+ ...(typeof message.isError === "boolean" ? { isError: message.isError } : {}),
6712
6799
  },
6713
6800
  ];
6714
6801
  });
@@ -6746,8 +6833,7 @@ export class AgentSession {
6746
6833
  return getAskAnswerSourceFromRegistry(this.sessionId);
6747
6834
  }
6748
6835
 
6749
- #constructWorkflowGateEmitter(): WorkflowGateEmitter {
6750
- const sessionId = this.sessionManager.getSessionId();
6836
+ #constructWorkflowGateEmitter(sessionId = this.sessionManager.getSessionId()): WorkflowGateEmitter {
6751
6837
  assertNonEmptyGjcSessionId(sessionId, "AgentSession workflow-gate session");
6752
6838
  const gateStore = this.sessionManager.isPersisted()
6753
6839
  ? new FileGateStore(path.join(sessionStateDir(this.sessionManager.getCwd(), sessionId), "workflow-gates.json"))
@@ -7266,6 +7352,22 @@ export class AgentSession {
7266
7352
  async prompt(text: string, options?: PromptOptions): Promise<void> {
7267
7353
  const expandPromptTemplates = options?.expandPromptTemplates ?? true;
7268
7354
 
7355
+ if (expandPromptTemplates && text.startsWith("/skill:") && !options?.images?.length) {
7356
+ const skillCommands = new Map(this.skills.map(skill => [getSkillSlashCommandName(skill), skill]));
7357
+ const invocations = parseSkillInvocations(text, skillCommands);
7358
+ if (invocations.length === 1) {
7359
+ const invocation = invocations[0];
7360
+ if (invocation) {
7361
+ await this.invokeSkill(
7362
+ invocation.skill.name,
7363
+ invocation.args,
7364
+ options?.onPreflightAccepted ? { onPreflightAccepted: options.onPreflightAccepted } : undefined,
7365
+ );
7366
+ return;
7367
+ }
7368
+ }
7369
+ }
7370
+
7269
7371
  // Handle extension commands first (execute immediately, even during streaming)
7270
7372
  if (expandPromptTemplates && text.startsWith("/")) {
7271
7373
  const handled = await this.#tryExecuteExtensionCommand(text);
@@ -7445,7 +7547,7 @@ export class AgentSession {
7445
7547
 
7446
7548
  async promptCustomMessage<T = unknown>(
7447
7549
  message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "attribution">,
7448
- options?: Pick<PromptOptions, "streamingBehavior" | "toolChoice" | "followUpQueuePolicy">,
7550
+ options?: Pick<PromptOptions, "streamingBehavior" | "toolChoice" | "followUpQueuePolicy" | "onPreflightAccepted">,
7449
7551
  ): Promise<void> {
7450
7552
  const textContent =
7451
7553
  typeof message.content === "string"
@@ -7844,6 +7946,7 @@ export class AgentSession {
7844
7946
  return tool ? { safeSummary: tool.safeSummary, safeSummaryFields: tool.safeSummaryFields } : undefined;
7845
7947
  },
7846
7948
  cycleModel: () => this.cycleModel(),
7949
+ setModelProfile: name => this.activateModelProfileForControl(name),
7847
7950
  cycleThinkingLevel: () => this.cycleThinkingLevel(),
7848
7951
  setQueueMode: (kind, mode) => {
7849
7952
  if (kind === "steering" && (mode === "all" || mode === "one-at-a-time")) {
@@ -7871,6 +7974,7 @@ export class AgentSession {
7871
7974
  getJobs: () => undefined,
7872
7975
  sdkBindings: () => [
7873
7976
  "cycleModel",
7977
+ "setModelProfile",
7874
7978
  "cycleThinkingLevel",
7875
7979
  "setQueueMode",
7876
7980
  "getSkillState",
@@ -9039,11 +9143,14 @@ export class AgentSession {
9039
9143
  this.#rebindProviderSessionState(new Map());
9040
9144
  this.agent.reset();
9041
9145
  if (!options?.drop) await this.sessionManager.flush();
9042
- await this.sessionManager.newSession(options);
9043
- // Gate the successor local:// root before the successor identity is
9044
- // published to the agent, the workflow-gate emitter, hooks, or any
9045
- // synchronous resolver (#2797 / #2925).
9046
- await initializeLocalRoot(this.#localProtocolOptions());
9146
+ const prepared = await this.sessionManager.prepareNewSession(options);
9147
+ try {
9148
+ // Last fallible gate while public getters still show the predecessor (#3138).
9149
+ await initializeLocalRoot(this.#localProtocolOptions(prepared));
9150
+ this.sessionManager.commitPreparedNewSession(prepared);
9151
+ } catch (error) {
9152
+ throw await discardPreparedNewSessionAfterFailure(this.sessionManager, prepared, error);
9153
+ }
9047
9154
  this.setTodoPhases([]);
9048
9155
  this.#syncAgentSessionId();
9049
9156
  this.#bindWorkflowGateEmitter(previousWorkflowGateSessionId);
@@ -9105,16 +9212,18 @@ export class AgentSession {
9105
9212
  if (!(await manager.cancelAndSettleOwnerJobs(ownerId))) {
9106
9213
  throw new Error("Owned async jobs did not settle before session replacement.");
9107
9214
  }
9108
-
9215
+ const prepared = await this.sessionManager.prepareNewSession(options);
9216
+ try {
9217
+ // Last fallible gate while public getters still show the predecessor (#3138).
9218
+ await initializeLocalRoot(this.#localProtocolOptions(prepared));
9219
+ this.sessionManager.commitPreparedNewSession(prepared);
9220
+ } catch (error) {
9221
+ throw await discardPreparedNewSessionAfterFailure(this.sessionManager, prepared, error);
9222
+ }
9109
9223
  this.#disconnectFromAgent();
9110
9224
  this.#closeAllProviderSessions("new session");
9111
9225
  this.#rebindProviderSessionState(new Map());
9112
9226
  this.agent.reset();
9113
- await this.sessionManager.newSession(options);
9114
- // Gate the successor local:// root before the successor identity is
9115
- // published to the agent, the workflow-gate emitter, hooks, or any
9116
- // synchronous resolver (#2797 / #2925).
9117
- await initializeLocalRoot(this.#localProtocolOptions());
9118
9227
  this.setTodoPhases([]);
9119
9228
  this.#syncAgentSessionId();
9120
9229
  this.#bindWorkflowGateEmitter(previousWorkflowGateSessionId);
@@ -9157,10 +9266,17 @@ export class AgentSession {
9157
9266
  }
9158
9267
  this.sessionManager.appendServiceTierChange(this.serviceTier ?? null);
9159
9268
  if (nextDiscoverySessionToolNames) {
9160
- await this.#applyActiveToolsByName(nextDiscoverySessionToolNames, {
9161
- persistMCPSelection: false,
9162
- nextSelectedDiscoveredBuiltinToolNames: [],
9163
- });
9269
+ // Avoid a full system-prompt rebuild when the active set is unchanged; rebuild
9270
+ // can block on shared workspace-tree work across repeated /new transitions.
9271
+ const current = this.getActiveToolNames().slice().sort().join("\0");
9272
+ const next = [...new Set(nextDiscoverySessionToolNames.map(n => n.toLowerCase()))].sort().join("\0");
9273
+ if (current !== next) {
9274
+ await this.#applyActiveToolsByName(nextDiscoverySessionToolNames, {
9275
+ persistMCPSelection: false,
9276
+ nextSelectedDiscoveredBuiltinToolNames: [],
9277
+ });
9278
+ } else {
9279
+ }
9164
9280
  }
9165
9281
  this.#todoReminderCount = 0;
9166
9282
  this.#planReferenceSent = false;
@@ -9173,6 +9289,7 @@ export class AgentSession {
9173
9289
  reason: "new",
9174
9290
  previousSessionFile,
9175
9291
  });
9292
+ } else {
9176
9293
  }
9177
9294
  }
9178
9295
 
@@ -9252,16 +9369,18 @@ export class AgentSession {
9252
9369
  // Flush current session to ensure all entries are written
9253
9370
  await this.sessionManager.flush();
9254
9371
 
9255
- // Fork the session (creates new session file with same entries)
9256
- const forkResult = await this.sessionManager.fork();
9257
- if (!forkResult) {
9372
+ // Prepare the copied successor and complete local:// readiness while all
9373
+ // public manager getters remain bound to the predecessor.
9374
+ const prepared = await this.sessionManager.prepareFork();
9375
+ if (!prepared) {
9258
9376
  return false;
9259
9377
  }
9260
-
9261
- // Fork rotates the session identity (and copies artifacts); gate the
9262
- // successor local:// root before that identity is published (#2797 / #2925).
9263
- await initializeLocalRoot(this.#localProtocolOptions());
9264
- // Update agent session ID
9378
+ try {
9379
+ await initializeLocalRoot(this.#localProtocolOptions(prepared));
9380
+ this.sessionManager.commitPreparedNewSession(prepared);
9381
+ } catch (error) {
9382
+ throw await discardPreparedNewSessionAfterFailure(this.sessionManager, prepared, error);
9383
+ }
9265
9384
  this.#syncAgentSessionId();
9266
9385
  this.#bindWorkflowGateEmitter(previousWorkflowGateSessionId);
9267
9386
  this.#rekeyHindsightMemoryForCurrentSessionId();
@@ -9350,6 +9469,17 @@ export class AgentSession {
9350
9469
  return this.#activeModelProfile;
9351
9470
  }
9352
9471
 
9472
+ /** Activate a complete model profile through a nonvisual session control. */
9473
+ async activateModelProfileForControl(profileName: string): Promise<boolean> {
9474
+ await activateModelProfile({
9475
+ session: this,
9476
+ modelRegistry: this.#modelRegistry,
9477
+ settings: this.settings,
9478
+ profileName,
9479
+ });
9480
+ return this.getActiveModelProfile() === profileName;
9481
+ }
9482
+
9353
9483
  /** Return the persisted configured fallback selectors for a model role. */
9354
9484
  getConfiguredModelChain(role: string): readonly string[] | undefined {
9355
9485
  return getSessionContextForInternalRead(this.sessionManager).configuredModelChains[role]?.entries;
@@ -10652,26 +10782,36 @@ export class AgentSession {
10652
10782
  // user work that agent.reset() would otherwise clear.
10653
10783
  const rollbackAgentSteeringQueue = this.agent.snapshotSteering();
10654
10784
  const rollbackAgentFollowUpQueue = this.agent.snapshotFollowUp();
10655
- let successorSessionFile: string | undefined;
10656
10785
  let savedPath: string | undefined;
10657
10786
  let committed = false;
10658
- // Suspend (do not destroy) the workflow-gate emitter so it can be fenced on
10659
- // commit or resumed on rollback. Suspension runs inside the transaction so a
10660
- // listener fault during suspension is rolled back like any other prepare step.
10661
- let suspendedWorkflowGateEmitter: WorkflowGateEmitter | undefined;
10787
+ let prepared: PreparedNewSession | undefined;
10662
10788
  try {
10663
- suspendedWorkflowGateEmitter = this.#suspendWorkflowGateEmitter(rollbackSessionState.sessionId);
10664
- // --- Prepare (reversible): build and persist the successor session
10665
- // without touching predecessor authority. No irreversible predecessor
10666
- // teardown or identity publication happens until the commit boundary.
10667
- await this.sessionManager.newSession(
10789
+ // Prepare successor entries, persistence, display state, and gate
10790
+ // construction without publishing manager identity. Managed local-root
10791
+ // readiness is the last fallible step before the sole commit (#3138).
10792
+ prepared = await this.sessionManager.prepareNewSession(
10668
10793
  previousSessionFile ? { parentSession: previousSessionFile } : undefined,
10669
10794
  );
10670
- successorSessionFile = this.sessionFile;
10671
- // Gate the successor local:// root inside the reversible prepare
10672
- // window, before the successor identity is published; a gate failure
10673
- // here rolls back (#2797 / #2925).
10674
- await initializeLocalRoot(this.#localProtocolOptions());
10795
+ if (model) this.sessionManager.appendPreparedModelChange(prepared, `${model.provider}/${model.id}`);
10796
+ this.sessionManager.appendPreparedThinkingLevelChange(prepared, this.thinkingLevel);
10797
+ this.sessionManager.appendPreparedServiceTierChange(prepared, this.serviceTier ?? null);
10798
+ this.sessionManager.appendPreparedCustomMessageEntry(
10799
+ prepared,
10800
+ "handoff",
10801
+ createHandoffContext(handoffText),
10802
+ true,
10803
+ undefined,
10804
+ "agent",
10805
+ );
10806
+ await this.sessionManager.ensurePreparedNewSessionOnDisk(prepared);
10807
+ const sessionContext = this.buildPreparedDisplaySessionContext(prepared);
10808
+ const successorGateEmitter = this.#constructWorkflowGateEmitter(prepared.sessionId);
10809
+ // Last fallible action: verified local:// readiness from immutable staged options.
10810
+ await initializeLocalRoot(this.#localProtocolOptions(prepared));
10811
+
10812
+ // --- Commit boundary: synchronous adoption is the sole identity publication.
10813
+ this.sessionManager.commitPreparedNewSession(prepared);
10814
+ committed = true;
10675
10815
  this.agent.reset();
10676
10816
  this.#syncAgentSessionId();
10677
10817
  this.#rekeyHindsightMemoryForCurrentSessionId();
@@ -10680,16 +10820,8 @@ export class AgentSession {
10680
10820
  this.#pendingNextTurnMessages = [];
10681
10821
  this.#scheduledHiddenNextTurnGeneration = undefined;
10682
10822
  this.#todoReminderCount = 0;
10683
- if (model) {
10684
- this.sessionManager.appendModelChange(`${model.provider}/${model.id}`);
10685
- }
10686
- this.sessionManager.appendThinkingLevelChange(this.thinkingLevel);
10687
- this.sessionManager.appendServiceTierChange(this.serviceTier ?? null);
10688
-
10689
- // Inject the handoff document as a custom message
10690
- const handoffContent = createHandoffContext(handoffText);
10691
- this.sessionManager.appendCustomMessageEntry("handoff", handoffContent, true, undefined, "agent");
10692
- await this.sessionManager.ensureOnDisk();
10823
+ this.agent.replaceMessages(sessionContext.messages);
10824
+ this.#syncTodoPhasesFromBranch();
10693
10825
  if (options?.autoTriggered && this.settings.get("compaction.handoffSaveToDisk")) {
10694
10826
  try {
10695
10827
  const artifactId = await this.sessionManager.saveArtifact(`${handoffText}\n`, "handoff");
@@ -10701,30 +10833,9 @@ export class AgentSession {
10701
10833
  }
10702
10834
  }
10703
10835
 
10704
- // Rebuild agent messages from session
10705
- const sessionContext = this.buildDisplaySessionContext();
10706
- this.agent.replaceMessages(sessionContext.messages);
10707
- this.#syncTodoPhasesFromBranch();
10708
-
10709
- // Construct the successor workflow-gate emitter in the reversible window:
10710
- // FileGateStore load / broker init can throw, and a failure here must roll
10711
- // back rather than corrupt an already-committed switch. Publication is
10712
- // deferred to the (no-throw) commit step below.
10713
- const successorGateEmitter = this.#constructWorkflowGateEmitter();
10714
-
10715
- // --- Commit boundary: the successor is durably persisted and every
10716
- // reversible, fallible step above has succeeded. Cross the irreversible
10717
- // commit BEFORE any identity rotation. Fencing the predecessor gate
10718
- // emitter and rotating provider sessions cannot be undone, so once we
10719
- // begin them a failure must RETAIN the committed successor rather than
10720
- // attempt a (now-impossible) rollback that would resume a fenced emitter.
10721
- committed = true;
10836
+ // The forward-only suffix starts only after the committed successor is live.
10722
10837
  this.#resetInjectedContextSignatures();
10723
- this.#publishWorkflowGateEmitter(
10724
- successorGateEmitter,
10725
- rollbackSessionState.sessionId,
10726
- suspendedWorkflowGateEmitter,
10727
- );
10838
+ this.#publishWorkflowGateEmitter(successorGateEmitter, rollbackSessionState.sessionId);
10728
10839
  this.#closeAllProviderSessions("session handoff");
10729
10840
  this.#rebindProviderSessionState(new Map());
10730
10841
  this.#resetHindsightConversationTrackingIfHindsight();
@@ -10760,12 +10871,12 @@ export class AgentSession {
10760
10871
  return { document: handoffText, savedPath };
10761
10872
  } catch (switchError) {
10762
10873
  if (committed) {
10763
- // The switch already committed; a post-commit step failed. Do not
10764
- // tear down the new session — surface success (the handoff exists).
10765
- logger.warn("Handoff post-commit step failed after the session switch committed", {
10766
- error: switchError instanceof Error ? switchError.message : String(switchError),
10874
+ throw Object.assign(new Error("Handoff committed but successor initialization was degraded."), {
10875
+ code: "handoff_committed_degraded",
10876
+ cause: switchError,
10877
+ handoffDocument: handoffText,
10878
+ savedPath,
10767
10879
  });
10768
- return { document: handoffText, savedPath };
10769
10880
  }
10770
10881
  // Reversible window: roll back to the pre-handoff session so the
10771
10882
  // failure is non-destructive. Predecessor gate emitter, provider
@@ -10773,7 +10884,6 @@ export class AgentSession {
10773
10884
  // were never mutated before commit, so they survive intact.
10774
10885
  this.sessionManager.restoreState(rollbackSessionState);
10775
10886
  this.#syncAgentSessionId(rollbackSessionState.sessionId);
10776
- this.#restoreWorkflowGateEmitter(suspendedWorkflowGateEmitter);
10777
10887
  this.#rekeyHindsightMemoryForCurrentSessionId();
10778
10888
  this.agent.replaceMessages(rollbackAgentMessages);
10779
10889
  this.agent.clearAllQueues();
@@ -10785,25 +10895,17 @@ export class AgentSession {
10785
10895
  this.#scheduledHiddenNextTurnGeneration = rollbackScheduledHiddenNextTurnGeneration;
10786
10896
  this.#todoReminderCount = rollbackTodoReminderCount;
10787
10897
  this.#syncTodoPhasesFromBranch();
10788
- // Remove the orphaned successor transcript/artifacts written before the
10789
- // failure so a rolled-back handoff leaves nothing behind.
10790
- if (successorSessionFile && successorSessionFile !== previousSessionFile) {
10791
- try {
10792
- await this.sessionManager.discardUncommittedSession(successorSessionFile);
10793
- } catch (cleanupError) {
10794
- logger.warn("Failed to remove orphaned handoff session after rollback", {
10795
- path: successorSessionFile,
10796
- error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError),
10797
- });
10798
- }
10799
- }
10898
+ // Exact-discard only the staged successor; predecessor state was never adopted.
10899
+ const rollbackError = prepared
10900
+ ? await discardPreparedNewSessionAfterFailure(this.sessionManager, prepared, switchError)
10901
+ : switchError;
10800
10902
  // Map to cancellation only for a genuine handoff-signal abort; a
10801
10903
  // downstream error keeps its cause and the generated document.
10802
10904
  if (handoffSignal.aborted) {
10803
10905
  throw new Error("Handoff cancelled");
10804
10906
  }
10805
10907
  // Preserve the generated handoff document for copy/retry.
10806
- throw Object.assign(switchError instanceof Error ? switchError : new Error(String(switchError)), {
10908
+ throw Object.assign(rollbackError instanceof Error ? rollbackError : new Error(String(rollbackError)), {
10807
10909
  handoffDocument: handoffText,
10808
10910
  });
10809
10911
  }
@@ -10811,7 +10913,7 @@ export class AgentSession {
10811
10913
  // Genuine handoff-signal cancellation maps to a cancellation error.
10812
10914
  // Errors surfaced by the inner transaction (which already rolled back
10813
10915
  // and, for non-aborts, attached the generated document) pass through.
10814
- if (handoffSignal.aborted) {
10916
+ if (handoffSignal.aborted && !(error instanceof AggregateError)) {
10815
10917
  throw new Error("Handoff cancelled");
10816
10918
  }
10817
10919
  throw error;
@@ -12501,8 +12603,14 @@ export class AgentSession {
12501
12603
  break;
12502
12604
  }
12503
12605
 
12504
- const baseDelayMs = retrySettings.baseDelayMs * 2 ** attempt;
12505
- const delayMs = retryAfterMs !== undefined ? Math.max(baseDelayMs, retryAfterMs) : baseDelayMs;
12606
+ // Legacy parsed Retry-After is capped at retry.maxDelayMs (see
12607
+ // compactionRetryDelay); only managed fallback is uncapped.
12608
+ const delayMs = compactionRetryDelay(
12609
+ retrySettings.baseDelayMs,
12610
+ retrySettings.maxDelayMs,
12611
+ attempt,
12612
+ retryAfterMs,
12613
+ );
12506
12614
 
12507
12615
  // If retry delay is too long (>30s), try next candidate instead of waiting
12508
12616
  const maxAcceptableDelayMs = 30_000;
@@ -12695,7 +12803,7 @@ export class AgentSession {
12695
12803
  );
12696
12804
  }
12697
12805
 
12698
- #alibabaTokenPlanCanonicalTimeoutForApi(api: string): string | undefined {
12806
+ #alibabaTokenPlanTimeoutMessagesForApi(api: string): ReadonlySet<string> | undefined {
12699
12807
  if (api === "openai-responses" || api === "openai-completions") {
12700
12808
  return ALIBABA_TOKEN_PLAN_FIRST_EVENT_TIMEOUT_MESSAGES[api];
12701
12809
  }
@@ -12704,18 +12812,23 @@ export class AgentSession {
12704
12812
 
12705
12813
  #isAlibabaTokenPlanFirstEventTimeout(message: AssistantMessage): boolean {
12706
12814
  if (message.stopReason !== "error" || message.provider !== ALIBABA_TOKEN_PLAN_PROVIDER) return false;
12707
- const canonicalMessage = this.#alibabaTokenPlanCanonicalTimeoutForApi(message.api);
12708
- return canonicalMessage !== undefined && message.errorMessage === canonicalMessage;
12815
+ const timeoutMessages = this.#alibabaTokenPlanTimeoutMessagesForApi(message.api);
12816
+ return timeoutMessages?.has(message.errorMessage ?? "") ?? false;
12709
12817
  }
12710
12818
 
12711
12819
  #isAlibabaTokenPlanCompactionTimeout(candidate: Model, errorMessage: string): boolean {
12712
12820
  if (candidate.provider !== ALIBABA_TOKEN_PLAN_PROVIDER) return false;
12713
- const canonicalMessage = this.#alibabaTokenPlanCanonicalTimeoutForApi(candidate.api);
12714
- if (canonicalMessage === undefined) return false;
12715
- return (
12716
- errorMessage === `Summarization failed: ${canonicalMessage}` ||
12717
- errorMessage === `Turn prefix summarization failed: ${canonicalMessage}`
12718
- );
12821
+ const timeoutMessages = this.#alibabaTokenPlanTimeoutMessagesForApi(candidate.api);
12822
+ if (!timeoutMessages) return false;
12823
+ for (const timeoutMessage of timeoutMessages) {
12824
+ if (
12825
+ errorMessage === `Summarization failed: ${timeoutMessage}` ||
12826
+ errorMessage === `Turn prefix summarization failed: ${timeoutMessage}`
12827
+ ) {
12828
+ return true;
12829
+ }
12830
+ }
12831
+ return false;
12719
12832
  }
12720
12833
 
12721
12834
  #isTerminalProviderFirstEventTimeout(message: AssistantMessage): boolean {
@@ -13067,7 +13180,7 @@ export class AgentSession {
13067
13180
  }
13068
13181
  if (this.#isTerminalProviderFirstEventTimeout(outcome.failure.message)) {
13069
13182
  // The managed transport discarded this attempt before session policy saw it.
13070
- // Remove its provisional controller charge and surface the original message.
13183
+ // Remove its provisional charge without changing sticky fallback selection.
13071
13184
  this.#defaultFallbackChain().discardStartedAttempt();
13072
13185
  return {
13073
13186
  type: "terminal",
@@ -14600,8 +14713,10 @@ export class AgentSession {
14600
14713
  try {
14601
14714
  await this.sessionManager.setSessionFile(sessionPath);
14602
14715
  // The successor identity is already rotated in the manager but not yet
14603
- // published; gate its local:// root before publication so no observer
14604
- // can resolve against an ungated root (#2797 / #2925).
14716
+ // published; gate its local:// root before publication so the agent,
14717
+ // workflow-gate emitter, and hooks cannot resolve against an ungated
14718
+ // root. The manager-rotation window itself is tracked in #3138
14719
+ // (#2797 / #2925).
14605
14720
  if (switchingToDifferentSession) await initializeLocalRoot(this.#localProtocolOptions());
14606
14721
  this.#syncAgentSessionId();
14607
14722
  this.#rekeyHindsightMemoryForCurrentSessionId();
@@ -14799,22 +14914,21 @@ export class AgentSession {
14799
14914
  skipConversationRestore = result?.skipConversationRestore ?? false;
14800
14915
  }
14801
14916
 
14802
- // Clear pending messages (bound to old session state)
14917
+ // Flush pending writes before preparing the successor.
14918
+ await this.sessionManager.flush();
14919
+ const prepared = selectedEntry.parentId
14920
+ ? await this.sessionManager.prepareBranchedSession(selectedEntry.parentId)
14921
+ : await this.sessionManager.prepareNewSession({ parentSession: previousSessionFile });
14922
+ try {
14923
+ await initializeLocalRoot(this.#localProtocolOptions(prepared));
14924
+ this.sessionManager.commitPreparedNewSession(prepared);
14925
+ } catch (error) {
14926
+ throw await discardPreparedNewSessionAfterFailure(this.sessionManager, prepared, error);
14927
+ }
14803
14928
  this.#pendingNextTurnMessages = [];
14804
14929
  this.#scheduledHiddenNextTurnGeneration = undefined;
14805
-
14806
- // Flush pending writes before branching
14807
- await this.sessionManager.flush();
14808
14930
  this.#cancelOwnAsyncJobs();
14809
14931
 
14810
- if (!selectedEntry.parentId) {
14811
- await this.sessionManager.newSession({ parentSession: previousSessionFile });
14812
- } else {
14813
- this.sessionManager.createBranchedSession(selectedEntry.parentId);
14814
- }
14815
- // Branch/tree-jump establishes a new session identity; gate the successor
14816
- // local:// root before that identity is published (#2797 / #2925).
14817
- await initializeLocalRoot(this.#localProtocolOptions());
14818
14932
  this.#syncTodoPhasesFromBranch();
14819
14933
  this.#syncAgentSessionId();
14820
14934
  this.#bindWorkflowGateEmitter(previousWorkflowGateSessionId);