@gajae-code/coding-agent 0.15.6 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/dist/types/cli/args.d.ts +2 -0
  3. package/dist/types/cli/root-flags.d.ts +14 -0
  4. package/dist/types/cli/update-cli.d.ts +13 -0
  5. package/dist/types/cli.d.ts +7 -0
  6. package/dist/types/commands/launch.d.ts +10 -0
  7. package/dist/types/config/autorouting-tier-map.d.ts +24 -0
  8. package/dist/types/config/file-lock.d.ts +5 -0
  9. package/dist/types/config/model-registry.d.ts +6 -0
  10. package/dist/types/config/settings-schema.d.ts +10 -1
  11. package/dist/types/config/settings.d.ts +2 -0
  12. package/dist/types/coordinator-mcp/projection-scan.d.ts +8 -0
  13. package/dist/types/crash/record-loader.d.ts +15 -0
  14. package/dist/types/gjc-runtime/launch-worktree-reservation.d.ts +21 -0
  15. package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +2 -0
  16. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +66 -1
  17. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +1 -0
  18. package/dist/types/gjc-runtime/tmux-sessions.d.ts +27 -0
  19. package/dist/types/master-mode/context.d.ts +21 -0
  20. package/dist/types/master-mode/first-request.d.ts +34 -0
  21. package/dist/types/master-mode/snapshot.d.ts +32 -0
  22. package/dist/types/modes/components/frictionless-onboarding-selector.d.ts +10 -0
  23. package/dist/types/modes/components/oauth-selector.d.ts +1 -0
  24. package/dist/types/modes/components/status-line/model-name.d.ts +18 -0
  25. package/dist/types/modes/components/status-line/priority-row.d.ts +34 -0
  26. package/dist/types/modes/components/status-line/segments.d.ts +10 -0
  27. package/dist/types/modes/print-mode.d.ts +17 -0
  28. package/dist/types/modes/ui-language.d.ts +7 -1
  29. package/dist/types/sdk/broker/broker.d.ts +77 -1
  30. package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
  31. package/dist/types/sdk/broker/index.d.ts +3 -0
  32. package/dist/types/sdk/broker/lifecycle.d.ts +26 -0
  33. package/dist/types/sdk/broker/master-capability.d.ts +10 -0
  34. package/dist/types/sdk/broker/session-index.d.ts +40 -11
  35. package/dist/types/sdk/broker/session-scope.d.ts +72 -0
  36. package/dist/types/sdk/broker/spawn-authority.d.ts +201 -0
  37. package/dist/types/sdk/broker/spawn-substrate.d.ts +30 -0
  38. package/dist/types/sdk/broker/state-version.d.ts +4 -1
  39. package/dist/types/sdk/broker/worktree-occupancy.d.ts +28 -0
  40. package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -0
  41. package/dist/types/sdk/bus/index.d.ts +6 -0
  42. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  43. package/dist/types/sdk/cli/index.d.ts +1 -0
  44. package/dist/types/sdk/cli/master-cli.d.ts +63 -0
  45. package/dist/types/sdk/cli/rows.d.ts +4 -3
  46. package/dist/types/sdk/cli/session-cli.d.ts +18 -2
  47. package/dist/types/sdk/client/client.d.ts +3 -2
  48. package/dist/types/sdk/host/control/dispatch.d.ts +10 -0
  49. package/dist/types/sdk/host/control/index.d.ts +1 -1
  50. package/dist/types/sdk/host/control/operations.d.ts +2 -0
  51. package/dist/types/sdk/host/control/runtime-gate.d.ts +21 -7
  52. package/dist/types/sdk/host/host.d.ts +10 -0
  53. package/dist/types/sdk/host/session-runtime.d.ts +52 -0
  54. package/dist/types/sdk/lifecycle/broker-client.d.ts +6 -0
  55. package/dist/types/sdk/lifecycle/service.d.ts +15 -3
  56. package/dist/types/sdk/lifecycle-session.d.ts +2 -0
  57. package/dist/types/sdk/router/session-router.d.ts +2 -0
  58. package/dist/types/sdk/session.d.ts +16 -0
  59. package/dist/types/sdk/startup-capability.d.ts +2 -1
  60. package/dist/types/session/agent-session.d.ts +5 -0
  61. package/dist/types/session/credential-store-errors.d.ts +1 -0
  62. package/dist/types/session/session-manager.d.ts +65 -0
  63. package/dist/types/session/yield-queue.d.ts +5 -4
  64. package/dist/types/setup/frictionless-onboarding.d.ts +1 -0
  65. package/dist/types/tools/bash.d.ts +4 -0
  66. package/dist/types/tools/gh.d.ts +8 -0
  67. package/dist/types/tools/index.d.ts +4 -0
  68. package/package.json +7 -7
  69. package/scripts/generate-sdk-operation-inventory.ts +6 -0
  70. package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
  71. package/src/cli/accounts-cli.ts +9 -5
  72. package/src/cli/args.ts +17 -0
  73. package/src/cli/fast-help.ts +1 -0
  74. package/src/cli/root-flags.ts +5 -0
  75. package/src/cli/update-cli.ts +77 -11
  76. package/src/commands/launch.ts +86 -18
  77. package/src/commands/sdk.ts +113 -21
  78. package/src/config/autorouting-tier-map.ts +12 -0
  79. package/src/config/file-lock.ts +158 -83
  80. package/src/config/model-preset-registry.ts +1 -5
  81. package/src/config/model-profiles.ts +5 -5
  82. package/src/config/model-registry.ts +23 -0
  83. package/src/config/model-resolver.ts +5 -0
  84. package/src/config/settings-schema.ts +8 -1
  85. package/src/config/settings.ts +5 -0
  86. package/src/coordinator-mcp/projection-scan.ts +21 -0
  87. package/src/coordinator-mcp/question-state.ts +9 -11
  88. package/src/coordinator-mcp/server.ts +94 -20
  89. package/src/crash/index-store.ts +2 -0
  90. package/src/crash/record-loader.ts +10 -0
  91. package/src/gjc-runtime/launch-worktree-reservation.ts +242 -0
  92. package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
  93. package/src/gjc-runtime/session-state-lock.ts +79 -6
  94. package/src/gjc-runtime/session-state-sidecar.ts +929 -23
  95. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
  96. package/src/gjc-runtime/tmux-owner-isolation.ts +2 -2
  97. package/src/gjc-runtime/tmux-sessions.ts +252 -13
  98. package/src/hooks/skill-state.ts +6 -0
  99. package/src/internal-urls/docs-index.generated.ts +7 -7
  100. package/src/internal-urls/issue-pr-protocol.ts +4 -2
  101. package/src/main.ts +57 -2
  102. package/src/master-mode/context.ts +50 -0
  103. package/src/master-mode/first-request.ts +65 -0
  104. package/src/master-mode/snapshot.ts +205 -0
  105. package/src/modes/acp/acp-agent.ts +605 -156
  106. package/src/modes/components/footer.ts +4 -2
  107. package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
  108. package/src/modes/components/oauth-selector.ts +6 -1
  109. package/src/modes/components/settings-selector.ts +1 -0
  110. package/src/modes/components/status-line/model-name.ts +53 -0
  111. package/src/modes/components/status-line/priority-row.ts +107 -0
  112. package/src/modes/components/status-line/segments.ts +23 -9
  113. package/src/modes/components/tool-status-header.ts +151 -15
  114. package/src/modes/controllers/input-controller.ts +28 -0
  115. package/src/modes/controllers/selector-controller.ts +191 -96
  116. package/src/modes/print-mode.ts +64 -0
  117. package/src/modes/ui-language.ts +49 -2
  118. package/src/prompts/system/master-mode.md +28 -0
  119. package/src/sdk/broker/broker.ts +1636 -26
  120. package/src/sdk/broker/endpoint-authority.ts +86 -0
  121. package/src/sdk/broker/ensure.ts +5 -0
  122. package/src/sdk/broker/index.ts +3 -0
  123. package/src/sdk/broker/lifecycle.ts +186 -41
  124. package/src/sdk/broker/master-capability.ts +232 -0
  125. package/src/sdk/broker/session-index.ts +121 -17
  126. package/src/sdk/broker/session-scope.ts +272 -0
  127. package/src/sdk/broker/spawn-authority.ts +1031 -0
  128. package/src/sdk/broker/spawn-substrate.ts +548 -0
  129. package/src/sdk/broker/state-version.ts +26 -14
  130. package/src/sdk/broker/transport.ts +1 -0
  131. package/src/sdk/broker/worktree-occupancy.ts +90 -0
  132. package/src/sdk/bus/chat-daemon-control.ts +5 -2
  133. package/src/sdk/bus/chat-daemon-runtime.ts +35 -4
  134. package/src/sdk/bus/index.ts +51 -4
  135. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  136. package/src/sdk/cli/index.ts +1 -0
  137. package/src/sdk/cli/master-cli.ts +259 -0
  138. package/src/sdk/cli/rows.ts +11 -8
  139. package/src/sdk/cli/session-cli.ts +264 -5
  140. package/src/sdk/client/client.ts +14 -8
  141. package/src/sdk/host/control/dispatch.ts +99 -38
  142. package/src/sdk/host/control/index.ts +2 -0
  143. package/src/sdk/host/control/operations.ts +2 -0
  144. package/src/sdk/host/control/runtime-gate.ts +203 -11
  145. package/src/sdk/host/host.ts +27 -2
  146. package/src/sdk/host/session-runtime.test.ts +210 -6
  147. package/src/sdk/host/session-runtime.ts +291 -49
  148. package/src/sdk/lifecycle/broker-client.ts +28 -0
  149. package/src/sdk/lifecycle/service.ts +279 -20
  150. package/src/sdk/lifecycle-session.ts +10 -2
  151. package/src/sdk/protocol/operation-inventory.generated.json +51 -0
  152. package/src/sdk/protocol/operation-registry.ts +13 -3
  153. package/src/sdk/router/session-router.ts +67 -59
  154. package/src/sdk/session.ts +357 -155
  155. package/src/sdk/startup-capability.ts +1 -0
  156. package/src/session/agent-session.ts +640 -103
  157. package/src/session/blob-store.ts +15 -5
  158. package/src/session/credential-store-errors.ts +2 -0
  159. package/src/session/internal/managed-session-storage.ts +153 -78
  160. package/src/session/session-manager.ts +337 -24
  161. package/src/session/yield-queue.ts +24 -10
  162. package/src/setup/frictionless-onboarding.ts +2 -0
  163. package/src/telemetry/events.ts +1138 -12
  164. package/src/tools/bash.ts +256 -42
  165. package/src/tools/gh.ts +104 -48
  166. package/src/tools/github-cache.ts +2 -2
  167. package/src/tools/index.ts +4 -0
