@playcraft/cli 0.1.66 → 0.1.68

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 (113) hide show
  1. package/dist/assets/client.js +4 -2
  2. package/dist/assets/messages.js +2 -0
  3. package/dist/audio-source/messages.js +35 -0
  4. package/dist/commands/assets.js +1 -1
  5. package/dist/commands/audio-program.js +82 -0
  6. package/dist/commands/audio-source.js +109 -0
  7. package/dist/commands/audio.js +13 -4
  8. package/dist/commands/build.js +4 -2
  9. package/dist/commands/tools-context.js +46 -0
  10. package/dist/commands/tools.js +2 -0
  11. package/dist/project-skills/builtin-import.js +69 -22
  12. package/dist/project-skills/commands.js +11 -2
  13. package/dist/project-skills/lifecycle.js +1 -1
  14. package/dist/project-skills/local-store.js +90 -31
  15. package/dist/project-skills/messages.js +16 -0
  16. package/dist/project-skills/migrate-lock.js +166 -0
  17. package/dist/project-skills/remote-cache.js +36 -5
  18. package/dist/utils/agent-api-client.js +18 -1
  19. package/dist/utils/agent-generation-provenance.js +8 -2
  20. package/dist/utils/audio-export.js +249 -0
  21. package/dist/utils/audio-processor.js +22 -99
  22. package/dist/utils/execution-context-bridge.js +28 -0
  23. package/dist/workspace-runtime/batch/control.js +13 -2
  24. package/dist/workspace-runtime/batch/generate.js +43 -2
  25. package/dist/workspace-runtime/batch/prepare.js +4 -0
  26. package/dist/workspace-runtime/batch/skill-artifacts.js +103 -0
  27. package/dist/workspace-runtime/batch/source-capture.js +6 -8
  28. package/dist/workspace-runtime/codex/app-server.js +26 -2
  29. package/dist/workspace-runtime/codex/execution-context-broker.js +61 -0
  30. package/dist/workspace-runtime/codex/execution-context-tools.js +37 -0
  31. package/dist/workspace-runtime/codex/generation-provenance-broker.js +9 -6
  32. package/dist/workspace-runtime/codex/level-editor-tools.js +18 -0
  33. package/dist/workspace-runtime/codex/model-broker.js +15 -2
  34. package/dist/workspace-runtime/codex/scheduled-task-tools.js +1 -1
  35. package/dist/workspace-runtime/previews/finalize.js +1 -1
  36. package/dist/workspace-runtime/previews/output.js +4 -1
  37. package/dist/workspace-runtime/previews/publish.js +5 -2
  38. package/dist/workspace-runtime/previews/runtime-preview.js +38 -3
  39. package/dist/workspace-runtime/serve.js +6 -0
  40. package/dist/workspace-runtime/server/accept-operation.js +85 -13
  41. package/dist/workspace-runtime/server/audio-assets.js +257 -0
  42. package/dist/workspace-runtime/server/audio-export.js +136 -0
  43. package/dist/workspace-runtime/server/directory-access.js +222 -67
  44. package/dist/workspace-runtime/server/dispatch.js +252 -29
  45. package/dist/workspace-runtime/server/http.js +97 -3
  46. package/dist/workspace-runtime/server/operation-record.js +1 -1
  47. package/dist/workspace-runtime/workspaces/artifact-files.js +10 -3
  48. package/dist/workspace-runtime/workspaces/artifacts.js +47 -6
  49. package/dist/workspace-runtime/workspaces/asset-library-import.js +27 -7
  50. package/dist/workspace-runtime/workspaces/audio-assets/artifact-package.js +104 -0
  51. package/dist/workspace-runtime/workspaces/audio-assets/binding-usage.js +71 -0
  52. package/dist/workspace-runtime/workspaces/audio-assets/build-inputs.js +80 -0
  53. package/dist/workspace-runtime/workspaces/audio-assets/library-import.js +149 -0
  54. package/dist/workspace-runtime/workspaces/audio-assets/library-package.js +26 -0
  55. package/dist/workspace-runtime/workspaces/audio-assets/program-artifact-audit.js +90 -0
  56. package/dist/workspace-runtime/workspaces/audio-assets/program-browser.js +194 -0
  57. package/dist/workspace-runtime/workspaces/audio-assets/program-build-inputs.js +122 -0
  58. package/dist/workspace-runtime/workspaces/audio-assets/program-engine.js +39 -0
  59. package/dist/workspace-runtime/workspaces/audio-assets/program-package.js +289 -0
  60. package/dist/workspace-runtime/workspaces/audio-assets/program-variant-build.js +217 -0
  61. package/dist/workspace-runtime/workspaces/audio-assets/references.js +107 -0
  62. package/dist/workspace-runtime/workspaces/audio-assets/render-worker.js +117 -0
  63. package/dist/workspace-runtime/workspaces/audio-assets/renderer.js +128 -0
  64. package/dist/workspace-runtime/workspaces/audio-assets/representations.js +73 -0
  65. package/dist/workspace-runtime/workspaces/audio-assets/service.js +459 -0
  66. package/dist/workspace-runtime/workspaces/audio-assets/storage.js +125 -0
  67. package/dist/workspace-runtime/workspaces/build-isolation.js +2 -0
  68. package/dist/workspace-runtime/workspaces/composer-context.js +40 -1
  69. package/dist/workspace-runtime/workspaces/empty-template.js +1 -1
  70. package/dist/workspace-runtime/workspaces/execution-context.js +24 -0
  71. package/dist/workspace-runtime/workspaces/files.js +3 -5
  72. package/dist/workspace-runtime/workspaces/git.js +7 -0
  73. package/dist/workspace-runtime/workspaces/level-editor-agent-store.js +143 -0
  74. package/dist/workspace-runtime/workspaces/level-editor-agent.js +410 -0
  75. package/dist/workspace-runtime/workspaces/level-editor-atomic.js +424 -0
  76. package/dist/workspace-runtime/workspaces/level-editor-extensions.js +140 -0
  77. package/dist/workspace-runtime/workspaces/level-editor-format.js +75 -0
  78. package/dist/workspace-runtime/workspaces/level-editor-history-store.js +250 -0
  79. package/dist/workspace-runtime/workspaces/level-editor-json5.js +132 -0
  80. package/dist/workspace-runtime/workspaces/level-editor-requests.js +724 -0
  81. package/dist/workspace-runtime/workspaces/level-editor.js +454 -0
  82. package/dist/workspace-runtime/workspaces/parameter-initialization-skill.js +62 -22
  83. package/dist/workspace-runtime/workspaces/parameter-production.js +1 -1
  84. package/dist/workspace-runtime/workspaces/remix-reference-skill.js +4 -3
  85. package/dist/workspace-runtime/workspaces/skill-artifact-loader.js +19 -5
  86. package/dist/workspace-runtime/workspaces/skills.js +48 -13
  87. package/package.json +9 -4
  88. package/project-template/.claude/skills/playcraft-audio-generation/SKILL.md +92 -4
  89. package/project-template/.claude/skills/playcraft-level-editor/SKILL.md +34 -0
  90. package/project-template/.claude/skills/playcraft-level-editor/references/authoring-contract.json +3475 -0
  91. package/project-template/.claude/skills/playcraft-level-editor/references/extensions.md +37 -0
  92. package/project-template/.claude/skills/playcraft-level-editor/references/preview.md +15 -0
  93. package/project-template/.claude/skills/playcraft-level-editor/references/protocol-v1.md +35 -0
  94. package/project-template/vite.config.ts +1 -1
  95. package/project-template-v2/.claude/skills/playcraft-audio-generation/SKILL.md +92 -4
  96. package/project-template-v2/.playcraft/skills.lock.json +9 -2
  97. package/project-template-v2/vite.config.ts +1 -1
  98. package/project-templates/empty/AGENTS.md +2 -0
  99. package/project-templates/phaser/.claude/skills/playcraft-audio-generation/SKILL.md +92 -4
  100. package/project-templates/phaser/vite.config.ts +1 -1
  101. package/project-templates/playable-template-phaser/vite.config.ts +1 -1
  102. package/project-templates/threejs/.claude/skills/playcraft-audio-generation/SKILL.md +92 -4
  103. package/project-templates/threejs/vite.config.ts +1 -1
  104. package/templates/preview-runtime/v1/README.md +12 -0
  105. package/templates/preview-runtime/v1/build-preview.mjs +17 -8
  106. package/templates/preview-runtime/v1/manifest.json +9 -6
  107. package/templates/preview-runtime/v1/release-guard.mjs +1 -1
  108. package/templates/preview-runtime/v1/runtime.d.mts +1 -3
  109. package/templates/preview-runtime/v1/runtime.d.ts +1 -3
  110. package/templates/preview-runtime/v1/runtime.mjs +1142 -257
  111. package/templates/preview-runtime/v1/types/host.d.ts +29 -0
  112. package/templates/preview-runtime/v1/types/index.d.ts +6 -0
  113. package/templates/preview-runtime/v1/types/level-editor.d.ts +123 -0
