@timurproko/a1 0.2.7-dev.1276 → 0.2.7-dev.1286

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 (184) hide show
  1. package/README.md +5 -1
  2. package/dist/app/session-shell/session-shell-root.d.ts +4 -68
  3. package/dist/app/session-shell/session-shell.js +5 -11
  4. package/dist/app/session-shell/shell-host.js +2 -2
  5. package/dist/app/session-shell/shell-options.d.ts +65 -0
  6. package/dist/app/session-shell/shell-options.js +1 -0
  7. package/dist/app/session-shell/workflow-router.d.ts +3 -1
  8. package/dist/app/session-shell/workflow-router.js +12 -0
  9. package/dist/cli/dispatch.js +27 -1
  10. package/dist/cli/packages.js +18 -2
  11. package/dist/cli/session-context.js +6 -2
  12. package/dist/composition/owned-ui.js +20 -8
  13. package/dist/composition/session-naming.js +1 -1
  14. package/dist/composition/session-runtime-registry.d.ts +2 -0
  15. package/dist/composition/session-runtime-registry.js +49 -9
  16. package/dist/contracts/agent-engine/package-ports.d.ts +10 -14
  17. package/dist/contracts/agent-engine/package-ports.js +5 -9
  18. package/dist/contracts/owned-ui/session-workflows.d.ts +6 -0
  19. package/dist/contracts/presentation/index.d.ts +1 -0
  20. package/dist/contracts/presentation/progress-palette.generated.d.ts +1 -1
  21. package/dist/contracts/presentation/progress-palette.generated.js +1 -1
  22. package/dist/contracts/presentation/progress-spinner.d.ts +7 -0
  23. package/dist/contracts/presentation/progress-spinner.js +1 -0
  24. package/dist/features/launch/session-context.d.ts +8 -0
  25. package/dist/features/launch/session-context.js +19 -10
  26. package/dist/features/owned-ui/reference-screen-app.js +9 -5
  27. package/dist/features/owned-ui/session-fork-prompt.d.ts +2 -1
  28. package/dist/features/owned-ui/session-fork-prompt.js +11 -5
  29. package/dist/features/owned-ui/settings-app.d.ts +1 -1
  30. package/dist/features/owned-ui/settings-app.js +14 -7
  31. package/dist/features/owned-ui/settings-spinner-preview.d.ts +9 -0
  32. package/dist/features/owned-ui/settings-spinner-preview.js +29 -0
  33. package/dist/features/session-naming/context.d.ts +17 -0
  34. package/dist/features/session-naming/context.js +39 -0
  35. package/dist/features/session-naming/controller.d.ts +1 -5
  36. package/dist/features/session-naming/controller.js +7 -4
  37. package/dist/features/session-naming/generate.d.ts +2 -1
  38. package/dist/features/session-naming/generate.js +14 -10
  39. package/dist/features/session-naming/index.d.ts +1 -1
  40. package/dist/foundation/lifecycle/index.d.ts +3 -1
  41. package/dist/foundation/lifecycle/index.js +3 -1
  42. package/dist/foundation/lifecycle/session-claim-lock.d.ts +2 -2
  43. package/dist/foundation/lifecycle/session-claim-lock.js +36 -10
  44. package/dist/foundation/lifecycle/session-context-retry.d.ts +2 -0
  45. package/dist/foundation/lifecycle/session-context-retry.js +17 -0
  46. package/dist/foundation/lifecycle/session-repository-context.d.ts +7 -1
  47. package/dist/foundation/lifecycle/session-repository-context.js +105 -39
  48. package/dist/foundation/lifecycle/session-worktree-entry.d.ts +3 -0
  49. package/dist/foundation/lifecycle/session-worktree-entry.js +87 -0
  50. package/dist/foundation/release/automatic-update.js +2 -2
  51. package/dist/foundation/release/bootstrap.js +19 -3
  52. package/dist/foundation/release/index.d.ts +1 -1
  53. package/dist/foundation/release/index.js +1 -1
  54. package/dist/foundation/release/release-gc.js +41 -5
  55. package/dist/foundation/release/release-state.d.ts +48 -4
  56. package/dist/foundation/release/release-state.js +168 -31
  57. package/dist/foundation/release/release-use.d.ts +10 -0
  58. package/dist/foundation/release/release-use.js +86 -0
  59. package/dist/foundation/release/update-activation.d.ts +3 -1
  60. package/dist/foundation/release/update-activation.js +23 -6
  61. package/dist/foundation/release/update-recovery.js +2 -2
  62. package/dist/foundation/release/update.d.ts +3 -1
  63. package/dist/foundation/release/update.js +61 -22
  64. package/dist/foundation/release/warmup.js +8 -9
  65. package/dist/foundation/session-writer-lock/index.d.ts +2 -2
  66. package/dist/foundation/session-writer-lock/index.js +82 -12
  67. package/dist/foundation/startup/startup-descriptor.js +2 -2
  68. package/dist/foundation/terminal-cleanup/fatal-diagnostic.d.ts +2 -0
  69. package/dist/foundation/terminal-cleanup/fatal-diagnostic.js +59 -0
  70. package/dist/foundation/terminal-cleanup/fatal-exit.js +1 -29
  71. package/dist/integrations/pi/components/resources/builtin-themes.d.ts +2 -2
  72. package/dist/integrations/pi/components/resources/builtin-themes.js +2 -2
  73. package/dist/integrations/pi/components/share-operation-dialog.d.ts +2 -1
  74. package/dist/integrations/pi/components/share-operation-dialog.js +13 -5
  75. package/dist/integrations/pi/components/shell-footer-status.d.ts +2 -1
  76. package/dist/integrations/pi/components/shell-footer-status.js +15 -8
  77. package/dist/integrations/pi/components/shell-session-info-reference.js +11 -8
  78. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +1 -1
  79. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.js +1 -1
  80. package/dist/integrations/pi/components/upstream/assets/earendil-image.json +2 -2
  81. package/dist/integrations/pi/components/upstream/components/auth-url.d.ts +1 -1
  82. package/dist/integrations/pi/components/upstream/components/auth-url.js +1 -1
  83. package/dist/integrations/pi/components/upstream/components/countdown-timer.d.ts +1 -1
  84. package/dist/integrations/pi/components/upstream/components/earendil-announcement.js +1 -1
  85. package/dist/integrations/pi/components/upstream/components/extension-editor.d.ts +1 -1
  86. package/dist/integrations/pi/components/upstream/components/extension-editor.js +1 -1
  87. package/dist/integrations/pi/components/upstream/components/extension-input.js +1 -1
  88. package/dist/integrations/pi/components/upstream/components/extension-selector.js +1 -1
  89. package/dist/integrations/pi/components/upstream/components/login-dialog.js +1 -1
  90. package/dist/integrations/pi/components/upstream/components/mermaid.js +1 -1
  91. package/dist/integrations/pi/components/upstream/components/oauth-selector.d.ts +1 -1
  92. package/dist/integrations/pi/components/upstream/components/oauth-selector.js +1 -1
  93. package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +1 -1
  94. package/dist/integrations/pi/components/upstream/components/owned-editor.js +1 -1
  95. package/dist/integrations/pi/components/upstream/components/pi-logo.js +1 -1
  96. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.d.ts +1 -1
  97. package/dist/integrations/pi/components/upstream/components/session-footer.js +14 -8
  98. package/dist/integrations/pi/components/upstream/components/session-selector-search.js +1 -1
  99. package/dist/integrations/pi/components/upstream/components/session-selector.js +1 -1
  100. package/dist/integrations/pi/components/upstream/components/skill-invocation-message.d.ts +1 -1
  101. package/dist/integrations/pi/components/upstream/components/skill-invocation-message.js +1 -1
  102. package/dist/integrations/pi/components/upstream/components/status-indicator.d.ts +1 -1
  103. package/dist/integrations/pi/components/upstream/components/status-indicator.js +1 -1
  104. package/dist/integrations/pi/components/upstream/components/themed-text.d.ts +1 -1
  105. package/dist/integrations/pi/components/upstream/components/themed-text.js +1 -1
  106. package/dist/integrations/pi/components/upstream/components/thinking-selector.d.ts +1 -1
  107. package/dist/integrations/pi/components/upstream/components/thinking-selector.js +1 -1
  108. package/dist/integrations/pi/components/upstream/components/tool-execution.js +1 -1
  109. package/dist/integrations/pi/components/upstream/components/tree-selector.d.ts +1 -1
  110. package/dist/integrations/pi/components/upstream/components/tree-selector.js +1 -1
  111. package/dist/integrations/pi/components/upstream/components/trust-selector.js +1 -1
  112. package/dist/integrations/pi/components/upstream/external-editor.js +1 -1
  113. package/dist/integrations/pi/components/upstream/history/editor-core.d.ts +1 -1
  114. package/dist/integrations/pi/components/upstream/history/editor-core.js +1 -1
  115. package/dist/integrations/pi/components/upstream/history/kill-ring.d.ts +1 -1
  116. package/dist/integrations/pi/components/upstream/history/kill-ring.js +1 -1
  117. package/dist/integrations/pi/components/upstream/history/printable-key.js +1 -1
  118. package/dist/integrations/pi/components/upstream/history/text-helpers.js +1 -1
  119. package/dist/integrations/pi/components/upstream/history/undo-stack.d.ts +1 -1
  120. package/dist/integrations/pi/components/upstream/history/undo-stack.js +1 -1
  121. package/dist/integrations/pi/components/upstream/history/word-navigation.js +1 -1
  122. package/dist/integrations/pi/components/upstream/model-search.d.ts +1 -1
  123. package/dist/integrations/pi/components/upstream/program-status-reporter.d.ts +1 -1
  124. package/dist/integrations/pi/components/upstream/theme/theme.js +1 -1
  125. package/dist/integrations/pi/engine/adapter.d.ts +3 -1
  126. package/dist/integrations/pi/engine/adapter.js +24 -6
  127. package/dist/integrations/pi/engine/changelog.js +5 -2
  128. package/dist/integrations/pi/engine/delivery-retention.d.ts +7 -0
  129. package/dist/integrations/pi/engine/delivery-retention.js +66 -0
  130. package/dist/integrations/pi/engine/event-delivery.d.ts +17 -1
  131. package/dist/integrations/pi/engine/event-delivery.js +25 -3
  132. package/dist/integrations/pi/engine/extension-ui-binding.d.ts +2 -1
  133. package/dist/integrations/pi/engine/extension-ui-binding.js +45 -0
  134. package/dist/integrations/pi/engine/host-ports.d.ts +2 -0
  135. package/dist/integrations/pi/engine/host.d.ts +3 -0
  136. package/dist/integrations/pi/engine/host.js +2 -0
  137. package/dist/integrations/pi/engine/package-integration.js +19 -4
  138. package/dist/integrations/pi/engine/pending-delivery.d.ts +4 -1
  139. package/dist/integrations/pi/engine/pending-delivery.js +51 -18
  140. package/dist/integrations/pi/engine/resources/changelog.json +4 -4
  141. package/dist/integrations/pi/engine/runtime-integration.d.ts +8 -0
  142. package/dist/integrations/pi/engine/runtime-integration.js +58 -9
  143. package/dist/integrations/pi/engine/session-cwd-recovery.d.ts +11 -0
  144. package/dist/integrations/pi/engine/session-cwd-recovery.js +18 -0
  145. package/dist/integrations/pi/engine/session-runtime.d.ts +3 -0
  146. package/dist/integrations/pi/engine/session-runtime.js +2 -0
  147. package/dist/integrations/pi/engine/session-selection.d.ts +3 -2
  148. package/dist/integrations/pi/engine/session-selection.js +25 -5
  149. package/dist/integrations/pi/engine/transcript-image-assets.d.ts +3 -0
  150. package/dist/integrations/pi/engine/transcript-image-assets.js +65 -14
  151. package/dist/integrations/pi/engine/view-state.d.ts +14 -0
  152. package/dist/integrations/pi/engine/view-state.js +17 -0
  153. package/dist/integrations/pi/engine/workflow-runner.d.ts +2 -0
  154. package/dist/integrations/pi/engine/workflow-runner.js +47 -26
  155. package/dist/integrations/pi/engine/worktree-tools.d.ts +4 -0
  156. package/dist/integrations/pi/engine/worktree-tools.js +26 -0
  157. package/dist/integrations/pi/session-presenters/components.d.ts +2 -1
  158. package/dist/integrations/pi/session-presenters/components.js +5 -3
  159. package/dist/integrations/pi/session-presenters/presenters.d.ts +3 -1
  160. package/dist/integrations/pi/session-presenters/presenters.js +1 -1
  161. package/dist/integrations/pi/startup-public.d.ts +1 -1
  162. package/dist/integrations/pi/startup-public.js +16 -20
  163. package/dist/integrations/pi/startup-public.manifest.json +29 -29
  164. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +1 -1
  165. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +1 -1
  166. package/dist/product-identity.json +1 -1
  167. package/dist/runtime-payload-inventory.json +5027 -5027
  168. package/dist/ui/components/index.d.ts +1 -1
  169. package/dist/ui/components/index.js +1 -1
  170. package/dist/ui/components/progress-status.d.ts +7 -0
  171. package/dist/ui/components/progress-status.js +21 -0
  172. package/dist/ui/components/value-menu.d.ts +1 -1
  173. package/dist/ui/components/value-menu.js +4 -2
  174. package/dist/ui/settings/declarations.d.ts +16 -4
  175. package/dist/ui/settings/declarations.js +13 -4
  176. package/docs/architecture/internal-naming.md +1 -1
  177. package/docs/architecture/project-structure.md +7 -11
  178. package/docs/features/session-exports.md +1 -1
  179. package/docs/features/session-naming.md +8 -6
  180. package/docs/features/worktrees.md +38 -0
  181. package/docs/local-worktree-cleanup.md +27 -4
  182. package/docs/openspec-archive-automation.md +30 -4
  183. package/docs/worktree-link-recovery.md +2 -2
  184. package/package.json +5 -5
