@gajae-code/coding-agent 0.11.6 → 0.11.8

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 (259) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/dist/types/capability/index.d.ts +10 -20
  3. package/dist/types/capability/types.d.ts +3 -0
  4. package/dist/types/cli/args.d.ts +0 -2
  5. package/dist/types/commands/daemon.d.ts +2 -2
  6. package/dist/types/commands/sdk.d.ts +20 -2
  7. package/dist/types/config/keybindings.d.ts +34 -4
  8. package/dist/types/config/model-profile-activation.d.ts +4 -3
  9. package/dist/types/config/model-profile-contract.d.ts +38 -0
  10. package/dist/types/config/settings-schema.d.ts +70 -7
  11. package/dist/types/edit/modes/patch.d.ts +13 -0
  12. package/dist/types/edit/path-mutation-lock.d.ts +15 -0
  13. package/dist/types/eval/py/executor.d.ts +1 -1
  14. package/dist/types/eval/py/runner-artifact.d.ts +8 -0
  15. package/dist/types/eval/py/tool-bridge.d.ts +1 -2
  16. package/dist/types/gjc-runtime/repository-binding.d.ts +63 -0
  17. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +6 -5
  18. package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +20 -0
  19. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +43 -5
  20. package/dist/types/main.d.ts +32 -1
  21. package/dist/types/modes/components/btw-panel.d.ts +1 -0
  22. package/dist/types/modes/components/model-selector.d.ts +2 -0
  23. package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +2 -1
  24. package/dist/types/modes/components/tool-status-header.d.ts +4 -2
  25. package/dist/types/modes/components/welcome.d.ts +2 -0
  26. package/dist/types/modes/controllers/btw-controller.d.ts +5 -0
  27. package/dist/types/modes/controllers/command-controller.d.ts +2 -0
  28. package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
  29. package/dist/types/modes/interactive-mode.d.ts +7 -2
  30. package/dist/types/modes/prompt-action-autocomplete.d.ts +6 -1
  31. package/dist/types/modes/prompt-suggestion-controller.d.ts +35 -0
  32. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +2 -2
  33. package/dist/types/modes/types.d.ts +4 -0
  34. package/dist/types/modes/utils/hotkeys-markdown.d.ts +2 -1
  35. package/dist/types/runtime-mcp/content-limits.d.ts +7 -0
  36. package/dist/types/runtime-mcp/manager.d.ts +1 -1
  37. package/dist/types/runtime-mcp/oauth-flow.d.ts +0 -5
  38. package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +14 -0
  39. package/dist/types/sdk/broker/broker.d.ts +2 -0
  40. package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
  41. package/dist/types/sdk/bus/chat-daemon-control.d.ts +6 -1
  42. package/dist/types/sdk/bus/config-commands.d.ts +2 -2
  43. package/dist/types/sdk/bus/config.d.ts +2 -0
  44. package/dist/types/sdk/bus/conversation-store.d.ts +1 -0
  45. package/dist/types/sdk/bus/index.d.ts +26 -1
  46. package/dist/types/sdk/bus/lifecycle-commands.d.ts +5 -4
  47. package/dist/types/sdk/bus/notification-orchestration.d.ts +1 -0
  48. package/dist/types/sdk/bus/notification-service.d.ts +34 -0
  49. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +105 -0
  50. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +18 -3
  51. package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -1
  52. package/dist/types/sdk/bus/telegram-daemon.d.ts +189 -4
  53. package/dist/types/sdk/bus/telegram-reference.d.ts +2 -0
  54. package/dist/types/sdk/bus/topic-registry.d.ts +88 -2
  55. package/dist/types/sdk/client/discovery.d.ts +2 -0
  56. package/dist/types/sdk/client/liveness.d.ts +7 -0
  57. package/dist/types/sdk/host/control/operations.d.ts +1 -1
  58. package/dist/types/sdk/host/host.d.ts +1 -1
  59. package/dist/types/sdk/host/query/handlers.d.ts +9 -0
  60. package/dist/types/sdk/index.d.ts +2 -0
  61. package/dist/types/sdk/prompt-status.d.ts +88 -0
  62. package/dist/types/sdk/session-directory.d.ts +1 -1
  63. package/dist/types/sdk/startup-capability.d.ts +3 -0
  64. package/dist/types/sdk/transport/auth-preface.d.ts +9 -0
  65. package/dist/types/sdk/transport/index.d.ts +12 -0
  66. package/dist/types/sdk/transport/relay.d.ts +43 -0
  67. package/dist/types/sdk/transport/serve-cli.d.ts +4 -0
  68. package/dist/types/sdk/transport/socket.d.ts +7 -0
  69. package/dist/types/sdk/transport/stdio.d.ts +3 -0
  70. package/dist/types/session/agent-session.d.ts +30 -5
  71. package/dist/types/session/btw-contract.d.ts +16 -0
  72. package/dist/types/session/internal/managed-session-scope.d.ts +32 -9
  73. package/dist/types/session/internal/managed-session-storage.d.ts +27 -10
  74. package/dist/types/session/internal/native-publish-outcome.d.ts +48 -0
  75. package/dist/types/session/session-manager.d.ts +25 -4
  76. package/dist/types/session/session-storage.d.ts +18 -0
  77. package/dist/types/setup/credential-import.d.ts +5 -1
  78. package/dist/types/setup/provider-onboarding.d.ts +2 -0
  79. package/dist/types/ssh/utils.d.ts +1 -0
  80. package/dist/types/task/discovery.d.ts +3 -1
  81. package/dist/types/task/index.d.ts +3 -1
  82. package/dist/types/task/receipt.d.ts +2 -0
  83. package/dist/types/task/types.d.ts +94 -0
  84. package/dist/types/tools/ask.d.ts +454 -174
  85. package/dist/types/tools/fetch.d.ts +5 -8
  86. package/dist/types/tools/image-gen.d.ts +63 -1
  87. package/dist/types/tools/index.d.ts +4 -0
  88. package/dist/types/tools/skill-discovery.d.ts +7 -0
  89. package/dist/types/tools/sqlite-reader.d.ts +7 -0
  90. package/dist/types/utils/prompt-suggestion.d.ts +33 -0
  91. package/dist/types/utils/shell-snapshot.d.ts +12 -0
  92. package/dist/types/web/insane/bridge.d.ts +1 -0
  93. package/dist/types/web/insane/url-guard.d.ts +17 -0
  94. package/package.json +8 -8
  95. package/scripts/capture-platform-shortcut-labels-showcase.ts +342 -0
  96. package/scripts/dogfood-repository-binding.ts +209 -0
  97. package/scripts/generate-sdk-operation-inventory.ts +4 -0
  98. package/scripts/run-test-manifest.ts +4 -4
  99. package/src/capability/index.ts +50 -42
  100. package/src/capability/types.ts +3 -0
  101. package/src/cli/args.ts +2 -92
  102. package/src/cli/notify-cli.ts +5 -38
  103. package/src/cli/setup-cli.ts +10 -4
  104. package/src/cli.ts +52 -9
  105. package/src/commands/daemon.ts +22 -5
  106. package/src/commands/sdk.ts +44 -11
  107. package/src/config/keybindings.ts +158 -25
  108. package/src/config/model-profile-activation.ts +7 -18
  109. package/src/config/model-profile-contract.ts +161 -0
  110. package/src/config/model-profiles.ts +18 -0
  111. package/src/config/model-registry.ts +11 -1
  112. package/src/config/settings-schema.ts +73 -7
  113. package/src/coordinator-mcp/model-preset.ts +21 -74
  114. package/src/defaults/gjc/skills/deep-interview/SKILL.md +2 -2
  115. package/src/defaults/gjc/skills/ralplan/SKILL.md +3 -1
  116. package/src/defaults/gjc/skills/ultragoal/SKILL.md +42 -1
  117. package/src/discovery/ssh.ts +12 -1
  118. package/src/edit/index.ts +3 -6
  119. package/src/edit/modes/patch.ts +38 -1
  120. package/src/edit/modes/replace.ts +42 -0
  121. package/src/edit/path-mutation-lock.ts +112 -0
  122. package/src/eval/py/executor.ts +30 -11
  123. package/src/eval/py/kernel.ts +1 -21
  124. package/src/eval/py/prelude.py +2 -2
  125. package/src/eval/py/runner-artifact.ts +84 -0
  126. package/src/eval/py/tool-bridge.ts +34 -18
  127. package/src/extensibility/gjc-plugins/runtime-adapters.ts +2 -1
  128. package/src/extensibility/plugins/marketplace/fetcher.ts +191 -7
  129. package/src/gjc-runtime/launch-tmux.ts +1 -1
  130. package/src/gjc-runtime/managed-owner-supervisor.ts +83 -4
  131. package/src/gjc-runtime/ralplan-runtime.ts +75 -3
  132. package/src/gjc-runtime/repository-binding.ts +267 -0
  133. package/src/gjc-runtime/session-state-sidecar.ts +25 -9
  134. package/src/gjc-runtime/tmux-owner-isolation.ts +2 -1
  135. package/src/gjc-runtime/tmux-sessions.ts +45 -6
  136. package/src/gjc-runtime/ultragoal-guard.ts +93 -15
  137. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +103 -0
  138. package/src/gjc-runtime/ultragoal-runtime.ts +456 -31
  139. package/src/gjc-runtime/workflow-manifest.generated.json +58 -0
  140. package/src/gjc-runtime/workflow-manifest.ts +23 -0
  141. package/src/internal-urls/docs-index.generated.ts +12 -10
  142. package/src/internal-urls/local-protocol.ts +64 -21
  143. package/src/internal-urls/mcp-protocol.ts +73 -11
  144. package/src/main.ts +111 -37
  145. package/src/modes/DESIGN.md +5 -0
  146. package/src/modes/acp/acp-agent.ts +50 -16
  147. package/src/modes/action-registry.ts +1 -1
  148. package/src/modes/components/btw-panel.ts +82 -16
  149. package/src/modes/components/model-selector.ts +73 -11
  150. package/src/modes/components/plan-preview-overlay.ts +1 -1
  151. package/src/modes/components/runtime-mcp-add-wizard.ts +5 -0
  152. package/src/modes/components/tool-status-header.ts +25 -4
  153. package/src/modes/components/welcome.ts +22 -18
  154. package/src/modes/controllers/btw-controller.ts +119 -36
  155. package/src/modes/controllers/command-controller.ts +97 -54
  156. package/src/modes/controllers/event-controller.ts +10 -0
  157. package/src/modes/controllers/input-controller.ts +38 -2
  158. package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -6
  159. package/src/modes/controllers/selector-controller.ts +149 -63
  160. package/src/modes/interactive-mode.ts +57 -34
  161. package/src/modes/prompt-action-autocomplete.ts +32 -13
  162. package/src/modes/prompt-suggestion-controller.ts +96 -0
  163. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +2 -2
  164. package/src/modes/types.ts +4 -0
  165. package/src/modes/utils/hotkeys-markdown.ts +64 -36
  166. package/src/modes/utils/ui-helpers.ts +5 -3
  167. package/src/prompts/agents/architect.md +0 -1
  168. package/src/prompts/agents/critic.md +0 -1
  169. package/src/prompts/system/btw-user.md +3 -8
  170. package/src/prompts/system/prompt-suggestion-system.md +32 -0
  171. package/src/prompts/system/system-prompt.md +6 -4
  172. package/src/prompts/tools/cron.md +3 -1
  173. package/src/prompts/tools/skill-discovery.md +1 -0
  174. package/src/runtime-mcp/client.ts +41 -55
  175. package/src/runtime-mcp/content-limits.ts +71 -0
  176. package/src/runtime-mcp/json-rpc.ts +23 -9
  177. package/src/runtime-mcp/manager.ts +51 -53
  178. package/src/runtime-mcp/oauth-flow.ts +0 -72
  179. package/src/runtime-mcp/plugin-network-boundary.ts +101 -0
  180. package/src/runtime-mcp/transports/http.ts +63 -22
  181. package/src/sdk/broker/broker.ts +34 -20
  182. package/src/sdk/broker/lifecycle.ts +185 -36
  183. package/src/sdk/bus/chat-daemon-control.ts +19 -5
  184. package/src/sdk/bus/config-commands.ts +2 -2
  185. package/src/sdk/bus/config.ts +9 -1
  186. package/src/sdk/bus/conversation-store.ts +40 -6
  187. package/src/sdk/bus/index.ts +244 -70
  188. package/src/sdk/bus/lifecycle-commands.ts +63 -10
  189. package/src/sdk/bus/lifecycle-control-runtime.ts +11 -2
  190. package/src/sdk/bus/lifecycle-orchestrator.ts +32 -1
  191. package/src/sdk/bus/notification-orchestration.ts +12 -0
  192. package/src/sdk/bus/notification-service.ts +68 -20
  193. package/src/sdk/bus/prompt-reconciliation.ts +239 -0
  194. package/src/sdk/bus/recent-activity.ts +90 -26
  195. package/src/sdk/bus/telegram-daemon-contract.ts +18 -3
  196. package/src/sdk/bus/telegram-daemon-control.ts +147 -35
  197. package/src/sdk/bus/telegram-daemon.ts +2463 -450
  198. package/src/sdk/bus/telegram-reference.ts +20 -2
  199. package/src/sdk/bus/topic-registry.ts +451 -12
  200. package/src/sdk/cli/session-cli.ts +15 -2
  201. package/src/sdk/client/discovery.ts +19 -3
  202. package/src/sdk/client/liveness.ts +49 -0
  203. package/src/sdk/host/control/dispatch.ts +1 -1
  204. package/src/sdk/host/control/operations.ts +1 -1
  205. package/src/sdk/host/host.ts +2 -6
  206. package/src/sdk/host/query/handlers.ts +70 -5
  207. package/src/sdk/index.ts +12 -0
  208. package/src/sdk/prompt-status.ts +97 -0
  209. package/src/sdk/protocol/operation-inventory.generated.json +58 -0
  210. package/src/sdk/protocol/operation-registry.ts +18 -4
  211. package/src/sdk/session-directory.ts +5 -1
  212. package/src/sdk/session.ts +31 -5
  213. package/src/sdk/startup-capability.ts +9 -1
  214. package/src/sdk/transport/auth-preface.ts +82 -0
  215. package/src/sdk/transport/index.ts +18 -0
  216. package/src/sdk/transport/relay.ts +252 -0
  217. package/src/sdk/transport/serve-cli.ts +105 -0
  218. package/src/sdk/transport/socket.ts +197 -0
  219. package/src/sdk/transport/stdio.ts +17 -0
  220. package/src/session/agent-session.ts +292 -154
  221. package/src/session/blob-store.ts +25 -7
  222. package/src/session/btw-contract.ts +59 -0
  223. package/src/session/internal/managed-session-scope.ts +1163 -137
  224. package/src/session/internal/managed-session-storage.ts +397 -234
  225. package/src/session/internal/native-publish-outcome.ts +266 -0
  226. package/src/session/session-manager.ts +620 -403
  227. package/src/session/session-storage.ts +128 -11
  228. package/src/setup/credential-import.ts +28 -3
  229. package/src/setup/provider-onboarding.ts +30 -1
  230. package/src/setup/provider-presets.json +2 -1
  231. package/src/slash-commands/builtin-registry.ts +1 -1
  232. package/src/ssh/utils.ts +14 -0
  233. package/src/task/discovery.ts +8 -2
  234. package/src/task/gjc-command.ts +2 -2
  235. package/src/task/index.ts +121 -15
  236. package/src/task/receipt.ts +3 -0
  237. package/src/task/types.ts +34 -3
  238. package/src/tools/ask.ts +199 -89
  239. package/src/tools/ast-edit.ts +2 -2
  240. package/src/tools/bash-pty-selection.ts +2 -2
  241. package/src/tools/browser/screenshot-format.ts +6 -1
  242. package/src/tools/cron.ts +1 -1
  243. package/src/tools/fetch.ts +13 -131
  244. package/src/tools/image-gen.ts +476 -23
  245. package/src/tools/index.ts +4 -0
  246. package/src/tools/read.ts +2 -0
  247. package/src/tools/render-utils.ts +7 -1
  248. package/src/tools/skill-discovery.ts +39 -3
  249. package/src/tools/sqlite-reader.ts +63 -5
  250. package/src/utils/edit-mode.ts +2 -2
  251. package/src/utils/pasted-image-path.ts +15 -6
  252. package/src/utils/prompt-suggestion.ts +265 -0
  253. package/src/utils/shell-snapshot.ts +173 -63
  254. package/src/utils/sixel.ts +3 -3
  255. package/src/web/insane/bridge.ts +5 -1
  256. package/src/web/insane/url-guard.ts +140 -27
  257. package/src/web/scrapers/docs-rs.ts +30 -5
  258. package/src/web/scrapers/types.ts +19 -49
  259. package/src/web/scrapers/utils.ts +12 -26
