@gajae-code/coding-agent 0.12.10 → 0.12.12

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 (120) hide show
  1. package/CHANGELOG.md +47 -2
  2. package/dist/types/cli/notify-cli.d.ts +42 -1
  3. package/dist/types/cli/update-cli.d.ts +9 -3
  4. package/dist/types/commands/notify.d.ts +6 -0
  5. package/dist/types/config/file-lock.d.ts +3 -0
  6. package/dist/types/coordinator/contract.d.ts +1 -1
  7. package/dist/types/coordinator-mcp/question-state.d.ts +3 -1
  8. package/dist/types/extensibility/extensions/index.d.ts +1 -0
  9. package/dist/types/extensibility/extensions/ouroboros-ooo-bridge.d.ts +13 -0
  10. package/dist/types/extensibility/extensions/prefix-command-bridge.d.ts +0 -1
  11. package/dist/types/extensibility/extensions/types.d.ts +2 -0
  12. package/dist/types/main.d.ts +6 -2
  13. package/dist/types/modes/components/session-selector.d.ts +1 -1
  14. package/dist/types/modes/utils/host-status.d.ts +23 -0
  15. package/dist/types/runtime-mcp/types.d.ts +2 -0
  16. package/dist/types/sdk/broker/lifecycle.d.ts +12 -0
  17. package/dist/types/sdk/bus/chat-daemon-cli.d.ts +3 -0
  18. package/dist/types/sdk/bus/chat-daemon-command-channel.d.ts +244 -0
  19. package/dist/types/sdk/bus/chat-daemon-control.d.ts +13 -1
  20. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +7 -0
  21. package/dist/types/sdk/bus/control-plane-frames.d.ts +43 -0
  22. package/dist/types/sdk/bus/conversation-store.d.ts +18 -0
  23. package/dist/types/sdk/bus/existing-thread-readiness.d.ts +71 -0
  24. package/dist/types/sdk/bus/notification-service.d.ts +10 -0
  25. package/dist/types/sdk/bus/slack-daemon.d.ts +27 -0
  26. package/dist/types/sdk/bus/slack-live-provider.d.ts +11 -0
  27. package/dist/types/sdk/bus/slack-provider.d.ts +14 -0
  28. package/dist/types/sdk/bus/slack-thread-binding.d.ts +140 -0
  29. package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +2 -0
  30. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +25 -7
  31. package/dist/types/sdk/bus/telegram-daemon.d.ts +104 -16
  32. package/dist/types/sdk/bus/telegram-reference.d.ts +10 -1
  33. package/dist/types/sdk/bus/topic-registry.d.ts +130 -85
  34. package/dist/types/sdk/host/host.d.ts +52 -0
  35. package/dist/types/sdk/lifecycle-session.d.ts +6 -0
  36. package/dist/types/sdk/session-activation.d.ts +41 -0
  37. package/dist/types/sdk/session.d.ts +18 -0
  38. package/dist/types/sdk/startup-capability.d.ts +8 -1
  39. package/dist/types/session/agent-session.d.ts +7 -0
  40. package/dist/types/session/artifacts.d.ts +2 -2
  41. package/dist/types/session/internal/managed-session-storage.d.ts +12 -2
  42. package/dist/types/session/session-manager.d.ts +40 -5
  43. package/dist/types/session/session-storage.d.ts +1 -0
  44. package/dist/types/tools/computer.d.ts +35 -2
  45. package/dist/types/tools/index.d.ts +8 -0
  46. package/dist/types/tools/todo-write.d.ts +2 -0
  47. package/dist/types/utils/npm-registry.d.ts +71 -0
  48. package/examples/extensions/README.md +38 -4
  49. package/examples/extensions/ooo-bridge.ts +15 -0
  50. package/package.json +8 -8
  51. package/scripts/generate-sdk-operation-inventory.ts +4 -0
  52. package/src/async/job-manager.ts +9 -2
  53. package/src/cli/notify-cli.ts +226 -2
  54. package/src/cli/update-cli.ts +90 -23
  55. package/src/commands/notify.ts +27 -3
  56. package/src/commands/sdk.ts +1 -0
  57. package/src/config/file-lock-gc.ts +18 -0
  58. package/src/config/file-lock.ts +61 -16
  59. package/src/config/model-profiles.ts +31 -15
  60. package/src/coordinator/contract.ts +1 -0
  61. package/src/coordinator-mcp/question-state.ts +2 -0
  62. package/src/coordinator-mcp/server.ts +358 -7
  63. package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -0
  64. package/src/extensibility/extensions/index.ts +1 -0
  65. package/src/extensibility/extensions/ouroboros-ooo-bridge.ts +235 -0
  66. package/src/extensibility/extensions/prefix-command-bridge.ts +4 -9
  67. package/src/extensibility/extensions/runner.ts +15 -2
  68. package/src/extensibility/extensions/types.ts +2 -0
  69. package/src/gjc-runtime/launch-tmux.ts +53 -1
  70. package/src/internal-urls/docs-index.generated.ts +13 -12
  71. package/src/main.ts +203 -67
  72. package/src/modes/DESIGN.md +8 -5
  73. package/src/modes/components/footer.ts +10 -18
  74. package/src/modes/components/session-selector.ts +26 -3
  75. package/src/modes/components/status-line/gh.ts +23 -2
  76. package/src/modes/controllers/event-controller.ts +3 -0
  77. package/src/modes/controllers/extension-ui-controller.ts +2 -0
  78. package/src/modes/controllers/input-controller.ts +11 -0
  79. package/src/modes/controllers/selector-controller.ts +8 -2
  80. package/src/modes/interactive-mode.ts +7 -1
  81. package/src/modes/utils/host-status.ts +38 -0
  82. package/src/prompts/tools/computer.md +6 -1
  83. package/src/runtime-mcp/types.ts +2 -0
  84. package/src/sdk/broker/lifecycle.ts +60 -3
  85. package/src/sdk/bus/chat-daemon-cli.ts +42 -0
  86. package/src/sdk/bus/chat-daemon-command-channel.ts +921 -0
  87. package/src/sdk/bus/chat-daemon-control.ts +18 -3
  88. package/src/sdk/bus/chat-daemon-runtime.ts +257 -21
  89. package/src/sdk/bus/control-plane-frames.ts +51 -0
  90. package/src/sdk/bus/conversation-store.ts +29 -1
  91. package/src/sdk/bus/existing-thread-readiness.ts +155 -0
  92. package/src/sdk/bus/index.ts +70 -0
  93. package/src/sdk/bus/notification-service.ts +28 -1
  94. package/src/sdk/bus/rich-render.ts +3 -2
  95. package/src/sdk/bus/slack-daemon.ts +99 -4
  96. package/src/sdk/bus/slack-live-provider.ts +32 -2
  97. package/src/sdk/bus/slack-provider.ts +11 -0
  98. package/src/sdk/bus/slack-thread-binding.ts +641 -0
  99. package/src/sdk/bus/telegram-daemon-cli.ts +11 -0
  100. package/src/sdk/bus/telegram-daemon-contract.ts +25 -7
  101. package/src/sdk/bus/telegram-daemon.ts +1961 -497
  102. package/src/sdk/bus/telegram-reference.ts +42 -6
  103. package/src/sdk/bus/topic-registry.ts +674 -181
  104. package/src/sdk/host/host.ts +180 -1
  105. package/src/sdk/lifecycle-session.ts +8 -2
  106. package/src/sdk/protocol/operation-inventory.generated.json +22 -0
  107. package/src/sdk/session-activation.ts +127 -0
  108. package/src/sdk/session.ts +139 -28
  109. package/src/sdk/startup-capability.ts +10 -1
  110. package/src/session/agent-session.ts +331 -30
  111. package/src/session/artifacts.ts +56 -13
  112. package/src/session/internal/managed-session-storage.ts +440 -243
  113. package/src/session/session-manager.ts +288 -44
  114. package/src/session/session-storage.ts +3 -0
  115. package/src/setup/provider-presets.json +1 -1
  116. package/src/task/index.ts +92 -44
  117. package/src/tools/computer.ts +322 -45
  118. package/src/tools/index.ts +8 -0
  119. package/src/tools/todo-write.ts +29 -11
  120. package/src/utils/npm-registry.ts +660 -0
