@gajae-code/coding-agent 0.11.3 → 0.11.5

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 (213) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/dist/types/cli/notify-cli.d.ts +5 -1
  3. package/dist/types/cli/worktree-cli.d.ts +8 -25
  4. package/dist/types/commands/gc.d.ts +4 -0
  5. package/dist/types/commands/worktree.d.ts +7 -32
  6. package/dist/types/config/settings-schema.d.ts +20 -0
  7. package/dist/types/export/html/template.generated.d.ts +1 -1
  8. package/dist/types/gjc-runtime/deep-interview-state.d.ts +28 -0
  9. package/dist/types/gjc-runtime/gc-runtime.d.ts +9 -0
  10. package/dist/types/gjc-runtime/launch-tmux.d.ts +3 -0
  11. package/dist/types/gjc-runtime/managed-owner-admission.d.ts +42 -0
  12. package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +41 -0
  13. package/dist/types/gjc-runtime/state-argv.d.ts +38 -0
  14. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +9 -0
  15. package/dist/types/gjc-runtime/ultragoal-owner-loss-recovery.d.ts +94 -0
  16. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
  17. package/dist/types/harness-control-plane/owner.d.ts +3 -1
  18. package/dist/types/internal-urls/local-protocol.d.ts +18 -0
  19. package/dist/types/main.d.ts +2 -2
  20. package/dist/types/modes/components/ansi-display-validator.d.ts +2 -0
  21. package/dist/types/modes/components/custom-editor.d.ts +8 -3
  22. package/dist/types/modes/components/notifications-settings-editor.d.ts +4 -0
  23. package/dist/types/modes/components/session-observer-overlay.d.ts +3 -1
  24. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  25. package/dist/types/modes/components/tool-transcript-format.d.ts +77 -0
  26. package/dist/types/modes/components/transcript-viewer-overlay.d.ts +3 -0
  27. package/dist/types/modes/controllers/goal-mode-controller.d.ts +8 -1
  28. package/dist/types/modes/controllers/input-controller.d.ts +8 -2
  29. package/dist/types/modes/controllers/selector-controller.d.ts +15 -0
  30. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +3 -0
  31. package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +16 -0
  32. package/dist/types/plan-mode/approved-plan.d.ts +1 -0
  33. package/dist/types/runtime-mcp/manager.d.ts +2 -0
  34. package/dist/types/runtime-mcp/oauth-discovery.d.ts +12 -1
  35. package/dist/types/sdk/broker/broker.d.ts +1 -0
  36. package/dist/types/sdk/broker/discovery.d.ts +10 -0
  37. package/dist/types/sdk/broker/ensure.d.ts +24 -1
  38. package/dist/types/sdk/broker/internal.d.ts +3 -0
  39. package/dist/types/sdk/broker/session-index.d.ts +12 -0
  40. package/dist/types/sdk/broker/state-version.d.ts +2 -1
  41. package/dist/types/sdk/bus/chat-daemon-cli.d.ts +3 -1
  42. package/dist/types/sdk/bus/chat-daemon-control.d.ts +39 -4
  43. package/dist/types/sdk/bus/config-commands.d.ts +8 -0
  44. package/dist/types/sdk/bus/config.d.ts +22 -0
  45. package/dist/types/sdk/bus/index.d.ts +162 -2
  46. package/dist/types/sdk/bus/notification-service.d.ts +71 -8
  47. package/dist/types/sdk/bus/session-control.d.ts +11 -8
  48. package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +5 -0
  49. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -3
  50. package/dist/types/sdk/bus/telegram-daemon-control.d.ts +23 -4
  51. package/dist/types/sdk/bus/telegram-daemon.d.ts +214 -20
  52. package/dist/types/sdk/session.d.ts +16 -1
  53. package/dist/types/secrets/index.d.ts +4 -2
  54. package/dist/types/secrets/obfuscator.d.ts +1 -1
  55. package/dist/types/session/agent-session.d.ts +31 -2
  56. package/dist/types/session/artifacts.d.ts +30 -8
  57. package/dist/types/session/internal/managed-session-scope.d.ts +7 -0
  58. package/dist/types/session/internal/managed-session-storage.d.ts +63 -6
  59. package/dist/types/session/session-manager.d.ts +88 -18
  60. package/dist/types/session/session-storage.d.ts +27 -0
  61. package/dist/types/session/streaming-output.d.ts +4 -0
  62. package/dist/types/task/executor.d.ts +9 -0
  63. package/dist/types/tool-discovery/tool-index.d.ts +2 -0
  64. package/dist/types/tools/ask.d.ts +87 -12
  65. package/dist/types/tools/bash-skill-urls.d.ts +2 -2
  66. package/dist/types/tools/browser/screenshot-format.d.ts +8 -0
  67. package/dist/types/tools/browser/tab-protocol.d.ts +11 -0
  68. package/dist/types/tools/browser/tab-supervisor.d.ts +1 -1
  69. package/dist/types/tools/browser/tab-worker-path-resolver.d.ts +2 -0
  70. package/dist/types/tools/browser/tab-worker-smoke.d.ts +16 -0
  71. package/dist/types/tools/index.d.ts +4 -0
  72. package/dist/types/tools/path-utils.d.ts +9 -14
  73. package/dist/types/tools/render-utils.d.ts +1 -8
  74. package/dist/types/tools/resource-gc.d.ts +14 -0
  75. package/dist/types/utils/image-loading.d.ts +23 -0
  76. package/dist/types/utils/image-resize.d.ts +2 -0
  77. package/dist/types/utils/pasted-image-loading.d.ts +39 -0
  78. package/dist/types/utils/pasted-image-path.d.ts +21 -19
  79. package/dist/types/web/scrapers/html-to-markdown.d.ts +5 -0
  80. package/dist/types/web/scrapers/types.d.ts +1 -5
  81. package/package.json +25 -9
  82. package/scripts/generate-sdk-operation-inventory.ts +2 -0
  83. package/scripts/run-threat-model-check.ts +60 -0
  84. package/src/cli/notify-cli.ts +40 -18
  85. package/src/cli/worktree-cli.ts +9 -287
  86. package/src/cli.ts +10 -9
  87. package/src/commands/gc.ts +11 -1
  88. package/src/commands/sdk.ts +10 -4
  89. package/src/commands/worktree.ts +10 -53
  90. package/src/config/model-profiles.ts +15 -15
  91. package/src/config/settings-schema.ts +17 -0
  92. package/src/daemon/runtime.ts +2 -2
  93. package/src/defaults/gjc/skills/deep-interview/SKILL.md +70 -49
  94. package/src/defaults/gjc/skills/ralplan/SKILL.md +0 -6
  95. package/src/defaults/gjc/skills/team/SKILL.md +7 -9
  96. package/src/defaults/gjc/skills/ultragoal/SKILL.md +0 -2
  97. package/src/eval/js/shared/runtime.ts +1 -1
  98. package/src/eval/py/index.ts +2 -2
  99. package/src/export/html/template.generated.ts +1 -1
  100. package/src/extensibility/plugins/marketplace/manager.ts +94 -10
  101. package/src/gjc-runtime/deep-interview-recorder.ts +7 -0
  102. package/src/gjc-runtime/deep-interview-runtime.ts +10 -0
  103. package/src/gjc-runtime/deep-interview-state.ts +127 -3
  104. package/src/gjc-runtime/gc-render.ts +19 -1
  105. package/src/gjc-runtime/gc-runtime.ts +68 -3
  106. package/src/gjc-runtime/launch-tmux.ts +61 -6
  107. package/src/gjc-runtime/managed-owner-admission.ts +352 -0
  108. package/src/gjc-runtime/managed-owner-supervisor.ts +162 -0
  109. package/src/gjc-runtime/state-argv.ts +292 -0
  110. package/src/gjc-runtime/state-runtime.ts +80 -222
  111. package/src/gjc-runtime/team-runtime.ts +1 -1
  112. package/src/gjc-runtime/tmux-owner-isolation.ts +117 -0
  113. package/src/gjc-runtime/ultragoal-owner-loss-recovery.ts +328 -0
  114. package/src/gjc-runtime/ultragoal-runtime.ts +14 -0
  115. package/src/harness-control-plane/owner.ts +59 -3
  116. package/src/hindsight/client.ts +65 -2
  117. package/src/internal-urls/agent-protocol.ts +76 -5
  118. package/src/internal-urls/docs-index.generated.ts +7 -6
  119. package/src/internal-urls/local-protocol.ts +416 -18
  120. package/src/lsp/index.ts +4 -88
  121. package/src/main.ts +37 -13
  122. package/src/modes/acp/acp-agent.ts +8 -0
  123. package/src/modes/components/ansi-display-validator.ts +54 -0
  124. package/src/modes/components/custom-editor.ts +103 -32
  125. package/src/modes/components/custom-provider-wizard.ts +26 -1
  126. package/src/modes/components/notifications-settings-editor.ts +26 -2
  127. package/src/modes/components/session-observer-overlay.ts +40 -37
  128. package/src/modes/components/settings-selector.ts +3 -1
  129. package/src/modes/components/tool-transcript-format.ts +393 -0
  130. package/src/modes/components/transcript-viewer-overlay.ts +63 -1
  131. package/src/modes/controllers/goal-mode-controller.ts +20 -1
  132. package/src/modes/controllers/input-controller.ts +98 -37
  133. package/src/modes/controllers/plan-mode-controller.ts +2 -0
  134. package/src/modes/controllers/runtime-mcp-command-controller.ts +3 -0
  135. package/src/modes/controllers/selector-controller.ts +71 -9
  136. package/src/modes/interactive-mode.ts +14 -2
  137. package/src/modes/shared/agent-wire/deep-interview-gate.ts +31 -1
  138. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +17 -1
  139. package/src/modes/shared/agent-wire/workflow-gate-schema.ts +4 -2
  140. package/src/modes/shared/agent-wire/workflow-gate-types.ts +120 -3
  141. package/src/plan-mode/approved-plan.ts +3 -1
  142. package/src/prompts/agents/architect.md +1 -0
  143. package/src/prompts/agents/critic.md +1 -0
  144. package/src/prompts/system/custom-system-prompt.md +1 -1
  145. package/src/prompts/system/system-prompt.md +2 -1
  146. package/src/prompts/tools/lsp.md +3 -3
  147. package/src/runtime-mcp/manager.ts +16 -1
  148. package/src/runtime-mcp/oauth-discovery.ts +116 -26
  149. package/src/sdk/broker/broker.ts +139 -32
  150. package/src/sdk/broker/discovery.ts +125 -0
  151. package/src/sdk/broker/ensure.ts +103 -19
  152. package/src/sdk/broker/internal.ts +10 -0
  153. package/src/sdk/broker/session-index.ts +309 -38
  154. package/src/sdk/broker/state-version.ts +5 -2
  155. package/src/sdk/bus/chat-daemon-cli.ts +72 -10
  156. package/src/sdk/bus/chat-daemon-control.ts +526 -130
  157. package/src/sdk/bus/chat-daemon-runtime.ts +1 -0
  158. package/src/sdk/bus/config-commands.ts +22 -0
  159. package/src/sdk/bus/config.ts +37 -0
  160. package/src/sdk/bus/index.ts +384 -92
  161. package/src/sdk/bus/lifecycle-control-runtime.ts +53 -3
  162. package/src/sdk/bus/notification-service.ts +284 -62
  163. package/src/sdk/bus/recent-activity.ts +14 -5
  164. package/src/sdk/bus/session-control.ts +150 -44
  165. package/src/sdk/bus/telegram-daemon-cli.ts +20 -14
  166. package/src/sdk/bus/telegram-daemon-contract.ts +6 -3
  167. package/src/sdk/bus/telegram-daemon-control.ts +294 -76
  168. package/src/sdk/bus/telegram-daemon.ts +1842 -281
  169. package/src/sdk/protocol/operation-inventory.generated.json +11 -0
  170. package/src/sdk/session.ts +155 -27
  171. package/src/secrets/index.ts +53 -10
  172. package/src/secrets/obfuscator.ts +21 -18
  173. package/src/secrets/regex.ts +2 -0
  174. package/src/session/agent-session.ts +495 -253
  175. package/src/session/artifacts.ts +188 -23
  176. package/src/session/context-estimation.ts +7 -2
  177. package/src/session/internal/managed-session-scope.ts +215 -39
  178. package/src/session/internal/managed-session-storage.ts +1152 -46
  179. package/src/session/session-manager.ts +1055 -148
  180. package/src/session/session-storage.ts +109 -11
  181. package/src/session/streaming-output.ts +6 -0
  182. package/src/task/executor.ts +71 -26
  183. package/src/task/index.ts +63 -20
  184. package/src/tool-discovery/tool-index.ts +18 -0
  185. package/src/tools/ask.ts +329 -57
  186. package/src/tools/bash-allowed-prefixes.ts +79 -29
  187. package/src/tools/bash-skill-urls.ts +14 -3
  188. package/src/tools/bash.ts +2 -2
  189. package/src/tools/browser/launch.ts +3 -1
  190. package/src/tools/browser/readable.ts +1 -1
  191. package/src/tools/browser/screenshot-format.ts +34 -0
  192. package/src/tools/browser/tab-protocol.ts +3 -0
  193. package/src/tools/browser/tab-supervisor.ts +125 -122
  194. package/src/tools/browser/tab-worker-entry.ts +33 -14
  195. package/src/tools/browser/tab-worker-path-resolver.ts +67 -0
  196. package/src/tools/browser/tab-worker-smoke.ts +87 -0
  197. package/src/tools/browser/tab-worker.ts +7 -5
  198. package/src/tools/index.ts +4 -0
  199. package/src/tools/path-utils.ts +42 -35
  200. package/src/tools/plan-mode-guard.ts +1 -0
  201. package/src/tools/read.ts +7 -1
  202. package/src/tools/render-utils.ts +1 -27
  203. package/src/tools/resource-gc.ts +123 -34
  204. package/src/tools/sqlite-reader.ts +3 -2
  205. package/src/tools/subagent.ts +2 -0
  206. package/src/utils/image-loading.ts +85 -34
  207. package/src/utils/image-resize.ts +16 -9
  208. package/src/utils/pasted-image-loading.ts +514 -0
  209. package/src/utils/pasted-image-path.ts +223 -98
  210. package/src/web/scrapers/html-to-markdown.ts +68 -0
  211. package/src/web/scrapers/readthedocs.ts +1 -1
  212. package/src/web/scrapers/types.ts +1 -68
  213. package/src/web/search/providers/openai-compatible.ts +76 -23