@@ -1,6 +1,6 @@
1
1
  import { AsyncLocalStorage } from 'node:async_hooks';
2
2
  import { randomUUID } from 'node:crypto';
3
- import { isTurnControl, takesWorkspaceDirectory, takesExecutionSlot, transitionOperation } from '@playcraft/common/cloud-workspace/runtime';
3
+ import { isTurnControl, takesWorkspaceDirectory, takesExecutionSlot, transitionOperation, } from '@playcraft/common/cloud-workspace/runtime';
4
4
  import { listMembers, readSnapshot } from '../batch/records.js';
5
5
  const context = new AsyncLocalStorage();
6
6
  const gates = new WeakMap();
@@ -8,9 +8,21 @@ const instances = new WeakMap();
8
8
  const executing = new WeakMap();
9
9
  const freezing = new WeakMap();
10
10
  const TERMINAL = new Set(['completed', 'failed', 'interrupted']);
11
- const EXCLUSIVE_OPERATIONS = new Set(['preview.publish', 'model.preview', 'model.prepare', 'directory.create',
12
- 'directory.rename', 'directory.delete', 'workspace.references.import', 'skills.update', 'parameter.verify',
13
- 'checkpoint.create', 'checkpoint.restore']);
11
+ const EXCLUSIVE_OPERATIONS = new Set([
12
+ 'level-editor.transaction',
13
+ 'preview.publish',
14
+ 'model.preview',
15
+ 'model.prepare',
16
+ 'directory.create',
17
+ 'directory.rename',
18
+ 'directory.delete',
19
+ 'workspace.references.import',
20
+ 'skills.update',
21
+ 'parameter.verify',
22
+ 'level-editor.verify',
23
+ 'checkpoint.create',
24
+ 'checkpoint.restore',
25
+ ]);
14
26
  export function managedDirectoryScope(state, value) {
15
27
  if (state.config.workspaceMode !== 'multi')
16
28
  return { directoryName: 'basic' };
@@ -18,22 +30,51 @@ export function managedDirectoryScope(state, value) {
18
30
  return { workspaceId, directoryName };
19
31
  }
20
32
  function overlaps(a, b) {
21
- return (a.workspaceId ?? '') === (b.workspaceId ?? '') &&
22
- ((a.directoryName ?? 'basic') === (b.directoryName ?? 'basic') || a.directoryName === 'root' || b.directoryName === 'root');
33
+ return ((a.workspaceId ?? '') === (b.workspaceId ?? '') &&
34
+ ((a.directoryName ?? 'basic') === (b.directoryName ?? 'basic') ||
35
+ a.directoryName === 'root' ||
36
+ b.directoryName === 'root'));
23
37
  }
24
38
  function covers(parent, child) {
25
- return (parent.workspaceId ?? '') === (child.workspaceId ?? '') &&
26
- (parent.directoryName === 'root' || (parent.directoryName ?? 'basic') === (child.directoryName ?? 'basic'));
39
+ return ((parent.workspaceId ?? '') === (child.workspaceId ?? '') &&
40
+ (parent.directoryName === 'root' ||
41
+ (parent.directoryName ?? 'basic') === (child.directoryName ?? 'basic')));
42
+ }
43
+ // A terminal is an interactive channel, not an ordinary operation. Keep its lease for
44
+ // captures and destructive directory maintenance, but do not serialize normal work behind it.
45
+ function terminalIndependent(a, b) {
46
+ return ((a.owner.startsWith('terminal:') && b.terminalIndependent === true) ||
47
+ (b.owner.startsWith('terminal:') && a.terminalIndependent === true));
48
+ }
49
+ function operationTerminalIndependent(operation) {
50
+ return ![
51
+ 'directory.rename',
52
+ 'directory.delete',
53
+ 'checkpoint.create',
54
+ 'checkpoint.restore',
55
+ 'preview.publish',
56
+ ].includes(operation.kind);
27
57
  }
28
58
  export function operationDirectoryMode(operation) {
29
- return operation.directoryAccessMode ?? (EXCLUSIVE_OPERATIONS.has(operation.kind) || operation.kind.startsWith('git.') ? 'exclusive' : 'shared');
59
+ if (operation.kind === 'level-editor.transaction')
60
+ return 'exclusive';
61
+ return (operation.directoryAccessMode ??
62
+ (EXCLUSIVE_OPERATIONS.has(operation.kind) || operation.kind.startsWith('git.')
63
+ ? 'exclusive'
64
+ : 'shared'));
30
65
  }
31
66
  function activeSessionOwner(state, gate, scope, sessionId, except) {
32
67
  if (!sessionId)
33
68
  return undefined;
34
- const operation = state.journal.listOperations().map(item => item.operation).find(item => item.sessionId === sessionId &&
35
- item.operationId !== except && ['session.turn', 'session.review', 'session.compact'].includes(item.kind) &&
36
- ['dispatched', 'accepted', 'uncertain'].includes(item.status) && covers(item, scope) && gate.has(`operation:${item.operationId}`));
69
+ const operation = state.journal
70
+ .listOperations()
71
+ .map((item) => item.operation)
72
+ .find((item) => item.sessionId === sessionId &&
73
+ item.operationId !== except &&
74
+ ['session.turn', 'session.review', 'session.compact'].includes(item.kind) &&
75
+ ['dispatched', 'accepted', 'uncertain'].includes(item.status) &&
76
+ covers(item, scope) &&
77
+ gate.has(`operation:${item.operationId}`));
37
78
  return operation ? `operation:${operation.operationId}` : undefined;
38
79
  }
39
80
  /** A directory has many ordinary writers, or one build, freeze, maintenance job or member Run. */
@@ -41,20 +82,20 @@ export class DirectoryAccessGate {
41
82
  normalize;
42
83
  held = new Map();
43
84
  pending = [];
44
- constructor(normalize = claim => claim) {
85
+ constructor(normalize = (claim) => claim) {
45
86
  this.normalize = normalize;
46
87
  }
47
88
  claim(input) {
48
89
  const claim = this.normalize(input);
49
90
  const held = this.held.get(claim.owner);
50
91
  if (held) {
51
- if (!covers(held, claim) || held.mode === 'shared' && claim.mode === 'exclusive')
92
+ if (!covers(held, claim) || (held.mode === 'shared' && claim.mode === 'exclusive'))
52
93
  return Promise.reject(new Error('directory_access_upgrade_forbidden'));
53
94
  return Promise.resolve();
54
95
  }
55
- const existing = this.pending.find(item => item.owner === claim.owner);
96
+ const existing = this.pending.find((item) => item.owner === claim.owner);
56
97
  if (existing) {
57
- if (!covers(existing, claim) || existing.mode === 'shared' && claim.mode === 'exclusive')
98
+ if (!covers(existing, claim) || (existing.mode === 'shared' && claim.mode === 'exclusive'))
58
99
  return Promise.reject(new Error('directory_access_upgrade_forbidden'));
59
100
  if (existing.paused && !claim.paused) {
60
101
  existing.paused = false;
@@ -64,16 +105,25 @@ export class DirectoryAccessGate {
64
105
  }
65
106
  let resolve;
66
107
  let reject;
67
- const ready = new Promise((done, fail) => { resolve = done; reject = fail; });
108
+ const ready = new Promise((done, fail) => {
109
+ resolve = done;
110
+ reject = fail;
111
+ });
68
112
  this.pending.push({ ...claim, ready, resolve, reject });
69
113
  this.drain();
70
114
  return ready;
71
115
  }
72
- restore(claim) { this.held.set(claim.owner, this.normalize(claim)); }
73
- has(owner) { return this.held.has(owner); }
116
+ restore(claim) {
117
+ this.held.set(claim.owner, this.normalize(claim));
118
+ }
119
+ has(owner) {
120
+ return this.held.has(owner);
121
+ }
74
122
  permits(owner, scope, mode) {
75
123
  const held = this.held.get(owner);
76
- return Boolean(held && covers(held, this.normalize({ ...scope, owner, mode })) && (held.mode === 'exclusive' || mode === 'shared'));
124
+ return Boolean(held &&
125
+ covers(held, this.normalize({ ...scope, owner, mode })) &&
126
+ (held.mode === 'exclusive' || mode === 'shared'));
77
127
  }
78
128
  tryClaim(claim) {
79
129
  const ready = this.claim(claim);
@@ -84,7 +134,7 @@ export class DirectoryAccessGate {
84
134
  return false;
85
135
  }
86
136
  adopt(owner, parentOwner) {
87
- const child = this.pending.find(item => item.owner === owner);
137
+ const child = this.pending.find((item) => item.owner === owner);
88
138
  const parent = this.held.get(parentOwner);
89
139
  if (!child || !parent || parent.mode !== 'exclusive' || !covers(parent, child))
90
140
  return;
@@ -92,21 +142,23 @@ export class DirectoryAccessGate {
92
142
  this.drain();
93
143
  }
94
144
  cancelPending(owner) {
95
- if (this.held.has(owner) || !this.pending.some(item => item.owner === owner))
145
+ if (this.held.has(owner) || !this.pending.some((item) => item.owner === owner))
96
146
  return false;
97
147
  this.release(owner);
98
148
  return true;
99
149
  }
100
150
  release(owner) {
101
151
  this.held.delete(owner);
102
- const pending = this.pending.find(item => item.owner === owner);
152
+ const pending = this.pending.find((item) => item.owner === owner);
103
153
  if (pending) {
104
154
  this.pending.splice(this.pending.indexOf(pending), 1);
105
155
  pending.reject(new Error('directory_access_cancelled'));
106
156
  }
107
157
  this.drain();
108
158
  }
109
- owners() { return [...this.held.keys(), ...this.pending.map(item => item.owner)]; }
159
+ owners() {
160
+ return [...this.held.keys(), ...this.pending.map((item) => item.owner)];
161
+ }
110
162
  drain() {
111
163
  const earlier = [];
112
164
  for (const item of [...this.pending]) {
@@ -116,9 +168,26 @@ export class DirectoryAccessGate {
116
168
  }
117
169
  const parent = item.parentOwner ? this.held.get(item.parentOwner) : undefined;
118
170
  const family = parent && covers(parent, item) && (parent.mode === 'exclusive' || item.mode === 'shared');
119
- const conflict = [...this.held.values()].some(held => !(family && (held.owner === parent.owner || held.parentOwner === parent.owner)) && overlaps(item, held) &&
120
- (item.mode === 'exclusive' || held.mode === 'exclusive' || Boolean(item.sessionId && held.sessionId === item.sessionId)));
121
- const queuedWriter = !family && earlier.some(prior => overlaps(item, prior) && (item.mode === 'exclusive' || prior.mode === 'exclusive'));
171
+ const conflict = [...this.held.values()].some((held) => !(family && (held.owner === parent.owner || held.parentOwner === parent.owner)) &&
172
+ overlaps(item, held) &&
173
+ !terminalIndependent(item, held) &&
174
+ (item.mode === 'exclusive' ||
175
+ held.mode === 'exclusive' ||
176
+ Boolean(item.sessionId && held.sessionId === item.sessionId)));
177
+ const queuedWriter = !family &&
178
+ earlier.some((prior) => {
179
+ if (!overlaps(item, prior) ||
180
+ terminalIndependent(item, prior) ||
181
+ (item.mode !== 'exclusive' && prior.mode !== 'exclusive'))
182
+ return false;
183
+ // A capture waiting on an open terminal must not form a permanent queue barrier
184
+ // for work that is allowed to run beside that terminal.
185
+ if (item.terminalIndependent &&
186
+ !prior.terminalIndependent &&
187
+ [...this.held.values()].some((held) => held.owner.startsWith('terminal:') && overlaps(prior, held)))
188
+ return false;
189
+ return true;
190
+ });
122
191
  if (conflict || queuedWriter) {
123
192
  earlier.push(item);
124
193
  continue;
@@ -133,32 +202,48 @@ async function gateFor(state) {
133
202
  let pending = gates.get(state);
134
203
  if (!pending) {
135
204
  pending = (async () => {
136
- const gate = new DirectoryAccessGate(claim => state.config.workspaceMode === 'multi'
137
- ? claim : { ...claim, workspaceId: undefined, directoryName: 'basic' });
205
+ const gate = new DirectoryAccessGate((claim) => state.config.workspaceMode === 'multi'
206
+ ? claim
207
+ : { ...claim, workspaceId: undefined, directoryName: 'basic' });
138
208
  instances.set(state, gate);
139
209
  const members = await listMembers(state);
140
210
  for (const member of members)
141
211
  if (member.holding)
142
212
  gate.restore({ ...member, owner: `run:${member.runId}`, mode: 'exclusive' });
143
213
  for (const { operation } of state.journal.listOperations()) {
144
- if (!takesWorkspaceDirectory(operation.kind) || isTurnControl(operation.kind) ||
214
+ if (!takesWorkspaceDirectory(operation.kind) ||
215
+ isTurnControl(operation.kind) ||
145
216
  !['dispatched', 'accepted', 'uncertain'].includes(operation.status) ||
146
- members.some(member => member.holding && (member.operationId === operation.operationId || Boolean(member.sessionId && member.sessionId === operation.sessionId))))
217
+ members.some((member) => member.holding &&
218
+ (member.operationId === operation.operationId ||
219
+ Boolean(member.sessionId && member.sessionId === operation.sessionId))))
147
220
  continue;
148
- gate.restore({ ...operation, owner: `operation:${operation.operationId}`, mode: operationDirectoryMode(operation) });
221
+ gate.restore({
222
+ ...operation,
223
+ owner: `operation:${operation.operationId}`,
224
+ mode: operationDirectoryMode(operation),
225
+ terminalIndependent: operationTerminalIndependent(operation),
226
+ });
149
227
  }
150
228
  for (const terminal of state.journal.listTerminals())
151
229
  if (terminal.status !== 'destroyed')
152
230
  gate.restore({ ...terminal, owner: `terminal:${terminal.terminalId}`, mode: 'shared' });
153
231
  for (const writer of state.journal.listManagedWrites())
154
232
  if (!writer.stoppedAt) {
155
- gate.restore({ ...managedDirectoryScope(state, writer.workspaceId), owner: `managed:${writer.writeId}`, mode: writer.directoryAccessMode ?? 'shared' });
233
+ gate.restore({
234
+ ...managedDirectoryScope(state, writer.workspaceId),
235
+ owner: `managed:${writer.writeId}`,
236
+ mode: writer.directoryAccessMode ?? 'shared',
237
+ terminalIndependent: true,
238
+ });
156
239
  }
157
240
  const waiting = [];
158
241
  for (const barrier of state.journal.directoryBarriers()) {
159
242
  const snapshot = barrier.owner.startsWith('freeze:')
160
- ? await readSnapshot(state, barrier.owner.slice('freeze:'.length)).catch(() => undefined) : undefined;
161
- if (snapshot?.sourceWorkspaceId === barrier.workspaceId && snapshot.sourceDirectoryName === barrier.directoryName) {
243
+ ? await readSnapshot(state, barrier.owner.slice('freeze:'.length)).catch(() => undefined)
244
+ : undefined;
245
+ if (snapshot?.sourceWorkspaceId === barrier.workspaceId &&
246
+ snapshot.sourceDirectoryName === barrier.directoryName) {
162
247
  await state.journal.deleteDirectoryBarrier(barrier.owner);
163
248
  continue;
164
249
  }
@@ -170,19 +255,31 @@ async function gateFor(state) {
170
255
  await state.journal.deleteDirectoryBarrier(barrier.owner);
171
256
  continue;
172
257
  }
173
- waiting.push({ claim: { ...barrier, mode: 'exclusive', paused: true }, queuedAt: barrier.queuedAt ?? '' });
258
+ waiting.push({
259
+ claim: { ...barrier, mode: 'exclusive', paused: true },
260
+ queuedAt: barrier.queuedAt ?? '',
261
+ });
174
262
  const timeout = setTimeout(() => {
175
263
  if (gate.cancelPending(barrier.owner))
176
264
  void state.journal.deleteDirectoryBarrier(barrier.owner).catch(() => undefined);
177
- }, Math.max(0, Date.parse(barrier.deadline ?? new Date(Date.now() + 60_000).toISOString()) - Date.now()));
265
+ }, Math.max(0, Date.parse(barrier.deadline ?? new Date(Date.now() + 60_000).toISOString()) -
266
+ Date.now()));
178
267
  timeout.unref?.();
179
268
  }
180
269
  for (const pending of state.journal.deferredDirectoryOperations()) {
181
270
  const operation = state.journal.getOperationById(pending.operationId);
182
271
  if (operation?.status !== 'queued')
183
272
  continue;
184
- waiting.push({ claim: { ...operation, owner: `operation:${operation.operationId}`, mode: operationDirectoryMode(operation),
185
- ...(operation.kind.startsWith('session.') ? { sessionId: operation.sessionId } : {}) }, queuedAt: pending.queuedAt ?? operation.createdAt });
273
+ waiting.push({
274
+ claim: {
275
+ ...operation,
276
+ owner: `operation:${operation.operationId}`,
277
+ mode: operationDirectoryMode(operation),
278
+ terminalIndependent: operationTerminalIndependent(operation),
279
+ ...(operation.kind.startsWith('session.') ? { sessionId: operation.sessionId } : {}),
280
+ },
281
+ queuedAt: pending.queuedAt ?? operation.createdAt,
282
+ });
186
283
  }
187
284
  for (const item of waiting.sort((a, b) => a.queuedAt.localeCompare(b.queuedAt)))
188
285
  void gate.claim(item.claim).catch(() => undefined);
@@ -209,7 +306,9 @@ export function reconcileDirectoryAccess(state) {
209
306
  gate.release(owner);
210
307
  }
211
308
  else if (owner.startsWith('managed:')) {
212
- if (state.journal.listManagedWrites().find(writer => writer.writeId === owner.slice('managed:'.length))?.stoppedAt)
309
+ if (state.journal
310
+ .listManagedWrites()
311
+ .find((writer) => writer.writeId === owner.slice('managed:'.length))?.stoppedAt)
213
312
  gate.release(owner);
214
313
  }
215
314
  }
@@ -217,8 +316,15 @@ export function reconcileDirectoryAccess(state) {
217
316
  export async function holdDirectoryAccess(state, scope, owner, mode = 'shared') {
218
317
  const gate = await gateFor(state);
219
318
  const inherited = context.getStore();
220
- await gate.claim({ ...scope, owner, mode,
221
- ...(inherited?.state === state && gate.permits(inherited.claim.owner, scope, mode) ? { parentOwner: inherited.claim.owner } : {}) });
319
+ await gate.claim({
320
+ ...scope,
321
+ owner,
322
+ mode,
323
+ ...(owner.startsWith('managed:') ? { terminalIndependent: true } : {}),
324
+ ...(inherited?.state === state && gate.permits(inherited.claim.owner, scope, mode)
325
+ ? { parentOwner: inherited.claim.owner }
326
+ : {}),
327
+ });
222
328
  return () => gate.release(owner);
223
329
  }
224
330
  /** A build may join its known installer that was queued behind the build's own exclusive lease. */
@@ -245,13 +351,24 @@ export async function assertOperationDirectoryAccess(state, operation, mode) {
245
351
  export async function withDirectoryWrite(state, scope, action) {
246
352
  const inherited = context.getStore();
247
353
  const gate = await gateFor(state);
248
- if (inherited?.state === state && gate.permits(inherited.claim.owner, scope, scope.mode ?? 'shared'))
354
+ if (inherited?.state === state &&
355
+ gate.permits(inherited.claim.owner, scope, scope.mode ?? 'shared'))
249
356
  return action();
250
- if (inherited?.state === state && scope.mode === 'exclusive' && gate.permits(inherited.claim.owner, scope, 'shared'))
357
+ if (inherited?.state === state &&
358
+ scope.mode === 'exclusive' &&
359
+ gate.permits(inherited.claim.owner, scope, 'shared'))
251
360
  throw new Error('directory_busy');
252
- const parentOwner = scope.mode !== 'exclusive' ? activeSessionOwner(state, gate, scope, scope.sessionId) : undefined;
253
- const claim = { ...scope, sessionId: undefined, owner: scope.owner ?? `write:${randomUUID()}`, mode: scope.mode ?? 'shared',
254
- ...(parentOwner ? { parentOwner } : {}) };
361
+ const parentOwner = scope.mode !== 'exclusive'
362
+ ? activeSessionOwner(state, gate, scope, scope.sessionId)
363
+ : undefined;
364
+ const claim = {
365
+ ...scope,
366
+ sessionId: undefined,
367
+ owner: scope.owner ?? `write:${randomUUID()}`,
368
+ mode: scope.mode ?? 'shared',
369
+ ...(scope.mode !== 'exclusive' ? { terminalIndependent: true } : {}),
370
+ ...(parentOwner ? { parentOwner } : {}),
371
+ };
255
372
  if (scope.wait === false && !gate.tryClaim(claim))
256
373
  throw new Error('directory_busy');
257
374
  if (scope.wait !== false)
@@ -265,7 +382,9 @@ export async function withDirectoryWrite(state, scope, action) {
265
382
  }
266
383
  }
267
384
  export async function withOperationDirectoryAccess(state, operation, action, deferred) {
268
- if (!takesWorkspaceDirectory(operation.kind) || isTurnControl(operation.kind) || operation.kind === 'runtime.destroy')
385
+ if (!takesWorkspaceDirectory(operation.kind) ||
386
+ isTurnControl(operation.kind) ||
387
+ operation.kind === 'runtime.destroy')
269
388
  return action();
270
389
  const inherited = context.getStore();
271
390
  const mode = operationDirectoryMode(operation);
@@ -278,11 +397,19 @@ export async function withOperationDirectoryAccess(state, operation, action, def
278
397
  return state.journal.getOperationById(operation.operationId) ?? operation;
279
398
  active.add(operation.operationId);
280
399
  const parentOwner = !takesExecutionSlot(operation.kind) && mode === 'shared'
281
- ? activeSessionOwner(state, gate, operation, operation.sessionId, operation.operationId) : undefined;
282
- const claim = { ...operation, owner: `operation:${operation.operationId}`, mode: operationDirectoryMode(operation),
400
+ ? activeSessionOwner(state, gate, operation, operation.sessionId, operation.operationId)
401
+ : undefined;
402
+ const claim = {
403
+ ...operation,
404
+ owner: `operation:${operation.operationId}`,
405
+ mode: operationDirectoryMode(operation),
406
+ terminalIndependent: operationTerminalIndependent(operation),
283
407
  ...(operation.kind.startsWith('session.') ? { sessionId: operation.sessionId } : {}),
284
- ...(parentOwner ? { parentOwner } : {}) };
285
- if (inherited?.state === state && mode === 'shared' && gate.permits(inherited.claim.owner, operation, mode))
408
+ ...(parentOwner ? { parentOwner } : {}),
409
+ };
410
+ if (inherited?.state === state &&
411
+ mode === 'shared' &&
412
+ gate.permits(inherited.claim.owner, operation, mode))
286
413
  claim.parentOwner = inherited.claim.owner;
287
414
  const ready = gate.claim(claim);
288
415
  const run = async () => {
@@ -315,16 +442,20 @@ export async function withOperationDirectoryAccess(state, operation, action, def
315
442
  gate.release(claim.owner);
316
443
  throw error;
317
444
  }
318
- void run().catch(async (error) => {
445
+ void run()
446
+ .catch(async (error) => {
319
447
  const current = state.journal.getOperationById(operation.operationId);
320
448
  if (!current || TERMINAL.has(current.status))
321
449
  return;
322
- const cancelled = current.status === 'queued' && error instanceof Error && error.message === 'directory_access_cancelled';
450
+ const cancelled = current.status === 'queued' &&
451
+ error instanceof Error &&
452
+ error.message === 'directory_access_cancelled';
323
453
  await state.journal.putOperation(deferred.scopeKey, transitionOperation(current, cancelled ? 'interrupted' : 'uncertain', {
324
454
  error: cancelled ? 'cancelled' : 'native_receipt_lost',
325
455
  }));
326
456
  await state.journal.deleteDeferredDirectoryOperation(operation.operationId);
327
- }).catch(() => undefined);
457
+ })
458
+ .catch(() => undefined);
328
459
  return state.journal.getOperationById(operation.operationId) ?? operation;
329
460
  }
330
461
  /** A queued command is replayed only if it never started; accepted or uncertain work is only observed. */
@@ -332,7 +463,8 @@ export async function restoreDeferredDirectoryOperations(state, execute) {
332
463
  for (const pending of state.journal.deferredDirectoryOperations()) {
333
464
  const operation = state.journal.getOperationById(pending.operationId);
334
465
  if (operation?.status === 'queued') {
335
- if (operation.generation !== state.config.generation || operation.runtimeEpoch !== state.runtimeEpoch) {
466
+ if (operation.generation !== state.config.generation ||
467
+ operation.runtimeEpoch !== state.runtimeEpoch) {
336
468
  await state.journal.putOperation(pending.scopeKey, transitionOperation(operation, 'failed', { error: 'generation_mismatch' }));
337
469
  continue;
338
470
  }
@@ -350,11 +482,13 @@ export function releaseBatchDirectory(state, member) {
350
482
  instances.get(state)?.release(`run:${member.runId}`);
351
483
  }
352
484
  export async function withDirectoryFreeze(state, scope, action, quiesce, options) {
353
- const pending = freezing.get(state) ?? new Map();
485
+ const pending = freezing.get(state) ??
486
+ new Map();
354
487
  freezing.set(state, pending);
355
488
  const existing = pending.get(scope.batchId);
356
489
  if (existing) {
357
- if (existing.scope.workspaceId !== scope.workspaceId || (existing.scope.directoryName ?? 'basic') !== (scope.directoryName ?? 'basic'))
490
+ if (existing.scope.workspaceId !== scope.workspaceId ||
491
+ (existing.scope.directoryName ?? 'basic') !== (scope.directoryName ?? 'basic'))
358
492
  throw new Error('batch_source_identity_mismatch');
359
493
  return existing.task;
360
494
  }
@@ -370,13 +504,18 @@ export async function withDirectoryFreeze(state, scope, action, quiesce, options
370
504
  }
371
505
  async function performDirectoryFreeze(state, scope, action, quiesce, options) {
372
506
  const deadline = options?.deadline ?? new Date(Date.now() + 60_000).toISOString();
373
- const signal = AbortSignal.any([...(options?.signal ? [options.signal] : []), AbortSignal.timeout(Math.max(0, Date.parse(deadline) - Date.now()))]);
507
+ const signal = AbortSignal.any([
508
+ ...(options?.signal ? [options.signal] : []),
509
+ AbortSignal.timeout(Math.max(0, Date.parse(deadline) - Date.now())),
510
+ ]);
374
511
  if (signal.aborted)
375
512
  throw new Error('directory_access_cancelled');
376
513
  const owner = `freeze:${scope.batchId}`;
377
514
  const gate = await gateFor(state);
378
- const previous = state.journal.directoryBarriers().find(barrier => barrier.owner === owner);
379
- if (previous && (previous.workspaceId !== scope.workspaceId || previous.directoryName !== (scope.directoryName ?? 'basic')))
515
+ const previous = state.journal.directoryBarriers().find((barrier) => barrier.owner === owner);
516
+ if (previous &&
517
+ (previous.workspaceId !== scope.workspaceId ||
518
+ previous.directoryName !== (scope.directoryName ?? 'basic')))
380
519
  throw new Error('batch_source_identity_mismatch');
381
520
  // A restored capture may still have writers. Retrying its publication is not a new admission,
382
521
  // and a missing candidate or a lost request cannot provide the old capture's stop proof.
@@ -393,22 +532,38 @@ async function performDirectoryFreeze(state, scope, action, quiesce, options) {
393
532
  signal.addEventListener('abort', cancel, { once: true });
394
533
  try {
395
534
  if (!restoredCapture)
396
- await state.journal.putDirectoryBarrier({ owner, workspaceId: scope.workspaceId,
397
- directoryName: scope.directoryName ?? 'basic', runtimeEpoch: state.runtimeEpoch, phase: 'pending', deadline });
535
+ await state.journal.putDirectoryBarrier({
536
+ owner,
537
+ workspaceId: scope.workspaceId,
538
+ directoryName: scope.directoryName ?? 'basic',
539
+ runtimeEpoch: state.runtimeEpoch,
540
+ phase: 'pending',
541
+ deadline,
542
+ });
398
543
  await quiesce?.();
399
544
  await claimed;
400
545
  if (signal.aborted)
401
546
  throw new Error('directory_access_cancelled');
402
547
  started = true;
403
548
  if (!restoredCapture)
404
- await state.journal.putDirectoryBarrier({ owner, workspaceId: scope.workspaceId,
405
- directoryName: scope.directoryName ?? 'basic', runtimeEpoch: state.runtimeEpoch, phase: 'capturing', deadline });
549
+ await state.journal.putDirectoryBarrier({
550
+ owner,
551
+ workspaceId: scope.workspaceId,
552
+ directoryName: scope.directoryName ?? 'basic',
553
+ runtimeEpoch: state.runtimeEpoch,
554
+ phase: 'capturing',
555
+ deadline,
556
+ });
406
557
  return await withDirectoryOwner(state, scope, owner, action);
407
558
  }
408
559
  finally {
409
560
  signal.removeEventListener('abort', cancel);
410
- const snapshot = restoredCapture ? await readSnapshot(state, scope.batchId).catch(() => undefined) : undefined;
411
- if (!restoredCapture || (snapshot?.sourceWorkspaceId === scope.workspaceId && snapshot.sourceDirectoryName === (scope.directoryName ?? 'basic'))) {
561
+ const snapshot = restoredCapture
562
+ ? await readSnapshot(state, scope.batchId).catch(() => undefined)
563
+ : undefined;
564
+ if (!restoredCapture ||
565
+ (snapshot?.sourceWorkspaceId === scope.workspaceId &&
566
+ snapshot.sourceDirectoryName === (scope.directoryName ?? 'basic'))) {
412
567
  await state.journal.deleteDirectoryBarrier(owner);
413
568
  gate.release(owner);
414
569
  }