@@ -18,7 +18,6 @@ import * as native from "@gajae-code/natives";
18
18
  import { getTerminalId } from "@gajae-code/tui";
19
19
  import {
20
20
  getBlobsDir,
21
- getAgentDir as getDefaultAgentDir,
22
21
  getProjectDir,
23
22
  getSessionsDir,
24
23
  getTerminalSessionsDir,
@@ -58,20 +57,28 @@ import {
58
57
  canonicalizeTrustedPath,
59
58
  deleteManagedSessionCandidate,
60
59
  listManagedCandidates,
60
+ type ManagedCandidateWriteAuthority,
61
61
  type ManagedMigrationPolicy,
62
+ type ManagedOpenCandidateResult,
62
63
  type ManagedScope,
63
64
  managedDirectoryAuthorityForScope,
64
65
  managedRootForScope,
65
66
  openManagedCandidateForWrite,
66
67
  prepareManagedSessionScopeForWriteSync,
67
68
  resolveManagedScope,
69
+ resolveManagedScopeForWrite,
68
70
  } from "./internal/managed-session-scope";
69
71
  import {
72
+ assertManagedDirectoryRoot,
73
+ fsyncManagedArtifactTree,
70
74
  type ManagedDirectoryRoot,
71
75
  type ManagedFileSnapshot,
72
76
  ManagedSessionDescendantStore,
77
+ type ManagedSessionSecurityPolicy,
78
+ mayCleanManagedTreeStaging,
73
79
  retainManagedDirectoryAuthority,
74
80
  } from "./internal/managed-session-storage";
81
+ import { classifyNativePublishOutcome, formatNativePublishDiagnostic } from "./internal/native-publish-outcome";
75
82
 
76
83
  import {
77
84
  type BashExecutionMessage,
@@ -1317,6 +1324,18 @@ function freezeInternalReadSnapshot<T>(value: T): T {
1317
1324
  return copy;
1318
1325
  }
1319
1326
 
1327
+ function managedScopeStartupError(
1328
+ action: "resolve" | "prepare",
1329
+ failure: Extract<ReturnType<typeof resolveManagedScopeForWrite>, { kind: "error" }>,
1330
+ ): Error {
1331
+ return new Error(`Could not ${action} managed session scope.`, {
1332
+ cause: {
1333
+ classification: failure.cause?.classification ?? failure.code,
1334
+ ...(failure.cause?.diagnostic === undefined ? {} : { diagnostic: failure.cause.diagnostic }),
1335
+ },
1336
+ });
1337
+ }
1338
+
1320
1339
  /** Resolve and prepare the default v2 session scope before any managed writer exists. */
1321
1340
  function computeDefaultSessionDir(
1322
1341
  cwd: string,
@@ -1324,10 +1343,13 @@ function computeDefaultSessionDir(
1324
1343
  sessionsRoot: string = getSessionsDir(),
1325
1344
  ): string {
1326
1345
  if (!(storage instanceof FileSessionStorage)) throw new SessionManagedStorageError();
1327
- const resolved = resolveManagedScope({ cwd, agentDir: path.resolve(sessionsRoot, ".."), sessionsRoot });
1328
- if (resolved.kind === "error") throw new Error(`Could not resolve managed session scope: ${resolved.message}`);
1329
- const prepared = prepareManagedSessionScopeForWriteSync(resolved.scope);
1330
- if (prepared.kind === "error") throw new Error(`Could not prepare managed session scope: ${prepared.message}`);
1346
+ const resolved = resolveManagedScopeForWrite({ cwd, agentDir: path.resolve(sessionsRoot, ".."), sessionsRoot });
1347
+ if (resolved.kind === "error") throw managedScopeStartupError("resolve", resolved);
1348
+ const prepared = prepareManagedSessionScopeForWriteSync(
1349
+ resolved.scope,
1350
+ process.platform === "win32" ? "windows-existing-verify-first" : "default",
1351
+ );
1352
+ if (prepared.kind === "error") throw managedScopeStartupError("prepare", prepared);
1331
1353
  return prepared.scope.directoryPath;
1332
1354
  }
1333
1355
 
@@ -1408,6 +1430,13 @@ function managedFileSnapshotEquals(left: ManagedFileSnapshot | null, right: Mana
1408
1430
  );
1409
1431
  }
1410
1432
  const trustedSessionDestinations = new WeakSet<SessionDestination>();
1433
+ const managedSecurityPolicies = new WeakMap<ManagedSessionSecurityContext, ManagedSessionSecurityPolicy>();
1434
+
1435
+ function managedSecurityPolicyForContext(securityContext: ManagedSessionSecurityContext): ManagedSessionSecurityPolicy {
1436
+ const policy = managedSecurityPolicies.get(securityContext);
1437
+ if (!policy) throw new Error("Managed session security policy unavailable");
1438
+ return policy;
1439
+ }
1411
1440
 
1412
1441
  function managedStoreFromContext(
1413
1442
  securityContext: ManagedSessionSecurityContext,
@@ -1419,6 +1448,7 @@ function managedStoreFromContext(
1419
1448
  securityContext.retainedAuthority
1420
1449
  ? { authority: securityContext.retainedAuthority, authorityBaseDir: directory }
1421
1450
  : undefined,
1451
+ managedSecurityPolicyForContext(securityContext),
1422
1452
  );
1423
1453
  }
1424
1454
 
@@ -1431,6 +1461,10 @@ function freezeManagedDestination(scope: ManagedScope): SessionDestination {
1431
1461
  rootAuthority,
1432
1462
  retainedAuthority: managedDirectoryAuthorityForScope(scope),
1433
1463
  });
1464
+ managedSecurityPolicies.set(
1465
+ securityContext,
1466
+ scope.platform === "win32" ? "windows-existing-verify-first" : "default",
1467
+ );
1434
1468
  const destination = Object.freeze({ kind: "managed" as const, directory: scope.directoryPath, securityContext });
1435
1469
  trustedSessionDestinations.add(destination);
1436
1470
  return destination;
@@ -1445,10 +1479,17 @@ function explicitDestination(directory: string): SessionDestination {
1445
1479
  function managedDestination(cwd: string, storage: SessionStorage, agentDir?: string): SessionDestination {
1446
1480
  if (!(storage instanceof FileSessionStorage)) throw new SessionManagedStorageError();
1447
1481
  const sessionsRoot = getSessionsDir(agentDir);
1448
- const resolved = resolveManagedScope({ cwd, agentDir: agentDir ?? path.resolve(sessionsRoot, ".."), sessionsRoot });
1449
- if (resolved.kind === "error") throw new Error(`Could not resolve managed session scope: ${resolved.message}`);
1450
- const prepared = prepareManagedSessionScopeForWriteSync(resolved.scope);
1451
- if (prepared.kind === "error") throw new Error(`Could not prepare managed session scope: ${prepared.message}`);
1482
+ const resolved = resolveManagedScopeForWrite({
1483
+ cwd,
1484
+ agentDir: agentDir ?? path.resolve(sessionsRoot, ".."),
1485
+ sessionsRoot,
1486
+ });
1487
+ if (resolved.kind === "error") throw managedScopeStartupError("resolve", resolved);
1488
+ const prepared = prepareManagedSessionScopeForWriteSync(
1489
+ resolved.scope,
1490
+ process.platform === "win32" ? "windows-existing-verify-first" : "default",
1491
+ );
1492
+ if (prepared.kind === "error") throw managedScopeStartupError("prepare", prepared);
1452
1493
  return freezeManagedDestination(prepared.scope);
1453
1494
  }
1454
1495
 
@@ -1814,12 +1855,46 @@ function inspectResumeSessionFile(
1814
1855
  mtimeNs: snapshot.mtimeNs,
1815
1856
  sha256: crypto.createHash("sha256").update(bytes).digest("hex"),
1816
1857
  };
1817
- return { identity, content: Uint8Array.from(bytes), entries, context, migrationApplied };
1858
+ return { identity, content: bytes, entries, context, migrationApplied };
1818
1859
  } catch {
1819
1860
  return { kind: "error", reason: "malformed" };
1820
1861
  }
1821
1862
  }
1822
1863
 
