@gajae-code/coding-agent 0.12.11 → 0.12.13

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 (140) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/dist/types/cli/marketplace-hint.d.ts +28 -0
  3. package/dist/types/cli/notify-cli.d.ts +42 -1
  4. package/dist/types/cli/update-cli.d.ts +9 -3
  5. package/dist/types/cli.d.ts +10 -0
  6. package/dist/types/commands/notify.d.ts +6 -0
  7. package/dist/types/config/file-lock-gc.d.ts +13 -1
  8. package/dist/types/config/file-lock.d.ts +3 -0
  9. package/dist/types/config/model-registry.d.ts +6 -0
  10. package/dist/types/config/models-config-schema.d.ts +99 -0
  11. package/dist/types/coordinator/contract.d.ts +1 -1
  12. package/dist/types/coordinator-mcp/question-state.d.ts +3 -1
  13. package/dist/types/extensibility/extensions/index.d.ts +1 -0
  14. package/dist/types/extensibility/extensions/ouroboros-ooo-bridge.d.ts +13 -0
  15. package/dist/types/extensibility/extensions/prefix-command-bridge.d.ts +0 -1
  16. package/dist/types/extensibility/extensions/types.d.ts +2 -0
  17. package/dist/types/gjc-runtime/deep-interview-state.d.ts +14 -0
  18. package/dist/types/gjc-runtime/gc-runtime.d.ts +11 -0
  19. package/dist/types/main.d.ts +6 -2
  20. package/dist/types/modes/acp/acp-agent.d.ts +24 -1
  21. package/dist/types/modes/components/session-selector.d.ts +1 -1
  22. package/dist/types/modes/utils/host-status.d.ts +23 -0
  23. package/dist/types/runtime-mcp/types.d.ts +2 -0
  24. package/dist/types/sdk/broker/lifecycle.d.ts +12 -0
  25. package/dist/types/sdk/bus/chat-daemon-cli.d.ts +3 -0
  26. package/dist/types/sdk/bus/chat-daemon-command-channel.d.ts +244 -0
  27. package/dist/types/sdk/bus/chat-daemon-control.d.ts +11 -1
  28. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +7 -0
  29. package/dist/types/sdk/bus/control-plane-frames.d.ts +43 -0
  30. package/dist/types/sdk/bus/conversation-store.d.ts +18 -0
  31. package/dist/types/sdk/bus/existing-thread-readiness.d.ts +71 -0
  32. package/dist/types/sdk/bus/html-format.d.ts +10 -0
  33. package/dist/types/sdk/bus/notification-service.d.ts +10 -0
  34. package/dist/types/sdk/bus/slack-daemon.d.ts +27 -0
  35. package/dist/types/sdk/bus/slack-live-provider.d.ts +11 -0
  36. package/dist/types/sdk/bus/slack-provider.d.ts +14 -0
  37. package/dist/types/sdk/bus/slack-thread-binding.d.ts +140 -0
  38. package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +2 -0
  39. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +28 -7
  40. package/dist/types/sdk/bus/telegram-daemon.d.ts +119 -17
  41. package/dist/types/sdk/bus/telegram-reference.d.ts +20 -1
  42. package/dist/types/sdk/bus/topic-registry.d.ts +130 -85
  43. package/dist/types/sdk/host/host.d.ts +52 -0
  44. package/dist/types/sdk/lifecycle-session.d.ts +6 -0
  45. package/dist/types/sdk/session-activation.d.ts +41 -0
  46. package/dist/types/sdk/session.d.ts +18 -0
  47. package/dist/types/sdk/startup-capability.d.ts +8 -1
  48. package/dist/types/session/agent-session.d.ts +7 -2
  49. package/dist/types/session/artifacts.d.ts +2 -2
  50. package/dist/types/session/session-manager.d.ts +40 -6
  51. package/dist/types/tools/index.d.ts +15 -0
  52. package/dist/types/utils/npm-registry.d.ts +71 -0
  53. package/examples/extensions/README.md +38 -4
  54. package/examples/extensions/ooo-bridge.ts +15 -0
  55. package/package.json +8 -8
  56. package/scripts/generate-sdk-operation-inventory.ts +4 -0
  57. package/src/async/job-manager.ts +9 -2
  58. package/src/cli/fast-help.ts +1 -0
  59. package/src/cli/marketplace-hint.ts +39 -0
  60. package/src/cli/notify-cli.ts +226 -2
  61. package/src/cli/plugin-cli.ts +10 -0
  62. package/src/cli/update-cli.ts +90 -23
  63. package/src/cli.ts +23 -0
  64. package/src/commands/notify.ts +27 -3
  65. package/src/commands/sdk.ts +1 -0
  66. package/src/config/file-lock-gc.ts +65 -26
  67. package/src/config/file-lock.ts +61 -16
  68. package/src/config/model-profiles.ts +46 -30
  69. package/src/config/model-registry.ts +17 -5
  70. package/src/config/models-config-schema.ts +9 -4
  71. package/src/coordinator/contract.ts +1 -0
  72. package/src/coordinator-mcp/question-state.ts +2 -0
  73. package/src/coordinator-mcp/server.ts +358 -7
  74. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
  75. package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -0
  76. package/src/extensibility/extensions/index.ts +1 -0
  77. package/src/extensibility/extensions/ouroboros-ooo-bridge.ts +235 -0
  78. package/src/extensibility/extensions/prefix-command-bridge.ts +4 -9
  79. package/src/extensibility/extensions/runner.ts +15 -2
  80. package/src/extensibility/extensions/types.ts +2 -0
  81. package/src/gjc-runtime/deep-interview-recorder.ts +9 -5
  82. package/src/gjc-runtime/deep-interview-state.ts +27 -3
  83. package/src/gjc-runtime/gc-render.ts +8 -1
  84. package/src/gjc-runtime/gc-runtime.ts +15 -1
  85. package/src/gjc-runtime/launch-tmux.ts +53 -1
  86. package/src/internal-urls/docs-index.generated.ts +14 -13
  87. package/src/main.ts +214 -70
  88. package/src/modes/DESIGN.md +8 -5
  89. package/src/modes/acp/acp-agent.ts +123 -7
  90. package/src/modes/components/footer.ts +10 -18
  91. package/src/modes/components/model-selector.ts +52 -5
  92. package/src/modes/components/session-selector.ts +26 -3
  93. package/src/modes/components/status-line/gh.ts +23 -2
  94. package/src/modes/controllers/event-controller.ts +3 -0
  95. package/src/modes/controllers/extension-ui-controller.ts +2 -0
  96. package/src/modes/controllers/input-controller.ts +11 -0
  97. package/src/modes/controllers/selector-controller.ts +8 -2
  98. package/src/modes/interactive-mode.ts +7 -1
  99. package/src/modes/print-mode.ts +10 -0
  100. package/src/modes/utils/host-status.ts +38 -0
  101. package/src/prompts/system/project-prompt.md +19 -0
  102. package/src/runtime-mcp/types.ts +2 -0
  103. package/src/sdk/broker/lifecycle.ts +60 -3
  104. package/src/sdk/bus/chat-daemon-cli.ts +42 -0
  105. package/src/sdk/bus/chat-daemon-command-channel.ts +921 -0
  106. package/src/sdk/bus/chat-daemon-control.ts +16 -3
  107. package/src/sdk/bus/chat-daemon-runtime.ts +257 -21
  108. package/src/sdk/bus/control-plane-frames.ts +51 -0
  109. package/src/sdk/bus/conversation-store.ts +29 -1
  110. package/src/sdk/bus/existing-thread-readiness.ts +155 -0
  111. package/src/sdk/bus/html-format.ts +19 -3
  112. package/src/sdk/bus/index.ts +91 -12
  113. package/src/sdk/bus/notification-service.ts +28 -1
  114. package/src/sdk/bus/rich-render.ts +3 -2
  115. package/src/sdk/bus/slack-daemon.ts +99 -4
  116. package/src/sdk/bus/slack-live-provider.ts +32 -2
  117. package/src/sdk/bus/slack-provider.ts +11 -0
  118. package/src/sdk/bus/slack-thread-binding.ts +641 -0
  119. package/src/sdk/bus/telegram-daemon-cli.ts +11 -0
  120. package/src/sdk/bus/telegram-daemon-contract.ts +28 -7
  121. package/src/sdk/bus/telegram-daemon.ts +2007 -509
  122. package/src/sdk/bus/telegram-reference.ts +114 -15
  123. package/src/sdk/bus/topic-registry.ts +674 -181
  124. package/src/sdk/host/host.ts +180 -1
  125. package/src/sdk/lifecycle-session.ts +8 -2
  126. package/src/sdk/protocol/operation-inventory.generated.json +22 -0
  127. package/src/sdk/session-activation.ts +127 -0
  128. package/src/sdk/session.ts +144 -29
  129. package/src/sdk/startup-capability.ts +10 -1
  130. package/src/session/agent-session.ts +220 -13
  131. package/src/session/artifacts.ts +56 -13
  132. package/src/session/internal/managed-session-storage.ts +228 -7
  133. package/src/session/session-manager.ts +225 -60
  134. package/src/setup/provider-presets.json +3 -3
  135. package/src/task/index.ts +92 -44
  136. package/src/thinking-metadata.ts +2 -1
  137. package/src/tools/ask.ts +10 -0
  138. package/src/tools/index.ts +15 -0
  139. package/src/tools/todo-write.ts +4 -4
  140. package/src/utils/npm-registry.ts +660 -0