@@ -4,18 +4,44 @@ import { join } from "node:path";
4
4
  import { setTimeout as delay } from "node:timers/promises";
5
5
  import { tryAcquireClaimGate } from "./claim-gate.js";
6
6
  const FILES = new Set(["owner.json", "owner.next", "recovery.json", "recovery.next"]);
7
- /** Inventory is advisory and read-only, including when abandoned metadata needs recovery. */
8
- export async function claimMutationPending(directory) {
7
+ /** Read a consistent claim snapshot without publishing or repairing mutation metadata. */
8
+ export async function withSessionClaimSnapshot(directory, options, operation) {
9
+ let canonical;
9
10
  try {
10
- const barrier = join(directory, "mutation.lock");
11
- const metadata = await lstat(barrier);
12
- if (!metadata.isDirectory() || metadata.isSymbolicLink())
13
- return true;
14
- const files = await readdir(barrier);
15
- return files.some(name => name !== "recovery.json");
11
+ await mkdir(directory, { recursive: true, mode: 0o700 });
12
+ canonical = await realpath(directory);
16
13
  }
17
- catch (error) {
18
- return !hasCode(error, "ENOENT");
14
+ catch (cause) {
15
+ throw failure("worktree-coordination-io", "cannot access worktree coordination; check permissions and filesystem availability", cause);
16
+ }
17
+ const deadline = Date.now() + (options.lockTimeoutMs ?? 5_000);
18
+ for (;;) {
19
+ options.signal?.throwIfAborted();
20
+ const gate = tryAcquireClaimGate(join(canonical, "mutation.authority"));
21
+ if (gate !== null) {
22
+ try {
23
+ const barrier = await lstat(join(canonical, "mutation.lock")).catch(error => {
24
+ if (hasCode(error, "ENOENT"))
25
+ return null;
26
+ throw error;
27
+ });
28
+ if (barrier !== null && (!barrier.isDirectory() || barrier.isSymbolicLink())) {
29
+ throw failure("worktree-legacy-recovery-required", "older coordination state requires recovery before a consistent inventory can be read");
30
+ }
31
+ return await operation();
32
+ }
33
+ catch (error) {
34
+ if (error instanceof Error && (error.name === "AbortError" || ("code" in error && String(error.code).startsWith("worktree-"))))
35
+ throw error;
36
+ throw failure("worktree-coordination-io", "cannot read worktree coordination; check permissions and filesystem availability", error);
37
+ }
38
+ finally {
39
+ gate.release();
40
+ }
41
+ }
42
+ if (Date.now() >= deadline)
43
+ throw failure("worktree-claim-busy", "worktree coordination did not settle within the operation budget; no worktree ownership was changed");
44
+ await delay(20, undefined, { signal: options.signal });
19
45
  }
20
46
  }
21
47
  /**
@@ -0,0 +1,2 @@
1
+ /** Transient coordination belongs to the operation, never an agent-authored retry procedure. */
2
+ export declare function retrySessionCoordination<T>(operation: () => Promise<T>, budgetMs?: number, signal?: AbortSignal): Promise<T>;
@@ -0,0 +1,17 @@
1
+ import { setTimeout as delay } from "node:timers/promises";
2
+ /** Transient coordination belongs to the operation, never an agent-authored retry procedure. */
3
+ export async function retrySessionCoordination(operation, budgetMs = 8_000, signal) {
4
+ const deadline = Date.now() + budgetMs;
5
+ for (;;) {
6
+ signal?.throwIfAborted();
7
+ try {
8
+ return await operation();
9
+ }
10
+ catch (error) {
11
+ const code = error instanceof Error && "code" in error ? error.code : null;
12
+ if (!["worktree-claim-busy", "session-runtime-unavailable", "worktree-context-changed"].includes(String(code)) || Date.now() >= deadline)
13
+ throw error;
14
+ await delay(Math.min(150, Math.max(1, deadline - Date.now())), undefined, { signal });
15
+ }
16
+ }
17
+ }
@@ -49,5 +49,11 @@ export interface SessionRepositoryObservation {
49
49
  * The callback must not perform asynchronous work; provider I/O never holds this gate.
50
50
  */
51
51
  export declare function withSessionRepositoryObservation<T>(identity: SessionRepositoryIdentity, consume: (context: SessionRepositoryObservation) => T, options?: SessionRepositoryContextOptions): Promise<T>;
52
- /** List same-repository worktrees without acquiring, releasing, or repairing a claim. */
52
+ /** Validate the command's existing runtime before any Git worktree creation. */
53
+ export declare function assertSessionRepositoryRuntime(identity: SessionRepositoryIdentity, options: SessionRepositoryContextOptions): Promise<void>;
54
+ /** Startup root used by product worktree operations; the durable session header stays unchanged. */
55
+ export declare function readSessionStartupRoot(identity: SessionRepositoryIdentity): Promise<string>;
56
+ /** Read a live attachment without activating or rewriting its claim. Invalid state never means detached. */
57
+ export declare function readActiveSessionRepositoryContext(identity: SessionRepositoryIdentity, options: SessionRepositoryContextOptions): Promise<SessionRepositoryObservation | null>;
58
+ /** List same-repository worktrees without acquiring, releasing, or repairing an editing claim. */
53
59
  export declare function listSessionRepositoryWorktrees(identity: SessionRepositoryIdentity, options?: SessionRepositoryContextOptions): Promise<readonly SessionWorktreeInventoryEntry[]>;
@@ -5,7 +5,7 @@ import { promisify } from "node:util";
5
5
  import { PRODUCT_IDENTITY } from "../../product-identity.js";
6
6
  import { inspectLiveProcess } from "./process-liveness.js";
7
7
  import { resolveProductPaths } from "./paths.js";
8
- import { claimMutationPending, withSessionClaimLock } from "./session-claim-lock.js";
8
+ import { withSessionClaimSnapshot, withSessionClaimLock } from "./session-claim-lock.js";
9
9
  const CONTEXT_SCHEMA = "a1-session-repository-context-v1";
10
10
  const RUNTIME_SCHEMA = "a1-session-runtime-v1";
11
11
  const CLAIM_SCHEMA = "a1-session-worktree-claim-v1";
@@ -77,9 +77,28 @@ export async function setSessionRepositoryContext(identity, worktreePath, option
77
77
  if (!samePath(startup.commonDir, selected.commonDir)) {
78
78
  throw contextError("associated worktree belongs to a different Git repository", "worktree-foreign-repository");
79
79
  }
80
+ if (samePath(selected.gitDir, selected.commonDir)) {
81
+ throw contextError("task attachment requires a linked worktree, not the primary checkout", "worktree-primary-checkout");
82
+ }
83
+ const registered = await listGitWorktrees(startup.root, startup.commonDir, options.signal);
84
+ if (!registered.some(entry => !("stale" in entry) && samePath(entry.root, selected.root) && samePath(entry.gitDir, selected.gitDir))) {
85
+ throw contextError("selected path is not a registered worktree", "worktree-not-registered");
86
+ }
87
+ if (await lstat(join(selected.gitDir, "locked")).then(() => true, error => { if (isMissingPathError(error))
88
+ return false; throw error; })) {
89
+ throw contextError("selected worktree is locked in Git", "worktree-git-locked");
90
+ }
80
91
  const owner = { runtimeId: requireRuntimeId(options), agentId: requireAgentId(options) };
81
92
  return await withClaimLock(options, async () => {
82
93
  const runtime = await requireLiveRuntime(header, owner, options);
94
+ const current = await readGitWorktreeIdentity(selected.root, true, options.signal);
95
+ if (!samePath(current.gitDir, selected.gitDir) || !samePath(current.commonDir, selected.commonDir) || current.branch !== selected.branch) {
96
+ throw contextError("worktree changed while attaching", "worktree-context-changed");
97
+ }
98
+ if (await lstat(join(selected.gitDir, "locked")).then(() => true, error => { if (isMissingPathError(error))
99
+ return false; throw error; })) {
100
+ throw contextError("selected worktree is locked in Git", "worktree-git-locked");
101
+ }
83
102
  const status = await claimAvailability(selected, owner, options);
84
103
  if (status === "busy")
85
104
  throw contextError("worktree is active in another session", "worktree-active-in-another-session");
@@ -185,7 +204,44 @@ export async function withSessionRepositoryObservation(identity, consume, option
185
204
  return consume(context);
186
205
  });
187
206
  }
188
- /** List same-repository worktrees without acquiring, releasing, or repairing a claim. */
207
+ /** Validate the command's existing runtime before any Git worktree creation. */
208
+ export async function assertSessionRepositoryRuntime(identity, options) {
209
+ await requireLiveRuntime(await readSessionHeader(identity), { runtimeId: requireRuntimeId(options), agentId: requireAgentId(options) }, options);
210
+ }
211
+ /** Startup root used by product worktree operations; the durable session header stays unchanged. */
212
+ export async function readSessionStartupRoot(identity) {
213
+ return (await readSessionHeader(identity)).cwd;
214
+ }
215
+ /** Read a live attachment without activating or rewriting its claim. Invalid state never means detached. */
216
+ export async function readActiveSessionRepositoryContext(identity, options) {
217
+ const header = await readSessionHeader(identity);
218
+ const owner = { runtimeId: requireRuntimeId(options), agentId: requireAgentId(options) };
219
+ await requireLiveRuntime(header, owner, options);
220
+ const path = contextRecordPath(header, options);
221
+ const metadata = await lstat(path).catch(error => { if (isMissingPathError(error))
222
+ return null; throw error; });
223
+ if (metadata === null)
224
+ return null;
225
+ if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size > MAX_RECORD_BYTES) {
226
+ throw contextError("repository attachment cannot be verified", "worktree-owner-unverifiable");
227
+ }
228
+ const record = await readContextRecord(header, options);
229
+ if (record === null)
230
+ throw contextError("repository attachment cannot be verified", "worktree-owner-unverifiable");
231
+ const selected = await validateContextRecord(header, record, options.signal);
232
+ const claim = await readClaim(selected, options);
233
+ if (claim === undefined)
234
+ throw contextError("the editing claim needs reactivation", "worktree-context-changed");
235
+ if (claim === null || !claimMatchesWorktree(claim, selected) || !ownedBy(claim, owner)) {
236
+ throw contextError("the session no longer owns its attached worktree", "worktree-owner-unverifiable");
237
+ }
238
+ const after = await readContextRecord(header, options);
239
+ if (after === null || after.revision !== record.revision || !samePath(after.worktreeGitDir, record.worktreeGitDir)) {
240
+ throw contextError("repository attachment changed during observation", "worktree-context-changed");
241
+ }
242
+ return { cwd: selected.root, branch: requireBranch(selected), revision: record.revision ?? "legacy" };
243
+ }
244
+ /** List same-repository worktrees without acquiring, releasing, or repairing an editing claim. */
189
245
  export async function listSessionRepositoryWorktrees(identity, options = {}) {
190
246
  const header = await readSessionHeader(identity);
191
247
  const owner = { runtimeId: requireRuntimeId(options), agentId: requireAgentId(options) };
@@ -194,22 +250,23 @@ export async function listSessionRepositoryWorktrees(identity, options = {}) {
194
250
  const worktrees = await listGitWorktrees(startup.root, startup.commonDir, options.signal);
195
251
  if (worktrees.length > MAX_WORKTREES)
196
252
  throw contextError("worktree inventory exceeds its bounded limit", "worktree-inventory-limit");
197
- const contended = await mutationLockExists(options);
198
- const entries = [];
199
- for (const selected of worktrees) {
200
- if ("stale" in selected) {
201
- entries.push({ cwd: selected.root, branch: selected.branch ?? "(detached)", status: "unverifiable", agentId: null });
202
- continue;
203
- }
204
- if (selected.branch === null) {
205
- entries.push({ cwd: selected.root, branch: "(detached)", status: "unverifiable", agentId: null });
206
- continue;
253
+ return await withSessionClaimSnapshot(claimStoreRoot(options), options, async () => {
254
+ const entries = [];
255
+ for (const selected of worktrees) {
256
+ if ("stale" in selected) {
257
+ entries.push({ cwd: selected.root, branch: selected.branch ?? "(detached)", status: "unverifiable", agentId: null });
258
+ continue;
259
+ }
260
+ if (selected.branch === null) {
261
+ entries.push({ cwd: selected.root, branch: "(detached)", status: "unverifiable", agentId: null });
262
+ continue;
263
+ }
264
+ const status = await claimAvailability(selected, owner, options);
265
+ const claim = status === "current" || status === "busy" ? await readClaim(selected, options) : null;
266
+ entries.push({ cwd: selected.root, branch: selected.branch, status, agentId: claim?.agentId ?? null });
207
267
  }
208
- const status = contended ? "unverifiable" : await claimAvailability(selected, owner, options);
209
- const claim = status === "current" || status === "busy" ? await readClaim(selected, options) : null;
210
- entries.push({ cwd: selected.root, branch: selected.branch, status, agentId: claim?.agentId ?? null });
211
- }
212
- return entries.sort((left, right) => pathKey(left.cwd).localeCompare(pathKey(right.cwd)));
268
+ return entries.sort((left, right) => pathKey(left.cwd).localeCompare(pathKey(right.cwd)));
269
+ });
213
270
  }
214
271
  async function requireLiveRuntime(header, owner, options) {
215
272
  const runtime = await readRuntimeRecord(owner, options);
@@ -466,27 +523,39 @@ async function listGitWorktrees(cwd, commonDir, signal) {
466
523
  if (current !== null)
467
524
  records.push(current);
468
525
  return await Promise.all(records.map(async (record) => {
469
- const root = await realpath(record.root).catch(error => { if (isMissingPathError(error))
470
- return null; throw error; });
471
- if (root === null)
526
+ try {
527
+ const root = await realpath(record.root).catch(error => { if (isMissingPathError(error))
528
+ return null; throw error; });
529
+ if (root === null)
530
+ return { stale: true, root: resolve(record.root), branch: record.branch };
531
+ const dotGit = join(root, ".git");
532
+ const metadata = await lstat(dotGit).catch(error => { if (isMissingPathError(error))
533
+ return null; throw error; });
534
+ if (metadata === null)
535
+ return { stale: true, root, branch: record.branch };
536
+ let gitDir;
537
+ if (metadata.isDirectory() && !metadata.isSymbolicLink())
538
+ gitDir = await realpath(dotGit);
539
+ else if (metadata.isFile() && !metadata.isSymbolicLink() && metadata.size > 0 && metadata.size <= MAX_RECORD_BYTES) {
540
+ const pointer = (await readFile(dotGit, { signal })).toString("utf8").trim();
541
+ if (!pointer.startsWith("gitdir: "))
542
+ throw new Error("worktree Git pointer is invalid");
543
+ gitDir = await realpath(resolve(root, pointer.slice("gitdir: ".length)));
544
+ }
545
+ else
546
+ throw new Error("worktree Git identity is invalid");
547
+ if (!samePath(gitDir, commonDir)) {
548
+ const common = await realpath(resolve(gitDir, (await readFile(join(gitDir, "commondir"), "utf8")).trim()));
549
+ if (!samePath(common, commonDir))
550
+ throw new Error("registered worktree points to a different repository");
551
+ }
552
+ return { root, commonDir, gitDir, branch: record.branch };
553
+ }
554
+ catch {
555
+ signal?.throwIfAborted();
556
+ // Invariant: a broken unrelated registration is one unverifiable row, not a repository-wide veto.
472
557
  return { stale: true, root: resolve(record.root), branch: record.branch };
473
- const dotGit = join(root, ".git");
474
- const metadata = await lstat(dotGit).catch(error => { if (isMissingPathError(error))
475
- return null; throw error; });
476
- if (metadata === null)
477
- return { stale: true, root, branch: record.branch };
478
- let gitDir;
479
- if (metadata.isDirectory() && !metadata.isSymbolicLink())
480
- gitDir = await realpath(dotGit);
481
- else if (metadata.isFile() && !metadata.isSymbolicLink() && metadata.size > 0 && metadata.size <= MAX_RECORD_BYTES) {
482
- const pointer = (await readFile(dotGit, { signal })).toString("utf8").trim();
483
- if (!pointer.startsWith("gitdir: "))
484
- throw new Error("worktree Git pointer is invalid");
485
- gitDir = await realpath(resolve(root, pointer.slice("gitdir: ".length)));
486
558
  }
487
- else
488
- throw new Error("worktree Git identity is invalid");
489
- return { root, commonDir, gitDir, branch: record.branch };
490
559
  }));
491
560
  }
492
561
  /** True for the errors a deleted directory or file produces, which mark a registered worktree as stale rather than broken. */
@@ -498,9 +567,6 @@ async function executeGit(arguments_, cwd, maxBuffer, signal) {
498
567
  const execute = promisify(execFile);
499
568
  return await execute("git", [...arguments_], { cwd, windowsHide: true, timeout: GIT_TIMEOUT_MS, maxBuffer, encoding: "utf8", signal });
500
569
  }
501
- async function mutationLockExists(options) {
502
- return await claimMutationPending(claimStoreRoot(options));
503
- }
504
570
  async function withClaimLock(options, operation) {
505
571
  return await withSessionClaimLock(claimStoreRoot(options), options, operation);
506
572
  }
@@ -0,0 +1,3 @@
1
+ import { type SessionRepositoryContext, type SessionRepositoryContextOptions, type SessionRepositoryIdentity } from "./session-repository-context.js";
2
+ /** Create or resume a matching registered checkout. Failed attachment never deletes existing work. */
3
+ export declare function enterSessionRepositoryWorktree(identity: SessionRepositoryIdentity, branch: string, base: string | undefined, options: SessionRepositoryContextOptions): Promise<SessionRepositoryContext>;
@@ -0,0 +1,87 @@
1
+ import { lstat, mkdir, realpath } from "node:fs/promises";
2
+ import { join, resolve } from "node:path";
3
+ import { promisify } from "node:util";
4
+ import { assertSessionRepositoryRuntime, readSessionStartupRoot, setSessionRepositoryContext, } from "./session-repository-context.js";
5
+ /** Create or resume a matching registered checkout. Failed attachment never deletes existing work. */
6
+ export async function enterSessionRepositoryWorktree(identity, branch, base, options) {
7
+ if (!/^[a-z][a-z0-9-]*\/[a-z0-9][a-z0-9-]{0,99}$/u.test(branch)) {
8
+ throw failure("worktree-branch-invalid", "use a task branch such as fix/short-description");
9
+ }
10
+ await assertSessionRepositoryRuntime(identity, options);
11
+ const startup = await readSessionStartupRoot(identity);
12
+ // Performance: unrelated lifecycle consumers do not need Git process bindings at import time.
13
+ const { execFile } = await import("node:child_process");
14
+ const execute = promisify(execFile);
15
+ const git = async (...args) => (await execute("git", ["-c", "core.hooksPath=/dev/null", ...args], {
16
+ cwd: startup, encoding: "utf8", windowsHide: true, timeout: 15_000, maxBuffer: 256 * 1024, signal: options.signal,
17
+ })).stdout.trim();
18
+ const inventory = await git("worktree", "list", "--porcelain", "-z");
19
+ const rows = inventory.split("\0\0").filter(Boolean).map(record => {
20
+ const fields = record.split("\0");
21
+ return {
22
+ root: fields.find(field => field.startsWith("worktree "))?.slice(9),
23
+ branch: fields.find(field => field.startsWith("branch refs/heads/"))?.slice(18),
24
+ };
25
+ });
26
+ const primary = rows[0]?.root;
27
+ if (primary === undefined)
28
+ throw failure("worktree-repository-invalid", "cannot determine the primary Git checkout");
29
+ const root = await realpath(primary);
30
+ const worktreeRoot = join(root, ".worktrees");
31
+ await mkdir(worktreeRoot, { recursive: true });
32
+ const directory = await lstat(worktreeRoot);
33
+ if (!directory.isDirectory() || directory.isSymbolicLink()) {
34
+ throw failure("worktree-path-collision", "the worktree directory is a link or unexpected entry; it was preserved");
35
+ }
36
+ const path = join(worktreeRoot, branch.slice(branch.indexOf("/") + 1));
37
+ const matching = rows.find(row => row.branch === branch);
38
+ if (matching !== undefined) {
39
+ if (matching.root === undefined || !samePath(await realpath(matching.root), path)) {
40
+ throw failure("worktree-branch-collision", "the task branch is checked out at another path; attach its exact worktree instead");
41
+ }
42
+ return await setSessionRepositoryContext(identity, path, options);
43
+ }
44
+ if (await lstat(path).then(() => true, error => { if (hasCode(error, "ENOENT"))
45
+ return false; throw error; })) {
46
+ throw failure("worktree-path-collision", "the task path already exists; existing files were preserved");
47
+ }
48
+ if (await git("branch", "--list", branch)) {
49
+ throw failure("worktree-branch-collision", "the branch already exists without a matching checkout; existing history was preserved");
50
+ }
51
+ // Security: never execute repository setup. The selected base is data, not a shell expression.
52
+ const configured = base ?? await git("config", "--get", "a1.worktreeBase").catch(() => "");
53
+ const remotes = (await git("remote")).split(/\r?\n/u);
54
+ let selectedBase = configured;
55
+ if (remotes.includes("origin")) {
56
+ if (!selectedBase) {
57
+ selectedBase = await git("symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD").catch(() => "");
58
+ if (!selectedBase) {
59
+ const head = await git("ls-remote", "--symref", "origin", "HEAD");
60
+ selectedBase = /^ref: refs\/heads\/([^\t\r\n]+)\tHEAD/mu.exec(head)?.[1] ?? "";
61
+ }
62
+ }
63
+ const name = selectedBase.replace(/^origin\//u, "");
64
+ if (!name || name.startsWith("-") || !/^[A-Za-z0-9][A-Za-z0-9._/-]*$/u.test(name)) {
65
+ throw failure("worktree-base-invalid", "cannot determine a safe base branch; select it with --base");
66
+ }
67
+ await git("check-ref-format", `refs/heads/${name}`);
68
+ await git("fetch", "--no-tags", "origin", `refs/heads/${name}:refs/remotes/origin/${name}`);
69
+ selectedBase = `refs/remotes/origin/${name}`;
70
+ }
71
+ else {
72
+ selectedBase ||= (await execute("git", ["symbolic-ref", "--quiet", "--short", "HEAD"], {
73
+ cwd: root, encoding: "utf8", windowsHide: true, timeout: 5_000, signal: options.signal,
74
+ })).stdout.trim();
75
+ if (!selectedBase || selectedBase.startsWith("-"))
76
+ throw failure("worktree-base-invalid", "select a base branch with --base");
77
+ await git("check-ref-format", `refs/heads/${selectedBase}`);
78
+ }
79
+ await git("worktree", "add", "--no-track", "-b", branch, path, selectedBase);
80
+ return await setSessionRepositoryContext(identity, path, options);
81
+ }
82
+ function samePath(left, right) {
83
+ const key = (value) => process.platform === "win32" ? resolve(value).toLowerCase() : resolve(value);
84
+ return key(left) === key(right);
85
+ }
86
+ function hasCode(error, code) { return error instanceof Error && "code" in error && error.code === code; }
87
+ function failure(code, message) { return Object.assign(new Error(message), { code }); }
@@ -1,4 +1,4 @@
1
- import { spawn } from "node:child_process";
1
+ import { spawnReleaseEntry } from "./release-use.js";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { watch } from "node:fs";
4
4
  import { mkdir, open, readFile, rename, rm, stat } from "node:fs/promises";
@@ -337,7 +337,7 @@ async function recordAutomaticPreflightFailure(parsed, dataDir) {
337
337
  }
338
338
  }
339
339
  async function spawnAutomaticUpdateWorker(entry, arguments_, environment) {
340
- const child = spawn(process.execPath, [entry, ...arguments_], {
340
+ const child = await spawnReleaseEntry(entry, arguments_, {
341
341
  detached: true,
342
342
  env: environment,
343
343
  stdio: "ignore",
@@ -4,6 +4,7 @@ import { deliverExitNotice, restoreAfterOwnedExit } from "../terminal-cleanup/in
4
4
  import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
5
5
  import { resolve } from "node:path";
6
6
  import { selectOrdinaryLaunchReleaseId } from "./ordinary-launch-selection.js";
7
+ import { acquireReleaseUse } from "./release-use.js";
7
8
  import { ReleaseStateStore } from "./release-state.js";
8
9
  import { assertLaunchProfileId, resolveProductPaths, sessionSelectionArguments } from "../lifecycle/index.js";
9
10
  import { UpdateTransactionStore } from "./update-transaction.js";
@@ -31,7 +32,7 @@ async function runBootstrapAttempt(options, retriesRemaining) {
31
32
  const paths = resolveProductPaths(environment);
32
33
  await mkdir(paths.runtimeDir, { recursive: true, mode: 0o700 });
33
34
  const launchSelected = async (release) => {
34
- const result = await launchUi(release, environment, sessionArgs, paths.runtimeDir);
35
+ const result = await launchUi(release, environment, sessionArgs, paths.runtimeDir, paths.dataDir);
35
36
  if (!result.releaseSuperseded)
36
37
  return result.code;
37
38
  if (retriesRemaining > 0)
@@ -205,7 +206,16 @@ export async function recordParentCertifiedRelease(release, dataDir) {
205
206
  * Run the selected release's UI as the one attached child. The launcher still owns the terminal,
206
207
  * so when the child ends uncleanly it restores the terminal and prints any notice the UI left armed.
207
208
  */
208
- async function launchUi(release, environment, sessionArgs, runtimeDir) {
209
+ async function launchUi(release, environment, sessionArgs, runtimeDir, dataDir) {
210
+ const use = await acquireReleaseUse(dataDir, release.releaseId);
211
+ try {
212
+ return await launchClaimedUi(release, environment, sessionArgs, runtimeDir, use);
213
+ }
214
+ finally {
215
+ await use.release().catch(() => undefined);
216
+ }
217
+ }
218
+ async function launchClaimedUi(release, environment, sessionArgs, runtimeDir, use) {
209
219
  await markStartupPhase(environment, "bootstrap-selected");
210
220
  const entry = await resolveReleaseEntryPoint(release, "bin/ui.js");
211
221
  const ownsTerminal = readLaunchContext(environment).launchProfile === "a1";
@@ -223,13 +233,19 @@ async function launchUi(release, environment, sessionArgs, runtimeDir) {
223
233
  stdio: ["inherit", "inherit", "inherit", "ipc"],
224
234
  windowsHide: false,
225
235
  });
236
+ let handoff = Promise.resolve();
237
+ child.once("spawn", () => {
238
+ // Security: failure keeps the durable launching reservation rather than guessing the child dead.
239
+ handoff = use.started(child.pid).catch(() => undefined);
240
+ });
226
241
  child.on("message", message => {
227
242
  if (message && typeof message === "object" && "type" in message && message.type === RELEASE_RESELECTION_MESSAGE) {
228
243
  releaseSuperseded = true;
229
244
  }
230
245
  });
231
246
  child.once("error", rejectPromise);
232
- child.once("close", (code, signal) => {
247
+ child.once("close", async (code, signal) => {
248
+ await handoff;
233
249
  const outcome = releaseSuperseded ? code ?? 1 : restoreAfterOwnedExit(ownsTerminal, code, signal);
234
250
  // Invariant: the UI removes its notice on a clean exit; one still armed means the UI died.
235
251
  if (ownsTerminal)
@@ -1,7 +1,7 @@
1
1
  export { certifyMaterializedRelease, recordParentCertifiedRelease, releaseEnvironment, runBootstrap, } from "./bootstrap.js";
2
2
  export type { BootstrapOptions } from "./bootstrap.js";
3
3
  export { selectOrdinaryLaunchReleaseId } from "./ordinary-launch-selection.js";
4
- export { ReleaseStateStore, RELEASE_STATE_SCHEMA, RELEASE_RETENTION_MS, emptyState, planProtectedReleases, withinRetention } from "./release-state.js";
4
+ export { ReleaseStateStore, ReleaseStateNewerError, isReleaseStateNewerError, RELEASE_STATE_SCHEMA, RELEASE_STATE_LEVEL, RELEASE_RETENTION_MS, emptyState, planProtectedReleases, withinRetention } from "./release-state.js";
5
5
  export type { ActiveReleaseTransactionReference, ReleaseState, OrphanReleaseDisposition, ProtectedReleaseInputs, ProtectedReleasePlan, ReleaseApproval, ReleaseCleanupDiagnostic, ReleaseCleanupDisposition, ReleaseCleanupStage, ReleaseCleanupState, ReleaseCleanupWorkerRun, ReleaseCleanupWorkerStatus, ReleaseCleanupWorkerSummary, ReleaseRecord, ReleaseReferences, ReleaseRetentionReconciliation, } from "./release-state.js";
6
6
  export { DEPENDENCY_LAYER_MANIFEST, DEPENDENCY_LAYER_SCHEMA, RUNTIME_PAYLOAD_INVENTORY, dependencyLayerCertificationPath, dependencyReference, generateDependencyRuntimePayload, legacyDependencyLayerCertificationPath, materializeDependencyLayer, readCertifiedDependencyLayer, selectDependencyRuntimePayload, selectPublishedDependencyRuntimePayload, verifyDependencyLayer, } from "./dependency-layer.js";
7
7
  export type { DependencyLayerIdentity, DependencyLayerOperationEvent, DependencyLayerReference, GeneratedRuntimePayload, MaterializeDependencyLayerOptions, MaterializedDependencyLayer, PublishedRuntimeFileClassification, PublishedRuntimePayload, ReadCertifiedDependencyLayerOptions, RuntimePayloadExclusion, RuntimePayloadInventory, SelectedRuntimePayload, } from "./dependency-layer.js";
@@ -1,6 +1,6 @@
1
1
  export { certifyMaterializedRelease, recordParentCertifiedRelease, releaseEnvironment, runBootstrap, } from "./bootstrap.js";
2
2
  export { selectOrdinaryLaunchReleaseId } from "./ordinary-launch-selection.js";
3
- export { ReleaseStateStore, RELEASE_STATE_SCHEMA, RELEASE_RETENTION_MS, emptyState, planProtectedReleases, withinRetention } from "./release-state.js";
3
+ export { ReleaseStateStore, ReleaseStateNewerError, isReleaseStateNewerError, RELEASE_STATE_SCHEMA, RELEASE_STATE_LEVEL, RELEASE_RETENTION_MS, emptyState, planProtectedReleases, withinRetention } from "./release-state.js";
4
4
  export { DEPENDENCY_LAYER_MANIFEST, DEPENDENCY_LAYER_SCHEMA, RUNTIME_PAYLOAD_INVENTORY, dependencyLayerCertificationPath, dependencyReference, generateDependencyRuntimePayload, legacyDependencyLayerCertificationPath, materializeDependencyLayer, readCertifiedDependencyLayer, selectDependencyRuntimePayload, selectPublishedDependencyRuntimePayload, verifyDependencyLayer, } from "./dependency-layer.js";
5
5
  export { processIsAlive } from "./process-liveness.js";
6
6
  export { PRODUCT_PACKAGE_NAME, createReleaseIdentity, deriveReleaseIdentity, digestManifestFiles, discoverReleasePayload, packageRootFromModule, releaseFileIdentity, resolveWithin, } from "./release.js";
@@ -1,9 +1,9 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { spawn } from "node:child_process";
2
+ import { spawnReleaseEntry } from "./release-use.js";
3
3
  import { lstat, mkdir, open, readFile, readdir, realpath, rename, rm, stat } from "node:fs/promises";
4
4
  import { dirname, resolve, sep } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
- import { ReleaseStateStore, } from "./release-state.js";
6
+ import { ReleaseStateStore, releaseUseIsLive, isReleaseStateNewerError, } from "./release-state.js";
7
7
  import { resolveProductPaths } from "../lifecycle/index.js";
8
8
  import { RELEASE_MANIFEST_FILENAME } from "./release-store.js";
9
9
  import { DEPENDENCY_LAYER_MANIFEST, readCertifiedDependencyLayer } from "./dependency-layer.js";
@@ -130,6 +130,16 @@ export async function runBoundedReleaseCleanup(dataDir, paths, options = {}) {
130
130
  * potentially slow recursive deletion. A later launch/update resumes work if spawning fails.
131
131
  */
132
132
  export async function scheduleReleaseCleanup(dataDir, paths, options = {}) {
133
+ try {
134
+ await scheduleOwnedReleaseCleanup(dataDir, paths, options);
135
+ }
136
+ catch (error) {
137
+ // Compatibility: a newer release owns state this code may not write; its own maintenance cleans up.
138
+ if (!isReleaseStateNewerError(error))
139
+ throw error;
140
+ }
141
+ }
142
+ async function scheduleOwnedReleaseCleanup(dataDir, paths, options) {
133
143
  const { store } = await prepareReleaseCleanup(dataDir, paths, options);
134
144
  const runId = randomUUID();
135
145
  await store.recordCleanupWorkerScheduled(runId);
@@ -152,6 +162,17 @@ export async function scheduleReleaseCleanup(dataDir, paths, options = {}) {
152
162
  }
153
163
  /** Entry used by the private cleanup executable shipped in every immutable release. */
154
164
  export async function runReleaseCleanupWorker(environment = process.env, workerOptions = {}) {
165
+ try {
166
+ return await runOwnedReleaseCleanupWorker(environment, workerOptions);
167
+ }
168
+ catch (error) {
169
+ // Compatibility: a worker started by an older release stands down once a newer release owns the state.
170
+ if (!isReleaseStateNewerError(error))
171
+ throw error;
172
+ return { planned: 0, attempted: 0, completed: 0, remaining: 0, durationMs: 0 };
173
+ }
174
+ }
175
+ async function runOwnedReleaseCleanupWorker(environment, workerOptions) {
155
176
  const paths = resolveProductPaths(environment);
156
177
  const store = new ReleaseStateStore(paths.dataDir);
157
178
  const runId = environment[WORKER_RUN_ID_ENV] ?? randomUUID();
@@ -233,7 +254,8 @@ async function collectDisposition(store, dataDir, disposition, options) {
233
254
  try {
234
255
  const inputs = await protectionInputs(options.transactionStore ?? new UpdateTransactionStore(dataDir), options);
235
256
  const current = await store.read();
236
- if (isProtectedDetachedRelease(current, inputs, releaseId))
257
+ if (isProtectedDetachedRelease(current, inputs, releaseId)
258
+ || (disposition.release.approval === "approved" && disposition.release.lifetimeClaims !== true))
237
259
  return false;
238
260
  const storeRoot = await canonicalReleaseStore(dataDir);
239
261
  const trashRoot = await canonicalTrashRoot(storeRoot);
@@ -252,7 +274,19 @@ async function collectDisposition(store, dataDir, disposition, options) {
252
274
  else {
253
275
  await assertReleaseDirectory(source, storeRoot, disposition.release, releaseId);
254
276
  trashPath = resolve(trashRoot, `${releaseId}--${randomUUID()}`);
255
- await (options.operations?.rename ?? rename)(source, trashPath);
277
+ let moved = false;
278
+ const destination = trashPath;
279
+ // Concurrency: claim admission and destructive rename share the state gate. A launch recorded
280
+ // after discovery cannot slip between the last protection read and deletion.
281
+ await store.update(async (latest) => {
282
+ if (!isProtectedDetachedRelease(latest, inputs, releaseId)) {
283
+ await (options.operations?.rename ?? rename)(source, destination);
284
+ moved = true;
285
+ }
286
+ return latest;
287
+ });
288
+ if (!moved)
289
+ return false;
256
290
  }
257
291
  await store.markCleanupTrash(releaseId, trashPath);
258
292
  }
@@ -274,6 +308,7 @@ function isProtectedDetachedRelease(state, inputs, releaseId) {
274
308
  .some(reference => reference === releaseId);
275
309
  // Concurrency: a launch may have recorded the same release again after it was detached.
276
310
  return selected
311
+ || Object.values(state.uses ?? {}).some(claim => claim.releaseId === releaseId && releaseUseIsLive(claim))
277
312
  || state.releases[releaseId] !== undefined
278
313
  || (inputs.ownReleaseIds ?? []).includes(releaseId)
279
314
  || (inputs.transaction?.status === "active" && inputs.transaction.priorActiveReleaseId === releaseId);
@@ -392,6 +427,7 @@ async function readReleaseRecord(path, parent, expectedName, dataDir) {
392
427
  releaseRoot: resolve(parent, manifest.releaseId),
393
428
  packageVersion: manifest.packageVersion,
394
429
  contentDigest: manifest.contentDigest,
430
+ ...(Array.isArray(manifest.files) && manifest.files.some(file => file?.path === "dist/foundation/release/release-use.js") ? { lifetimeClaims: true } : {}),
395
431
  approval: "approved",
396
432
  materializedAt: new Date(0).toISOString(),
397
433
  certifiedAt: null,
@@ -661,7 +697,7 @@ function sortArtifacts(artifacts, state) {
661
697
  || artifactName(left).localeCompare(artifactName(right)));
662
698
  }
663
699
  async function spawnCleanupWorker(entry, environment) {
664
- const child = spawn(process.execPath, [entry], {
700
+ const child = await spawnReleaseEntry(entry, [], {
665
701
  detached: true,
666
702
  windowsHide: true,
667
703
  stdio: "ignore",