1864
+ /** Revalidate previously parsed authority without another decode, parse, migration, or context build. */
1865
+ function revalidateResumeSessionIdentity(
1866
+ filePath: string,
1867
+ storage: SessionStorage,
1868
+ expected: ResumeSessionIdentity,
1869
+ ): StrictSessionOpenFailure | { kind: "valid" } {
1870
+ const canonicalPath = resolveEquivalentPath(path.resolve(filePath));
1871
+ let before: SessionStorageStat;
1872
+ let snapshot: SessionStorageSnapshot;
1873
+ let after: SessionStorageStat;
1874
+ try {
1875
+ before = storage.statSync(canonicalPath);
1876
+ if (!before.isFile || !storage.readSnapshotSync) return { kind: "error", reason: "read-failed" };
1877
+ snapshot = storage.readSnapshotSync(canonicalPath);
1878
+ after = storage.statSync(canonicalPath);
1879
+ } catch (error) {
1880
+ return resumeReadFailure(error, storage, canonicalPath);
1881
+ }
1882
+ if (!sameResumeStat(before, snapshot.stat) || !sameResumeStat(snapshot.stat, after)) {
1883
+ return { kind: "error", reason: "unstable" };
1884
+ }
1885
+ const observed: ResumeSessionIdentity = {
1886
+ canonicalPath,
1887
+ sessionId: expected.sessionId,
1888
+ dev: snapshot.stat.dev,
1889
+ ino: snapshot.stat.ino,
1890
+ size: snapshot.stat.size,
1891
+ mtimeMs: snapshot.stat.mtimeMs,
1892
+ mtimeNs: snapshot.stat.mtimeNs,
1893
+ sha256: crypto.createHash("sha256").update(snapshot.bytes).digest("hex"),
1894
+ };
1895
+ return sameResumeIdentity(expected, observed) ? { kind: "valid" } : { kind: "error", reason: "identity-mismatch" };
1896
+ }
1897
+
1823
1898
  /**
1824
1899
  * Convert legacy persisted blob references in loaded entries into resident sentinels.
1825
1900
  * Images then materialize lazily at provider/display/export chokepoints instead of
@@ -2099,23 +2174,6 @@ function formatTimeAgo(date: Date): string {
2099
2174
  return date.toLocaleDateString();
2100
2175
  }
2101
2176
 
2102
- async function syncPathAndParent(filePath: string): Promise<void> {
2103
- const file = await fs.promises.open(filePath, "r");
2104
- try {
2105
- await file.sync();
2106
- } finally {
2107
- await file.close();
2108
- }
2109
- if (process.platform !== "win32") {
2110
- const parent = await fs.promises.open(path.dirname(filePath), "r");
2111
- try {
2112
- await parent.sync();
2113
- } finally {
2114
- await parent.close();
2115
- }
2116
- }
2117
- }
2118
-
2119
2177
  interface SessionMoveDirectoryHandle {
2120
2178
  sync(): Promise<void>;
2121
2179
  close(): Promise<void>;
@@ -2135,40 +2193,6 @@ export async function syncSessionMoveDirectory(
2135
2193
  }
2136
2194
  }
2137
2195
 
2138
- /** Durably flush every copied file and directory before an EXDEV source removal. */
2139
- async function syncCopiedTree(treePath: string): Promise<void> {
2140
- const stat = await fs.promises.lstat(treePath);
2141
- if (stat.isSymbolicLink()) throw new Error("Refusing to fsync a copied symbolic link during a cross-device move");
2142
- if (stat.isFile()) {
2143
- const file = await fs.promises.open(treePath, "r");
2144
- try {
2145
- await file.sync();
2146
- } finally {
2147
- await file.close();
2148
- }
2149
- return;
2150
- }
2151
- if (!stat.isDirectory()) throw new Error("Copied cross-device move contains an unsupported filesystem entry");
2152
- const directory = await fs.promises.opendir(treePath);
2153
- try {
2154
- for (;;) {
2155
- const entry = await directory.read();
2156
- if (entry === null) break;
2157
- await syncCopiedTree(path.join(treePath, entry.name));
2158
- }
2159
- } finally {
2160
- await directory.close();
2161
- }
2162
- if (process.platform !== "win32") {
2163
- const handle = await fs.promises.open(treePath, "r");
2164
- try {
2165
- await handle.sync();
2166
- } finally {
2167
- await handle.close();
2168
- }
2169
- }
2170
- }
2171
-
2172
2196
  type CrossDeviceTreeIdentity = string;
2173
2197
 
2174
2198
  /** Captures content plus topology; a later capture must match before source removal. */
@@ -2216,14 +2240,6 @@ async function captureCrossDeviceTreeIdentity(treePath: string): Promise<CrossDe
2216
2240
  return `directory:${before.dev}:${before.ino}:${before.size}:${before.mtimeNs}:[${children.join(",")}]`;
2217
2241
  }
2218
2242
 