@@ -159,7 +159,6 @@ export interface ForkContextSeed {
159
159
  messages: Message[];
160
160
  agentMessages: AgentMessage[];
161
161
  metadata: ForkContextSeedMetadata;
162
- cacheIdentity?: string;
163
162
  appendOnlyPrefixSnapshot?: StablePrefixSnapshot;
164
163
  }
165
164
 
@@ -167,7 +166,6 @@ export interface ForkContextSeedOptions {
167
166
  maxMessages: number;
168
167
  maxTokens: number;
169
168
  preserveLatestUser?: boolean;
170
- cacheIdentity?: string;
171
169
  signal?: AbortSignal;
172
170
  }
173
171
 
@@ -184,7 +182,7 @@ import {
184
182
  } from "@gajae-code/utils";
185
183
 
186
184
  import { createAppendOnlyContextManager, resolveAppendOnlyMode } from "../append-only-mode";
187
- import { type AsyncJob, type AsyncJobDeliveryState, AsyncJobManager } from "../async";
185
+ import { type AsyncJob, type AsyncJobDeliveryState, AsyncJobManager, type OwnerSubagentShutdownLease } from "../async";
188
186
  import { reset as resetCapabilities } from "../capability";
189
187
  import type { Rule } from "../capability/rule";
190
188
  import type { CasReceipt } from "../config/atomic-yaml-patch";