@@ -7299,8 +7299,49 @@ export class SessionManager {
7299
7299
  /** Serializes model, SDK, and ACP cwd transitions; dispose joins this tail. */
7300
7300
  #cwdTransitionTail: Promise<void> = Promise.resolve();
7301
7301
  #cwdTransitionOwner: symbol | undefined;
7302
+ #cwdMoveAdmissionClosed = false;
7302
7303
  #cwdReadLeaseOwner = Symbol("cwd-read-lease-owner");
7303
7304
  #cwdGeneration = 0;
7305
+ /**
7306
+ * Listeners invoked after every committed `moveTo`, for every move surface
7307
+ * (`move_session`, TUI `/move`, SDK/ACP `session.cwd.move`). They receive the previous
7308
+ * and new cwd and the previous session file, and run best-effort: a listener throw is
7309
+ * logged and never turns an already-durable move into a rejection.
7310
+ */
7311
+ #afterMoveListeners = new Set<
7312
+ (move: { previousCwd: string; newCwd: string; previousSessionFile: string | undefined }) => void | Promise<void>
7313
+ >();
7314
+ #beforeMoveListeners = new Set<
7315
+ (move: {
7316
+ previousCwd: string;
7317
+ newCwd: string;
7318
+ previousSessionFile: string | undefined;
7319
+ newSessionFile: string | undefined;
7320
+ previousCwdIdentity: { dev: bigint; ino: bigint };
7321
+ newCwdIdentity: { dev: bigint; ino: bigint };
7322
+ }) => void | Promise<void>
7323
+ >();
7324
+ #moveAbortListeners = new Set<
7325
+ (move: {
7326
+ previousCwd: string;
7327
+ newCwd: string;
7328
+ previousSessionFile: string | undefined;
7329
+ newSessionFile: string | undefined;
7330
+ previousCwdIdentity: { dev: bigint; ino: bigint };
7331
+ newCwdIdentity: { dev: bigint; ino: bigint };
7332
+ preserveRecoveryJournal?: boolean;
7333
+ }) => void | Promise<void>
7334
+ >();
7335
+ #movePublicationListeners = new Set<
7336
+ (move: {
7337
+ previousCwd: string;
7338
+ newCwd: string;
7339
+ previousSessionFile: string | undefined;
7340
+ newSessionFile: string | undefined;
7341
+ previousCwdIdentity: { dev: bigint; ino: bigint };
7342
+ newCwdIdentity: { dev: bigint; ino: bigint };
7343
+ }) => void | Promise<void>
7344
+ >();
7304
7345
  /** Number of tool executions currently holding a shared read lease on `cwd`. */
7305
7346
  #cwdReaderCount = 0;
7306
7347
  /** Resolved when the last outstanding read lease is released. */
@@ -10728,10 +10769,139 @@ export class SessionManager {
10728
10769
  async joinCwdTransition(): Promise<void> {
10729
10770
  await this.#cwdTransitionTail;
10730
10771
  }
10772
+
10773
+ async closeCwdMoveAdmission(): Promise<void> {
10774
+ await this.runExclusiveCwdTransition(async () => {
10775
+ this.#cwdMoveAdmissionClosed = true;
10776
+ });
10777
+ }
10731
10778
  getCwdGeneration(): number {
10732
10779
  return this.#cwdGeneration;
10733
10780
  }
10734
10781
 
10782
+ /**
10783
+ * Register a listener invoked after every committed `moveTo`. Returns an idempotent
10784
+ * unregister handle. Every move surface funnels through `moveTo`, so a single
10785
+ * registration covers `move_session`, `/move`, and SDK/ACP `session.cwd.move`.
10786
+ */
10787
+ registerAfterMoveListener(
10788
+ listener: (move: {
10789
+ previousCwd: string;
10790
+ newCwd: string;
10791
+ previousSessionFile: string | undefined;
10792
+ }) => void | Promise<void>,
10793
+ ): () => void {
10794
+ this.#afterMoveListeners.add(listener);
10795
+ return () => {
10796
+ this.#afterMoveListeners.delete(listener);
10797
+ };
10798
+ }
10799
+
10800
+ registerBeforeMoveListener(
10801
+ listener: (move: {
10802
+ previousCwd: string;
10803
+ newCwd: string;
10804
+ previousSessionFile: string | undefined;
10805
+ newSessionFile: string | undefined;
10806
+ previousCwdIdentity: { dev: bigint; ino: bigint };
10807
+ newCwdIdentity: { dev: bigint; ino: bigint };
10808
+ preserveRecoveryJournal?: boolean;
10809
+ }) => void | Promise<void>,
10810
+ ): () => void {
10811
+ this.#beforeMoveListeners.add(listener);
10812
+ return () => {
10813
+ this.#beforeMoveListeners.delete(listener);
10814
+ };
10815
+ }
10816
+
10817
+ async #runBeforeMoveListeners(move: {
10818
+ previousCwd: string;
10819
+ newCwd: string;
10820
+ previousSessionFile: string | undefined;
10821
+ newSessionFile: string | undefined;
10822
+ previousCwdIdentity: { dev: bigint; ino: bigint };
10823
+ newCwdIdentity: { dev: bigint; ino: bigint };
10824
+ }): Promise<void> {
10825
+ for (const listener of [...this.#beforeMoveListeners]) await listener(move);
10826
+ }
10827
+
10828
+ registerMovePublicationListener(
10829
+ listener: (move: {
10830
+ previousCwd: string;
10831
+ newCwd: string;
10832
+ previousSessionFile: string | undefined;
10833
+ newSessionFile: string | undefined;
10834
+ previousCwdIdentity: { dev: bigint; ino: bigint };
10835
+ newCwdIdentity: { dev: bigint; ino: bigint };
10836
+ preserveRecoveryJournal?: boolean;
10837
+ }) => void | Promise<void>,
10838
+ ): () => void {
10839
+ this.#movePublicationListeners.add(listener);
10840
+ return () => {
10841
+ this.#movePublicationListeners.delete(listener);
10842
+ };
10843
+ }
10844
+
10845
+ async #runMovePublicationListeners(move: {
10846
+ previousCwd: string;
10847
+ newCwd: string;
10848
+ previousSessionFile: string | undefined;
10849
+ newSessionFile: string | undefined;
10850
+ previousCwdIdentity: { dev: bigint; ino: bigint };
10851
+ newCwdIdentity: { dev: bigint; ino: bigint };
10852
+ }): Promise<void> {
10853
+ for (const listener of [...this.#movePublicationListeners]) await listener(move);
10854
+ }
10855
+
10856
+ registerMoveAbortListener(
10857
+ listener: (move: {
10858
+ previousCwd: string;
10859
+ newCwd: string;
10860
+ previousSessionFile: string | undefined;
10861
+ newSessionFile: string | undefined;
10862
+ previousCwdIdentity: { dev: bigint; ino: bigint };
10863
+ newCwdIdentity: { dev: bigint; ino: bigint };
10864
+ preserveRecoveryJournal?: boolean;
10865
+ }) => void | Promise<void>,
10866
+ ): () => void {
10867
+ this.#moveAbortListeners.add(listener);
10868
+ return () => {
10869
+ this.#moveAbortListeners.delete(listener);
10870
+ };
10871
+ }
10872
+
10873
+ async #runMoveAbortListeners(move: {
10874
+ previousCwd: string;
10875
+ newCwd: string;
10876
+ previousSessionFile: string | undefined;
10877
+ newSessionFile: string | undefined;
10878
+ previousCwdIdentity: { dev: bigint; ino: bigint };
10879
+ newCwdIdentity: { dev: bigint; ino: bigint };
10880
+ preserveRecoveryJournal?: boolean;
10881
+ }): Promise<void> {
10882
+ for (const listener of [...this.#moveAbortListeners]) {
10883
+ try {
10884
+ await listener(move);
10885
+ } catch (error) {
10886
+ logger.warn("move-abort listener failed", { error: String(error), cwd: move.newCwd });
10887
+ }
10888
+ }
10889
+ }
10890
+
10891
+ async #runAfterMoveListeners(move: {
10892
+ previousCwd: string;
10893
+ newCwd: string;
10894
+ previousSessionFile: string | undefined;
10895
+ }): Promise<void> {
10896
+ for (const listener of [...this.#afterMoveListeners]) {
10897
+ try {
10898
+ await listener(move);
10899
+ } catch (error) {
10900
+ logger.warn("after-move listener failed", { error: String(error), cwd: move.newCwd });
10901
+ }
10902
+ }
10903
+ }
10904
+
10735
10905
  #ownsCwdTransition(): boolean {
10736
10906
  const owner = this.#cwdTransitionOwner;
10737
10907
  return owner !== undefined && cwdTransitionAls.getStore() === owner;
@@ -10802,15 +10972,73 @@ export class SessionManager {
10802
10972
  options?: {
10803
10973
  expectedIdentity?: { dev: bigint; ino: bigint };
10804
10974
  targetHandle?: { stat: (opts: { bigint: true }) => Promise<fs.BigIntStats> };
10975
+ expectedSourceIdentity?: { dev: bigint; ino: bigint };
10976
+ sourceHandle?: { stat: (opts: { bigint: true }) => Promise<fs.BigIntStats> };
10805
10977
  },
10806
10978
  ): Promise<void> {
10807
10979
  if (!this.#ownsCwdTransition()) {
10808
10980
  return this.runExclusiveCwdTransition(() => this.moveTo(newCwd, options));
10809
10981
  }
10810
- const resolvedCwd = path.resolve(newCwd);
10982
+ if (this.#cwdMoveAdmissionClosed) throw new Error("Session cwd move admission is closed.");
10983
+ if (!options?.sourceHandle) {
10984
+ const sourceHandle = await SessionManager.openNoFollowDirectory(this.cwd);
10985
+ try {
10986
+ const sourceIdentity = await sourceHandle.stat({ bigint: true });
10987
+ if (!sourceIdentity.isDirectory()) throw new Error(`Current cwd is not a directory: ${this.cwd}`);
10988
+ return await this.moveTo(newCwd, {
10989
+ ...options,
10990
+ expectedSourceIdentity: options?.expectedSourceIdentity ?? {
10991
+ dev: sourceIdentity.dev,
10992
+ ino: sourceIdentity.ino,
10993
+ },
10994
+ sourceHandle,
10995
+ });
10996
+ } finally {
10997
+ await sourceHandle.close();
10998
+ }
10999
+ }
11000
+ const resolvedCwd = await fs.promises.realpath(path.resolve(newCwd));
11001
+ const resolvedIdentity = await fs.promises.lstat(resolvedCwd, { bigint: true });
11002
+ if (!resolvedIdentity.isDirectory() || resolvedIdentity.isSymbolicLink()) {
11003
+ throw new Error(`Refusing to rescope: target is not a directory: ${resolvedCwd}`);
11004
+ }
11005
+ if (!options?.targetHandle) {
11006
+ const targetHandle = await SessionManager.openNoFollowDirectory(resolvedCwd);
11007
+ try {
11008
+ const targetIdentity = await targetHandle.stat({ bigint: true });
11009
+ const revalidatedCwd = await fs.promises.realpath(path.resolve(newCwd));
11010
+ const namedIdentity = await fs.promises.lstat(revalidatedCwd, { bigint: true });
11011
+ if (
11012
+ revalidatedCwd !== resolvedCwd ||
11013
+ !namedIdentity.isDirectory() ||
11014
+ namedIdentity.isSymbolicLink() ||
11015
+ namedIdentity.dev !== resolvedIdentity.dev ||
11016
+ namedIdentity.ino !== resolvedIdentity.ino ||
11017
+ targetIdentity.dev !== resolvedIdentity.dev ||
11018
+ targetIdentity.ino !== resolvedIdentity.ino
11019
+ )
11020
+ throw new Error(`Refusing to rescope: target identity changed: ${resolvedCwd}`);
11021
+ return await this.moveTo(resolvedCwd, {
11022
+ ...options,
11023
+ expectedIdentity: options?.expectedIdentity ?? {
11024
+ dev: targetIdentity.dev,
11025
+ ino: targetIdentity.ino,
11026
+ },
11027
+ targetHandle,
11028
+ });
11029
+ } finally {
11030
+ await targetHandle.close();
11031
+ }
11032
+ }
10811
11033
  if (options?.expectedIdentity || options?.targetHandle) {
10812
11034
  await this.#assertCwdTargetIdentity(resolvedCwd, options);
10813
11035
  }
11036
+ if (options?.expectedSourceIdentity || options?.sourceHandle) {
11037
+ await this.#assertCwdTargetIdentity(this.cwd, {
11038
+ expectedIdentity: options.expectedSourceIdentity,
11039
+ targetHandle: options.sourceHandle,
11040
+ });
11041
+ }
10814
11042
  if (resolvedCwd === this.cwd) return;
10815
11043
  const previousCwd = this.cwd;
10816
11044
  const previousSessionDir = this.sessionDir;
@@ -10826,6 +11054,19 @@ export class SessionManager {
10826
11054
  )
10827
11055
  : this.destination;
10828
11056
  const newSessionDir = nextDestination.directory;
11057
+ const nextSessionFile = this.#sessionFile
11058
+ ? path.join(newSessionDir, path.basename(this.#sessionFile))
11059
+ : undefined;
11060
+ const pinnedSourceIdentity = await options.sourceHandle.stat({ bigint: true });
11061
+ const pinnedTargetIdentity = await options.targetHandle.stat({ bigint: true });
11062
+ const moveDetails = {
11063
+ previousCwd,
11064
+ newCwd: resolvedCwd,
11065
+ previousSessionFile,
11066
+ newSessionFile: nextSessionFile,
11067
+ previousCwdIdentity: { dev: pinnedSourceIdentity.dev, ino: pinnedSourceIdentity.ino },
11068
+ newCwdIdentity: { dev: pinnedTargetIdentity.dev, ino: pinnedTargetIdentity.ino },
11069
+ };
10829
11070
  let hadSessionFile = false;
10830
11071
  const managedMove = this.destination.kind === "managed" && nextDestination.kind === "managed";
10831
11072
  let managedSourceStore: ManagedSessionDescendantStore | undefined;
@@ -10833,16 +11074,29 @@ export class SessionManager {
10833
11074
  let rollbackManagedMove: (() => Promise<void>) | undefined;
10834
11075
  let residentTransition: PreparedResidentStoreTransition | undefined;
10835
11076
  const hadPersistedSession = this.#ensuredOnDisk;
11077
+ try {
11078
+ await this.#runBeforeMoveListeners(moveDetails);
11079
+ } catch (error) {
11080
+ await this.#runMoveAbortListeners(moveDetails);
11081
+ throw error;
11082
+ }
10836
11083
 
11084
+ let targetIdentityRevalidated = false;
11085
+ let publicationStarted = false;
10837
11086
  if (this.persist && this.#sessionFile) {
10838
11087
  // Close the persist writer before moving files
10839
- await this.#closePersistWriter();
11088
+ try {
11089
+ await this.#closePersistWriter();
11090
+ } catch (error) {
11091
+ await this.#runMoveAbortListeners(moveDetails);
11092
+ throw error;
11093
+ }
10840
11094
  this.#persistChain = Promise.resolve();
10841
11095
  this.#persistError = undefined;
10842
11096
  this.#persistErrorReported = false;
10843
11097
 
10844
11098
  const oldSessionFile = this.#sessionFile;
10845
- const newSessionFile = path.join(newSessionDir, path.basename(oldSessionFile));
11099
+ const newSessionFile = nextSessionFile!;
10846
11100
  const oldArtifactDir = oldSessionFile.slice(0, -6); // strip .jsonl
10847
11101
  const newArtifactDir = newSessionFile.slice(0, -6);
10848
11102
  let managedTranscript!: ReturnType<ManagedSessionDescendantStore["readExpected"]>;
@@ -10852,24 +11106,30 @@ export class SessionManager {
10852
11106
  hadSessionFile = managedMove ? false : this.#storage.existsSync(oldSessionFile);
10853
11107
  let movedSessionFile = false;
10854
11108
  let movedArtifactDir = false;
10855
- const materializedEntries = materializeResidentEntriesForReadSync(
10856
- this.#fileEntries,
10857
- this.#residentBlobStores(),
10858
- );
10859
- const transitionEntries: FileEntry[] = materializedEntries.map(entry =>
10860
- entry.type === "session" ? { ...entry, cwd: resolvedCwd } : entry,
10861
- );
10862
- residentTransition = this.#prepareResidentTextStoreTransition(
10863
- {
10864
- target: { sessionId: this.#sessionId, sessionFile: newSessionFile },
10865
- primary: {
10866
- mode: "materialize",
10867
- sourceEntries: transitionEntries,
10868
- sourceStores: { textStore: null, imageStore: this.#residentImageBlobStore },
11109
+ try {
11110
+ const materializedEntries = materializeResidentEntriesForReadSync(
11111
+ this.#fileEntries,
11112
+ this.#residentBlobStores(),
11113
+ );
11114
+ const transitionEntries: FileEntry[] = materializedEntries.map(entry =>
11115
+ entry.type === "session" ? { ...entry, cwd: resolvedCwd } : entry,
11116
+ );
11117
+ residentTransition = this.#prepareResidentTextStoreTransition(
11118
+ {
11119
+ target: { sessionId: this.#sessionId, sessionFile: newSessionFile },
11120
+ primary: {
11121
+ mode: "materialize",
11122
+ sourceEntries: transitionEntries,
11123
+ sourceStores: { textStore: null, imageStore: this.#residentImageBlobStore },
11124
+ },
10869
11125
  },
10870
- },
10871
- "retain-and-throw",
10872
- );
11126
+ "retain-and-throw",
11127
+ );
11128
+ } catch (error) {
11129
+ residentTransition?.dispose();
11130
+ await this.#runMoveAbortListeners(moveDetails);
11131
+ throw error;
11132
+ }
10873
11133
  const discardResidentTransitionAndThrow = (error: unknown): never => {
10874
11134
  residentTransition?.dispose();
10875
11135
  throw error;
@@ -10912,6 +11172,14 @@ export class SessionManager {
10912
11172
  throw new Error("artifact_destination_mismatch");
10913
11173
  destination.fsyncTree();
10914
11174
  };
11175
+ try {
11176
+ await this.#runMovePublicationListeners(moveDetails);
11177
+ publicationStarted = true;
11178
+ } catch (error) {
11179
+ residentTransition?.dispose();
11180
+ await this.#runMoveAbortListeners(moveDetails);
11181
+ throw error;
11182
+ }
10915
11183
  try {
10916
11184
  if (managedMove) {
10917
11185
  if (previousDestination.kind !== "managed" || nextDestination.kind !== "managed")
@@ -10997,6 +11265,16 @@ export class SessionManager {
10997
11265
  if (!isEnoent(err)) throw err;
10998
11266
  }
10999
11267
  }
11268
+ if (options?.expectedIdentity || options?.targetHandle) {
11269
+ if (options.expectedSourceIdentity || options.sourceHandle) {
11270
+ await this.#assertCwdTargetIdentity(previousCwd, {
11271
+ expectedIdentity: options.expectedSourceIdentity,
11272
+ targetHandle: options.sourceHandle,
11273
+ });
11274
+ }
11275
+ await this.#assertCwdTargetIdentity(resolvedCwd, options);
11276
+ targetIdentityRevalidated = true;
11277
+ }
11000
11278
  } catch (err) {
11001
11279
  if (managedMove && managedDestinationStore) {
11002
11280
  try {
@@ -11038,6 +11316,7 @@ export class SessionManager {
11038
11316
  )
11039
11317
  await managedSourceStore.publishNoReplace(path.basename(oldSessionFile), managedTranscript.bytes);
11040
11318
  } catch (rollbackErr) {
11319
+ await this.#runMoveAbortListeners({ ...moveDetails, preserveRecoveryJournal: true });
11041
11320
  discardResidentTransitionAndThrow(
11042
11321
  new Error(`Failed to rollback managed move: ${toError(rollbackErr).message}`, {
11043
11322
  cause: toError(err),
@@ -11045,18 +11324,46 @@ export class SessionManager {
11045
11324
  );
11046
11325
  }
11047
11326
  } else {
11048
- if (movedArtifactDir) await movePathAcrossDevicesSafe(newArtifactDir, oldArtifactDir);
11049
- if (movedSessionFile) await movePathAcrossDevicesSafe(newSessionFile, oldSessionFile);
11327
+ try {
11328
+ if (movedArtifactDir) await movePathAcrossDevicesSafe(newArtifactDir, oldArtifactDir);
11329
+ if (movedSessionFile) await movePathAcrossDevicesSafe(newSessionFile, oldSessionFile);
11330
+ } catch (rollbackError) {
11331
+ await this.#runMoveAbortListeners({ ...moveDetails, preserveRecoveryJournal: true });
11332
+ discardResidentTransitionAndThrow(
11333
+ new Error(`Failed to rollback move: ${toError(rollbackError).message}`, { cause: toError(err) }),
11334
+ );
11335
+ }
11050
11336
  }
11337
+ await this.#runMoveAbortListeners(moveDetails);
11051
11338
  discardResidentTransitionAndThrow(err);
11052
11339
  }
11053
11340
  this.#sessionFile = newSessionFile;
11054
11341
  }
11342
+ if (!publicationStarted) {
11343
+ try {
11344
+ await this.#runMovePublicationListeners(moveDetails);
11345
+ publicationStarted = true;
11346
+ } catch (error) {
11347
+ await this.#runMoveAbortListeners(moveDetails);
11348
+ throw error;
11349
+ }
11350
+ }
11055
11351
 
11056
11352
  // Update cwd and sessionDir after physical publication succeeds. Metadata failures restore the source
11057
11353
  // authority but deliberately retain any destination publication evidence rather than deleting it.
11058
- if (options?.expectedIdentity || options?.targetHandle) {
11059
- await this.#assertCwdTargetIdentity(resolvedCwd, options);
11354
+ if (!targetIdentityRevalidated && (options?.expectedIdentity || options?.targetHandle)) {
11355
+ try {
11356
+ if (options.expectedSourceIdentity || options.sourceHandle) {
11357
+ await this.#assertCwdTargetIdentity(previousCwd, {
11358
+ expectedIdentity: options.expectedSourceIdentity,
11359
+ targetHandle: options.sourceHandle,
11360
+ });
11361
+ }
11362
+ await this.#assertCwdTargetIdentity(resolvedCwd, options);
11363
+ } catch (error) {
11364
+ await this.#runMoveAbortListeners(moveDetails);
11365
+ throw error;
11366
+ }
11060
11367
  }
11061
11368
  this.#cwdGeneration += 1;
11062
11369
  this.cwd = resolvedCwd;
@@ -11096,6 +11403,7 @@ export class SessionManager {
11096
11403
  await rollbackManagedMove();
11097
11404
  } catch (rollbackError) {
11098
11405
  if (toError(rollbackError).message !== "cleanup_pending") {
11406
+ await this.#runMoveAbortListeners({ ...moveDetails, preserveRecoveryJournal: true });
11099
11407
  throw new Error(`Failed to rollback managed move: ${toError(rollbackError).message}`, {
11100
11408
  cause: toError(error),
11101
11409
  });
@@ -11116,12 +11424,14 @@ export class SessionManager {
11116
11424
  const header = this.#fileEntries.find(entry => entry.type === "session") as SessionHeader | undefined;
11117
11425
  if (header) applyHeaderPatch(header, { cwd: previousCwd });
11118
11426
  this.#headerExportRevision++;
11427
+ await this.#runMoveAbortListeners(moveDetails);
11119
11428
  throw error;
11120
11429
  }
11121
11430
  // The destination has already been published. Retain it rather than moving it
11122
11431
  // back over the source after a metadata failure.
11123
11432
  this.#managedTranscriptStoreCache = null;
11124
11433
  this.#headerExportRevision++;
11434
+ await this.#runAfterMoveListeners(moveDetails);
11125
11435
  throw error;
11126
11436
  }
11127
11437
  if (residentTransition) this.#commitResidentTextStoreTransition(residentTransition);
@@ -11130,6 +11440,9 @@ export class SessionManager {
11130
11440
  if (this.#sessionFile) {
11131
11441
  writeTerminalBreadcrumb(resolvedCwd, this.#sessionFile);
11132
11442
  }
11443
+ // The move is fully committed here; notify every surface's after-move listeners
11444
+ // (coordinator runtime-state relocation, postmortem finalizer rebinding, ...).
11445
+ await this.#runAfterMoveListeners(moveDetails);
11133
11446
  }
11134
11447
 
11135
11448
  /** Sync version for initial creation (no existing writer to close). */
@@ -18,8 +18,9 @@ export interface YieldDispatcher<P> {
18
18
  export interface YieldQueueOptions {
19
19
  isStreaming: () => boolean;
20
20
  injectStreaming(msg: AgentMessage): void;
21
- injectIdle(messages: AgentMessage[]): Promise<void>;
22
- scheduleIdleFlush(run: () => Promise<void>): void;
21
+ injectIdle(messages: AgentMessage[], signal?: AbortSignal): Promise<void>;
22
+ scheduleIdleFlush(run: (signal?: AbortSignal) => Promise<void>, onSkip: () => void): void;
23
+ getIdleFlushSignal?(): AbortSignal | undefined;
23
24
  }
24
25
 
25
26
  type YieldFlushMode = "streaming" | "idle";
@@ -39,6 +40,7 @@ export class YieldQueue {
39
40
  readonly #dispatchers = new Map<string, StoredDispatcher>();
40
41
  readonly #entries = new Map<string, unknown[]>();
41
42
  #idleFlushPending = false;
43
+ #idleFlushPendingOwner: symbol | undefined;
42
44
 
43
45
  constructor(options: YieldQueueOptions) {
44
46
  this.#options = options;
@@ -82,9 +84,10 @@ export class YieldQueue {
82
84
  return false;
83
85
  }
84
86
 
85
- async flush(mode: YieldFlushMode): Promise<void> {
87
+ async flush(mode: YieldFlushMode, signal?: AbortSignal): Promise<void> {
86
88
  if (mode === "idle") {
87
89
  this.#idleFlushPending = false;
90
+ this.#idleFlushPendingOwner = undefined;
88
91
  }
89
92
  const idleMessages: AgentMessage[] = [];
90
93
  for (const [kind, dispatcher] of this.#dispatchers) {
@@ -105,16 +108,20 @@ export class YieldQueue {
105
108
  }
106
109
  if (mode === "idle" && idleMessages.length > 0) {
107
110
  try {
108
- await this.#options.injectIdle(idleMessages);
111
+ await this.#options.injectIdle(idleMessages, signal ?? this.#options.getIdleFlushSignal?.());
109
112
  } catch (error) {
110
113
  logger.warn("Yield queue idle dispatch failed", { error: formatError(error) });
111
114
  }
112
115
  }
113
116
  }
114
117
 
115
- clear(): void {
118
+ clear(onDrop?: (kind: string, entries: readonly unknown[]) => void): void {
119
+ if (onDrop) {
120
+ for (const [kind, entries] of this.#entries) onDrop(kind, entries);
121
+ }
116
122
  this.#entries.clear();
117
123
  this.#idleFlushPending = false;
124
+ this.#idleFlushPendingOwner = undefined;
118
125
  }
119
126
 
120
127
  /** Drop only the queued entries of a single kind, leaving other kinds intact. */
@@ -140,14 +147,21 @@ export class YieldQueue {
140
147
  #scheduleIdleFlush(): void {
141
148
  if (this.#idleFlushPending) return;
142
149
  this.#idleFlushPending = true;
150
+ const owner = Symbol("idle-flush");
151
+ this.#idleFlushPendingOwner = owner;
152
+ const releaseOwner = () => {
153
+ if (this.#idleFlushPendingOwner !== owner) return;
154
+ this.#idleFlushPendingOwner = undefined;
155
+ this.#idleFlushPending = false;
156
+ };
143
157
  try {
144
- this.#options.scheduleIdleFlush(async () => {
145
- this.#idleFlushPending = false;
158
+ this.#options.scheduleIdleFlush(async signal => {
159
+ releaseOwner();
146
160
  if (this.#options.isStreaming()) return;
147
- await this.flush("idle");
148
- });
161
+ await this.flush("idle", signal);
162
+ }, releaseOwner);
149
163
  } catch (error) {
150
- this.#idleFlushPending = false;
164
+ releaseOwner();
151
165
  logger.warn("Yield queue idle flush scheduling failed", { error: formatError(error) });
152
166
  }
153
167
  }
@@ -748,6 +748,7 @@ export function shouldOfferOnboarding(state: FrictionlessOnboardingState): boole
748
748
 
749
749
  export interface AutomaticOnboardingInput {
750
750
  normalInteractive: boolean;
751
+ automation?: boolean;
751
752
  initialMessage?: string;
752
753
  initialMessages: readonly string[];
753
754
  initialImages?: readonly unknown[];
@@ -757,6 +758,7 @@ export interface AutomaticOnboardingInput {
757
758
  export function shouldOfferAutomaticOnboarding(input: AutomaticOnboardingInput): boolean {
758
759
  return (
759
760
  input.normalInteractive &&
761
+ !input.automation &&
760
762
  !input.initialMessage &&
761
763
  input.initialMessages.length === 0 &&
762
764
  (input.initialImages?.length ?? 0) === 0 &&