2219
- async function removeCrossDeviceSourceIfUnchanged(source: string, expected: CrossDeviceTreeIdentity): Promise<void> {
2220
- if ((await captureCrossDeviceTreeIdentity(source)) !== expected)
2221
- throw new Error("Cross-device move source changed before removal");
2222
- const stat = await fs.promises.lstat(source);
2223
- if (stat.isDirectory()) await fs.promises.rm(source, { recursive: true, force: false });
2224
- else await fs.promises.unlink(source);
2225
- }
2226
-
2227
2243
  /**
2228
2244
  * An EXDEV retirement must be driven by a native descriptor/handle-bound
2229
2245
  * snapshot-copy-verify transaction. Do not fall back to pathname copy/remove:
@@ -2239,83 +2255,25 @@ class CrossDeviceMoveUnsupportedError extends Error {
2239
2255
  }
2240
2256
  }
2241
2257
 
2242
- class PathMoveCompletedWithSyncFailure extends Error {
2243
- constructor(readonly cause: unknown) {
2244
- super("Path move completed but final source-directory sync failed.", { cause });
2245
- }
2246
- }
2247
-
2248
2258
  async function movePathAcrossDevicesSafe(source: string, destination: string): Promise<void> {
2249
2259
  const sourceIdentity = await captureCrossDeviceTreeIdentity(source);
2250
- const renamed = native.renameNoReplacePath(source, destination);
2251
- if (renamed.ok) {
2252
- try {
2253
- if ((await captureCrossDeviceTreeIdentity(destination)) !== sourceIdentity)
2254
- throw new Error("Atomic session rename did not preserve the captured source identity");
2255
- await syncSessionMoveDirectory(path.dirname(destination));
2256
- if (path.dirname(source) !== path.dirname(destination)) await syncSessionMoveDirectory(path.dirname(source));
2257
- } catch (error) {
2258
- throw new PathMoveCompletedWithSyncFailure(error);
2259
- }
2260
+ const outcome = classifyNativePublishOutcome(native.renameNoReplacePath(source, destination));
2261
+ if (outcome.ok) {
2262
+ if ((await captureCrossDeviceTreeIdentity(destination)) !== sourceIdentity)
2263
+ throw new Error("Atomic session rename did not preserve the captured source identity");
2264
+ await syncSessionMoveDirectory(path.dirname(destination));
2265
+ if (path.dirname(source) !== path.dirname(destination)) await syncSessionMoveDirectory(path.dirname(source));
2260
2266
  return;
2261
2267
  }
2262
- if (renamed.code === "quarantine_collision") {
2268
+ if (outcome.reason === "destination_exists") {
2263
2269
  const error = new Error(`Session move destination already exists: ${destination}`) as NodeJS.ErrnoException;
2264
2270
  error.code = "EEXIST";
2265
2271
  throw error;
2266
2272
  }
2267
- if (renamed.code !== "atomic_unavailable")
2268
- throw new Error(`Atomic session rename failed: ${renamed.code ?? "unknown"}`);
2269
- const sourceStat = await fs.promises.lstat(source);
2270
- if (!sourceStat.isDirectory()) {
2271
- let destinationCreated = false;
2272
- let sourceRemoved = false;
2273
- try {
2274
- try {
2275
- await fs.promises.link(source, destination);
2276
- destinationCreated = true;
2277
- } catch (error) {
2278
- if (hasFsCode(error, "EXDEV")) throw new CrossDeviceMoveUnsupportedError(source, destination, error);
2279
- if (
2280
- !hasFsCode(error, "EPERM") &&
2281
- !hasFsCode(error, "EACCES") &&
2282
- !hasFsCode(error, "ENOTSUP") &&
2283
- !hasFsCode(error, "EOPNOTSUPP")
2284
- )
2285
- throw error;
2286
- await fs.promises.copyFile(source, destination, fs.constants.COPYFILE_EXCL);
2287
- destinationCreated = true;
2288
- }
2289
- await syncPathAndParent(destination);
2290
- await removeCrossDeviceSourceIfUnchanged(source, sourceIdentity);
2291
- sourceRemoved = true;
2292
- await syncSessionMoveDirectory(path.dirname(source));
2293
- return;
2294
- } catch (error) {
2295
- if (!sourceRemoved && destinationCreated) await fs.promises.unlink(destination).catch(() => undefined);
2296
- if (sourceRemoved && !(error instanceof PathMoveCompletedWithSyncFailure))
2297
- throw new PathMoveCompletedWithSyncFailure(error);
2298
- throw error;
2299
- }
2300
- }
2301
-
2302
- const destinationParent = await fs.promises.lstat(path.dirname(destination));
2303
- if (sourceStat.dev !== destinationParent.dev) throw new CrossDeviceMoveUnsupportedError(source, destination);
2304
-
2305
- try {
2306
- await fs.promises.cp(source, destination, { recursive: true, force: false, errorOnExist: true });
2307
- await syncCopiedTree(destination);
2308
- await removeCrossDeviceSourceIfUnchanged(source, sourceIdentity);
2309
- try {
2310
- await syncSessionMoveDirectory(path.dirname(source));
2311
- } catch (error) {
2312
- throw new PathMoveCompletedWithSyncFailure(error);
2313
- }
2314
- } catch (error) {
2315
- if (!(error instanceof PathMoveCompletedWithSyncFailure))
2316
- await fs.promises.rm(destination, { recursive: true, force: true }).catch(() => undefined);
2317
- throw error;
2318
- }
2273
+ if (outcome.reason === "atomic_unavailable")
2274
+ throw new CrossDeviceMoveUnsupportedError(source, destination, new Error(formatNativePublishDiagnostic(outcome)));
2275
+ const message = `Atomic session rename failed: ${outcome.code ?? outcome.reason}`;
2276
+ throw new Error(message, { cause: new Error(formatNativePublishDiagnostic(outcome)) });
2319
2277
  }
2320
2278
 
2321
2279
  const MAX_PERSIST_CHARS = 500_000;
@@ -3501,6 +3459,64 @@ function getTaskToolUsage(details: unknown): Usage | undefined {
3501
3459
  return usage as Usage;
3502
3460
  }
3503
3461
 
3462
+ interface ValidatedUsageTotals {
3463
+ input: number;
3464
+ output: number;
3465
+ cacheRead: number;
3466
+ cacheWrite: number;
3467
+ premiumRequests: number;
3468
+ cost: number;
3469
+ }
3470
+
3471
+ /** Max malformed-usage entry ids sampled for a single resume-time report. */
3472
+ const MALFORMED_USAGE_SAMPLE_LIMIT = 8;
3473
+
3474
+ function isFiniteNonNegativeNumber(value: unknown): value is number {
3475
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
3476
+ }
3477
+
3478
+ /**
3479
+ * Validate an untrusted persisted `usage` record before it is aggregated into
3480
+ * session usage statistics. `parseSessionEntries` accepts any parseable JSON, so a
3481
+ * torn/corrupt record can carry a `usage` that is still valid JSON yet malformed:
3482
+ * absent (`{}` yields NaN), numeric strings (`"10"` coerces sums into strings), or
3483
+ * negative buckets that silently reduce totals. Returns the validated finite
3484
+ * non-negative totals, or null when any bucket or `cost.total` is malformed — the
3485
+ * caller then skips and reports that record rather than poisoning every
3486
+ * getUsageStatistics() consumer. Absent `premiumRequests`/`cost` default to 0
3487
+ * (backward-compatible with older transcripts); a present-but-invalid field is
3488
+ * rejected.
3489
+ */
3490
+ function validatePersistedUsageTotals(usage: unknown): ValidatedUsageTotals | null {
3491
+ if (typeof usage !== "object" || usage === null) return null;
3492
+ const record = usage as Record<string, unknown>;
3493
+ const input = record.input;
3494
+ const output = record.output;
3495
+ const cacheRead = record.cacheRead;
3496
+ const cacheWrite = record.cacheWrite;
3497
+ if (
3498
+ !isFiniteNonNegativeNumber(input) ||
3499
+ !isFiniteNonNegativeNumber(output) ||
3500
+ !isFiniteNonNegativeNumber(cacheRead) ||
3501
+ !isFiniteNonNegativeNumber(cacheWrite)
3502
+ )
3503
+ return null;
3504
+ // Default only on truly absent (=== undefined) properties; a present-but-null/invalid
3505
+ // premiumRequests is malformed, not zero.
3506
+ const premiumRequests = record.premiumRequests === undefined ? 0 : record.premiumRequests;
3507
+ if (!isFiniteNonNegativeNumber(premiumRequests)) return null;
3508
+ const rawCost = record.cost;
3509
+ let cost: unknown = 0;
3510
+ if (rawCost !== undefined) {
3511
+ // A present `cost` must be a non-array record carrying a finite non-negative `total`;
3512
+ // null, arrays, and an absent/invalid `total` are malformed — never silently zero.
3513
+ if (typeof rawCost !== "object" || rawCost === null || Array.isArray(rawCost)) return null;
3514
+ cost = (rawCost as Record<string, unknown>).total;
3515
+ }
3516
+ if (!isFiniteNonNegativeNumber(cost)) return null;
3517
+ return { input, output, cacheRead, cacheWrite, premiumRequests, cost };
3518
+ }
3519
+
3504
3520
  function extractTextFromContent(content: Message["content"]): string {
3505
3521
  if (typeof content === "string") return content;
3506
3522
  return content
@@ -3867,9 +3883,11 @@ export async function resolveResumableSession(
3867
3883
  cwd: string,
3868
3884
  sessionDir?: string,
3869
3885
  storage: SessionStorage = new FileSessionStorage(),
3886
+ managedAgentDir?: string,
3870
3887
  ): Promise<ResolvedSessionMatch | undefined> {
3871
- const localSessionDir = sessionDir ?? SessionManager.getDefaultSessionDir(cwd, undefined, storage);
3872
- const localSessions = await SessionManager.list(cwd, localSessionDir, storage);
3888
+ const localSessions = sessionDir
3889
+ ? await SessionManager.list(cwd, sessionDir, storage)
3890
+ : await SessionManager.listManagedForResumePickerReadOnly(cwd, managedAgentDir, storage);
3873
3891
  const localMatch = localSessions.find(session => sessionMatchesResumeArg(session, sessionArg));
3874
3892
  if (localMatch) {
3875
3893
  return { session: localMatch, scope: "local" };
@@ -3879,7 +3897,7 @@ export async function resolveResumableSession(
3879
3897
  return undefined;
3880
3898
  }
3881
3899
 
3882
- const globalSessions = await SessionManager.listAll(storage);
3900
+ const globalSessions = await SessionManager.listAll(storage, managedAgentDir);
3883
3901
  const globalMatch = globalSessions.find(session => sessionMatchesResumeArg(session, sessionArg));
3884
3902
  if (!globalMatch) {
3885
3903
  return undefined;
@@ -3911,6 +3929,7 @@ export class SessionManager {
3911
3929
  #ensuredOnDisk: boolean = false;
3912
3930
  #recoveryHydrationContext: RecoveryHydrationContext | undefined;
3913
3931
  #fileEntries: FileEntry[] = [];
3932
+ #pendingStrictAdoption: { canonicalPath: string; identity: ResumeSessionIdentity } | undefined;
3914
3933
  #byId: Map<string, SessionEntry> = new Map();
3915
3934
  #labelsById: Map<string, string> = new Map();
3916
3935
  #leafId: string | null = null;
@@ -4157,15 +4176,16 @@ export class SessionManager {
4157
4176
  }
4158
4177
 
4159
4178
  async #hydrateExistingSession(sessionFile: string, entries: FileEntry[], migrationApplied: boolean): Promise<void> {
4160
- const ownedEntries = structuredClone(entries) as FileEntry[];
4161
- const header = ownedEntries[0] as SessionHeader;
4179
+ // Strict inspection creates these entries solely for this hydration path. Adopt that
4180
+ // final, validated representation instead of cloning the complete transcript again.
4181
+ const header = entries[0] as SessionHeader;
4162
4182
  this.#sessionFile = this.storage instanceof FileSessionStorage ? path.resolve(sessionFile) : sessionFile;
4163
4183
  this.#sessionId = header.id;
4164
4184
  this.#sessionName = header.title;
4165
4185
  this.#titleSource = header.titleSource;
4166
4186
  this.#needsFullRewriteOnNextPersist = migrationApplied;
4167
- await resolveBlobRefsInEntries(ownedEntries, this.#blobStore);
4168
- this.#fileEntries = ownedEntries;
4187
+ await resolveBlobRefsInEntries(entries, this.#blobStore);
4188
+ this.#fileEntries = entries;
4169
4189
  this.#resetResidentTextBlobStore();
4170
4190
  this.#fileEntries = this.#fileEntries.map(entry =>
4171
4191
  prepareEntryForResidentSync(entry, this.#residentBlobStores()),
@@ -4184,12 +4204,25 @@ export class SessionManager {
4184
4204
 
4185
4205
  /** Switch to a different session file (used for resume and branching) */
4186
4206
  async setSessionFile(sessionFile: string): Promise<void> {
4207
+ const resolvedSessionFile = this.storage instanceof FileSessionStorage ? path.resolve(sessionFile) : sessionFile;
4208
+ const strictAdoption = this.#pendingStrictAdoption;
4209
+ this.#pendingStrictAdoption = undefined;
4210
+ if (strictAdoption && resolvedSessionFile === strictAdoption.canonicalPath) {
4211
+ const inspected = inspectResumeSessionFile(resolvedSessionFile, this.storage);
4212
+ if ("kind" in inspected || !sameResumeIdentity(strictAdoption.identity, inspected.identity))
4213
+ throw new Error("Prepared managed session changed before strict adoption.");
4214
+ }
4187
4215
  await this.#closePersistWriter();
4188
4216
  this.#persistError = undefined;
4189
4217
  this.#persistErrorReported = false;
4190
- this.#sessionFile = this.storage instanceof FileSessionStorage ? path.resolve(sessionFile) : sessionFile;
4218
+ this.#sessionFile = resolvedSessionFile;
4191
4219
  writeTerminalBreadcrumb(this.cwd, this.#sessionFile);
4192
4220
  this.#fileEntries = await loadEntriesFromFile(this.#sessionFile, this.storage);
4221
+ if (strictAdoption) {
4222
+ const inspected = inspectResumeSessionFile(this.#sessionFile, this.storage);
4223
+ if ("kind" in inspected || !sameResumeIdentity(strictAdoption.identity, inspected.identity))
4224
+ throw new Error("Prepared managed session changed during strict adoption.");
4225
+ }
4193
4226
  if (this.#fileEntries.length > 0) {
4194
4227
  const header = this.#fileEntries.find(e => e.type === "session") as SessionHeader | undefined;
4195
4228
  this.#sessionId = header?.id ?? createSessionId();
@@ -4255,7 +4288,8 @@ export class SessionManager {
4255
4288
  const candidate = listing.owned.find(candidate => path.resolve(candidate.path) === requestedPath);
4256
4289
  if (!candidate) throw new Error("Managed session deletion requires exact logical authorization.");
4257
4290
  const deleted = await deleteManagedSessionCandidate(resolved.scope, candidate);
4258
- if (deleted.kind === "error") throw new Error(`Could not delete managed session: ${deleted.message}`);
4291
+ if (deleted.kind !== "deleted" && deleted.kind !== "already_deleted")
4292
+ throw new Error(`Could not delete managed session: ${deleted.message}`);
4259
4293
  } else {
4260
4294
  await this.storage.deleteSessionWithArtifacts(sessionPath);
4261
4295
  }
@@ -4384,27 +4418,20 @@ export class SessionManager {
4384
4418
  throw new Error("managed_fork_transcript_changed");
4385
4419
  }
4386
4420
  } catch (error) {
4387
- let failure = error;
4388
- const failedSessionFile = this.#sessionFile;
4389
- if (this.destination.kind === "managed") {
4421
+ const failure = error;
4422
+ if (forkArtifactPublication) {
4390
4423
  try {
4391
- if (forkTranscriptStore && forkTranscriptPublication)
4392
- forkTranscriptStore.removeExpected(path.basename(failedSessionFile), forkTranscriptPublication);
4393
- if (forkArtifactPublication)
4394
- forkArtifactPublication.cleanupStore.removeTreeExpected(
4395
- forkArtifactPublication.cleanupRelativePath,
4396
- forkArtifactPublication.snapshot,
4397
- );
4424
+ forkArtifactPublication.cleanupStore.removeTreeExpected(
4425
+ forkArtifactPublication.cleanupRelativePath,
4426
+ forkArtifactPublication.snapshot,
4427
+ );
4398
4428
  } catch (cleanupError) {
4399
- failure = new Error(`Failed to clean up managed fork destination: ${toError(cleanupError).message}`, {
4400
- cause: toError(error),
4401
- });
4402
- }
4403
- } else {
4404
- try {
4405
- await this.storage.unlink(failedSessionFile);
4406
- } catch {
4407
- // Preserve ambiguous cleanup evidence while restoring the active manager.
4429
+ const cleanupMessage = toError(cleanupError).message;
4430
+ if (cleanupMessage !== "cleanup_pending") {
4431
+ throw new Error(`Failed to clean up fork artifacts: ${cleanupMessage}`, {
4432
+ cause: toError(failure),
4433
+ });
4434
+ }
4408
4435
  }
4409
4436
  }
4410
4437
  this.#sessionId = oldSessionId;
@@ -4483,8 +4510,7 @@ export class SessionManager {
4483
4510
  }
4484
4511
  if (JSON.stringify(parentStore.captureTree(sourceName)) !== JSON.stringify(sourceSnapshot))
4485
4512
  throw new Error("artifact_source_changed");
4486
- parentStore.moveTreeNoReplace(stagingName, destinationName, stagingSnapshot);
4487
- publishedSnapshot = parentStore.captureTree(destinationName);
4513
+ publishedSnapshot = parentStore.moveTreeNoReplace(stagingName, destinationName, stagingSnapshot);
4488
4514
  const comparable = (tree: native.NativeDirectoryTreeSnapshot) =>
4489
4515
  tree.entries.map(entry => ({
4490
4516
  relativePath: entry.relativePath,
@@ -4508,12 +4534,15 @@ export class SessionManager {
4508
4534
  };
4509
4535
  } catch (error) {
4510
4536
  try {
4511
- if (publishedSnapshot) parentStore.removeTreeExpected(destinationName, publishedSnapshot);
4512
- else if (stagingSnapshot) parentStore.removeTreeExpected(stagingName, stagingSnapshot);
4537
+ if (stagingSnapshot && mayCleanManagedTreeStaging(error))
4538
+ parentStore.removeTreeExpected(stagingName, stagingSnapshot);
4513
4539
  } catch (cleanupError) {
4514
- throw new Error(`Failed to clean up managed fork artifacts: ${toError(cleanupError).message}`, {
4515
- cause: toError(error),
4516
- });
4540
+ const cleanupMessage = toError(cleanupError).message;
4541
+ if (cleanupMessage !== "cleanup_pending") {
4542
+ throw new Error(`Failed to clean up managed fork artifacts: ${cleanupMessage}`, {
4543
+ cause: toError(error),
4544
+ });
4545
+ }
4517
4546
  }
4518
4547
  throw error;
4519
4548
  }
@@ -4531,19 +4560,15 @@ export class SessionManager {
4531
4560
  const previousSessionDir = this.sessionDir;
4532
4561
  const previousSessionFile = this.#sessionFile;
4533
4562
  const previousDestination = this.destination;
4534
- const previousArtifactDir = previousSessionFile?.slice(0, -6);
4535
4563
 
4536
- const nextDestination = (() => {
4537
- if (this.destination.kind === "managed") {
4538
- return managedDestination(resolvedCwd, this.storage, this.destination.securityContext.agentDir);
4539
- }
4540
- if (!(this.storage instanceof FileSessionStorage)) return this.destination;
4541
- const managedSessionsRoot = resolveManagedSessionRoot(this.sessionDir, this.cwd);
4542
- const directory = managedSessionsRoot
4543
- ? computeDefaultSessionDir(resolvedCwd, this.storage, managedSessionsRoot)
4544
- : computeDefaultSessionDir(resolvedCwd, this.storage);
4545
- return explicitDestination(directory);
4546
- })();
4564
+ const nextDestination =
4565
+ this.storage instanceof FileSessionStorage
4566
+ ? managedDestination(
4567
+ resolvedCwd,
4568
+ this.storage,
4569
+ this.destination.kind === "managed" ? this.destination.securityContext.agentDir : undefined,
4570
+ )
4571
+ : this.destination;
4547
4572
  const newSessionDir = nextDestination.directory;
4548
4573
  let hadSessionFile = false;
4549
4574
  const managedMove = this.destination.kind === "managed" && nextDestination.kind === "managed";
@@ -4646,15 +4671,8 @@ export class SessionManager {
4646
4671
  managedPublishedArtifacts,
4647
4672
  );
4648
4673
  }
4649
- if (managedTranscript)
4674
+ if (managedTranscript && !managedSourceStore.readExpected(path.basename(oldSessionFile)))
4650
4675
  await managedSourceStore.publishNoReplace(path.basename(oldSessionFile), managedTranscript.bytes);
4651
- if (managedPublishedArtifacts)
4652
- managedDestinationStore.removeTreeExpected(
4653
- path.basename(newArtifactDir),
4654
- managedPublishedArtifacts,
4655
- );
4656
- if (managedPublishedTranscript)
4657
- managedDestinationStore.removeExpected(path.basename(newSessionFile), managedPublishedTranscript);
4658
4676
  };
4659
4677
  managedTranscript = managedSourceStore.readExpected(path.basename(oldSessionFile));
4660
4678
  hadSessionFile = managedTranscript !== null;
@@ -4691,10 +4709,20 @@ export class SessionManager {
4691
4709
  if (!retainedTreeSnapshotEquals(terminalArtifacts, managedPublishedArtifacts))
4692
4710
  throw new Error("managed_move_destination_artifacts_changed");
4693
4711
  }
4694
- if (managedArtifacts)
4695
- managedSourceStore.removeTreeExpected(path.basename(oldArtifactDir), managedArtifacts);
4696
- if (managedTranscript)
4697
- managedSourceStore.removeExpected(path.basename(oldSessionFile), managedTranscript);
4712
+ if (managedArtifacts) {
4713
+ try {
4714
+ managedSourceStore.removeTreeExpected(path.basename(oldArtifactDir), managedArtifacts);
4715
+ } catch (cleanupError) {
4716
+ if (toError(cleanupError).message !== "cleanup_pending") throw cleanupError;
4717
+ }
4718
+ }
4719
+ if (managedTranscript) {
4720
+ try {
4721
+ managedSourceStore.removeExpected(path.basename(oldSessionFile), managedTranscript);
4722
+ } catch (cleanupError) {
4723
+ if (toError(cleanupError).message !== "cleanup_pending") throw cleanupError;
4724
+ }
4725
+ }
4698
4726
  } else {
4699
4727
  if (hadSessionFile && oldSessionFile !== newSessionFile) {
4700
4728
  await movePathAcrossDevicesSafe(oldSessionFile, newSessionFile);
@@ -4750,13 +4778,6 @@ export class SessionManager {
4750
4778
  !managedSourceStore.readExpected(path.basename(oldSessionFile))
4751
4779
  )
4752
4780
  await managedSourceStore.publishNoReplace(path.basename(oldSessionFile), managedTranscript.bytes);
4753
- if (managedPublishedArtifacts)
4754
- managedDestinationStore.removeTreeExpected(
4755
- path.basename(newArtifactDir),
4756
- managedPublishedArtifacts,
4757
- );
4758
- if (managedPublishedTranscript)
4759
- managedDestinationStore.removeExpected(path.basename(newSessionFile), managedPublishedTranscript);
4760
4781
  } catch (rollbackErr) {
4761
4782
  restoreResidentStateAndThrow(
4762
4783
  new Error(`Failed to rollback managed move: ${toError(rollbackErr).message}`, {
@@ -4777,7 +4798,8 @@ export class SessionManager {
4777
4798
  this.#bumpAllRevisions();
4778
4799
  }
4779
4800
 
4780
- // Update cwd and sessionDir after the physical move succeeds, but roll the move back if metadata persistence fails.
4801
+ // Update cwd and sessionDir after physical publication succeeds. Metadata failures restore the source
4802
+ // authority but deliberately retain any destination publication evidence rather than deleting it.
4781
4803
  this.cwd = resolvedCwd;
4782
4804
  this.sessionDir = newSessionDir;
4783
4805
  this.destination = nextDestination;
@@ -4798,60 +4820,37 @@ export class SessionManager {
4798
4820
  await this.#appendHeaderPatch({ cwd: resolvedCwd });
4799
4821
  }
4800
4822
  } catch (error) {
4801
- const failedSessionFile = this.#sessionFile;
4802
- const failedArtifactDir = failedSessionFile?.slice(0, -6);
4803
- try {
4804
- await this.#closePersistWriter().catch(() => {});
4805
- this.#persistChain = Promise.resolve();
4806
- this.#persistError = undefined;
4807
- this.#persistErrorReported = false;
4808
- if (rollbackManagedMove) {
4823
+ await this.#closePersistWriter().catch(() => {});
4824
+ this.#persistChain = Promise.resolve();
4825
+ this.#persistError = undefined;
4826
+ this.#persistErrorReported = false;
4827
+ if (rollbackManagedMove) {
4828
+ try {
4809
4829
  await rollbackManagedMove();
4810
- this.#sessionFile = previousSessionFile;
4811
- this.cwd = previousCwd;
4812
- this.sessionDir = previousSessionDir;
4813
- this.destination = previousDestination;
4814
- this.#managedTranscriptStoreCache =
4815
- managedMove && managedSourceStore
4816
- ? { directory: path.resolve(previousSessionDir), store: managedSourceStore }
4817
- : null;
4818
- const header = this.#fileEntries.find(entry => entry.type === "session") as SessionHeader | undefined;
4819
- if (header) applyHeaderPatch(header, { cwd: previousCwd });
4820
- this.#headerExportRevision++;
4821
- throw error;
4822
- }
4823
- if (
4824
- this.storage instanceof FileSessionStorage &&
4825
- failedArtifactDir &&
4826
- previousArtifactDir &&
4827
- failedArtifactDir !== previousArtifactDir &&
4828
- fs.existsSync(failedArtifactDir)
4829
- ) {
4830
- await movePathAcrossDevicesSafe(failedArtifactDir, previousArtifactDir);
4831
- }
4832
- if (
4833
- this.storage instanceof FileSessionStorage &&
4834
- failedSessionFile &&
4835
- previousSessionFile &&
4836
- failedSessionFile !== previousSessionFile &&
4837
- fs.existsSync(failedSessionFile)
4838
- ) {
4839
- await movePathAcrossDevicesSafe(failedSessionFile, previousSessionFile);
4830
+ } catch (rollbackError) {
4831
+ if (toError(rollbackError).message !== "cleanup_pending") {
4832
+ throw new Error(`Failed to rollback managed move: ${toError(rollbackError).message}`, {
4833
+ cause: toError(error),
4834
+ });
4835
+ }
4840
4836
  }
4841
4837
  this.#sessionFile = previousSessionFile;
4842
4838
  this.cwd = previousCwd;
4843
4839
  this.sessionDir = previousSessionDir;
4844
4840
  this.destination = previousDestination;
4845
- this.#managedTranscriptStoreCache = null;
4841
+ this.#managedTranscriptStoreCache =
4842
+ managedMove && managedSourceStore
4843
+ ? { directory: path.resolve(previousSessionDir), store: managedSourceStore }
4844
+ : null;
4846
4845
  const header = this.#fileEntries.find(entry => entry.type === "session") as SessionHeader | undefined;
4847
4846
  if (header) applyHeaderPatch(header, { cwd: previousCwd });
4848
4847
  this.#headerExportRevision++;
4849
- if (previousSessionFile && this.storage.existsSync(previousSessionFile)) await this.#rewriteFile();
4850
- } catch (rollbackError) {
4851
- throw new Error(`Failed to rollback session move: ${toError(rollbackError).message}`, {
4852
- cause: toError(error),
4853
- });
4848
+ throw error;
4854
4849
  }
4850
+ // The destination has already been published. Retain it rather than moving it
4851
+ // back over the source after a metadata failure.
4852
+ this.#managedTranscriptStoreCache = null;
4853
+ this.#headerExportRevision++;
4855
4854
  throw error;
4856
4855
  }
4857
4856
 
@@ -4911,6 +4910,8 @@ export class SessionManager {
4911
4910
  this.#labelsById.clear();
4912
4911
  this.#leafId = null;
4913
4912
  this.#usageStatistics = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, premiumRequests: 0, cost: 0 };
4913
+ let malformedUsageRecords = 0;
4914
+ const malformedUsageSample: string[] = [];
4914
4915
  for (const entry of this.#fileEntries) {
4915
4916
  if (entry.type === "session") continue;
4916
4917
  this.#byId.set(entry.id, entry);
@@ -4922,28 +4923,80 @@ export class SessionManager {
4922
4923
  this.#labelsById.delete(entry.targetId);
4923
4924
  }
4924
4925
  }
4925
- if (entry.type === "message" && entry.message.role === "assistant") {
4926
- const usage = entry.message.usage;
4927
- this.#usageStatistics.input += usage.input;
4928
- this.#usageStatistics.output += usage.output;
4929
- this.#usageStatistics.cacheRead += usage.cacheRead;
4930
- this.#usageStatistics.cacheWrite += usage.cacheWrite;
4931
- this.#usageStatistics.premiumRequests += usage.premiumRequests ?? 0;
4932
- this.#usageStatistics.cost += usage.cost.total;
4926
+ // Aggregate this entry's persisted usage through the shared validated,
4927
+ // overflow-guarded path (see #accumulateEntryUsage). parseSessionEntries accepts
4928
+ // any parseable JSON, so a torn/corrupt record can carry a still-valid-JSON but
4929
+ // malformed `usage` (absent, {}, numeric strings, negatives, a non-record cost, or
4930
+ // a cumulative overflow); such a record is skipped and reported below instead of
4931
+ // poisoning every getUsageStatistics() consumer.
4932
+ if (this.#accumulateEntryUsage(entry)) {
4933
+ malformedUsageRecords++;
4934
+ if (malformedUsageSample.length < MALFORMED_USAGE_SAMPLE_LIMIT) malformedUsageSample.push(entry.id);
4933
4935
  }
4936
+ }
4937
+ if (malformedUsageRecords > 0) {
4938
+ logger.warn("Skipped malformed or overflowing persisted usage records during resume aggregation", {
4939
+ sessionFile: this.#sessionFile,
4940
+ count: malformedUsageRecords,
4941
+ sampleEntryIds: malformedUsageSample,
4942
+ });
4943
+ }
4944
+ }
4934
4945
 
4935
- if (entry.type === "message" && entry.message.role === "toolResult" && entry.message.toolName === "task") {
4936
- const usage = getTaskToolUsage(entry.message.details);
4937
- if (usage) {
4938
- this.#usageStatistics.input += usage.input;
4939
- this.#usageStatistics.output += usage.output;
4940
- this.#usageStatistics.cacheRead += usage.cacheRead;
4941
- this.#usageStatistics.cacheWrite += usage.cacheWrite;
4942
- this.#usageStatistics.premiumRequests += usage.premiumRequests ?? 0;
4943
- this.#usageStatistics.cost += usage.cost.total;
4944
- }
4945
- }
4946
+ /**
4947
+ * Commit a validated record's totals only if every cumulative sum stays finite.
4948
+ * Individually-finite buckets can still overflow to Infinity in aggregate (e.g. two
4949
+ * Number.MAX_VALUE records), which would poison getUsageStatistics(); such a record is
4950
+ * rejected atomically (no partial commit). Returns true when committed, false when
4951
+ * rejected for cumulative overflow.
4952
+ */
4953
+ #addValidatedUsage(totals: ValidatedUsageTotals): boolean {
4954
+ const next = {
4955
+ input: this.#usageStatistics.input + totals.input,
4956
+ output: this.#usageStatistics.output + totals.output,
4957
+ cacheRead: this.#usageStatistics.cacheRead + totals.cacheRead,
4958
+ cacheWrite: this.#usageStatistics.cacheWrite + totals.cacheWrite,
4959
+ premiumRequests: this.#usageStatistics.premiumRequests + totals.premiumRequests,
4960
+ cost: this.#usageStatistics.cost + totals.cost,
4961
+ };
4962
+ if (
4963
+ !Number.isFinite(next.input) ||
4964
+ !Number.isFinite(next.output) ||
4965
+ !Number.isFinite(next.cacheRead) ||
4966
+ !Number.isFinite(next.cacheWrite) ||
4967
+ !Number.isFinite(next.premiumRequests) ||
4968
+ !Number.isFinite(next.cost)
4969
+ )
4970
+ return false;
4971
+ this.#usageStatistics.input = next.input;
4972
+ this.#usageStatistics.output = next.output;
4973
+ this.#usageStatistics.cacheRead = next.cacheRead;
4974
+ this.#usageStatistics.cacheWrite = next.cacheWrite;
4975
+ this.#usageStatistics.premiumRequests = next.premiumRequests;
4976
+ this.#usageStatistics.cost = next.cost;
4977
+ return true;
4978
+ }
4979
+
4980
+ /**
4981
+ * Validate and accumulate one entry's persisted usage through the single shared,
4982
+ * overflow-guarded aggregation path used by both #buildIndex (resume) and #appendEntry
4983
+ * (runtime), covering the assistant and `task` tool-result shapes. Returns true when a
4984
+ * present usage record was skipped as malformed or overflowing (so the caller can report
4985
+ * it); false when the entry has no usage or was aggregated cleanly.
4986
+ */
4987
+ #accumulateEntryUsage(entry: SessionEntry): boolean {
4988
+ if (entry.type !== "message") return false;
4989
+ if (entry.message.role === "assistant") {
4990
+ const totals = validatePersistedUsageTotals(entry.message.usage);
4991
+ return !(totals && this.#addValidatedUsage(totals));
4992
+ }
4993
+ if (entry.message.role === "toolResult" && entry.message.toolName === "task") {
4994
+ const rawTaskUsage = getTaskToolUsage(entry.message.details);
4995
+ if (rawTaskUsage === undefined) return false;
4996
+ const totals = validatePersistedUsageTotals(rawTaskUsage);
4997
+ return !(totals && this.#addValidatedUsage(totals));
4946
4998
  }
4999
+ return false;
4947
5000
  }
4948
5001
 
4949
5002
  #recordPersistError(err: unknown): Error {
@@ -5523,6 +5576,17 @@ export class SessionManager {
5523
5576
  return this.sessionDir;
5524
5577
  }
5525
5578
 
5579
+ /** Lists picker candidates within this manager's captured destination authority. */
5580
+ async listForResumePickerReadOnly(): Promise<SessionInfo[]> {
5581
+ return this.destination.kind === "managed"
5582
+ ? await SessionManager.listManagedForResumePickerReadOnly(
5583
+ this.cwd,
5584
+ this.destination.securityContext.agentDir,
5585
+ this.storage,
5586
+ )
5587
+ : await SessionManager.listForResumePickerReadOnly(this.cwd, this.destination.directory, this.storage);
5588
+ }
5589
+
5526
5590
  getSessionId(): string {
5527
5591
  return this.#sessionId;
5528
5592
  }
@@ -5552,13 +5616,22 @@ export class SessionManager {
5552
5616
  getManagedLegacyLocalMigrationSource(): ManagedLegacyLocalMigrationSource | null {
5553
5617
  if (this.destination.kind !== "managed" || !this.#sessionFile || this.#adoptedArtifactManager) return null;
5554
5618
  const store = this.#managedTranscriptStore();
5619
+ const legacyArtifactsRoot = path.basename(this.#sessionFile.slice(0, -6));
5620
+ const legacyLocalRoot = path.posix.join(legacyArtifactsRoot, "local");
5555
5621
  return {
5556
5622
  capture: async () => {
5557
5623
  let snapshot: native.NativeDirectoryTreeSnapshot;
5558
5624
  try {
5559
- snapshot = store.captureTree("local");
5625
+ snapshot = store.captureTree(legacyLocalRoot);
5560
5626
  } catch (error) {
5561
5627
  if (error instanceof Error && error.message === "not_found") return null;
5628
+ if (error instanceof Error && error.message === "reparse_point") {
5629
+ try {
5630
+ store.captureTree(legacyArtifactsRoot);
5631
+ } catch (artifactsError) {
5632
+ if (artifactsError instanceof Error && artifactsError.message === "not_found") return null;
5633
+ }
5634
+ }
5562
5635
  throw error;
5563
5636
  }
5564
5637
  let totalBytes = 0;
@@ -5566,7 +5639,7 @@ export class SessionManager {
5566
5639
  if (totalBytes > 64 * 1024 * 1024) throw new Error("Legacy local:// migration exceeds the safe size limit");
5567
5640
  const entries = snapshot.entries.map(entry => {
5568
5641
  if (entry.kind === "directory") return { relativePath: entry.relativePath, kind: "directory" as const };
5569
- const captured = store.readExpected(path.posix.join("local", entry.relativePath));
5642
+ const captured = store.readExpected(path.posix.join(legacyLocalRoot, entry.relativePath));
5570
5643
  if (
5571
5644
  !captured ||
5572
5645
  captured.identity.dev.toString() !== entry.dev ||
@@ -5582,12 +5655,12 @@ export class SessionManager {
5582
5655
  sha256: entry.sha256,
5583
5656
  };
5584
5657
  });
5585
- const verified = store.captureTree("local");
5658
+ const verified = store.captureTree(legacyLocalRoot);
5586
5659
  if (JSON.stringify(verified) !== JSON.stringify(snapshot))
5587
5660
  throw new Error("Legacy local:// migration source changed during capture");
5588
5661
  return { snapshot, entries };
5589
5662
  },
5590
- retire: snapshot => store.removeTreeExpected("local", snapshot),
5663
+ retire: snapshot => store.removeTreeExpected(legacyLocalRoot, snapshot),
5591
5664
  };
5592
5665
  }
5593
5666
 
@@ -5914,26 +5987,14 @@ export class SessionManager {
5914
5987
  this.#leafRevision++;
5915
5988
  if (entry.type === "label") this.#labelRevision++;
5916
5989
  this._persist(residentEntry);
5917
- if (entry.type === "message" && entry.message.role === "assistant") {
5918
- const usage = entry.message.usage;
5919
- this.#usageStatistics.input += usage.input;
5920
- this.#usageStatistics.output += usage.output;
5921
- this.#usageStatistics.cacheRead += usage.cacheRead;
5922
- this.#usageStatistics.cacheWrite += usage.cacheWrite;
5923
- this.#usageStatistics.premiumRequests += usage.premiumRequests ?? 0;
5924
- this.#usageStatistics.cost += usage.cost.total;
5925
- }
5926
-
5927
- if (entry.type === "message" && entry.message.role === "toolResult" && entry.message.toolName === "task") {
5928
- const usage = getTaskToolUsage(entry.message.details);
5929
- if (usage) {
5930
- this.#usageStatistics.input += usage.input;
5931
- this.#usageStatistics.output += usage.output;
5932
- this.#usageStatistics.cacheRead += usage.cacheRead;
5933
- this.#usageStatistics.cacheWrite += usage.cacheWrite;
5934
- this.#usageStatistics.premiumRequests += usage.premiumRequests ?? 0;
5935
- this.#usageStatistics.cost += usage.cost.total;
5936
- }
5990
+ // Same validated, overflow-guarded aggregation as the resume path (#buildIndex): a
5991
+ // malformed task-result or assistant usage shape reaching the append path must not
5992
+ // crash or poison getUsageStatistics() either.
5993
+ if (this.#accumulateEntryUsage(entry)) {
5994
+ logger.warn("Skipped malformed or overflowing usage on appended entry", {
5995
+ sessionFile: this.#sessionFile,
5996
+ entryId: entry.id,
5997
+ });
5937
5998
  }
5938
5999
  }
5939
6000
 
@@ -7080,6 +7141,12 @@ export class SessionManager {
7080
7141
  authority instanceof ManagedSessionDescendantStore
7081
7142
  ? authority.retainAuthority()
7082
7143
  : retainManagedDirectoryAuthority(rootAuthority, directory);
7144
+ const policy =
7145
+ authority instanceof ManagedSessionDescendantStore
7146
+ ? authority.securityPolicy
7147
+ : process.platform === "win32"
7148
+ ? "windows-existing-verify-first"
7149
+ : "default";
7083
7150
  const securityContext = createManagedSessionSecurityContext({
7084
7151
  agentDir: rootAuthority.canonicalPath,
7085
7152
  sessionsRoot: rootAuthority.canonicalPath,
@@ -7087,6 +7154,7 @@ export class SessionManager {
7087
7154
  rootAuthority,
7088
7155
  retainedAuthority,
7089
7156
  });
7157
+ managedSecurityPolicies.set(securityContext, policy);
7090
7158
  const destination = Object.freeze({ kind: "managed" as const, directory, securityContext });
7091
7159
  trustedSessionDestinations.add(destination);
7092
7160
  return destination;
@@ -7115,20 +7183,61 @@ export class SessionManager {
7115
7183
  return manager;
7116
7184
  }
7117
7185
 
7186
+ /** Prepare a selected candidate using this manager's captured managed destination authority. */
7187
+ async prepareManagedCandidateForWrite(
7188
+ filePath: string,
7189
+ migrationPolicy: SessionDirectoryMigrationPolicy,
7190
+ expectedIdentity?: ResumeSessionIdentity,
7191
+ ): Promise<string> {
7192
+ return SessionManager.prepareManagedCandidateForWrite(
7193
+ filePath,
7194
+ migrationPolicy,
7195
+ this.destination,
7196
+ expectedIdentity,
7197
+ );
7198
+ }
7199
+
7200
+ /** Prepare a managed candidate and retain its exact post-preparation identity for the next adoption. */
7201
+ async prepareManagedCandidateForStrictAdoption(
7202
+ filePath: string,
7203
+ migrationPolicy: SessionDirectoryMigrationPolicy,
7204
+ expectedIdentity: ResumeSessionIdentity,
7205
+ ): Promise<string> {
7206
+ const preparedPath = await this.prepareManagedCandidateForWrite(filePath, migrationPolicy, expectedIdentity);
7207
+ const inspected = inspectResumeSessionFile(preparedPath, this.storage);
7208
+ if ("kind" in inspected) throw new Error(`Could not inspect prepared managed session: ${inspected.reason}`);
7209
+ this.#pendingStrictAdoption = { canonicalPath: inspected.identity.canonicalPath, identity: inspected.identity };
7210
+ return preparedPath;
7211
+ }
7212
+
7118
7213
  /** Resolve a default-managed candidate through binding validation and copy-retain migration before mutation. */
7119
7214
  static async prepareManagedCandidateForWrite(
7120
7215
  filePath: string,
7121
7216
  migrationPolicy: SessionDirectoryMigrationPolicy,
7217
+ destination: SessionDestination,
7218
+ expectedIdentity?: ResumeSessionIdentity,
7122
7219
  ): Promise<string> {
7220
+ if (!trustedSessionDestinations.has(destination) || destination.kind !== "managed")
7221
+ throw new Error("Managed candidate preparation requires a trusted managed destination authority");
7123
7222
  const storage = new FileSessionStorage();
7223
+ const managedDestinationStore = managedStoreFromContext(destination.securityContext, destination.directory);
7224
+ const assertManagedDestinationBound = (): void => {
7225
+ managedDestinationStore.assertBound();
7226
+ if (!destination.securityContext.retainedAuthority)
7227
+ assertManagedDirectoryRoot(destination.securityContext.rootAuthority);
7228
+ };
7229
+
7230
+ assertManagedDestinationBound();
7124
7231
  const inspected = inspectResumeSessionFile(filePath, storage);
7125
7232
  if ("kind" in inspected) {
7126
7233
  if (inspected.reason === "missing") return filePath;
7127
7234
  throw new Error(`Could not inspect managed session: ${inspected.reason}`);
7128
7235
  }
7236
+ if (expectedIdentity && !sameResumeIdentity(expectedIdentity, inspected.identity))
7237
+ throw new Error("Managed session changed before migration authority was adopted.");
7129
7238
  const header = inspected.entries.find(entry => entry.type === "session") as SessionHeader | undefined;
7130
7239
  if (!header?.cwd) return filePath;
7131
- const sessionsRoot = getSessionsDir();
7240
+ const sessionsRoot = destination.securityContext.sessionsRoot;
7132
7241
  const resolvedPath = path.resolve(filePath);
7133
7242
  const relativeToManagedRoot = path.relative(sessionsRoot, resolvedPath);
7134
7243
  const isManagedPath =
@@ -7136,14 +7245,17 @@ export class SessionManager {
7136
7245
  (!relativeToManagedRoot.startsWith("..") && !path.isAbsolute(relativeToManagedRoot));
7137
7246
 
7138
7247
  if (!isManagedPath) return filePath;
7248
+ assertManagedDestinationBound();
7139
7249
  const resolved = resolveManagedScope({
7140
7250
  cwd: header.cwd,
7141
- agentDir: path.resolve(sessionsRoot, ".."),
7251
+ agentDir: destination.securityContext.agentDir,
7142
7252
  sessionsRoot,
7143
7253
  });
7144
7254
  if (resolved.kind === "error") throw new Error(`Could not resolve managed session scope: ${resolved.message}`);
7145
7255
 
7256
+ assertManagedDestinationBound();
7146
7257
  const listing = listManagedCandidates(resolved.scope);
7258
+ assertManagedDestinationBound();
7147
7259
  if (listing.kind !== "complete") throw new Error(`Managed session scan failed: ${listing.message}`);
7148
7260
 
7149
7261
  const candidate = listing.owned.find(item => path.resolve(item.path) === resolvedPath);
@@ -7160,11 +7272,36 @@ export class SessionManager {
7160
7272
  candidate.identity.sha256 !== inspected.identity.sha256
7161
7273
  )
7162
7274
  throw new Error("Managed session changed before migration authority was adopted.");
7163
- const opened = await openManagedCandidateForWrite(resolved.scope, candidate, migrationPolicy);
7275
+ assertManagedDestinationBound();
7276
+ const authority: ManagedCandidateWriteAuthority = {
7277
+ rootAuthority: destination.securityContext.rootAuthority,
7278
+ ...(path.resolve(destination.directory) === path.resolve(resolved.scope.directoryPath)
7279
+ ? {
7280
+ retainedAuthority: destination.securityContext.retainedAuthority,
7281
+ retainedDirectory: destination.directory,
7282
+ }
7283
+ : {}),
7284
+ };
7285
+ let opened: ManagedOpenCandidateResult;
7286
+ try {
7287
+ opened = await openManagedCandidateForWrite(
7288
+ resolved.scope,
7289
+ candidate,
7290
+ expectedIdentity ?? inspected.identity,
7291
+ migrationPolicy,
7292
+ authority,
7293
+ );
7294
+ } catch (error) {
7295
+ if (error instanceof Error && error.message.startsWith("Managed root authority changed"))
7296
+ managedDestinationStore.assertBound();
7297
+ throw error;
7298
+ }
7164
7299
  if (opened.kind === "error") {
7300
+ managedDestinationStore.assertBound();
7165
7301
  if (opened.code === "legacy_migration_disabled") throw new SessionMigrationPolicyError();
7166
7302
  throw new Error(`Could not open managed session: ${opened.message}`);
7167
7303
  }
7304
+ assertManagedDestinationBound();
7168
7305
  return opened.path;
7169
7306
  }
7170
7307
 
@@ -7182,7 +7319,7 @@ export class SessionManager {
7182
7319
  const destination = destinationFor(cwd, destinationInput, storage);
7183
7320
  const managedSourcePath =
7184
7321
  destination.kind === "managed" && storage instanceof FileSessionStorage
7185
- ? await SessionManager.prepareManagedCandidateForWrite(sourcePath, migrationPolicy)
7322
+ ? await SessionManager.prepareManagedCandidateForWrite(sourcePath, migrationPolicy, destination)
7186
7323
  : sourcePath;
7187
7324
  const dir = destination.directory;
7188
7325
  const manager = new SessionManager(cwd, dir, true, storage, destination);
@@ -7246,10 +7383,9 @@ export class SessionManager {
7246
7383
  return manager;
7247
7384
  }
7248
7385
 
7249
- const managedPath = await SessionManager.prepareManagedCandidateForWrite(filePath, migrationPolicy);
7250
- const inspected = inspectResumeSessionFile(managedPath, storage);
7386
+ const inspected = inspectResumeSessionFile(filePath, storage);
7251
7387
  if ("kind" in inspected) {
7252
- if (inspected.reason === "missing" && path.resolve(managedPath) === path.resolve(filePath)) {
7388
+ if (inspected.reason === "missing") {
7253
7389
  const manager = new SessionManager(getProjectDir(), destination.directory, true, storage, destination);
7254
7390
  await manager.#initSessionFile(filePath);
7255
7391
  return manager;
@@ -7317,46 +7453,47 @@ export class SessionManager {
7317
7453
  return manager;
7318
7454
  }
7319
7455
  /**
7320
- * List sessions for the resume picker without recovery or other maintenance writes.
7456
+ * List default-managed sessions for the resume picker without recovery or other
7457
+ * maintenance writes. This is the only picker inventory that includes legacy
7458
+ * sibling directories.
7459
+ */
7460
+ static async listManagedForResumePickerReadOnly(
7461
+ cwd: string,
7462
+ managedAgentDir?: string,
7463
+ storage: SessionStorage = new FileSessionStorage(),
7464
+ ): Promise<SessionInfo[]> {
7465
+ if (!(storage instanceof FileSessionStorage)) return [];
7466
+ const sessionsRoot = getSessionsDir(managedAgentDir);
7467
+ const resolved = resolveManagedScope({
7468
+ cwd,
7469
+ agentDir: managedAgentDir ?? path.resolve(sessionsRoot, ".."),
7470
+ sessionsRoot,
7471
+ });
7472
+ if (resolved.kind === "error") return [];
7473
+ const listing = listManagedCandidates(resolved.scope);
7474
+ if (listing.kind === "error") return [];
7475
+ return await collectSessionsFromFiles(
7476
+ listing.owned.map(candidate => candidate.path),
7477
+ storage,
7478
+ );
7479
+ }
7480
+
7481
+ /**
7482
+ * List sessions from an explicitly supplied picker directory without recovery
7483
+ * or other maintenance writes. Unlike managed inventory, this never scans
7484
+ * legacy sibling directories.
7321
7485
  */
7322
7486
  static async listForResumePickerReadOnly(
7323
7487
  cwd: string,
7324
7488
  sessionDir?: string,
7325
7489
  storage: SessionStorage = new FileSessionStorage(),
7326
7490
  ): Promise<SessionInfo[]> {
7327
- if (!sessionDir && !(storage instanceof FileSessionStorage)) return [];
7328
- if (sessionDir && !resolveManagedSessionRoot(sessionDir, cwd)) {
7329
- try {
7330
- return await collectSessionsFromFiles(storage.listFilesSync(sessionDir, "*.jsonl"), storage);
7331
- } catch {
7332
- return [];
7333
- }
7334
- }
7335
- const sessionsRoot = sessionDir
7336
- ? (resolveManagedSessionRoot(sessionDir, cwd) ?? getSessionsDir())
7337
- : getSessionsDir();
7338
- const resolved = resolveManagedScope({ cwd, agentDir: path.resolve(sessionsRoot, ".."), sessionsRoot });
7339
- if (resolved.kind === "error") return [];
7340
- if (storage instanceof FileSessionStorage) {
7341
- const listing = listManagedCandidates(resolved.scope);
7342
- if (listing.kind === "error") return [];
7343
- return await collectSessionsFromFiles(
7344
- listing.owned.map(candidate => candidate.path),
7345
- storage,
7346
- );
7347
- }
7348
- const legacyDirectory = `--${resolved.scope.canonicalCwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
7349
- const files = new Set<string>();
7491
+ if (!sessionDir) return await SessionManager.listManagedForResumePickerReadOnly(cwd, undefined, storage);
7350
7492
  try {
7351
- for (const directory of [resolved.scope.directoryPath, path.join(sessionsRoot, legacyDirectory)]) {
7352
- for (const file of storage.listFilesSync(directory, "*.jsonl")) files.add(file);
7353
- }
7493
+ return await collectSessionsFromFiles(storage.listFilesSync(sessionDir, "*.jsonl"), storage);
7354
7494
  } catch {
7355
7495
  return [];
7356
7496
  }
7357
- return (await collectSessionsFromFiles([...files], storage)).filter(
7358
- session => resolveEquivalentPath(session.cwd) === resolved.scope.canonicalCwd,
7359
- );
7360
7497
  }
7361
7498
 
7362
7499
  /** Tombstone and exact-delete a default-managed candidate. */
@@ -7377,7 +7514,8 @@ export class SessionManager {
7377
7514
  const candidate = listing.owned.find(item => path.resolve(item.path) === path.resolve(sessionPath));
7378
7515
  if (!candidate) throw new Error("Session is not an authorized managed candidate.");
7379
7516
  const deleted = await deleteManagedSessionCandidate(resolved.scope, candidate);
7380
- if (deleted.kind === "error") throw new Error(`Could not delete managed session: ${deleted.message}`);
7517
+ if (deleted.kind !== "deleted" && deleted.kind !== "already_deleted")
7518
+ throw new Error(`Could not delete managed session: ${deleted.message}`);
7381
7519
  }
7382
7520
 
7383
7521
  /** Capture exact source content for a strict fork without granting write ownership. */
@@ -7437,16 +7575,22 @@ export class SessionManager {
7437
7575
  }
7438
7576
 
7439
7577
  const dir = destination.directory;
7440
- const removeSessionDirOnFailure =
7578
+ const privateStagingDir =
7441
7579
  destination.kind !== "managed" &&
7442
7580
  snapshot.storage instanceof FileSessionStorage &&
7443
- !snapshot.storage.existsSync(dir);
7581
+ !snapshot.storage.existsSync(dir)
7582
+ ? fs.mkdtempSync(path.join(path.dirname(dir), `.${path.basename(dir)}.fork-staging-`))
7583
+ : undefined;
7584
+ const forkDestination = privateStagingDir ? explicitDestination(privateStagingDir) : destination;
7444
7585
  let managedForkStore: ManagedSessionDescendantStore | undefined;
7445
7586
  let managedForkTranscript: ManagedFileSnapshot | null = null;
7446
7587
  let manager: SessionManager | undefined;
7447
7588
  let authorityFailure: StrictSessionOpenFailure | undefined;
7589
+ let privateStagingSnapshot: native.NativeDirectoryTreeSnapshot | undefined;
7590
+ let privateStagingPublished = false;
7591
+
7448
7592
  try {
7449
- manager = new SessionManager(cwd, dir, true, snapshot.storage, destination);
7593
+ manager = new SessionManager(cwd, privateStagingDir ?? dir, true, snapshot.storage, forkDestination);
7450
7594
  await resolveBlobRefsInEntries(forkEntries, manager.#blobStore);
7451
7595
  manager.#fileEntries = forkEntries;
7452
7596
  const sourceHeader = manager.#fileEntries.find(e => e.type === "session") as SessionHeader | undefined;
@@ -7476,6 +7620,13 @@ export class SessionManager {
7476
7620
  throw new Error("Captured fork source authority changed before destination write.");
7477
7621
  }
7478
7622
  await manager.#rewriteFile();
7623
+ if (privateStagingDir) {
7624
+ const capturedStaging = native.snapshotDirectoryTree(privateStagingDir);
7625
+ if (!capturedStaging.ok || !capturedStaging.snapshot)
7626
+ throw new Error(capturedStaging.code ?? "fork_staging_snapshot_failed");
7627
+ privateStagingSnapshot = capturedStaging.snapshot;
7628
+ }
7629
+
7479
7630
  if (destination.kind === "managed" && manager.#sessionFile) {
7480
7631
  managedForkStore = manager.#managedTranscriptStore();
7481
7632
  managedForkTranscript = managedForkStore.readExpected(path.basename(manager.#sessionFile));
@@ -7495,66 +7646,57 @@ export class SessionManager {
7495
7646
  if (!managedFileSnapshotEquals(terminalTranscript, managedForkTranscript))
7496
7647
  throw new Error("managed_fork_transcript_changed");
7497
7648
  }
7649
+ if (privateStagingDir && manager.#sessionFile) {
7650
+ const materializedEntries = materializeResidentEntriesForReadSync(
7651
+ manager.#fileEntries,
7652
+ manager.#residentBlobStores(),
7653
+ );
7654
+ manager.#fileEntries = materializedEntries;
7655
+ manager.#disposeResidentTextBlobStore();
7656
+ const capturedStaging = native.snapshotDirectoryTree(privateStagingDir);
7657
+ if (!capturedStaging.ok || !capturedStaging.snapshot)
7658
+ throw new Error(capturedStaging.code ?? "fork_staging_snapshot_failed");
7659
+ privateStagingSnapshot = capturedStaging.snapshot;
7660
+ fsyncManagedArtifactTree(privateStagingDir);
7661
+ const outcome = classifyNativePublishOutcome(native.renameNoReplacePath(privateStagingDir, dir));
7662
+ if (!outcome.ok) throw new Error(outcome.code ?? "fork_destination_publish_failed");
7663
+ privateStagingPublished = true;
7664
+ const terminal = native.snapshotDirectoryTree(dir);
7665
+ if (
7666
+ !terminal.ok ||
7667
+ !terminal.snapshot ||
7668
+ !retainedTreeSnapshotEqualsAfterRename(terminal.snapshot, privateStagingSnapshot)
7669
+ )
7670
+ throw new Error("fork_destination_terminal_identity_changed");
7671
+ await syncSessionMoveDirectory(path.dirname(dir));
7672
+ const durableTerminal = native.snapshotDirectoryTree(dir);
7673
+ if (
7674
+ !durableTerminal.ok ||
7675
+ !durableTerminal.snapshot ||
7676
+ !retainedTreeSnapshotEqualsAfterRename(durableTerminal.snapshot, privateStagingSnapshot)
7677
+ )
7678
+ throw new Error("fork_destination_durability_identity_changed");
7679
+ manager.sessionDir = dir;
7680
+ manager.destination = destination;
7681
+ manager.#sessionFile = path.join(dir, path.basename(manager.#sessionFile));
7682
+ manager.#resetResidentTextBlobStore();
7683
+ manager.#reexternalizeFileEntriesForResidentStore();
7684
+ manager.#bumpAllRevisions();
7685
+ }
7498
7686
  if (manager.#sessionFile) writeTerminalBreadcrumb(manager.cwd, manager.#sessionFile);
7499
7687
  return { kind: "forked", manager };
7500
7688
  } catch (error) {
7501
7689
  if (manager) {
7502
- const sessionFile = manager.#sessionFile;
7503
7690
  const cleanupErrors: Error[] = [];
7504
7691
  try {
7505
7692
  await manager.close();
7506
7693
  } catch (cleanupError) {
7507
- cleanupErrors.push(toError(cleanupError));
7694
+ if (toError(cleanupError).message !== "cleanup_pending") cleanupErrors.push(toError(cleanupError));
7508
7695
  }
7509
- if (sessionFile) {
7510
- if (destination.kind === "managed") {
7511
- try {
7512
- if (managedForkStore && managedForkTranscript)
7513
- managedForkStore.removeExpected(path.basename(sessionFile), managedForkTranscript);
7514
- } catch (cleanupError) {
7515
- cleanupErrors.push(toError(cleanupError));
7516
- }
7517
- } else {
7518
- try {
7519
- await manager.storage.unlink(sessionFile);
7520
- } catch (cleanupError) {
7521
- if (!isEnoent(cleanupError)) cleanupErrors.push(toError(cleanupError));
7522
- }
7523
- if (manager.storage instanceof FileSessionStorage) {
7524
- try {
7525
- await fs.promises.rm(sessionFile.slice(0, -6), { recursive: true, force: true });
7526
- } catch (cleanupError) {
7527
- cleanupErrors.push(toError(cleanupError));
7528
- }
7529
- try {
7530
- const sessionFileName = path.basename(sessionFile);
7531
- const transientPrefix = `.${sessionFileName}.`;
7532
- const backupPrefix = `${sessionFileName}.`;
7533
- for (const name of await fs.promises.readdir(path.dirname(sessionFile))) {
7534
- if (
7535
- (name.startsWith(transientPrefix) && name.endsWith(".tmp")) ||
7536
- (name.startsWith(backupPrefix) && name.endsWith(".bak"))
7537
- ) {
7538
- await fs.promises.rm(path.join(path.dirname(sessionFile), name), {
7539
- recursive: true,
7540
- force: true,
7541
- });
7542
- }
7543
- }
7544
- } catch (cleanupError) {
7545
- if (!isEnoent(cleanupError)) cleanupErrors.push(toError(cleanupError));
7546
- }
7547
- }
7548
- }
7549
- }
7550
- if (removeSessionDirOnFailure) {
7551
- try {
7552
- await fs.promises.rmdir(dir);
7553
- } catch (cleanupError) {
7554
- const code = (cleanupError as NodeJS.ErrnoException).code;
7555
- if (code !== "ENOENT" && code !== "ENOTEMPTY" && code !== "EEXIST")
7556
- cleanupErrors.push(toError(cleanupError));
7557
- }
7696
+ if (!privateStagingPublished && privateStagingDir && privateStagingSnapshot) {
7697
+ const removed = native.exactRemoveDirectoryTree(privateStagingDir, privateStagingSnapshot);
7698
+ if (!removed.ok && removed.code !== "not_found" && removed.code !== "cleanup_pending")
7699
+ cleanupErrors.push(new Error(removed.code ?? "fork_staging_cleanup_failed"));
7558
7700
  }
7559
7701
  if (cleanupErrors.length > 0) {
7560
7702
  throw new Error(`Failed to clean up fork destination: ${cleanupErrors[0]!.message}`, {
@@ -7658,36 +7800,39 @@ export class SessionManager {
7658
7800
  let sessionPath = identity.canonicalPath;
7659
7801
  if (destination.kind === "managed" && storage instanceof FileSessionStorage) {
7660
7802
  try {
7661
- sessionPath = await SessionManager.prepareManagedCandidateForWrite(sessionPath, migrationPolicy);
7803
+ sessionPath = await SessionManager.prepareManagedCandidateForWrite(
7804
+ sessionPath,
7805
+ migrationPolicy,
7806
+ destination,
7807
+ identity,
7808
+ );
7662
7809
  } catch (error) {
7663
7810
  if (error instanceof SessionMigrationPolicyError)
7664
7811
  return { kind: "error", reason: "legacy_migration_disabled" };
7812
+ if (
7813
+ error instanceof Error &&
7814
+ (error.message.includes("source_changed") || error.message.includes("changed before migration"))
7815
+ )
7816
+ return { kind: "error", reason: "identity-mismatch" };
7665
7817
  throw error;
7666
7818
  }
7667
7819
  }
7668
7820
  const inspected = inspectResumeSessionFile(sessionPath, storage);
7669
7821
  if ("kind" in inspected) return inspected;
7670
- if (
7671
- (sessionPath === identity.canonicalPath && !sameResumeIdentity(identity, inspected.identity)) ||
7672
- (sessionPath !== identity.canonicalPath &&
7673
- (identity.sessionId !== inspected.identity.sessionId || identity.sha256 !== inspected.identity.sha256))
7674
- ) {
7822
+ if (sessionPath === identity.canonicalPath && !sameResumeIdentity(identity, inspected.identity)) {
7675
7823
  return { kind: "error", reason: "identity-mismatch" };
7676
7824
  }
7825
+
7677
7826
  const entries = inspected.entries;
7678
7827
  const header = entries[0] as SessionHeader;
7679
7828
  const dir = destination.directory;
7680
7829
  const manager = new SessionManager(header.cwd || getProjectDir(), dir, true, storage, destination);
7681
7830
  await manager.#hydrateExistingSession(sessionPath, entries, inspected.migrationApplied);
7682
- const ownershipInspection = inspectResumeSessionFile(sessionPath, storage);
7683
- if ("kind" in ownershipInspection) {
7831
+ const ownershipInspection = revalidateResumeSessionIdentity(sessionPath, storage, inspected.identity);
7832
+ if (ownershipInspection.kind === "error") {
7684
7833
  await manager.close();
7685
7834
  return ownershipInspection;
7686
7835
  }
7687
- if (!sameResumeIdentity(inspected.identity, ownershipInspection.identity)) {
7688
- await manager.close();
7689
- return { kind: "error", reason: "identity-mismatch" };
7690
- }
7691
7836
  try {
7692
7837
  await manager.#sanitizeLoadedOpenAIResponsesReplayMetadataAndPersist();
7693
7838
  writeTerminalBreadcrumb(manager.cwd, sessionPath);
@@ -7731,9 +7876,20 @@ export class SessionManager {
7731
7876
  const managedCandidates =
7732
7877
  destination.kind === "explicit"
7733
7878
  ? []
7734
- : await SessionManager.listForResumePickerReadOnly(cwd, destination.directory, storage);
7879
+ : await SessionManager.listManagedForResumePickerReadOnly(
7880
+ cwd,
7881
+ destination.securityContext.agentDir,
7882
+ storage,
7883
+ );
7884
+
7735
7885
  const terminalSession = await readTerminalBreadcrumb(cwd);
7736
- if (terminalSession) {
7886
+ const terminalSessionIsInExplicitRoot = (() => {
7887
+ if (destination.kind === "managed" || !terminalSession) return destination.kind === "managed";
7888
+ const canonicalRoot = canonicalizeTrustedPath(destination.directory);
7889
+ const canonicalSessionPath = canonicalizeTrustedPath(terminalSession);
7890
+ return pathIsWithin(canonicalRoot, canonicalSessionPath);
7891
+ })();
7892
+ if (terminalSession && terminalSessionIsInExplicitRoot) {
7737
7893
  const opened = await openSelectedStrictly(terminalSession);
7738
7894
  if (opened) return opened;
7739
7895
  }
@@ -7771,7 +7927,7 @@ export class SessionManager {
7771
7927
  sessionDir?: string,
7772
7928
  storage: SessionStorage = new FileSessionStorage(),
7773
7929
  ): Promise<SessionInfo[]> {
7774
- if (!sessionDir) return await SessionManager.listForResumePickerReadOnly(cwd, undefined, storage);
7930
+ if (!sessionDir) return await SessionManager.listManagedForResumePickerReadOnly(cwd, undefined, storage);
7775
7931
  try {
7776
7932
  await recoverOrphanedBackups(sessionDir, storage);
7777
7933
  return await collectSessionsFromFiles(storage.listFilesSync(sessionDir, "*.jsonl"), storage);
@@ -7783,9 +7939,12 @@ export class SessionManager {
7783
7939
  /**
7784
7940
  * List all sessions across all project directories.
7785
7941
  */
7786
- static async listAll(storage: SessionStorage = new FileSessionStorage()): Promise<SessionInfo[]> {
7942
+ static async listAll(
7943
+ storage: SessionStorage = new FileSessionStorage(),
7944
+ managedAgentDir?: string,
7945
+ ): Promise<SessionInfo[]> {
7787
7946
  if (!(storage instanceof FileSessionStorage)) return [];
7788
- const sessionsRoot = path.join(getDefaultAgentDir(), "sessions");
7947
+ const sessionsRoot = getSessionsDir(managedAgentDir);
7789
7948
  try {
7790
7949
  const directories = await fs.promises.readdir(sessionsRoot, { withFileTypes: true });
7791
7950
  const seedFiles = directories
@@ -7821,6 +7980,19 @@ export class SessionManager {
7821
7980
  return [];
7822
7981
  }
7823
7982
  }
7983
+ /**
7984
+ * Strict inventory bound to this manager's captured session authority. Managed
7985
+ * managers include authorized legacy and current candidates; explicit managers
7986
+ * authorize only their exact directory.
7987
+ */
7988
+ inventorySessionsStrict(): StrictInventoryResult {
7989
+ return SessionManager.inventorySessionsStrict(this.cwd, {
7990
+ sessionDir: this.destination.kind === "explicit" ? this.destination.directory : undefined,
7991
+ storage: this.storage,
7992
+ destination: this.destination,
7993
+ });
7994
+ }
7995
+
7824
7996
  /**
7825
7997
  * Strict raw scoped inventory for ACP authorization. Enumerates the scoped
7826
7998
  * session directory without suppressing any root/scan/lstat/read/parse/stat/
@@ -7830,10 +8002,55 @@ export class SessionManager {
7830
8002
  */
7831
8003
  static inventorySessionsStrict(
7832
8004
  cwd: string,
7833
- options?: { sessionDir?: string; storage?: SessionStorage },
8005
+ options?: { sessionDir?: string; storage?: SessionStorage; destination?: SessionDestination },
7834
8006
  ): StrictInventoryResult {
7835
8007
  const storage = options?.storage ?? new FileSessionStorage();
7836
- const dir = options?.sessionDir ?? SessionManager.getDefaultSessionDir(cwd, undefined, storage);
8008
+ const destination = options?.destination;
8009
+ const dir =
8010
+ options?.sessionDir ?? destination?.directory ?? SessionManager.getDefaultSessionDir(cwd, undefined, storage);
8011
+ if (destination?.kind === "managed") {
8012
+ if (!trustedSessionDestinations.has(destination) || !(storage instanceof FileSessionStorage)) {
8013
+ return {
8014
+ kind: "failure",
8015
+ failures: [{ kind: "scan", message: "Managed strict inventory authority is unavailable", path: dir }],
8016
+ };
8017
+ }
8018
+ try {
8019
+ const store = managedStoreFromContext(destination.securityContext, destination.directory);
8020
+ assertManagedDirectoryRoot(destination.securityContext.rootAuthority);
8021
+ store.assertBound();
8022
+ const resolved = resolveManagedScope({
8023
+ cwd,
8024
+ agentDir: destination.securityContext.agentDir,
8025
+ sessionsRoot: destination.securityContext.sessionsRoot,
8026
+ });
8027
+ if (resolved.kind === "error")
8028
+ return { kind: "failure", failures: [{ kind: "root", message: resolved.message, path: dir }] };
8029
+ const listing = listManagedCandidates(resolved.scope);
8030
+ if (listing.kind === "error")
8031
+ return { kind: "failure", failures: [{ kind: "scan", message: listing.message, path: dir }] };
8032
+ const failures: StrictInventoryFailure[] = [];
8033
+ const candidates: StrictInventoryCandidate[] = [];
8034
+ for (const managedCandidate of listing.owned) {
8035
+ const candidate = inventoryReadCandidate(
8036
+ storage,
8037
+ managedCandidate.path,
8038
+ cwd,
8039
+ path.dirname(managedCandidate.path),
8040
+ );
8041
+ if ("failures" in candidate) failures.push(...candidate.failures);
8042
+ else candidates.push(candidate.candidate);
8043
+ }
8044
+ assertManagedDirectoryRoot(destination.securityContext.rootAuthority);
8045
+ store.assertBound();
8046
+ return failures.length > 0 ? { kind: "failure", failures } : { kind: "complete", candidates };
8047
+ } catch {
8048
+ return {
8049
+ kind: "failure",
8050
+ failures: [{ kind: "root", message: "Managed strict inventory authority changed", path: dir }],
8051
+ };
8052
+ }
8053
+ }
7837
8054
  if (!storage.listFilesStrictSync) {
7838
8055
  return {
7839
8056
  kind: "failure",