@@ -610,6 +608,8 @@ export interface AgentSessionConfig {
610
608
  discoveryMode?: "off" | "mcp-only" | "all";
611
609
  /** MCP tool names to activate for the current session when discovery mode is enabled. */
612
610
  initialSelectedMCPToolNames?: string[];
611
+ /** Keep persisted MCP names until a deferred exact catalog becomes available. */
612
+ preserveUnavailableInitialMCPToolSelection?: boolean;
613
613
  /** Built-in discoverable tool names restored for the current all-discovery session. */
614
614
  initialSelectedDiscoveredBuiltinToolNames?: string[];
615
615
  /** Discoverable built-ins active for configured or explicit reasons independently of persisted discovery selection. */
@@ -656,6 +656,8 @@ export interface AgentSessionConfig {
656
656
  * process-global manager **MUST NOT** dispose it on their own teardown.
657
657
  */
658
658
  ownedMcpManager?: MCPManager;
659
+ /** Optional startup dependency that must settle before the first provider turn. */
660
+ startupTurnBarrier?: Promise<void>;
659
661
  /** Optional fork-context seed used to initialize a child session before its first prompt. */
660
662
  forkContextSeed?: ForkContextSeed;
661
663
  /** Optional provider state override. Fork-context children should omit this by default. */
@@ -1947,6 +1949,7 @@ export class AgentSession {
1947
1949
  */
1948
1950
  readonly #ownedAsyncJobManager: AsyncJobManager | undefined;
1949
1951
  readonly #ownedMcpManager: MCPManager | undefined;
1952
+ #startupTurnBarrier: Promise<void> | undefined;
1950
1953
  #pendingPythonMessages: PythonExecutionMessage[] = [];
1951
1954
  #activeEvalExecutions = new Set<Promise<unknown>>();
1952
1955
  #evalExecutionDisposing = false;
@@ -1966,6 +1969,8 @@ export class AgentSession {
1966
1969
  #isDisposed = false;
1967
1970
  #disposePromise: Promise<void> | undefined;
1968
1971
  readonly #toolSessionCleanups = new Set<() => Promise<void> | void>();
1972
+ readonly #toolSessionTransitionCleanups = new Set<() => Promise<void> | void>();
1973
+ readonly #deferredOwnerShutdownFinalizations = new Set<Promise<void>>();
1969
1974
  #newSessionTransition: Promise<boolean> | undefined;
1970
1975
  // Extension system
1971
1976
  #extensionRunner: ExtensionRunner | undefined = undefined;
@@ -2269,10 +2274,23 @@ export class AgentSession {
2269
2274
  next.ready.resolve();
2270
2275
  }
2271
2276
 
2277
+ async #awaitStartupTurnBarrier(): Promise<void> {
2278
+ const barrier = this.#startupTurnBarrier;
2279
+ if (!barrier) return;
2280
+ await barrier;
2281
+ if (this.#startupTurnBarrier === barrier) this.#startupTurnBarrier = undefined;
2282
+ }
2283
+ extendStartupTurnBarrier(barrier: Promise<void>): void {
2284
+ const current = this.#startupTurnBarrier;
2285
+ this.#startupTurnBarrier = current ? Promise.all([current, barrier]).then(() => {}) : barrier;
2286
+ void this.#startupTurnBarrier.catch(() => {});
2287
+ }
2288
+
2272
2289
  async #withSessionAdmission<T>(
2273
2290
  kind: SessionAdmissionKind,
2274
2291
  body: (lease: SessionAdmissionLease) => Promise<T>,
2275
2292
  ): Promise<T> {
2293
+ if (kind === "prompt") await this.#awaitStartupTurnBarrier();
2276
2294
  const owner = this.#sessionAdmissionContext.getStore();
2277
2295
  if (owner && !owner.released) throw this.#sessionAdmissionBusyError();
2278
2296
  if (this.#sessionAdmissionClosed || this.#isDisposed) throw this.#sessionAdmissionBusyError();
@@ -2601,6 +2619,7 @@ export class AgentSession {
2601
2619
  this.#ownedAsyncJobManager = config.ownedAsyncJobManager;
2602
2620
  this.#retainedMemorySampler = config.retainedMemorySampler;
2603
2621
  this.#ownedMcpManager = config.ownedMcpManager;
2622
+ this.#startupTurnBarrier = config.startupTurnBarrier;
2604
2623
  this.#scopedModels = config.scopedModels ?? [];
2605
2624
  this.#thinkingLevel = config.thinkingLevel;
2606
2625
  this.#promptTemplates = config.promptTemplates ?? [];
@@ -2661,6 +2680,8 @@ export class AgentSession {
2661
2680
  injectIdle: async messages => {
2662
2681
  const first = messages[0];
2663
2682
  if (!first) return;
2683
+ await this.#awaitStartupTurnBarrier();
2684
+ if (this.#isDisposed) return;
2664
2685
  if (messages.length === 1) {
2665
2686
  await this.agent.prompt(first, this.#managedFallbackPromptOptions());
2666
2687
  } else {
@@ -2723,7 +2744,7 @@ export class AgentSession {
2723
2744
  config.initialDiscoveredBuiltinToolSelectionIsExplicit === true
2724
2745
  ? this.#selectRestorableDiscoveredBuiltinToolNames(config.initialPersistedDiscoveredBuiltinToolNames ?? [])
2725
2746
  : undefined;
2726
- this.#pruneSelectedMCPToolNames();
2747
+ if (!config.preserveUnavailableInitialMCPToolSelection) this.#pruneSelectedMCPToolNames();
2727
2748
  const persistInitialMCPToolSelection =
2728
2749
  config.persistInitialMCPToolSelection === true && config.initialMCPToolSelectionIsExplicit !== false;
2729
2750
  const persistInitialDiscoveredBuiltinToolSelection =
@@ -3095,7 +3116,6 @@ export class AgentSession {
3095
3116
  maxTokens,
3096
3117
  skippedReasons: {},
3097
3118
  },
3098
- cacheIdentity: options.cacheIdentity ?? this.sessionId,
3099
3119
  };
3100
3120
  }
3101
3121
  const transformedMessages = await this.#transformContext([...this.messages], options.signal);
@@ -3295,7 +3315,6 @@ export class AgentSession {
3295
3315
  maxTokens,
3296
3316
  skippedReasons,
3297
3317
  },
3298
- cacheIdentity: options.cacheIdentity ?? this.sessionId,
3299
3318
  appendOnlyPrefixSnapshot,
3300
3319
  };
3301
3320
  }
@@ -3377,6 +3396,9 @@ export class AgentSession {
3377
3396
  getAgentId(): string | undefined {
3378
3397
  return this.#agentId;
3379
3398
  }
3399
+ get isDisposed(): boolean {
3400
+ return this.#isDisposed;
3401
+ }
3380
3402
 
3381
3403
  registerToolSessionCleanup(cleanup: () => Promise<void> | void): () => void {
3382
3404
  if (this.#isDisposed) throw new Error("Cannot register tool cleanup after session disposal has started.");
@@ -3384,6 +3406,23 @@ export class AgentSession {
3384
3406
  return () => this.#toolSessionCleanups.delete(cleanup);
3385
3407
  }
3386
3408
 
3409
+ registerToolSessionTransitionCleanup(cleanup: () => Promise<void> | void): () => void {
3410
+ if (this.#isDisposed)
3411
+ throw new Error("Cannot register tool transition cleanup after session disposal has started.");
3412
+ this.#toolSessionTransitionCleanups.add(cleanup);
3413
+ return () => this.#toolSessionTransitionCleanups.delete(cleanup);
3414
+ }
3415
+
3416
+ async #runToolSessionTransitionCleanups(): Promise<void> {
3417
+ const cleanups = Array.from(this.#toolSessionTransitionCleanups);
3418
+ this.#toolSessionTransitionCleanups.clear();
3419
+ const results = await Promise.allSettled(cleanups.map(async cleanup => await cleanup()));
3420
+ for (const result of results) {
3421
+ if (result.status === "rejected")
3422
+ logger.warn("Tool session transition cleanup failed", { error: String(result.reason) });
3423
+ }
3424
+ }
3425
+
3387
3426
  async #runToolSessionCleanups(): Promise<void> {
3388
3427
  const cleanups = Array.from(this.#toolSessionCleanups);
3389
3428
  this.#toolSessionCleanups.clear();
@@ -3436,6 +3475,61 @@ export class AgentSession {
3436
3475
  manager.cancelAll({ ownerId: this.#agentId });
3437
3476
  }
3438
3477
 
3478
+ async #settleOwnAsyncJobsBeforeArtifactRetirement(): Promise<void> {
3479
+ const ownerId = this.#agentId;
3480
+ const manager = AsyncJobManager.instance();
3481
+ if (!ownerId || !manager) return;
3482
+ const lease = manager.beginOwnerSubagentShutdown(ownerId);
3483
+ if (!lease) throw new Error("Owned async cleanup is already in progress before artifact retirement.");
3484
+ let committed = false;
3485
+ try {
3486
+ manager.runOwnerProducerCleanupsStrict({ ownerId });
3487
+ const proof = await manager.cancelAndProveOwnerSubagents(lease);
3488
+ if (!proof.confirmed)
3489
+ throw new Error("Owned subagent cleanup could not be confirmed before artifact retirement.");
3490
+ if (!(await manager.waitForOwnerInFlightDeliveries(ownerId)))
3491
+ throw new Error("Owned async deliveries did not settle before artifact retirement.");
3492
+ if (!(await manager.cancelAndSettleOwnerJobs(ownerId)))
3493
+ throw new Error("Owned async jobs did not settle before artifact retirement.");
3494
+ committed = true;
3495
+ } finally {
3496
+ manager.finishOwnerSubagentShutdown(lease, committed ? "commit" : "release");
3497
+ }
3498
+ }
3499
+
3500
+ #scheduleDeferredOwnerShutdownFinalization(
3501
+ manager: AsyncJobManager,
3502
+ lease: OwnerSubagentShutdownLease,
3503
+ ownerId: string,
3504
+ ): void {
3505
+ const finalization = (async () => {
3506
+ while (!this.#isDisposed) {
3507
+ try {
3508
+ manager.runOwnerProducerCleanupsStrict({ ownerId });
3509
+ const proof = await manager.cancelAndProveOwnerSubagents(lease);
3510
+ if (!proof.confirmed) throw new Error(`owner_subagent_${proof.reason}`);
3511
+ if (!(await manager.waitForOwnerInFlightDeliveries(ownerId)))
3512
+ throw new Error("owner_delivery_settlement_timeout");
3513
+ if (!(await manager.cancelAndSettleOwnerJobs(ownerId))) throw new Error("owner_job_settlement_timeout");
3514
+ if (this.#isDisposed) break;
3515
+ this.sessionManager.retireEphemeralArtifactsAfterTransition();
3516
+ await this.#runToolSessionTransitionCleanups();
3517
+ manager.finishOwnerSubagentShutdown(lease, "commit");
3518
+ return;
3519
+ } catch (error) {
3520
+ logger.warn("Deferred owner shutdown finalization retry failed", {
3521
+ ownerId,
3522
+ error: error instanceof Error ? error.message : String(error),
3523
+ });
3524
+ await Bun.sleep(25);
3525
+ }
3526
+ }
3527
+ manager.finishOwnerSubagentShutdown(lease, "release");
3528
+ })();
3529
+ this.#deferredOwnerShutdownFinalizations.add(finalization);
3530
+ void finalization.finally(() => this.#deferredOwnerShutdownFinalizations.delete(finalization));
3531
+ }
3532
+
3439
3533
  #suppressOwnAsyncJobDeliveries(): void {
3440
3534
  if (!this.#agentId) return;
3441
3535
  const manager = AsyncJobManager.instance();
@@ -4647,6 +4741,11 @@ export class AgentSession {
4647
4741
  settleLease();
4648
4742
  return;
4649
4743
  }
4744
+ await this.#awaitStartupTurnBarrier();
4745
+ if (!canContinue()) {
4746
+ settleLease();
4747
+ return;
4748
+ }
4650
4749
  try {
4651
4750
  if (!options?.skipCompactionCheck) {
4652
4751
  await this.#checkEstimatedContextBeforePrompt();
@@ -5996,6 +6095,7 @@ export class AgentSession {
5996
6095
  // the session that owns the manager goes on to dispose it (which itself
5997
6096
  // nukes any leftover jobs and pending deliveries).
5998
6097
  this.#cancelOwnAsyncJobs();
6098
+ await Promise.allSettled(this.#deferredOwnerShutdownFinalizations);
5999
6099
  const ownedAsyncManager = this.#ownedAsyncJobManager;
6000
6100
  if (ownedAsyncManager) {
6001
6101
  const drained = await ownedAsyncManager.dispose({ timeoutMs: 3_000 });
@@ -6007,6 +6107,7 @@ export class AgentSession {
6007
6107
  AsyncJobManager.setInstance(undefined);
6008
6108
  }
6009
6109
  }
6110
+ await this.#runToolSessionTransitionCleanups();
6010
6111
  await this.#runToolSessionCleanups();
6011
6112
  // Only disconnect the MCP manager THIS session owns (top-level sessions that
6012
6113
  // connected plugin-bundle MCP servers). Subagents and callers that merely
@@ -6834,6 +6935,12 @@ export class AgentSession {
6834
6935
  const built = await this.#runAdmittedBaseSystemPromptRebuild(() =>
6835
6936
  this.#rebuildSystemPrompt!(validToolNames, this.#toolRegistry),
6836
6937
  );
6938
+ if (this.#isDisposed) {
6939
+ if (generation === this.#baseSystemPromptGeneration) {
6940
+ this.#pendingAppliedToolSignature = undefined;
6941
+ }
6942
+ return;
6943
+ }
6837
6944
  if (generation === this.#baseSystemPromptGeneration) {
6838
6945
  this.#baseSystemPrompt = built.systemPrompt;
6839
6946
  this.agent.setSystemPrompt(this.#baseSystemPrompt);
@@ -7319,6 +7426,8 @@ export class AgentSession {
7319
7426
  if (!canContinuePersistedHistory(this.agent.state.messages)) {
7320
7427
  throw new Error("Cannot continue from persisted message history");
7321
7428
  }
7429
+ await this.#awaitStartupTurnBarrier();
7430
+ if (this.#sessionAdmissionClosed || this.#isDisposed) throw this.#sessionAdmissionBusyError();
7322
7431
  this.#beginInFlight();
7323
7432
  let hindsightRecall: string | undefined;
7324
7433
  try {
@@ -10183,6 +10292,7 @@ export class AgentSession {
10183
10292
  // Last fallible gate while public getters still show the predecessor (#3138).
10184
10293
  await initializeLocalRoot(this.#localProtocolOptions(prepared));
10185
10294
  this.sessionManager.commitPreparedNewSession(prepared);
10295
+ await this.#runToolSessionTransitionCleanups();
10186
10296
  } catch (error) {
10187
10297
  throw await discardPreparedNewSessionAfterFailure(this.sessionManager, prepared, error);
10188
10298
  }
@@ -10252,6 +10362,7 @@ export class AgentSession {
10252
10362
  // Last fallible gate while public getters still show the predecessor (#3138).
10253
10363
  await initializeLocalRoot(this.#localProtocolOptions(prepared));
10254
10364
  this.sessionManager.commitPreparedNewSession(prepared);
10365
+ await this.#runToolSessionTransitionCleanups();
10255
10366
  } catch (error) {
10256
10367
  throw await discardPreparedNewSessionAfterFailure(this.sessionManager, prepared, error);
10257
10368
  }
@@ -10418,7 +10529,9 @@ export class AgentSession {
10418
10529
  }
10419
10530
  try {
10420
10531
  await initializeLocalRoot(this.#localProtocolOptions(prepared));
10532
+ await this.#settleOwnAsyncJobsBeforeArtifactRetirement();
10421
10533
  this.sessionManager.commitPreparedNewSession(prepared);
10534
+ await this.#runToolSessionTransitionCleanups();
10422
10535
  } catch (error) {
10423
10536
  throw await discardPreparedNewSessionAfterFailure(this.sessionManager, prepared, error);
10424
10537
  }
@@ -11535,6 +11648,7 @@ export class AgentSession {
11535
11648
  const artifactManager = this.sessionManager.getArtifactManager();
11536
11649
  const prunedArtifacts: Array<{ entryId: string; id: string; toolType: string; originalText: string }> = [];
11537
11650
  let reservedArtifactId: string | undefined;
11651
+ let artifactAllocationAvailable = artifactManager !== null;
11538
11652
  if (artifactManager) {
11539
11653
  try {
11540
11654
  reservedArtifactId = (await artifactManager.allocatePath("tool-output")).id;
@@ -11542,14 +11656,24 @@ export class AgentSession {
11542
11656
  logger.warn("Failed to reserve artifact ID for pruned tool output", {
11543
11657
  error: error instanceof Error ? error.message : String(error),
11544
11658
  });
11659
+ artifactAllocationAvailable = false;
11545
11660
  }
11546
11661
  }
11547
11662
  const result = pruneToolOutputs(branchEntries, DEFAULT_PRUNE_CONFIG, {
11548
11663
  relaxedMinimum: overThreshold ? 0 : undefined,
11549
11664
  artifactRefMaxChars: PRUNED_ARTIFACT_REF_MAX_CHARS,
11550
11665
  artifactRef: candidate => {
11551
- if (!artifactManager) return undefined;
11552
- const id = reservedArtifactId ?? String(artifactManager.allocateId());
11666
+ if (!artifactManager || !artifactAllocationAvailable) return undefined;
11667
+ let id: string;
11668
+ try {
11669
+ id = reservedArtifactId ?? String(artifactManager.allocateId());
11670
+ } catch (error) {
11671
+ artifactAllocationAvailable = false;
11672
+ logger.warn("Failed to allocate artifact ID for pruned tool output", {
11673
+ error: error instanceof Error ? error.message : String(error),
11674
+ });
11675
+ return undefined;
11676
+ }
11553
11677
  reservedArtifactId = undefined;
11554
11678
  const toolType = (candidate.toolName ?? "tool-output").replace(/[^a-zA-Z0-9_-]/g, "_") || "tool-output";
11555
11679
  prunedArtifacts.push({ entryId: candidate.entryId, id, toolType, originalText: candidate.originalText });
@@ -12161,10 +12285,12 @@ export class AgentSession {
12161
12285
  const successorGateEmitter = this.#constructWorkflowGateEmitter(prepared.sessionId);
12162
12286
  // Last fallible action: verified local:// readiness from immutable staged options.
12163
12287
  await initializeLocalRoot(this.#localProtocolOptions(prepared));
12288
+ await this.#settleOwnAsyncJobsBeforeArtifactRetirement();
12164
12289
 
12165
12290
  // --- Commit boundary: synchronous adoption is the sole identity publication.
12166
12291
  this.sessionManager.commitPreparedNewSession(prepared);
12167
12292
  committed = true;
12293
+ await this.#runToolSessionTransitionCleanups();
12168
12294
  this.agent.reset();
12169
12295
  this.#syncAgentSessionId();
12170
12296
  this.#rekeyHindsightMemoryForCurrentSessionId();
@@ -12195,7 +12321,6 @@ export class AgentSession {
12195
12321
  this.#resetIrcRosterDeliveryState();
12196
12322
  this.#planReferenceSent = false;
12197
12323
  this.#planReferencePath = "local://PLAN.md";
12198
- this.#cancelOwnAsyncJobs();
12199
12324
  // The predecessor's fenced async-job results (queued while the
12200
12325
  // transition held the delivery fence) belong to the handed-off session,
12201
12326
  // not the successor. Suppress and drop ONLY the async-result kind so they
@@ -14937,6 +15062,17 @@ export class AgentSession {
14937
15062
  // before the failure. This bypasses #hasCleanRetryReplaySafety because the
14938
15063
  // content-free check is the replay-safety guarantee for credential rotation.
14939
15064
  const canReplayRotatedCredential = credentialRotated;
15065
+ // Universal automatic replay-safety gate (#3791): once the failed attempt
15066
+ // carries observable assistant text, thinking, or tool-call content,
15067
+ // automatic session retry must not re-issue the request. This covers
15068
+ // configured legacy retry (which previously only gated first-event
15069
+ // timeouts) as well as bare defaults. Managed fallback keeps its
15070
+ // provisional discard path (events never published to the session).
15071
+ // Content-free credential rotation remains eligible above; first-event
15072
+ // timeout keeps its additional typed/scope checks earlier in this method.
15073
+ if (!managedFallback && assistantMessageHasVisibleOrToolContent(message)) {
15074
+ return false;
15075
+ }
14940
15076
  // Bare defaults retain their narrow watchdog and Codex admissions. A
14941
15077
  // first-event timeout adds the typed, content-free, current-clean-scope
14942
15078
  // requirement above; other transient watchdogs preserve legacy behavior.
@@ -16300,6 +16436,10 @@ export class AgentSession {
16300
16436
  onTransitionMutationStarted?: () => void;
16301
16437
  },
16302
16438
  ): Promise<boolean> {
16439
+ let ownerShutdownManager: AsyncJobManager | undefined;
16440
+ let ownerShutdownLease: OwnerSubagentShutdownLease | undefined;
16441
+ let ownerShutdownTransitionCommitted = false;
16442
+ let ownerShutdownFinalizationDeferred = false;
16303
16443
  this.#beginSessionTransition("switch-session");
16304
16444
  try {
16305
16445
  const previousSessionFile = this.sessionManager.getSessionFile();
@@ -16320,9 +16460,31 @@ export class AgentSession {
16320
16460
  }
16321
16461
  options?.onTransitionMutationStarted?.();
16322
16462
 
16323
- this.#disconnectFromAgent();
16463
+ const asyncManager = AsyncJobManager.instance();
16464
+ const ownerId = this.#agentId;
16465
+ const lease =
16466
+ switchingToDifferentSession && asyncManager && ownerId
16467
+ ? asyncManager.beginOwnerSubagentShutdown(ownerId)
16468
+ : undefined;
16469
+ if (switchingToDifferentSession && asyncManager && ownerId && !lease) {
16470
+ this.emitNotice(
16471
+ "error",
16472
+ "Cannot switch sessions while owned subagent cleanup is already in progress.",
16473
+ "switch-session-subagent-cleanup",
16474
+ );
16475
+ return false;
16476
+ }
16477
+ if (lease && asyncManager) {
16478
+ ownerShutdownManager = asyncManager;
16479
+ ownerShutdownLease = lease;
16480
+ }
16324
16481
  await this.abort();
16482
+ if (this.isCompacting) {
16483
+ this.abortCompaction();
16484
+ while (this.isCompacting) await Bun.sleep(10);
16485
+ }
16325
16486
 
16487
+ this.#disconnectFromAgent();
16326
16488
  // Flush pending writes before switching so restore snapshots reflect committed state.
16327
16489
  await this.sessionManager.flush();
16328
16490
  const previousSessionState = this.sessionManager.captureState();
@@ -16353,9 +16515,13 @@ export class AgentSession {
16353
16515
  ? this.#suspendWorkflowGateEmitter(previousSessionState.sessionId)
16354
16516
  : undefined;
16355
16517
  let unavailableDefaultChainMessage: string | undefined;
16518
+ let transitionCleanupCommitted = false;
16356
16519
 
16357
16520
  try {
16358
- await this.sessionManager.setSessionFile(sessionPath);
16521
+ await this.sessionManager.setSessionFile(sessionPath, {
16522
+ deferEphemeralArtifactRetirement: switchingToDifferentSession,
16523
+ });
16524
+ if (switchingToDifferentSession) this.sessionManager.stageAdoptedArtifactManagerForTransition();
16359
16525
  // The successor identity is already rotated in the manager but not yet
16360
16526
  // published; gate its local:// root before publication so the agent,
16361
16527
  // workflow-gate emitter, and hooks cannot resolve against an ungated
@@ -16455,6 +16621,38 @@ export class AgentSession {
16455
16621
  this.#resetIrcRosterDeliveryState();
16456
16622
  }
16457
16623
 
16624
+ if (switchingToDifferentSession) {
16625
+ let ownerShutdownSettled = true;
16626
+ if (ownerShutdownManager && ownerShutdownLease && ownerId) {
16627
+ try {
16628
+ ownerShutdownManager.runOwnerProducerCleanupsStrict({ ownerId });
16629
+ const proof = await ownerShutdownManager.cancelAndProveOwnerSubagents(ownerShutdownLease);
16630
+ if (!proof.confirmed)
16631
+ throw new Error("Owned subagent cleanup could not be confirmed after successor validation.");
16632
+ if (!(await ownerShutdownManager.waitForOwnerInFlightDeliveries(ownerId)))
16633
+ throw new Error("Owned async deliveries did not settle after successor validation.");
16634
+ if (!(await ownerShutdownManager.cancelAndSettleOwnerJobs(ownerId)))
16635
+ throw new Error("Owned async jobs did not settle after successor validation.");
16636
+ } catch (error) {
16637
+ ownerShutdownSettled = false;
16638
+ ownerShutdownFinalizationDeferred = true;
16639
+ this.#scheduleDeferredOwnerShutdownFinalization(ownerShutdownManager, ownerShutdownLease, ownerId);
16640
+ this.#suppressOwnAsyncJobDeliveries();
16641
+ this.emitNotice(
16642
+ "error",
16643
+ `Successor session is active, but predecessor async cleanup did not settle: ${error instanceof Error ? error.message : String(error)}`,
16644
+ "switch-session-subagent-cleanup",
16645
+ );
16646
+ }
16647
+ }
16648
+ transitionCleanupCommitted = true;
16649
+ // Different files may intentionally carry the same copied session id; pathname transition is the commit signal.
16650
+ ownerShutdownTransitionCommitted = true;
16651
+ if (ownerShutdownSettled) {
16652
+ this.sessionManager.retireEphemeralArtifactsAfterTransition();
16653
+ await this.#runToolSessionTransitionCleanups();
16654
+ }
16655
+ }
16458
16656
  this.#reconnectToAgent();
16459
16657
  // Fence predecessor continuations before session_switch starts SDK runtime
16460
16658
  // teardown. The previous runtime waits for those continuations to settle;
@@ -16463,7 +16661,8 @@ export class AgentSession {
16463
16661
  this.#bindWorkflowGateEmitter(previousSessionState.sessionId, suspendedWorkflowGateEmitter);
16464
16662
  // session_switch is the post-commit identity signal. SDK authority and
16465
16663
  // other identity-bound integrations must not observe the successor until
16466
- // messages, model state, MCP selections, and the agent subscription are live.
16664
+ // messages, model state, MCP selections, the agent subscription, and
16665
+ // session-scoped tool cleanup are complete.
16467
16666
  if (this.#extensionRunner) {
16468
16667
  await this.#extensionRunner.emit({
16469
16668
  type: "session_switch",
@@ -16474,6 +16673,7 @@ export class AgentSession {
16474
16673
  }
16475
16674
  return true;
16476
16675
  } catch (error) {
16676
+ if (transitionCleanupCommitted) throw error;
16477
16677
  this.sessionManager.restoreState(previousSessionState);
16478
16678
  this.#defaultFallbackController = undefined;
16479
16679
  this.#syncAgentSessionId(previousSessionState.sessionId);
@@ -16532,6 +16732,12 @@ export class AgentSession {
16532
16732
  throw error;
16533
16733
  }
16534
16734
  } finally {
16735
+ if (ownerShutdownManager && ownerShutdownLease && !ownerShutdownFinalizationDeferred) {
16736
+ ownerShutdownManager.finishOwnerSubagentShutdown(
16737
+ ownerShutdownLease,
16738
+ ownerShutdownTransitionCommitted ? "commit" : "release",
16739
+ );
16740
+ }
16535
16741
  this.#endSessionTransition();
16536
16742
  }
16537
16743
  }
@@ -16583,13 +16789,14 @@ export class AgentSession {
16583
16789
  : await this.sessionManager.prepareNewSession({ parentSession: previousSessionFile });
16584
16790
  try {
16585
16791
  await initializeLocalRoot(this.#localProtocolOptions(prepared));
16792
+ await this.#settleOwnAsyncJobsBeforeArtifactRetirement();
16586
16793
  this.sessionManager.commitPreparedNewSession(prepared);
16794
+ await this.#runToolSessionTransitionCleanups();
16587
16795
  } catch (error) {
16588
16796
  throw await discardPreparedNewSessionAfterFailure(this.sessionManager, prepared, error);
16589
16797
  }
16590
16798
  this.#pendingNextTurnMessages = [];
16591
16799
  this.#scheduledHiddenNextTurnGeneration = undefined;
16592
- this.#cancelOwnAsyncJobs();
16593
16800
 
16594
16801
  this.#syncTodoPhasesFromBranch();
16595
16802
  this.#syncAgentSessionId();
@@ -13,6 +13,7 @@ import {
13
13
  ensureManagedDirectory,
14
14
  type ManagedSessionDescendantStore,
15
15
  publishManagedFileNoReplace,
16
+ publishManagedFileNoReplaceSync,
16
17
  } from "./internal/managed-session-storage";
17
18
  import { DEFAULT_ARTIFACT_MAX_BYTES, truncateHeadBytes } from "./streaming-output";
18
19
 
@@ -78,7 +79,8 @@ export class ArtifactManager {
78
79
  readonly #dir: string;
79
80
  readonly #store: ManagedSessionDescendantStore | undefined;
80
81
  #dirCreated = false;
81
- #initialized = false;
82
+ #initialized: Promise<void> | undefined;
83
+ #initializedComplete = false;
82
84
 
83
85
  /**
84
86
  * @param dir Directory that will hold artifact files. Created lazily on first save.
@@ -118,10 +120,10 @@ export class ArtifactManager {
118
120
  else ensureManagedDirectory(this.#dir);
119
121
  this.#dirCreated = true;
120
122
  }
121
- if (!this.#initialized) {
122
- await this.#scanExistingIds();
123
- this.#initialized = true;
124
- }
123
+ // Memoized: concurrent first writers must not each rescan and restart the
124
+ // ID counter at the same value, which would collide on publish.
125
+ this.#initialized ??= this.#scanExistingIds();
126
+ await this.#initialized;
125
127
  }
126
128
 
127
129
  #filename(id: string, toolType: string): string {
@@ -129,6 +131,44 @@ export class ArtifactManager {
129
131
  return `${id}.${toolType}.log`;
130
132
  }
131
133
 
134
+ #claimFilename(id: number): string {
135
+ return `.artifact-id-${id}`;
136
+ }
137
+
138
+ #nextCandidateId(): number {
139
+ const id = this.#nextId++;
140
+ if (!Number.isSafeInteger(id) || id < 0) throw new Error("artifact_id_out_of_range");
141
+ return id;
142
+ }
143
+
144
+ async #claimNextId(): Promise<number> {
145
+ while (true) {
146
+ const id = this.#nextCandidateId();
147
+ try {
148
+ await this.#publish("", this.#claimFilename(id));
149
+ return id;
150
+ } catch (error) {
151
+ if (!(error instanceof Error) || error.message !== "destination_conflict") throw error;
152
+ }
153
+ }
154
+ }
155
+
156
+ #claimNextIdSync(): number {
157
+ if (!this.#initializedComplete)
158
+ throw new Error("ArtifactManager must be initialized before synchronous allocation");
159
+ while (true) {
160
+ const id = this.#nextCandidateId();
161
+ try {
162
+ const filename = this.#claimFilename(id);
163
+ if (this.#store) this.#store.publishNoReplaceSync(filename, new Uint8Array());
164
+ else publishManagedFileNoReplaceSync(path.join(this.#dir, filename), new Uint8Array());
165
+ return id;
166
+ } catch (error) {
167
+ if (!(error instanceof Error) || error.message !== "destination_conflict") throw error;
168
+ }
169
+ }
170
+ }
171
+
132
172
  async #publish(content: string, filename: string): Promise<void> {
133
173
  if (this.#store) await this.#store.publishNoReplace(filename, Buffer.from(content, "utf8"));
134
174
  else await publishManagedFileNoReplace(path.join(this.#dir, filename), Buffer.from(content, "utf8"));
@@ -250,22 +290,25 @@ export class ArtifactManager {
250
290
  const files = await this.listFiles();
251
291
  let maxId = -1;
252
292
  for (const file of files) {
253
- // Files are named: {id}.{toolType}.log
254
- const match = file.match(/^(\d+)\..*\.log$/);
293
+ // Published artifacts are `{id}.{toolType}.log`; hidden claim files reserve
294
+ // the numeric namespace across independent managers and processes.
295
+ const match = file.match(/^(\d+)\..*\.log$/) ?? file.match(/^\.artifact-id-(\d+)$/);
255
296
  if (match) {
256
- const id = parseInt(match[1], 10);
297
+ const id = Number(match[1]);
298
+ if (!Number.isSafeInteger(id) || id < 0) throw new Error("artifact_id_out_of_range");
257
299
  if (id > maxId) maxId = id;
258
300
  }
259
301
  }
260
302
  this.#nextId = maxId + 1;
303
+ this.#initializedComplete = true;
261
304
  }
262
305
 
263
306
  /**
264
- * Atomically allocate next artifact ID.
265
- * IDs are sequential within the session.
307
+ * Atomically claim the next artifact ID after this manager has been initialized.
308
+ * Prefer `allocatePath` or `save`; this synchronous seam exists for pruning callbacks.
266
309
  */
267
310
  allocateId(): number {
268
- return this.#nextId++;
311
+ return this.#claimNextIdSync();
269
312
  }
270
313
 
271
314
  /**
@@ -276,7 +319,7 @@ export class ArtifactManager {
276
319
  */
277
320
  async allocatePath(toolType: string): Promise<{ id: string; path?: string }> {
278
321
  await this.#ensureDir();
279
- const id = String(this.allocateId());
322
+ const id = String(await this.#claimNextId());
280
323
  if (this.#store) return { id };
281
324
  return { id, path: path.join(this.#dir, this.#filename(id, toolType)) };
282
325
  }
@@ -288,7 +331,7 @@ export class ArtifactManager {
288
331
  */
289
332
  async save(content: string, toolType: string, options: ArtifactSaveOptions = {}): Promise<string> {
290
333
  await this.#ensureDir();
291
- const id = String(this.allocateId());
334
+ const id = String(await this.#claimNextId());
292
335
  const maxBytes = Math.max(0, options.maxBytes ?? DEFAULT_ARTIFACT_MAX_BYTES);
293
336
  const contentBytes = Buffer.byteLength(content, "utf-8");
294
337
  const published =