@@ -14,13 +14,16 @@ import {
14
14
  import {
15
15
  acquireManagedLock,
16
16
  captureManagedFileNoFollow,
17
+ captureManagedFilePrefixNoFollow,
17
18
  copyManagedFileNoReplace,
18
19
  ensureManagedDirectory,
19
20
  fsyncManagedArtifactTree,
21
+ ManagedSessionDescendantStore,
20
22
  type ManagedStorageLock,
23
+ managedDirectoryRoot,
21
24
  publishManagedFileNoReplace,
22
- publishManagedFileNoReplaceSync,
23
25
  publishManagedTombstone,
26
+ retainManagedDirectoryAuthority,
24
27
  validateManagedArtifactTree,
25
28
  } from "./managed-session-storage";
26
29
 
@@ -41,6 +44,49 @@ export interface ManagedScope {
41
44
  platform: "posix" | "win32";
42
45
  }
43
46
 
47
+ const managedRoots = new WeakMap<ManagedScope, ReturnType<typeof managedDirectoryRoot>>();
48
+ const managedDirectoryIdentities = new WeakMap<ManagedScope, { dev: bigint; ino: bigint }>();
49
+ const managedDirectoryAuthorities = new WeakMap<ManagedScope, native.RecoveryFsRoot | undefined>();
50
+
51
+ export function managedDirectoryAuthorityForScope(scope: ManagedScope): native.RecoveryFsRoot | undefined {
52
+ if (!managedDirectoryAuthorities.has(scope)) throw new Error("Managed session directory authority was not prepared");
53
+ return managedDirectoryAuthorities.get(scope);
54
+ }
55
+
56
+ export function managedDirectoryIdentityForScope(scope: ManagedScope): { dev: bigint; ino: bigint } {
57
+ const identity = managedDirectoryIdentities.get(scope);
58
+ if (!identity) throw new Error("Managed session directory identity was not prepared");
59
+ return identity;
60
+ }
61
+
62
+ function configuredRootPath(scope: ManagedScope): string {
63
+ let candidate = pathIsWithin(scope.agentDir, scope.sessionsRoot) ? scope.agentDir : path.dirname(scope.sessionsRoot);
64
+ for (;;) {
65
+ try {
66
+ const stat = fs.lstatSync(candidate);
67
+ if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error(`Unsafe configured root: ${candidate}`);
68
+ return fs.realpathSync.native(candidate);
69
+ } catch (error) {
70
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
71
+ const parent = path.dirname(candidate);
72
+ if (parent === candidate) throw new Error("Configured managed root is unavailable.");
73
+ candidate = parent;
74
+ }
75
+ }
76
+ }
77
+
78
+ function scopeRoot(scope: ManagedScope) {
79
+ const retained = managedRoots.get(scope);
80
+ if (retained) return retained;
81
+ const root = managedDirectoryRoot(configuredRootPath(scope));
82
+ managedRoots.set(scope, root);
83
+ return root;
84
+ }
85
+
86
+ export function managedRootForScope(scope: ManagedScope) {
87
+ return scopeRoot(scope);
88
+ }
89
+
44
90
  export type ManagedMigrationPolicy = "copy-retain" | "disabled";
45
91
 
46
92
  export type ManagedScopeErrorCode =
@@ -99,6 +145,7 @@ export type ManagedDeleteCandidateResult =
99
145
  type NativeIdentity =
100
146
  | { ok: true; platform: "posix" | "win32"; canonicalPath: string }
101
147
  | { ok: false; code: NativeIdentityFailureCode };
148
+ type CanonicalNativeIdentity = Extract<NativeIdentity, { ok: true }>;
102
149
 
103
150
  type NativeIdentityFailureCode =
104
151
  | "not_found"
@@ -149,6 +196,18 @@ function identityFor(cwd: string): NativeIdentity {
149
196
  return canonicalExistingDirectoryIdentity(cwd) as NativeIdentity;
150
197
  }
151
198
 
199
+ function canonicalExistingPathForIo(base: string, identity: CanonicalNativeIdentity): string {
200
+ if (identity.platform !== "win32") return identity.canonicalPath;
201
+ try {
202
+ // Native identity uses a stable Volume GUID path on Windows. Bun 1.3.14
203
+ // cannot reliably create/read files through that path, so retain the
204
+ // symlink-resolved DOS path for JavaScript filesystem I/O.
205
+ return fs.realpathSync.native(base);
206
+ } catch {
207
+ return path.resolve(base);
208
+ }
209
+ }
210
+
152
211
  /**
153
212
  * Resolve benign symlinks in the deepest existing ancestor of a trusted storage
154
213
  * root (e.g. macOS `/var -> /private/var`, or a symlinked `$HOME`) while keeping
@@ -164,7 +223,8 @@ export function canonicalizeTrustedPath(target: string): string {
164
223
  for (;;) {
165
224
  const identity = canonicalExistingDirectoryIdentity(base) as NativeIdentity;
166
225
  if (identity.ok) {
167
- return suffix.length === 0 ? identity.canonicalPath : path.join(identity.canonicalPath, ...suffix);
226
+ const canonicalBase = canonicalExistingPathForIo(base, identity);
227
+ return suffix.length === 0 ? canonicalBase : path.join(canonicalBase, ...suffix);
168
228
  }
169
229
  if (identity.code !== "not_found" && identity.code !== "not_directory") return path.resolve(target);
170
230
  const parent = path.dirname(base);
@@ -214,15 +274,11 @@ function isBinding(value: unknown): value is Binding {
214
274
  );
215
275
  }
216
276
 
217
- function validateExistingBinding(scope: ManagedScope): ManagedScopeResolution | undefined {
218
- const bindingPath = path.join(scope.directoryPath, MANAGED_SESSION_BINDING_FILE);
219
- let raw: string;
277
+ function validateBindingRaw(scope: ManagedScope, raw: string): ManagedScopeResolution | undefined {
220
278
  let parsed: unknown;
221
279
  try {
222
- raw = captureManagedFileNoFollow(bindingPath).bytes.toString("utf8");
223
280
  parsed = JSON.parse(raw);
224
- } catch (error) {
225
- if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
281
+ } catch {
226
282
  return { kind: "error", code: "binding_invalid", message: "The managed scope binding is invalid JSON." };
227
283
  }
228
284
  if (!isBinding(parsed))
@@ -249,6 +305,18 @@ function validateExistingBinding(scope: ManagedScope): ManagedScopeResolution |
249
305
  return undefined;
250
306
  }
251
307
 
308
+ function validateExistingBinding(scope: ManagedScope): ManagedScopeResolution | undefined {
309
+ const bindingPath = path.join(scope.directoryPath, MANAGED_SESSION_BINDING_FILE);
310
+ let raw: string;
311
+ try {
312
+ raw = captureManagedFileNoFollow(bindingPath).bytes.toString("utf8");
313
+ } catch (error) {
314
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
315
+ return { kind: "error", code: "binding_invalid", message: "The managed scope binding is invalid JSON." };
316
+ }
317
+ return validateBindingRaw(scope, raw);
318
+ }
319
+
252
320
  export function resolveManagedScope(input: {
253
321
  cwd: string;
254
322
  agentDir: string;
@@ -256,6 +324,23 @@ export function resolveManagedScope(input: {
256
324
  }): ManagedScopeResolution {
257
325
  const identity = identityFor(input.cwd);
258
326
  if (!identity.ok) return nativeFailure(identity.code);
327
+ try {
328
+ if (fs.lstatSync(input.sessionsRoot).isSymbolicLink()) {
329
+ return {
330
+ kind: "error",
331
+ code: "sessions_root_unavailable",
332
+ message: "The sessions root is not a safe directory.",
333
+ };
334
+ }
335
+ } catch (error) {
336
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
337
+ return {
338
+ kind: "error",
339
+ code: "sessions_root_unavailable",
340
+ message: "The sessions root could not be inspected.",
341
+ };
342
+ }
343
+ }
259
344
  const sessionsRoot = canonicalizeTrustedPath(input.sessionsRoot);
260
345
  const agentDir = canonicalizeTrustedPath(input.agentDir);
261
346
  const digest = scopeDigest(identity.platform, identity.canonicalPath);
@@ -376,6 +461,49 @@ function fsyncManagedParent(pathname: string): void {
376
461
  }
377
462
  }
378
463
 
464
+ type CandidatePreflight =
465
+ | { kind: "capture"; identity: { dev: bigint; ino: bigint; size: number; mtimeNs: bigint } }
466
+ | {
467
+ kind: "foreign";
468
+ }
469
+ | {
470
+ kind: "invalid";
471
+ code: string;
472
+ };
473
+
474
+ function preflightCandidate(filePath: string, scope: ManagedScope): CandidatePreflight {
475
+ try {
476
+ const snapshot = captureManagedFilePrefixNoFollow(filePath, HEADER_MAX_BYTES);
477
+ const lineEnd = snapshot.bytes.indexOf(0x0a);
478
+ if (lineEnd < 0) return { kind: "invalid", code: "invalid_header" };
479
+ const value: unknown = JSON.parse(snapshot.bytes.subarray(0, lineEnd).toString("utf8"));
480
+ if (!value || typeof value !== "object" || Array.isArray(value))
481
+ return { kind: "invalid", code: "invalid_header" };
482
+ const header = value as Record<string, unknown>;
483
+ if (header.type !== "session" || typeof header.id !== "string" || typeof header.cwd !== "string")
484
+ return { kind: "invalid", code: "invalid_header" };
485
+ const candidateIdentity = identityFor(header.cwd);
486
+ if (
487
+ candidateIdentity.ok &&
488
+ (candidateIdentity.platform !== scope.platform || candidateIdentity.canonicalPath !== scope.canonicalCwd)
489
+ )
490
+ return { kind: "foreign" };
491
+ return { kind: "capture", identity: snapshot.identity };
492
+ } catch {
493
+ return { kind: "invalid", code: "unreadable_candidate" };
494
+ }
495
+ }
496
+
497
+ function matchesPreflightIdentity(candidate: ManagedCandidate, preflight: CandidatePreflight): boolean {
498
+ return (
499
+ preflight.kind === "capture" &&
500
+ candidate.identity.dev === preflight.identity.dev &&
501
+ candidate.identity.ino === preflight.identity.ino &&
502
+ candidate.identity.size === preflight.identity.size &&
503
+ candidate.identity.mtimeNs === preflight.identity.mtimeNs
504
+ );
505
+ }
506
+
379
507
  function inspectCandidate(filePath: string, provenance: "v2" | "legacy"): ManagedCandidate | { code: string } {
380
508
  try {
381
509
  const snapshot = captureManagedFileNoFollow(filePath);
@@ -386,7 +514,8 @@ function inspectCandidate(filePath: string, provenance: "v2" | "legacy"): Manage
386
514
  const header = value as Record<string, unknown>;
387
515
  if (header.type !== "session" || typeof header.id !== "string" || typeof header.cwd !== "string")
388
516
  return { code: "invalid_header" };
389
- if (!identityFor(header.cwd).ok) return { code: "invalid_cwd" };
517
+ const cwdIdentity = identityFor(header.cwd);
518
+ if (!cwdIdentity.ok) return { code: `cwd_${cwdIdentity.code}` };
390
519
  const named = fs.lstatSync(filePath, { bigint: true });
391
520
  if (
392
521
  !named.isFile() ||
@@ -436,10 +565,13 @@ function discoveredLegacyDirectoryNames(scope: ManagedScope): readonly string[]
436
565
  return [...known, ...discovered];
437
566
  }
438
567
 
568
+ type CandidateInspection = ManagedCandidate | { code: string } | { foreign: true };
569
+
439
570
  function listDirectoryCandidates(
440
571
  directory: string,
441
572
  provenance: "v2" | "legacy",
442
- ): readonly (ManagedCandidate | { code: string })[] {
573
+ scope: ManagedScope,
574
+ ): readonly CandidateInspection[] {
443
575
  let directoryStat: fs.Stats;
444
576
  try {
445
577
  directoryStat = fs.lstatSync(directory);
@@ -451,17 +583,26 @@ function listDirectoryCandidates(
451
583
  return fs
452
584
  .readdirSync(directory, { withFileTypes: true })
453
585
  .filter(entry => entry.name.endsWith(".jsonl"))
454
- .map(entry => inspectCandidate(path.join(directory, entry.name), provenance));
586
+ .map(entry => {
587
+ const filePath = path.join(directory, entry.name);
588
+ const preflight = preflightCandidate(filePath, scope);
589
+ if (preflight.kind === "invalid") return { code: preflight.code };
590
+ if (preflight.kind === "foreign") return { foreign: true };
591
+ const candidate = inspectCandidate(filePath, provenance);
592
+ if ("code" in candidate) return candidate;
593
+ return matchesPreflightIdentity(candidate, preflight) ? candidate : { code: "source_changed" };
594
+ });
455
595
  }
456
596
 
457
597
  export async function ensureManagedScope(scope: ManagedScope): Promise<ManagedScopeResolution> {
458
598
  try {
459
- ensureManagedDirectory(scope.sessionsRoot);
460
- ensureManagedDirectory(scope.directoryPath);
599
+ const root = scopeRoot(scope);
600
+ ensureManagedDirectory(scope.sessionsRoot, root);
601
+ ensureManagedDirectory(scope.directoryPath, root);
461
602
  const bindingPath = path.join(scope.directoryPath, MANAGED_SESSION_BINDING_FILE);
462
603
  const binding = `${JSON.stringify(bindingFor(scope))}\n`;
463
604
  try {
464
- await publishManagedFileNoReplace(bindingPath, new TextEncoder().encode(binding));
605
+ await publishManagedFileNoReplace(bindingPath, new TextEncoder().encode(binding), undefined, root);
465
606
  } catch (error) {
466
607
  if ((error as Error).message !== "destination_conflict") throw error;
467
608
  }
@@ -478,24 +619,46 @@ export async function ensureManagedScope(scope: ManagedScope): Promise<ManagedSc
478
619
  /** Synchronously create and validate the v2 binding before a default session writer exists. */
479
620
  export function prepareManagedSessionScopeForWriteSync(scope: ManagedScope): ManagedScopeResolution {
480
621
  try {
481
- ensureManagedDirectory(scope.sessionsRoot);
482
- ensureManagedDirectory(scope.directoryPath);
483
- const bindingPath = path.join(scope.directoryPath, MANAGED_SESSION_BINDING_FILE);
622
+ const root = scopeRoot(scope);
623
+ ensureManagedDirectory(scope.sessionsRoot, root);
624
+ ensureManagedDirectory(scope.directoryPath, root);
625
+ const preparedDirectory = fs.lstatSync(scope.directoryPath, { bigint: true });
626
+ if (!preparedDirectory.isDirectory() || preparedDirectory.isSymbolicLink())
627
+ throw new Error("Managed session directory changed");
628
+ const retainedAuthority = retainManagedDirectoryAuthority(root, scope.directoryPath, {
629
+ dev: preparedDirectory.dev,
630
+ ino: preparedDirectory.ino,
631
+ });
632
+ const store = new ManagedSessionDescendantStore(
633
+ root,
634
+ scope.directoryPath,
635
+ retainedAuthority ? { authority: retainedAuthority, authorityBaseDir: scope.directoryPath } : undefined,
636
+ );
637
+ const binding = new TextEncoder().encode(`${JSON.stringify(bindingFor(scope))}\n`);
484
638
  try {
485
- publishManagedFileNoReplaceSync(
486
- bindingPath,
487
- new TextEncoder().encode(`${JSON.stringify(bindingFor(scope))}\n`),
488
- );
639
+ store.publishNoReplaceSync(MANAGED_SESSION_BINDING_FILE, binding);
489
640
  } catch (error) {
490
641
  if ((error as Error).message !== "destination_conflict") throw error;
491
642
  }
492
- const validated = validateExistingBinding(scope);
643
+ const capturedBinding = store.readExpected(MANAGED_SESSION_BINDING_FILE);
644
+ if (!capturedBinding) throw new Error("Managed scope binding is unavailable");
645
+ const validated = validateBindingRaw(scope, capturedBinding.bytes.toString("utf8"));
646
+ managedDirectoryAuthorities.set(scope, retainedAuthority);
647
+ const directoryStat = fs.lstatSync(scope.directoryPath, { bigint: true });
648
+ if (
649
+ !directoryStat.isDirectory() ||
650
+ directoryStat.isSymbolicLink() ||
651
+ directoryStat.dev !== preparedDirectory.dev ||
652
+ directoryStat.ino !== preparedDirectory.ino
653
+ )
654
+ throw new Error("Managed session directory changed");
655
+ managedDirectoryIdentities.set(scope, { dev: preparedDirectory.dev, ino: preparedDirectory.ino });
493
656
  if (validated) return validated;
494
657
  const internal = managedInternalDirectory(scope);
495
- ensureManagedDirectory(internal);
496
- ensureManagedDirectory(path.join(internal, MANAGED_LOCKS_DIRECTORY));
497
- ensureManagedDirectory(path.join(internal, MANAGED_RECEIPTS_DIRECTORY));
498
- ensureManagedDirectory(path.join(internal, MANAGED_TOMBSTONES_DIRECTORY));
658
+ ensureManagedDirectory(internal, root);
659
+ ensureManagedDirectory(path.join(internal, MANAGED_LOCKS_DIRECTORY), root);
660
+ ensureManagedDirectory(path.join(internal, MANAGED_RECEIPTS_DIRECTORY), root);
661
+ ensureManagedDirectory(path.join(internal, MANAGED_TOMBSTONES_DIRECTORY), root);
499
662
  return { kind: "resolved", scope };
500
663
  } catch (error) {
501
664
  return {
@@ -530,14 +693,18 @@ export function listManagedCandidates(scope: ManagedScope): ManagedCandidateList
530
693
  ];
531
694
  const seen = new Set<string>();
532
695
  for (const directory of directories) {
533
- for (const candidate of listDirectoryCandidates(directory.path, directory.provenance)) {
696
+ for (const candidate of listDirectoryCandidates(directory.path, directory.provenance, scope)) {
534
697
  if ("code" in candidate) {
535
698
  invalid.push({ code: candidate.code });
536
699
  continue;
537
700
  }
701
+ if ("foreign" in candidate) {
702
+ foreignCount++;
703
+ continue;
704
+ }
538
705
  const candidateIdentity = identityFor(candidate.cwd);
539
706
  if (!candidateIdentity.ok) {
540
- invalid.push({ code: "invalid_cwd" });
707
+ invalid.push({ code: `cwd_${candidateIdentity.code}` });
541
708
  continue;
542
709
  }
543
710
  if (
@@ -1074,6 +1241,7 @@ function artifactTreeSnapshot(value: unknown): NativeDirectoryTreeSnapshot | und
1074
1241
  typeof item.ino === "string" &&
1075
1242
  typeof item.size === "string" &&
1076
1243
  typeof item.mtimeNs === "string" &&
1244
+ typeof item.ctimeNs === "string" &&
1077
1245
  (item.sha256 === undefined || typeof item.sha256 === "string")
1078
1246
  );
1079
1247
  })
@@ -1612,12 +1780,14 @@ function restoreDetachedArtifactRoot(detached: DetachedArtifactRoot): void {
1612
1780
  }
1613
1781
 
1614
1782
  async function copyArtifacts(
1783
+ scope: ManagedScope,
1615
1784
  sourceTranscript: string,
1616
1785
  destinationTranscript: string,
1617
1786
  manifest: readonly ArtifactManifestEntry[],
1618
1787
  lock: ManagedStorageLock,
1619
1788
  sourceRootOverride?: string,
1620
1789
  ): Promise<void> {
1790
+ const root = scopeRoot(scope);
1621
1791
  if (manifest.length === 0) return;
1622
1792
  const sourceRoot = sourceRootOverride ?? sourceTranscript.slice(0, -6);
1623
1793
  if (!manifestMatches(sourceTranscript, manifest, sourceRoot)) throw new Error("source_changed");
@@ -1627,8 +1797,8 @@ async function copyArtifacts(
1627
1797
  const source = path.join(sourceRoot, entry.path);
1628
1798
  const destination = path.join(destinationRoot, entry.path);
1629
1799
  if (entry.kind === "directory") {
1630
- if (entry.path === "") ensureManagedDirectory(destinationRoot);
1631
- else ensureManagedDirectory(destination);
1800
+ if (entry.path === "") ensureManagedDirectory(destinationRoot, root);
1801
+ else ensureManagedDirectory(destination, root);
1632
1802
  continue;
1633
1803
  }
1634
1804
  const snapshot = captureManagedFileNoFollow(source);
@@ -1638,7 +1808,7 @@ async function copyArtifacts(
1638
1808
  )
1639
1809
  throw new Error("source_changed");
1640
1810
  try {
1641
- await copyManagedFileNoReplace(source, destination, snapshot);
1811
+ await copyManagedFileNoReplace(source, destination, snapshot, root);
1642
1812
  } catch (error) {
1643
1813
  if ((error as Error).message !== "destination_conflict") throw error;
1644
1814
  }
@@ -1755,6 +1925,7 @@ export async function reconcileManagedTombstones(scope: ManagedScope): Promise<v
1755
1925
  lock = await acquireManagedLock(
1756
1926
  path.join(managedInternalDirectory(scope), MANAGED_LOCKS_DIRECTORY),
1757
1927
  path.basename(tombstone, ".json"),
1928
+ scopeRoot(scope),
1758
1929
  );
1759
1930
  const lockedTargets = retiredTargets(scope, tombstone);
1760
1931
  if (!lockedTargets) continue;
@@ -1866,10 +2037,11 @@ export async function prepareManagedSessionScopeForWrite(scope: ManagedScope): P
1866
2037
  if (prepared.kind === "error") return prepared;
1867
2038
  try {
1868
2039
  const internal = managedInternalDirectory(scope);
1869
- ensureManagedDirectory(internal);
1870
- ensureManagedDirectory(path.join(internal, MANAGED_LOCKS_DIRECTORY));
1871
- ensureManagedDirectory(path.join(internal, MANAGED_RECEIPTS_DIRECTORY));
1872
- ensureManagedDirectory(path.join(internal, MANAGED_TOMBSTONES_DIRECTORY));
2040
+ const root = scopeRoot(scope);
2041
+ ensureManagedDirectory(internal, root);
2042
+ ensureManagedDirectory(path.join(internal, MANAGED_LOCKS_DIRECTORY), root);
2043
+ ensureManagedDirectory(path.join(internal, MANAGED_RECEIPTS_DIRECTORY), root);
2044
+ ensureManagedDirectory(path.join(internal, MANAGED_TOMBSTONES_DIRECTORY), root);
1873
2045
  await reconcileManagedTombstones(scope);
1874
2046
  return { kind: "resolved", scope };
1875
2047
  } catch (error) {
@@ -1920,7 +2092,7 @@ export async function openManagedCandidateForWrite(
1920
2092
  let lock: ManagedStorageLock | undefined;
1921
2093
  let detachedArtifacts: DetachedArtifactRoot | undefined;
1922
2094
  try {
1923
- lock = await acquireManagedLock(path.join(internal, MANAGED_LOCKS_DIRECTORY), operation);
2095
+ lock = await acquireManagedLock(path.join(internal, MANAGED_LOCKS_DIRECTORY), operation, scopeRoot(scope));
1924
2096
  const afterLock = validateCandidateForScope(scope, current);
1925
2097
  if (!afterLock)
1926
2098
  return { kind: "error", code: "source_changed", message: "The legacy candidate changed during migration." };
@@ -1986,11 +2158,11 @@ export async function openManagedCandidateForWrite(
1986
2158
  if (artifactPlan && fs.existsSync(artifactPlan.detachedPath)) throw new Error("destination_conflict");
1987
2159
  detachedArtifacts = artifactPlan ? detachArtifactRootForMigration(artifactPlan) : undefined;
1988
2160
  manifest = detachedArtifacts ? artifactManifestFromSnapshot(detachedArtifacts.tree) : [];
1989
- await copyArtifacts(afterLock.path, destination, manifest, lock, detachedArtifacts?.detachedPath);
2161
+ await copyArtifacts(scope, afterLock.path, destination, manifest, lock, detachedArtifacts?.detachedPath);
1990
2162
  if (!existing) {
1991
2163
  try {
1992
2164
  lock.assertOwned();
1993
- await copyManagedFileNoReplace(afterLock.path, destination, sourceSnapshot);
2165
+ await copyManagedFileNoReplace(afterLock.path, destination, sourceSnapshot, scopeRoot(scope));
1994
2166
  } catch (error) {
1995
2167
  if ((error as Error).message !== "destination_conflict") throw error;
1996
2168
  }
@@ -2087,7 +2259,11 @@ export async function deleteManagedSessionCandidate(
2087
2259
  const operation = path.basename(tombstone, ".json");
2088
2260
  let lock: ManagedStorageLock | undefined;
2089
2261
  try {
2090
- lock = await acquireManagedLock(path.join(managedInternalDirectory(scope), MANAGED_LOCKS_DIRECTORY), operation);
2262
+ lock = await acquireManagedLock(
2263
+ path.join(managedInternalDirectory(scope), MANAGED_LOCKS_DIRECTORY),
2264
+ operation,
2265
+ scopeRoot(scope),
2266
+ );
2091
2267
  let targets = retiredTargets(scope, tombstone);
2092
2268
  if (!targets) {
2093
2269
  if (!current) throw new Error("source_changed");