@@ -184,7 +184,7 @@ import {
184
184
  } from "@gajae-code/utils";
185
185
 
186
186
  import { createAppendOnlyContextManager, resolveAppendOnlyMode } from "../append-only-mode";
187
- import { type AsyncJob, type AsyncJobDeliveryState, AsyncJobManager } from "../async";
187
+ import { type AsyncJob, type AsyncJobDeliveryState, AsyncJobManager, type OwnerSubagentShutdownLease } from "../async";
188
188
  import { reset as resetCapabilities } from "../capability";
189
189
  import type { Rule } from "../capability/rule";
190
190
  import type { CasReceipt } from "../config/atomic-yaml-patch";
@@ -427,6 +427,7 @@ import {
427
427
  getLatestCompactionEntry,
428
428
  getSessionMessageEntryId,
429
429
  getSessionMessageObservationId,
430
+ SessionAppendPersistenceError,
430
431
  transferSessionMessageIdentity,
431
432
  } from "./session-manager";
432
433
  import { getEntriesForInternalRead, getSessionContextForInternalRead } from "./session-manager-internal";
@@ -609,6 +610,8 @@ export interface AgentSessionConfig {
609
610
  discoveryMode?: "off" | "mcp-only" | "all";
610
611
  /** MCP tool names to activate for the current session when discovery mode is enabled. */
611
612
  initialSelectedMCPToolNames?: string[];
613
+ /** Keep persisted MCP names until a deferred exact catalog becomes available. */
614
+ preserveUnavailableInitialMCPToolSelection?: boolean;
612
615
  /** Built-in discoverable tool names restored for the current all-discovery session. */
613
616
  initialSelectedDiscoveredBuiltinToolNames?: string[];
614
617
  /** Discoverable built-ins active for configured or explicit reasons independently of persisted discovery selection. */
@@ -655,6 +658,8 @@ export interface AgentSessionConfig {
655
658
  * process-global manager **MUST NOT** dispose it on their own teardown.
656
659
  */
657
660
  ownedMcpManager?: MCPManager;
661
+ /** Optional startup dependency that must settle before the first provider turn. */
662
+ startupTurnBarrier?: Promise<void>;
658
663
  /** Optional fork-context seed used to initialize a child session before its first prompt. */
659
664
  forkContextSeed?: ForkContextSeed;
660
665
  /** Optional provider state override. Fork-context children should omit this by default. */
@@ -1946,6 +1951,7 @@ export class AgentSession {
1946
1951
  */
1947
1952
  readonly #ownedAsyncJobManager: AsyncJobManager | undefined;
1948
1953
  readonly #ownedMcpManager: MCPManager | undefined;
1954
+ #startupTurnBarrier: Promise<void> | undefined;
1949
1955
  #pendingPythonMessages: PythonExecutionMessage[] = [];
1950
1956
  #activeEvalExecutions = new Set<Promise<unknown>>();
1951
1957
  #evalExecutionDisposing = false;
@@ -1965,6 +1971,8 @@ export class AgentSession {
1965
1971
  #isDisposed = false;
1966
1972
  #disposePromise: Promise<void> | undefined;
1967
1973
  readonly #toolSessionCleanups = new Set<() => Promise<void> | void>();
1974
+ readonly #toolSessionTransitionCleanups = new Set<() => Promise<void> | void>();
1975
+ readonly #deferredOwnerShutdownFinalizations = new Set<Promise<void>>();
1968
1976
  #newSessionTransition: Promise<boolean> | undefined;
1969
1977
  // Extension system
1970
1978
  #extensionRunner: ExtensionRunner | undefined = undefined;
@@ -2268,10 +2276,23 @@ export class AgentSession {
2268
2276
  next.ready.resolve();
2269
2277
  }
2270
2278
 
2279
+ async #awaitStartupTurnBarrier(): Promise<void> {
2280
+ const barrier = this.#startupTurnBarrier;
2281
+ if (!barrier) return;
2282
+ await barrier;
2283
+ if (this.#startupTurnBarrier === barrier) this.#startupTurnBarrier = undefined;
2284
+ }
2285
+ extendStartupTurnBarrier(barrier: Promise<void>): void {
2286
+ const current = this.#startupTurnBarrier;
2287
+ this.#startupTurnBarrier = current ? Promise.all([current, barrier]).then(() => {}) : barrier;
2288
+ void this.#startupTurnBarrier.catch(() => {});
2289
+ }
2290
+
2271
2291
  async #withSessionAdmission<T>(
2272
2292
  kind: SessionAdmissionKind,
2273
2293
  body: (lease: SessionAdmissionLease) => Promise<T>,
2274
2294
  ): Promise<T> {
2295
+ if (kind === "prompt") await this.#awaitStartupTurnBarrier();
2275
2296
  const owner = this.#sessionAdmissionContext.getStore();
2276
2297
  if (owner && !owner.released) throw this.#sessionAdmissionBusyError();
2277
2298
  if (this.#sessionAdmissionClosed || this.#isDisposed) throw this.#sessionAdmissionBusyError();
@@ -2600,6 +2621,7 @@ export class AgentSession {
2600
2621
  this.#ownedAsyncJobManager = config.ownedAsyncJobManager;
2601
2622
  this.#retainedMemorySampler = config.retainedMemorySampler;
2602
2623
  this.#ownedMcpManager = config.ownedMcpManager;
2624
+ this.#startupTurnBarrier = config.startupTurnBarrier;
2603
2625
  this.#scopedModels = config.scopedModels ?? [];
2604
2626
  this.#thinkingLevel = config.thinkingLevel;
2605
2627
  this.#promptTemplates = config.promptTemplates ?? [];
@@ -2660,6 +2682,8 @@ export class AgentSession {
2660
2682
  injectIdle: async messages => {
2661
2683
  const first = messages[0];
2662
2684
  if (!first) return;
2685
+ await this.#awaitStartupTurnBarrier();
2686
+ if (this.#isDisposed) return;
2663
2687
  if (messages.length === 1) {
2664
2688
  await this.agent.prompt(first, this.#managedFallbackPromptOptions());
2665
2689
  } else {
@@ -2722,7 +2746,7 @@ export class AgentSession {
2722
2746
  config.initialDiscoveredBuiltinToolSelectionIsExplicit === true
2723
2747
  ? this.#selectRestorableDiscoveredBuiltinToolNames(config.initialPersistedDiscoveredBuiltinToolNames ?? [])
2724
2748
  : undefined;
2725
- this.#pruneSelectedMCPToolNames();
2749
+ if (!config.preserveUnavailableInitialMCPToolSelection) this.#pruneSelectedMCPToolNames();
2726
2750
  const persistInitialMCPToolSelection =
2727
2751
  config.persistInitialMCPToolSelection === true && config.initialMCPToolSelectionIsExplicit !== false;
2728
2752
  const persistInitialDiscoveredBuiltinToolSelection =
@@ -3376,6 +3400,9 @@ export class AgentSession {
3376
3400
  getAgentId(): string | undefined {
3377
3401
  return this.#agentId;
3378
3402
  }
3403
+ get isDisposed(): boolean {
3404
+ return this.#isDisposed;
3405
+ }
3379
3406
 
3380
3407
  registerToolSessionCleanup(cleanup: () => Promise<void> | void): () => void {
3381
3408
  if (this.#isDisposed) throw new Error("Cannot register tool cleanup after session disposal has started.");
@@ -3383,6 +3410,23 @@ export class AgentSession {
3383
3410
  return () => this.#toolSessionCleanups.delete(cleanup);
3384
3411
  }
3385
3412
 
3413
+ registerToolSessionTransitionCleanup(cleanup: () => Promise<void> | void): () => void {
3414
+ if (this.#isDisposed)
3415
+ throw new Error("Cannot register tool transition cleanup after session disposal has started.");
3416
+ this.#toolSessionTransitionCleanups.add(cleanup);
3417
+ return () => this.#toolSessionTransitionCleanups.delete(cleanup);
3418
+ }
3419
+
3420
+ async #runToolSessionTransitionCleanups(): Promise<void> {
3421
+ const cleanups = Array.from(this.#toolSessionTransitionCleanups);
3422
+ this.#toolSessionTransitionCleanups.clear();
3423
+ const results = await Promise.allSettled(cleanups.map(async cleanup => await cleanup()));
3424
+ for (const result of results) {
3425
+ if (result.status === "rejected")
3426
+ logger.warn("Tool session transition cleanup failed", { error: String(result.reason) });
3427
+ }
3428
+ }
3429
+
3386
3430
  async #runToolSessionCleanups(): Promise<void> {
3387
3431
  const cleanups = Array.from(this.#toolSessionCleanups);
3388
3432
  this.#toolSessionCleanups.clear();
@@ -3435,6 +3479,61 @@ export class AgentSession {
3435
3479
  manager.cancelAll({ ownerId: this.#agentId });
3436
3480
  }
3437
3481
 
3482
+ async #settleOwnAsyncJobsBeforeArtifactRetirement(): Promise<void> {
3483
+ const ownerId = this.#agentId;
3484
+ const manager = AsyncJobManager.instance();
3485
+ if (!ownerId || !manager) return;
3486
+ const lease = manager.beginOwnerSubagentShutdown(ownerId);
3487
+ if (!lease) throw new Error("Owned async cleanup is already in progress before artifact retirement.");
3488
+ let committed = false;
3489
+ try {
3490
+ manager.runOwnerProducerCleanupsStrict({ ownerId });
3491
+ const proof = await manager.cancelAndProveOwnerSubagents(lease);
3492
+ if (!proof.confirmed)
3493
+ throw new Error("Owned subagent cleanup could not be confirmed before artifact retirement.");
3494
+ if (!(await manager.waitForOwnerInFlightDeliveries(ownerId)))
3495
+ throw new Error("Owned async deliveries did not settle before artifact retirement.");
3496
+ if (!(await manager.cancelAndSettleOwnerJobs(ownerId)))
3497
+ throw new Error("Owned async jobs did not settle before artifact retirement.");
3498
+ committed = true;
3499
+ } finally {
3500
+ manager.finishOwnerSubagentShutdown(lease, committed ? "commit" : "release");
3501
+ }
3502
+ }
3503
+
3504
+ #scheduleDeferredOwnerShutdownFinalization(
3505
+ manager: AsyncJobManager,
3506
+ lease: OwnerSubagentShutdownLease,
3507
+ ownerId: string,
3508
+ ): void {
3509
+ const finalization = (async () => {
3510
+ while (!this.#isDisposed) {
3511
+ try {
3512
+ manager.runOwnerProducerCleanupsStrict({ ownerId });
3513
+ const proof = await manager.cancelAndProveOwnerSubagents(lease);
3514
+ if (!proof.confirmed) throw new Error(`owner_subagent_${proof.reason}`);
3515
+ if (!(await manager.waitForOwnerInFlightDeliveries(ownerId)))
3516
+ throw new Error("owner_delivery_settlement_timeout");
3517
+ if (!(await manager.cancelAndSettleOwnerJobs(ownerId))) throw new Error("owner_job_settlement_timeout");
3518
+ if (this.#isDisposed) break;
3519
+ this.sessionManager.retireEphemeralArtifactsAfterTransition();
3520
+ await this.#runToolSessionTransitionCleanups();
3521
+ manager.finishOwnerSubagentShutdown(lease, "commit");
3522
+ return;
3523
+ } catch (error) {
3524
+ logger.warn("Deferred owner shutdown finalization retry failed", {
3525
+ ownerId,
3526
+ error: error instanceof Error ? error.message : String(error),
3527
+ });
3528
+ await Bun.sleep(25);
3529
+ }
3530
+ }
3531
+ manager.finishOwnerSubagentShutdown(lease, "release");
3532
+ })();
3533
+ this.#deferredOwnerShutdownFinalizations.add(finalization);
3534
+ void finalization.finally(() => this.#deferredOwnerShutdownFinalizations.delete(finalization));
3535
+ }
3536
+
3438
3537
  #suppressOwnAsyncJobDeliveries(): void {
3439
3538
  if (!this.#agentId) return;
3440
3539
  const manager = AsyncJobManager.instance();
@@ -3899,14 +3998,19 @@ export class AgentSession {
3899
3998
  ) {
3900
3999
  const isRosterReminder = event.message.role === "custom" && event.message.customType === "irc-peer-roster";
3901
4000
  if (!isRosterReminder) {
3902
- this.#appendCustomMessageEntry(
3903
- event.message.customType,
3904
- event.message.content,
3905
- event.message.display,
3906
- event.message.details,
3907
- event.message.attribution ?? "agent",
3908
- getSessionMessageObservationId(event.message),
3909
- );
4001
+ try {
4002
+ this.#appendCustomMessageEntry(
4003
+ event.message.customType,
4004
+ event.message.content,
4005
+ event.message.display,
4006
+ event.message.details,
4007
+ event.message.attribution ?? "agent",
4008
+ getSessionMessageObservationId(event.message),
4009
+ );
4010
+ } catch (error) {
4011
+ this.agent.abort();
4012
+ throw error;
4013
+ }
3910
4014
  }
3911
4015
  } else if (
3912
4016
  event.message.role === "user" ||
@@ -3915,7 +4019,73 @@ export class AgentSession {
3915
4019
  event.message.role === "toolResult" ||
3916
4020
  event.message.role === "fileMention"
3917
4021
  ) {
3918
- this.sessionManager.appendMessage(event.message);
4022
+ try {
4023
+ this.sessionManager.appendMessage(event.message);
4024
+ } catch (error) {
4025
+ if (
4026
+ event.message.role !== "toolResult" ||
4027
+ event.message.toolName !== "todo_write" ||
4028
+ !(error instanceof SessionAppendPersistenceError) ||
4029
+ error.phase !== "current_append"
4030
+ ) {
4031
+ this.agent.abort();
4032
+ throw error;
4033
+ }
4034
+ this.agent.abort();
4035
+
4036
+ const failure = error.persistenceError.message;
4037
+ const failedEntryId = error.entryId;
4038
+ this.#syncTodoPhasesFromBranch();
4039
+ event.message.isError = true;
4040
+ event.message.content = [
4041
+ {
4042
+ type: "text",
4043
+ text: `Todo state persistence failed: ${failure}\nDo not change the payload solely because of this failure. The durable outcome is unknown; reconcile the session state before retrying or continuing.`,
4044
+ },
4045
+ ];
4046
+ event.message.details = {
4047
+ ...(event.message.details && typeof event.message.details === "object" ? event.message.details : {}),
4048
+ phases: this.getTodoPhases(),
4049
+ failureKind: "persistence",
4050
+ };
4051
+ this.agent.touchContext();
4052
+ let recovered = false;
4053
+ try {
4054
+ await this.sessionManager.recoverPersistenceFailure();
4055
+ recovered = true;
4056
+ } catch (recoveryError) {
4057
+ logger.warn("Todo persistence recovery failed", {
4058
+ error: recoveryError instanceof Error ? recoveryError.message : String(recoveryError),
4059
+ });
4060
+ }
4061
+ if (recovered) {
4062
+ const durableTodoResult = this.sessionManager
4063
+ .getBranch()
4064
+ .find(
4065
+ entry =>
4066
+ entry.id === failedEntryId &&
4067
+ entry.type === "message" &&
4068
+ entry.message.role === "toolResult" &&
4069
+ entry.message.toolName === "todo_write",
4070
+ );
4071
+ this.#syncTodoPhasesFromBranch();
4072
+ if (durableTodoResult?.type === "message" && durableTodoResult.message.role === "toolResult") {
4073
+ event.message.content = durableTodoResult.message.content;
4074
+ event.message.details = durableTodoResult.message.details;
4075
+ event.message.isError = durableTodoResult.message.isError;
4076
+ } else {
4077
+ event.message.details = {
4078
+ ...(event.message.details && typeof event.message.details === "object"
4079
+ ? event.message.details
4080
+ : {}),
4081
+ phases: this.getTodoPhases(),
4082
+ failureKind: "persistence",
4083
+ };
4084
+ this.sessionManager.appendMessage(event.message);
4085
+ this.agent.touchContext();
4086
+ }
4087
+ }
4088
+ }
3919
4089
  }
3920
4090
  }
3921
4091
 
@@ -4189,7 +4359,13 @@ export class AgentSession {
4189
4359
  if (event.message.role === "toolResult") {
4190
4360
  const { toolName, details, isError, content } = event.message as {
4191
4361
  toolName?: string;
4192
- details?: { path?: string; phases?: TodoPhase[]; report?: string; startedAt?: string };
4362
+ details?: {
4363
+ path?: string;
4364
+ phases?: TodoPhase[];
4365
+ report?: string;
4366
+ startedAt?: string;
4367
+ failureKind?: "payload_rejected" | "argument_validation" | "execution" | "persistence";
4368
+ };
4193
4369
  isError?: boolean;
4194
4370
  content?: Array<TextContent | ImageContent>;
4195
4371
  };
@@ -4202,22 +4378,34 @@ export class AgentSession {
4202
4378
  }
4203
4379
  if (toolName === "todo_write" && isError) {
4204
4380
  const errorText = content?.find(part => part.type === "text")?.text;
4381
+ const payloadRejected =
4382
+ details?.failureKind === "payload_rejected" || details?.failureKind === "argument_validation";
4205
4383
  const reminderText = [
4206
4384
  "<system-reminder>",
4207
- "todo_write failed, so todo progress is not visible to the user.",
4385
+ payloadRejected
4386
+ ? "todo_write rejected its payload. The requested todo update was not applied."
4387
+ : "todo_write did not complete because the turn aborted during runtime execution or persistence.",
4208
4388
  errorText ? `Failure: ${errorText}` : "Failure: todo_write returned an error.",
4209
- "Fix the todo payload and call todo_write again before continuing.",
4389
+ payloadRejected
4390
+ ? "Correct the payload and call todo_write again before continuing."
4391
+ : "Do not change the payload solely because of this failure. The durable outcome is unknown; reconcile the session state before retrying or continuing.",
4210
4392
  "</system-reminder>",
4211
4393
  ].join("\n");
4212
- await this.sendCustomMessage(
4213
- {
4214
- customType: "todo-write-error-reminder",
4215
- content: reminderText,
4216
- display: false,
4217
- details: { toolName, errorText },
4218
- },
4219
- { deliverAs: "nextTurn" },
4220
- );
4394
+ try {
4395
+ await this.sendCustomMessage(
4396
+ {
4397
+ customType: "todo-write-error-reminder",
4398
+ content: reminderText,
4399
+ display: false,
4400
+ details: { toolName, errorText },
4401
+ },
4402
+ { deliverAs: "nextTurn" },
4403
+ );
4404
+ } catch (error) {
4405
+ logger.warn("Todo write error reminder persistence failed", {
4406
+ error: error instanceof Error ? error.message : String(error),
4407
+ });
4408
+ }
4221
4409
  }
4222
4410
  if (toolName === "checkpoint" && !isError) {
4223
4411
  const checkpointEntryId = this.sessionManager.getLeafId();
@@ -4557,6 +4745,11 @@ export class AgentSession {
4557
4745
  settleLease();
4558
4746
  return;
4559
4747
  }
4748
+ await this.#awaitStartupTurnBarrier();
4749
+ if (!canContinue()) {
4750
+ settleLease();
4751
+ return;
4752
+ }
4560
4753
  try {
4561
4754
  if (!options?.skipCompactionCheck) {
4562
4755
  await this.#checkEstimatedContextBeforePrompt();
@@ -5906,6 +6099,7 @@ export class AgentSession {
5906
6099
  // the session that owns the manager goes on to dispose it (which itself
5907
6100
  // nukes any leftover jobs and pending deliveries).
5908
6101
  this.#cancelOwnAsyncJobs();
6102
+ await Promise.allSettled(this.#deferredOwnerShutdownFinalizations);
5909
6103
  const ownedAsyncManager = this.#ownedAsyncJobManager;
5910
6104
  if (ownedAsyncManager) {
5911
6105
  const drained = await ownedAsyncManager.dispose({ timeoutMs: 3_000 });
@@ -5917,6 +6111,7 @@ export class AgentSession {
5917
6111
  AsyncJobManager.setInstance(undefined);
5918
6112
  }
5919
6113
  }
6114
+ await this.#runToolSessionTransitionCleanups();
5920
6115
  await this.#runToolSessionCleanups();
5921
6116
  // Only disconnect the MCP manager THIS session owns (top-level sessions that
5922
6117
  // connected plugin-bundle MCP servers). Subagents and callers that merely
@@ -6744,6 +6939,12 @@ export class AgentSession {
6744
6939
  const built = await this.#runAdmittedBaseSystemPromptRebuild(() =>
6745
6940
  this.#rebuildSystemPrompt!(validToolNames, this.#toolRegistry),
6746
6941
  );
6942
+ if (this.#isDisposed) {
6943
+ if (generation === this.#baseSystemPromptGeneration) {
6944
+ this.#pendingAppliedToolSignature = undefined;
6945
+ }
6946
+ return;
6947
+ }
6747
6948
  if (generation === this.#baseSystemPromptGeneration) {
6748
6949
  this.#baseSystemPrompt = built.systemPrompt;
6749
6950
  this.agent.setSystemPrompt(this.#baseSystemPrompt);
@@ -7229,6 +7430,8 @@ export class AgentSession {
7229
7430
  if (!canContinuePersistedHistory(this.agent.state.messages)) {
7230
7431
  throw new Error("Cannot continue from persisted message history");
7231
7432
  }
7433
+ await this.#awaitStartupTurnBarrier();
7434
+ if (this.#sessionAdmissionClosed || this.#isDisposed) throw this.#sessionAdmissionBusyError();
7232
7435
  this.#beginInFlight();
7233
7436
  let hindsightRecall: string | undefined;
7234
7437
  try {
@@ -10093,6 +10296,7 @@ export class AgentSession {
10093
10296
  // Last fallible gate while public getters still show the predecessor (#3138).
10094
10297
  await initializeLocalRoot(this.#localProtocolOptions(prepared));
10095
10298
  this.sessionManager.commitPreparedNewSession(prepared);
10299
+ await this.#runToolSessionTransitionCleanups();
10096
10300
  } catch (error) {
10097
10301
  throw await discardPreparedNewSessionAfterFailure(this.sessionManager, prepared, error);
10098
10302
  }
@@ -10162,6 +10366,7 @@ export class AgentSession {
10162
10366
  // Last fallible gate while public getters still show the predecessor (#3138).
10163
10367
  await initializeLocalRoot(this.#localProtocolOptions(prepared));
10164
10368
  this.sessionManager.commitPreparedNewSession(prepared);
10369
+ await this.#runToolSessionTransitionCleanups();
10165
10370
  } catch (error) {
10166
10371
  throw await discardPreparedNewSessionAfterFailure(this.sessionManager, prepared, error);
10167
10372
  }
@@ -10328,7 +10533,9 @@ export class AgentSession {
10328
10533
  }
10329
10534
  try {
10330
10535
  await initializeLocalRoot(this.#localProtocolOptions(prepared));
10536
+ await this.#settleOwnAsyncJobsBeforeArtifactRetirement();
10331
10537
  this.sessionManager.commitPreparedNewSession(prepared);
10538
+ await this.#runToolSessionTransitionCleanups();
10332
10539
  } catch (error) {
10333
10540
  throw await discardPreparedNewSessionAfterFailure(this.sessionManager, prepared, error);
10334
10541
  }
@@ -11445,6 +11652,7 @@ export class AgentSession {
11445
11652
  const artifactManager = this.sessionManager.getArtifactManager();
11446
11653
  const prunedArtifacts: Array<{ entryId: string; id: string; toolType: string; originalText: string }> = [];
11447
11654
  let reservedArtifactId: string | undefined;
11655
+ let artifactAllocationAvailable = artifactManager !== null;
11448
11656
  if (artifactManager) {
11449
11657
  try {
11450
11658
  reservedArtifactId = (await artifactManager.allocatePath("tool-output")).id;
@@ -11452,14 +11660,24 @@ export class AgentSession {
11452
11660
  logger.warn("Failed to reserve artifact ID for pruned tool output", {
11453
11661
  error: error instanceof Error ? error.message : String(error),
11454
11662
  });
11663
+ artifactAllocationAvailable = false;
11455
11664
  }
11456
11665
  }
11457
11666
  const result = pruneToolOutputs(branchEntries, DEFAULT_PRUNE_CONFIG, {
11458
11667
  relaxedMinimum: overThreshold ? 0 : undefined,
11459
11668
  artifactRefMaxChars: PRUNED_ARTIFACT_REF_MAX_CHARS,
11460
11669
  artifactRef: candidate => {
11461
- if (!artifactManager) return undefined;
11462
- const id = reservedArtifactId ?? String(artifactManager.allocateId());
11670
+ if (!artifactManager || !artifactAllocationAvailable) return undefined;
11671
+ let id: string;
11672
+ try {
11673
+ id = reservedArtifactId ?? String(artifactManager.allocateId());
11674
+ } catch (error) {
11675
+ artifactAllocationAvailable = false;
11676
+ logger.warn("Failed to allocate artifact ID for pruned tool output", {
11677
+ error: error instanceof Error ? error.message : String(error),
11678
+ });
11679
+ return undefined;
11680
+ }
11463
11681
  reservedArtifactId = undefined;
11464
11682
  const toolType = (candidate.toolName ?? "tool-output").replace(/[^a-zA-Z0-9_-]/g, "_") || "tool-output";
11465
11683
  prunedArtifacts.push({ entryId: candidate.entryId, id, toolType, originalText: candidate.originalText });
@@ -12071,10 +12289,12 @@ export class AgentSession {
12071
12289
  const successorGateEmitter = this.#constructWorkflowGateEmitter(prepared.sessionId);
12072
12290
  // Last fallible action: verified local:// readiness from immutable staged options.
12073
12291
  await initializeLocalRoot(this.#localProtocolOptions(prepared));
12292
+ await this.#settleOwnAsyncJobsBeforeArtifactRetirement();
12074
12293
 
12075
12294
  // --- Commit boundary: synchronous adoption is the sole identity publication.
12076
12295
  this.sessionManager.commitPreparedNewSession(prepared);
12077
12296
  committed = true;
12297
+ await this.#runToolSessionTransitionCleanups();
12078
12298
  this.agent.reset();
12079
12299
  this.#syncAgentSessionId();
12080
12300
  this.#rekeyHindsightMemoryForCurrentSessionId();
@@ -12105,7 +12325,6 @@ export class AgentSession {
12105
12325
  this.#resetIrcRosterDeliveryState();
12106
12326
  this.#planReferenceSent = false;
12107
12327
  this.#planReferencePath = "local://PLAN.md";
12108
- this.#cancelOwnAsyncJobs();
12109
12328
  // The predecessor's fenced async-job results (queued while the
12110
12329
  // transition held the delivery fence) belong to the handed-off session,
12111
12330
  // not the successor. Suppress and drop ONLY the async-result kind so they
@@ -14847,6 +15066,17 @@ export class AgentSession {
14847
15066
  // before the failure. This bypasses #hasCleanRetryReplaySafety because the
14848
15067
  // content-free check is the replay-safety guarantee for credential rotation.
14849
15068
  const canReplayRotatedCredential = credentialRotated;
15069
+ // Universal automatic replay-safety gate (#3791): once the failed attempt
15070
+ // carries observable assistant text, thinking, or tool-call content,
15071
+ // automatic session retry must not re-issue the request. This covers
15072
+ // configured legacy retry (which previously only gated first-event
15073
+ // timeouts) as well as bare defaults. Managed fallback keeps its
15074
+ // provisional discard path (events never published to the session).
15075
+ // Content-free credential rotation remains eligible above; first-event
15076
+ // timeout keeps its additional typed/scope checks earlier in this method.
15077
+ if (!managedFallback && assistantMessageHasVisibleOrToolContent(message)) {
15078
+ return false;
15079
+ }
14850
15080
  // Bare defaults retain their narrow watchdog and Codex admissions. A
14851
15081
  // first-event timeout adds the typed, content-free, current-clean-scope
14852
15082
  // requirement above; other transient watchdogs preserve legacy behavior.
@@ -16210,6 +16440,10 @@ export class AgentSession {
16210
16440
  onTransitionMutationStarted?: () => void;
16211
16441
  },
16212
16442
  ): Promise<boolean> {
16443
+ let ownerShutdownManager: AsyncJobManager | undefined;
16444
+ let ownerShutdownLease: OwnerSubagentShutdownLease | undefined;
16445
+ let ownerShutdownTransitionCommitted = false;
16446
+ let ownerShutdownFinalizationDeferred = false;
16213
16447
  this.#beginSessionTransition("switch-session");
16214
16448
  try {
16215
16449
  const previousSessionFile = this.sessionManager.getSessionFile();
@@ -16230,9 +16464,31 @@ export class AgentSession {
16230
16464
  }
16231
16465
  options?.onTransitionMutationStarted?.();
16232
16466
 
16233
- this.#disconnectFromAgent();
16467
+ const asyncManager = AsyncJobManager.instance();
16468
+ const ownerId = this.#agentId;
16469
+ const lease =
16470
+ switchingToDifferentSession && asyncManager && ownerId
16471
+ ? asyncManager.beginOwnerSubagentShutdown(ownerId)
16472
+ : undefined;
16473
+ if (switchingToDifferentSession && asyncManager && ownerId && !lease) {
16474
+ this.emitNotice(
16475
+ "error",
16476
+ "Cannot switch sessions while owned subagent cleanup is already in progress.",
16477
+ "switch-session-subagent-cleanup",
16478
+ );
16479
+ return false;
16480
+ }
16481
+ if (lease && asyncManager) {
16482
+ ownerShutdownManager = asyncManager;
16483
+ ownerShutdownLease = lease;
16484
+ }
16234
16485
  await this.abort();
16486
+ if (this.isCompacting) {
16487
+ this.abortCompaction();
16488
+ while (this.isCompacting) await Bun.sleep(10);
16489
+ }
16235
16490
 
16491
+ this.#disconnectFromAgent();
16236
16492
  // Flush pending writes before switching so restore snapshots reflect committed state.
16237
16493
  await this.sessionManager.flush();
16238
16494
  const previousSessionState = this.sessionManager.captureState();
@@ -16263,9 +16519,13 @@ export class AgentSession {
16263
16519
  ? this.#suspendWorkflowGateEmitter(previousSessionState.sessionId)
16264
16520
  : undefined;
16265
16521
  let unavailableDefaultChainMessage: string | undefined;
16522
+ let transitionCleanupCommitted = false;
16266
16523
 
16267
16524
  try {
16268
- await this.sessionManager.setSessionFile(sessionPath);
16525
+ await this.sessionManager.setSessionFile(sessionPath, {
16526
+ deferEphemeralArtifactRetirement: switchingToDifferentSession,
16527
+ });
16528
+ if (switchingToDifferentSession) this.sessionManager.stageAdoptedArtifactManagerForTransition();
16269
16529
  // The successor identity is already rotated in the manager but not yet
16270
16530
  // published; gate its local:// root before publication so the agent,
16271
16531
  // workflow-gate emitter, and hooks cannot resolve against an ungated
@@ -16365,6 +16625,38 @@ export class AgentSession {
16365
16625
  this.#resetIrcRosterDeliveryState();
16366
16626
  }
16367
16627
 
16628
+ if (switchingToDifferentSession) {
16629
+ let ownerShutdownSettled = true;
16630
+ if (ownerShutdownManager && ownerShutdownLease && ownerId) {
16631
+ try {
16632
+ ownerShutdownManager.runOwnerProducerCleanupsStrict({ ownerId });
16633
+ const proof = await ownerShutdownManager.cancelAndProveOwnerSubagents(ownerShutdownLease);
16634
+ if (!proof.confirmed)
16635
+ throw new Error("Owned subagent cleanup could not be confirmed after successor validation.");
16636
+ if (!(await ownerShutdownManager.waitForOwnerInFlightDeliveries(ownerId)))
16637
+ throw new Error("Owned async deliveries did not settle after successor validation.");
16638
+ if (!(await ownerShutdownManager.cancelAndSettleOwnerJobs(ownerId)))
16639
+ throw new Error("Owned async jobs did not settle after successor validation.");
16640
+ } catch (error) {
16641
+ ownerShutdownSettled = false;
16642
+ ownerShutdownFinalizationDeferred = true;
16643
+ this.#scheduleDeferredOwnerShutdownFinalization(ownerShutdownManager, ownerShutdownLease, ownerId);
16644
+ this.#suppressOwnAsyncJobDeliveries();
16645
+ this.emitNotice(
16646
+ "error",
16647
+ `Successor session is active, but predecessor async cleanup did not settle: ${error instanceof Error ? error.message : String(error)}`,
16648
+ "switch-session-subagent-cleanup",
16649
+ );
16650
+ }
16651
+ }
16652
+ transitionCleanupCommitted = true;
16653
+ // Different files may intentionally carry the same copied session id; pathname transition is the commit signal.
16654
+ ownerShutdownTransitionCommitted = true;
16655
+ if (ownerShutdownSettled) {
16656
+ this.sessionManager.retireEphemeralArtifactsAfterTransition();
16657
+ await this.#runToolSessionTransitionCleanups();
16658
+ }
16659
+ }
16368
16660
  this.#reconnectToAgent();
16369
16661
  // Fence predecessor continuations before session_switch starts SDK runtime
16370
16662
  // teardown. The previous runtime waits for those continuations to settle;
@@ -16373,7 +16665,8 @@ export class AgentSession {
16373
16665
  this.#bindWorkflowGateEmitter(previousSessionState.sessionId, suspendedWorkflowGateEmitter);
16374
16666
  // session_switch is the post-commit identity signal. SDK authority and
16375
16667
  // other identity-bound integrations must not observe the successor until
16376
- // messages, model state, MCP selections, and the agent subscription are live.
16668
+ // messages, model state, MCP selections, the agent subscription, and
16669
+ // session-scoped tool cleanup are complete.
16377
16670
  if (this.#extensionRunner) {
16378
16671
  await this.#extensionRunner.emit({
16379
16672
  type: "session_switch",
@@ -16384,6 +16677,7 @@ export class AgentSession {
16384
16677
  }
16385
16678
  return true;
16386
16679
  } catch (error) {
16680
+ if (transitionCleanupCommitted) throw error;
16387
16681
  this.sessionManager.restoreState(previousSessionState);
16388
16682
  this.#defaultFallbackController = undefined;
16389
16683
  this.#syncAgentSessionId(previousSessionState.sessionId);
@@ -16442,6 +16736,12 @@ export class AgentSession {
16442
16736
  throw error;
16443
16737
  }
16444
16738
  } finally {
16739
+ if (ownerShutdownManager && ownerShutdownLease && !ownerShutdownFinalizationDeferred) {
16740
+ ownerShutdownManager.finishOwnerSubagentShutdown(
16741
+ ownerShutdownLease,
16742
+ ownerShutdownTransitionCommitted ? "commit" : "release",
16743
+ );
16744
+ }
16445
16745
  this.#endSessionTransition();
16446
16746
  }
16447
16747
  }
@@ -16493,13 +16793,14 @@ export class AgentSession {
16493
16793
  : await this.sessionManager.prepareNewSession({ parentSession: previousSessionFile });
16494
16794
  try {
16495
16795
  await initializeLocalRoot(this.#localProtocolOptions(prepared));
16796
+ await this.#settleOwnAsyncJobsBeforeArtifactRetirement();
16496
16797
  this.sessionManager.commitPreparedNewSession(prepared);
16798
+ await this.#runToolSessionTransitionCleanups();
16497
16799
  } catch (error) {
16498
16800
  throw await discardPreparedNewSessionAfterFailure(this.sessionManager, prepared, error);
16499
16801
  }
16500
16802
  this.#pendingNextTurnMessages = [];
16501
16803
  this.#scheduledHiddenNextTurnGeneration = undefined;
16502
- this.#cancelOwnAsyncJobs();
16503
16804
 
16504
16805
  this.#syncTodoPhasesFromBranch();
16505
16806
  this.#syncAgentSessionId();