@ctrl-spc/cs 0.7.13 → 0.7.15

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.
@@ -0,0 +1,756 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { readOperation } from './daemon-lock.js';
5
+ import { lifecycleDir } from './config.js';
6
+ import { getOwnedProcessIdentity, inspectProcessTree, inspectWindowsJob, terminateOwnedTree, windowsJobName, validMacCoalition } from './win-shell.js';
7
+ let context = null;
8
+ let admitting = true;
9
+ let forcedOperationId = null;
10
+ const claims = new Set();
11
+ const liveRpcClaims = new Set();
12
+ const claimWaiters = new Set();
13
+ const admissionWaiters = new Set();
14
+ const handles = new Map();
15
+ const creating = new Set();
16
+ function path() { return join(lifecycleDir(), 'owned-work.json'); }
17
+ function record(value) { return !!value && typeof value === 'object' && !Array.isArray(value); }
18
+ function identity(value) {
19
+ return record(value) && Number.isInteger(value.pid) && value.pid > 0
20
+ && Number.isInteger(value.ppid) && typeof value.owner === 'string'
21
+ && typeof value.birth === 'string' && typeof value.commandHash === 'string'
22
+ && (value.pgid === undefined || Number.isInteger(value.pgid) && value.pgid > 0)
23
+ && (value.windowsJob === undefined || typeof value.windowsJob === 'string')
24
+ && (value.macCoalition === undefined || validMacCoalition(value.macCoalition))
25
+ && (value.macProcessVersion === undefined || Number.isInteger(value.macProcessVersion) && value.macProcessVersion > 0);
26
+ }
27
+ function reference(value) {
28
+ if (!record(value))
29
+ return false;
30
+ const ids = (v) => v === null || Array.isArray(v) && v.every((id) => typeof id === 'string');
31
+ if (value.surface === 'panel')
32
+ return typeof value.runId === 'string' && typeof value.cardId === 'string'
33
+ && (value.processToken === null || typeof value.processToken === 'string') && typeof value.startedAt === 'string'
34
+ && (value.resumedAt === null || typeof value.resumedAt === 'string') && ids(value.observedPendingTurnIds)
35
+ && (value.pid === undefined || value.pid === null || Number.isInteger(value.pid) && value.pid > 0);
36
+ if (value.surface === 'worker')
37
+ return typeof value.todoId === 'string' && (value.workerId === null || typeof value.workerId === 'string')
38
+ && typeof value.claimedAt === 'string' && ids(value.observedPendingMessageIds);
39
+ if (value.surface === 'reply')
40
+ return typeof value.todoId === 'string' && ids(value.observedPendingMessageIds)
41
+ && Array.isArray(value.messageLeases) && value.messageLeases.every((v) => record(v) && typeof v.id === 'string' && typeof v.taken_at === 'string')
42
+ && Array.isArray(value.grantLeases) && value.grantLeases.every((v) => record(v) && typeof v.id === 'string' && typeof v.resume_taken_at === 'string');
43
+ return false;
44
+ }
45
+ function readStore() {
46
+ if (!existsSync(path()))
47
+ return { version: 1, entries: [], legacy: [] };
48
+ const value = JSON.parse(readFileSync(path(), 'utf8'));
49
+ if (!record(value) || (value.legacyBootstrap !== undefined && typeof value.legacyBootstrap !== 'boolean') || (value.rpcClaims !== undefined && (!Array.isArray(value.rpcClaims) || !value.rpcClaims.every((row) => record(row) && typeof row.id === 'string' && ['panel', 'cli'].includes(String(row.surface)) && typeof row.action === 'string' && typeof row.accountId === 'string' && typeof row.machineId === 'string' && typeof row.instanceNonce === 'string'))) || value.version !== 1 || !Array.isArray(value.entries) || !Array.isArray(value.legacy)
50
+ || !value.entries.every((entry) => record(entry) && typeof entry.id === 'string'
51
+ && typeof entry.accountId === 'string' && typeof entry.machineId === 'string' && typeof entry.instanceNonce === 'string'
52
+ && (entry.harness === null || typeof entry.harness === 'string') && (entry.reference === null || reference(entry.reference))
53
+ && (entry.process === null || identity(entry.process)) && Array.isArray(entry.descendants) && entry.descendants.every(identity)
54
+ && ['pending', 'running', 'exited'].includes(String(entry.state))
55
+ && (entry.rpcId === undefined || typeof entry.rpcId === 'string')
56
+ && (entry.unstarted === undefined || typeof entry.unstarted === 'boolean')
57
+ && (entry.spawning === undefined || typeof entry.spawning === 'boolean')
58
+ && (entry.gatedCreation === undefined || typeof entry.gatedCreation === 'boolean')
59
+ && (entry.windowsJob === undefined || typeof entry.windowsJob === 'string')
60
+ && (entry.rawPid === undefined || Number.isInteger(entry.rawPid) && entry.rawPid > 0)
61
+ && (entry.preparationFailure === undefined || record(entry.preparationFailure)
62
+ && typeof entry.preparationFailure.reason === 'string' && [1, 2, 3].includes(Number(entry.preparationFailure.level))
63
+ && record(entry.reference) && entry.reference.surface === 'panel')
64
+ && (entry.interruption === null || record(entry.interruption) && typeof entry.interruption.operationId === 'string' && typeof entry.interruption.interruptedAt === 'string'))
65
+ || !value.legacy.every((item) => record(item) && typeof item.accountId === 'string' && typeof item.machineId === 'string'
66
+ && typeof item.complete === 'boolean' && (item.panelComplete === undefined || typeof item.panelComplete === 'boolean') && Array.isArray(item.holds)
67
+ && item.holds.every((hold) => record(hold) && typeof hold.todoId === 'string'
68
+ && Array.isArray(hold.messageIds) && hold.messageIds.every((id) => typeof id === 'string')
69
+ && Array.isArray(hold.messageSources) && hold.messageSources.every((source) => record(source) && typeof source.id === 'string' && typeof source.taken_at === 'string')
70
+ && Array.isArray(hold.permissionSources) && hold.permissionSources.every((source) => record(source) && typeof source.id === 'string' && typeof source.state === 'string' && (source.consumed_at === null || typeof source.consumed_at === 'string'))))) {
71
+ throw new Error('Owned execution records are incomplete. Work remains protected; inspect the local lifecycle record.');
72
+ }
73
+ const store = value;
74
+ const intents = readIntents();
75
+ for (const entry of store.entries) {
76
+ if (!entry.preparationFailure)
77
+ entry.interruption ??= intents[entry.id] ?? intents[`instance:${entry.instanceNonce}`] ?? null;
78
+ }
79
+ return store;
80
+ }
81
+ function readIntents() {
82
+ const intentPath = join(lifecycleDir(), 'interruption-intent.json');
83
+ if (!existsSync(intentPath))
84
+ return {};
85
+ const intents = JSON.parse(readFileSync(intentPath, 'utf8'));
86
+ if (!record(intents) || !Object.values(intents).every((intent) => record(intent)
87
+ && typeof intent.operationId === 'string' && typeof intent.interruptedAt === 'string')) {
88
+ throw new Error('The local interruption intent is incomplete. Work remains protected.');
89
+ }
90
+ return intents;
91
+ }
92
+ function writeStore(value) {
93
+ mkdirSync(lifecycleDir(), { recursive: true, mode: 0o700 });
94
+ const temporary = `${path()}.${randomUUID()}.tmp`;
95
+ try {
96
+ writeFileSync(temporary, JSON.stringify(value), { mode: 0o600, flag: 'wx' });
97
+ renameSync(temporary, path());
98
+ }
99
+ finally {
100
+ if (existsSync(temporary))
101
+ rmSync(temporary);
102
+ }
103
+ }
104
+ function changeEntry(id, update) {
105
+ const store = readStore();
106
+ const entry = store.entries.find((candidate) => candidate.id === id);
107
+ if (!entry)
108
+ throw new Error('The owned execution record disappeared. No further work may start.');
109
+ update(entry);
110
+ writeStore(store);
111
+ return entry;
112
+ }
113
+ function removeEntry(id) {
114
+ const store = readStore();
115
+ store.entries = store.entries.filter((entry) => entry.id !== id);
116
+ writeStore(store);
117
+ handles.delete(id);
118
+ creating.delete(id);
119
+ }
120
+ function requireContext() {
121
+ if (!context?.accountId)
122
+ throw new Error('No authenticated local execution owner is available.');
123
+ return { ...context, accountId: context.accountId };
124
+ }
125
+ export function initializeOwnedWork(input) {
126
+ if (context?.instanceNonce !== input.instanceNonce) {
127
+ claims.clear();
128
+ liveRpcClaims.clear();
129
+ for (const id of creating)
130
+ if (!handles.has(id))
131
+ creating.delete(id);
132
+ }
133
+ context = { ...input, legacyUpgrade: input.legacyUpgrade ?? false };
134
+ admitting = true;
135
+ forcedOperationId = null;
136
+ if (input.legacyUpgrade) {
137
+ const store = readStore();
138
+ store.legacyBootstrap = true;
139
+ writeStore(store);
140
+ }
141
+ if (input.accountId)
142
+ setOwnedWorkAccount(input.accountId);
143
+ }
144
+ export function setOwnedWorkAccount(accountId) {
145
+ if (!context)
146
+ throw new Error('The local runtime must own execution before authentication starts.');
147
+ context.accountId = accountId;
148
+ const store = readStore();
149
+ if (context.legacyUpgrade || store.legacyBootstrap) {
150
+ if (!store.legacy.some((item) => item.accountId === accountId && item.machineId === context.machineId)) {
151
+ store.legacy.push({ accountId, machineId: context.machineId, complete: false, panelComplete: false, holds: [] });
152
+ }
153
+ store.legacyBootstrap = false;
154
+ writeStore(store);
155
+ }
156
+ }
157
+ export function hasOwnedWorkContext() { return context !== null; }
158
+ export function ownedWorkAllowed() {
159
+ if (!context)
160
+ return true;
161
+ const operation = readOperation();
162
+ return admitting && !readStore().entries.some((entry) => entry.preparationFailure
163
+ && entry.accountId === context.accountId && entry.machineId === context.machineId)
164
+ && (!operation || !['stop', 'restart'].includes(operation.action) || operation.successor === context.instanceNonce);
165
+ }
166
+ /** A claim remains visible until its caller has created pending execution records. */
167
+ export function beginOwnedClaim() {
168
+ if (!ownedWorkAllowed())
169
+ throw new Error('CTRL+SPC is stopping and is not accepting new work.');
170
+ if (!context)
171
+ return () => { };
172
+ const id = randomUUID();
173
+ claims.add(id);
174
+ return () => {
175
+ claims.delete(id);
176
+ if (claims.size === 0)
177
+ for (const resolve of claimWaiters)
178
+ resolve();
179
+ };
180
+ }
181
+ export async function suspendOwnedWorkAdmission(options = {}) {
182
+ admitting = false;
183
+ if (options.waitForClaims !== false && claims.size)
184
+ await new Promise((resolve) => { claimWaiters.add(resolve); }).finally(() => claimWaiters.clear());
185
+ }
186
+ export function resumeOwnedWorkAdmission() {
187
+ admitting = true;
188
+ for (const resolve of admissionWaiters)
189
+ resolve();
190
+ admissionWaiters.clear();
191
+ }
192
+ export function reserveOwnedWork(ref, harness = null, workId) {
193
+ const owner = requireContext();
194
+ const store = readStore();
195
+ const prior = store.entries.find((row) => row.unstarted && row.instanceNonce === owner.instanceNonce
196
+ && row.reference && (ref ? sameAttempt(row.reference, ref) : workId ? row.reference.surface === 'panel' && row.reference.runId === workId : false));
197
+ const entry = prior ?? {
198
+ id: randomUUID(), accountId: owner.accountId, machineId: owner.machineId, instanceNonce: owner.instanceNonce,
199
+ harness, reference: ref, process: null, descendants: [], state: 'pending', interruption: null,
200
+ };
201
+ entry.unstarted = false;
202
+ if (harness)
203
+ entry.harness = harness;
204
+ if (ref) {
205
+ entry.reference = ref;
206
+ store.entries = store.entries.filter((row) => row.id === entry.id || !row.unstarted || !row.reference || !sameAttempt(row.reference, ref));
207
+ }
208
+ if (!prior)
209
+ store.entries.push(entry);
210
+ writeStore(store);
211
+ let registering = false;
212
+ const interrupted = () => readStore().entries.find((candidate) => candidate.id === entry.id)?.interruption != null;
213
+ const waitForPromptAdmission = async () => {
214
+ let waited = false;
215
+ while (!ownedWorkAllowed() && !interrupted() && context?.accountId === owner.accountId && context.machineId === owner.machineId) {
216
+ waited = true;
217
+ await new Promise((resolve) => setTimeout(resolve, 100));
218
+ }
219
+ if (interrupted())
220
+ throw new Error('Work was interrupted by the local service command.');
221
+ if (context?.accountId !== owner.accountId || context.machineId !== owner.machineId) {
222
+ throw new Error('This execution belongs to the previous signed-in account.');
223
+ }
224
+ return waited;
225
+ };
226
+ return {
227
+ id: entry.id,
228
+ interrupted,
229
+ waitForPromptAdmission,
230
+ beforeSpawn: () => {
231
+ if (context?.accountId !== owner.accountId || context.machineId !== owner.machineId) {
232
+ throw new Error('This execution belongs to the previous signed-in account.');
233
+ }
234
+ if (forcedOperationId || interrupted()) {
235
+ changeEntry(entry.id, (row) => {
236
+ if (row.reference) {
237
+ row.state = 'exited';
238
+ row.interruption ??= { operationId: forcedOperationId ?? readOperation()?.id ?? randomUUID(), interruptedAt: new Date().toISOString() };
239
+ }
240
+ });
241
+ throw new Error('Work was interrupted before its harness could start.');
242
+ }
243
+ changeEntry(entry.id, (row) => {
244
+ row.spawning = true;
245
+ row.gatedCreation = true;
246
+ if (process.platform === 'win32')
247
+ row.windowsJob = windowsJobName(row.id);
248
+ });
249
+ creating.add(entry.id);
250
+ },
251
+ complete: () => {
252
+ const current = readStore().entries.find((row) => row.id === entry.id);
253
+ if (current?.preparationFailure && current.state !== 'exited')
254
+ throw new Error('The unprompted harness must exit before its preparation failure is released.');
255
+ if (current && !current.interruption)
256
+ removeEntry(entry.id);
257
+ },
258
+ deferFailure: (reason, level) => {
259
+ changeEntry(entry.id, (row) => {
260
+ if (row.reference?.surface !== 'panel')
261
+ throw new Error('A failed preparation requires its exact panel attempt.');
262
+ if (row.interruption)
263
+ return;
264
+ row.preparationFailure = { reason, level };
265
+ if (!row.process && !row.spawning)
266
+ row.state = 'exited';
267
+ });
268
+ },
269
+ exited: () => confirmOwnedExecutionExited(entry.id),
270
+ finishPreparation: () => {
271
+ if (registering)
272
+ return;
273
+ const current = readStore().entries.find((row) => row.id === entry.id);
274
+ if (!current)
275
+ return;
276
+ if (current.preparationFailure)
277
+ return;
278
+ if (current.interruption && current.reference)
279
+ changeEntry(entry.id, (row) => { row.state = 'exited'; });
280
+ else
281
+ removeEntry(entry.id);
282
+ },
283
+ setReference: (value, agent) => {
284
+ const current = readStore();
285
+ const row = current.entries.find((item) => item.id === entry.id);
286
+ if (!row)
287
+ throw new Error('The pending execution record disappeared.');
288
+ row.reference = value;
289
+ if (agent)
290
+ row.harness = agent;
291
+ current.entries = current.entries.filter((item) => item.id === entry.id || !item.unstarted || !item.reference || !sameAttempt(item.reference, value));
292
+ writeStore(current);
293
+ },
294
+ abandon: () => {
295
+ const current = readStore().entries.find((candidate) => candidate.id === entry.id);
296
+ if (!current)
297
+ return;
298
+ if (current.preparationFailure)
299
+ return;
300
+ if (current.process)
301
+ throw new Error('A registered process must be observed exited before its record is released.');
302
+ if (current.interruption && current.reference)
303
+ changeEntry(entry.id, (row) => { row.state = 'exited'; });
304
+ else
305
+ removeEntry(entry.id);
306
+ },
307
+ register: async (child, agent) => {
308
+ registering = true;
309
+ handles.set(entry.id, child);
310
+ changeEntry(entry.id, (row) => { if (typeof child.pid === 'number')
311
+ row.rawPid = child.pid; if (agent)
312
+ row.harness = agent; });
313
+ const process = await getOwnedProcessIdentity(child);
314
+ if (!process) {
315
+ if (readStore().entries.find((row) => row.id === entry.id)?.gatedCreation) {
316
+ changeEntry(entry.id, (row) => { row.spawning = false; row.state = 'exited'; });
317
+ creating.delete(entry.id);
318
+ }
319
+ throw new Error('The harness exited before its process identity could be recorded.');
320
+ }
321
+ changeEntry(entry.id, (row) => { row.process = process; row.state = 'running'; row.spawning = false; });
322
+ creating.delete(entry.id);
323
+ const descendants = await inspectProcessTree(process);
324
+ changeEntry(entry.id, (row) => { row.descendants = descendants; });
325
+ await waitForPromptAdmission();
326
+ return { id: entry.id, interrupted, exited: () => confirmOwnedExecutionExited(entry.id) };
327
+ },
328
+ };
329
+ }
330
+ function assertWorkDeadline(deadline) {
331
+ if (deadline !== undefined && Date.now() >= deadline)
332
+ throw new Error('Service operation did not finish within its deadline. No replacement may start.');
333
+ }
334
+ async function remainingProcesses(entry, deadline) {
335
+ assertWorkDeadline(deadline);
336
+ if (!entry.process) {
337
+ if (!entry.spawning)
338
+ return [];
339
+ if (!entry.gatedCreation)
340
+ throw new Error('A harness creation did not record its operating system identity. Its ownership must be verified before replacement.');
341
+ if (process.platform === 'win32' && !entry.windowsJob)
342
+ throw new Error('The Windows execution gate has no durable Job identity. Its closure cannot be verified.');
343
+ const handle = handles.get(entry.id);
344
+ if (handle) {
345
+ const process = await getOwnedProcessIdentity(handle, deadline);
346
+ assertWorkDeadline(deadline);
347
+ if (process) {
348
+ entry.process = process;
349
+ changeEntry(entry.id, (row) => { row.process = process; row.spawning = false; row.state = 'running'; });
350
+ }
351
+ else
352
+ changeEntry(entry.id, (row) => { row.spawning = false; row.state = 'exited'; });
353
+ creating.delete(entry.id);
354
+ }
355
+ else {
356
+ if (creating.has(entry.id))
357
+ throw new Error('The owned execution gate is still being created.');
358
+ // A grant is sent only after the boundary above is durable. A dead
359
+ // creator without that identity could never have admitted a harness.
360
+ if (entry.windowsJob && (await inspectWindowsJob(entry.windowsJob, deadline)).exists) {
361
+ throw new Error('The unprompted execution gate is still closing. Retry the service command.');
362
+ }
363
+ assertWorkDeadline(deadline);
364
+ changeEntry(entry.id, (row) => { row.spawning = false; row.state = 'exited'; });
365
+ }
366
+ if (!entry.process)
367
+ return [];
368
+ }
369
+ const remaining = await inspectProcessTree(entry.process, deadline, entry.descendants);
370
+ assertWorkDeadline(deadline);
371
+ return remaining;
372
+ }
373
+ export async function confirmOwnedExecutionExited(id, deadline) {
374
+ assertWorkDeadline(deadline);
375
+ const entry = readStore().entries.find((candidate) => candidate.id === id);
376
+ if (!entry)
377
+ return;
378
+ const remaining = await remainingProcesses(entry, deadline);
379
+ if (remaining.length) {
380
+ changeEntry(id, (row) => { row.descendants = remaining; });
381
+ throw new Error('The harness wrapper exited while owned execution remains. The service still owns that work.');
382
+ }
383
+ assertWorkDeadline(deadline);
384
+ changeEntry(id, (row) => { row.state = 'exited'; });
385
+ handles.delete(id);
386
+ }
387
+ function summary(entry) {
388
+ const ref = entry.reference;
389
+ return {
390
+ id: entry.id, surface: ref?.surface ?? 'claim', harness: entry.harness,
391
+ workId: ref ? ref.surface === 'panel' ? ref.runId : ref.todoId : null,
392
+ ...(ref?.surface === 'panel' ? { cardId: ref.cardId } : {}), pid: entry.process?.pid ?? null,
393
+ };
394
+ }
395
+ export async function snapshotOwnedWork(deadline) {
396
+ assertWorkDeadline(deadline);
397
+ const store = readStore();
398
+ const result = { active: [], pending: [], claimPending: [], unknown: [], receipts: 0, legacyHeldTodoIds: [] };
399
+ for (const entry of store.entries) {
400
+ if (entry.preparationFailure) {
401
+ result.receipts++;
402
+ if (entry.state === 'exited')
403
+ continue;
404
+ }
405
+ if (entry.interruption)
406
+ result.receipts++;
407
+ if (entry.state === 'exited') {
408
+ if (!entry.interruption) {
409
+ if (context && entry.instanceNonce !== context.instanceNonce) {
410
+ if (entry.reference) {
411
+ changeEntry(entry.id, (row) => { row.interruption = { operationId: row.rpcId ?? randomUUID(), interruptedAt: new Date().toISOString() }; });
412
+ result.receipts++;
413
+ }
414
+ else
415
+ removeEntry(entry.id);
416
+ }
417
+ else
418
+ result.pending.push(summary(entry));
419
+ }
420
+ continue;
421
+ }
422
+ if (!entry.process) {
423
+ if (entry.spawning) {
424
+ if (creating.has(entry.id) && !handles.has(entry.id)) {
425
+ result.pending.push(summary(entry));
426
+ continue;
427
+ }
428
+ const liveCreation = handles.has(entry.id);
429
+ try {
430
+ const alive = await remainingProcesses(entry, deadline);
431
+ if (alive.length) {
432
+ result.active.push(summary(entry));
433
+ continue;
434
+ }
435
+ if (!entry.preparationFailure && !entry.interruption && entry.reference && !liveCreation) {
436
+ changeEntry(entry.id, (row) => { row.interruption = { operationId: row.rpcId ?? randomUUID(), interruptedAt: new Date().toISOString() }; });
437
+ result.receipts++;
438
+ }
439
+ else if (!entry.preparationFailure && !entry.interruption && liveCreation)
440
+ result.pending.push(summary(entry));
441
+ }
442
+ catch {
443
+ result.unknown.push(summary(entry));
444
+ }
445
+ continue;
446
+ }
447
+ if (!entry.reference && context && entry.instanceNonce !== context.instanceNonce)
448
+ removeEntry(entry.id);
449
+ else if (entry.reference && context && entry.instanceNonce !== context.instanceNonce) {
450
+ changeEntry(entry.id, (row) => { row.state = 'exited'; row.interruption ??= { operationId: row.rpcId ?? randomUUID(), interruptedAt: new Date().toISOString() }; });
451
+ result.receipts++;
452
+ }
453
+ else
454
+ result.pending.push(summary(entry));
455
+ continue;
456
+ }
457
+ try {
458
+ const alive = await remainingProcesses(entry, deadline);
459
+ if (alive.length) {
460
+ changeEntry(entry.id, (row) => { row.descendants = alive; });
461
+ result.active.push(summary(entry));
462
+ }
463
+ else if (entry.preparationFailure)
464
+ changeEntry(entry.id, (row) => { row.state = 'exited'; });
465
+ else if (!entry.interruption && context && entry.instanceNonce !== context.instanceNonce) {
466
+ // Process exit did not acknowledge cloud settlement. Preserve the exact
467
+ // old attempt so startup can stop it without touching a newer claim.
468
+ if (entry.reference) {
469
+ changeEntry(entry.id, (row) => { row.state = 'exited'; row.interruption = { operationId: row.rpcId ?? randomUUID(), interruptedAt: new Date().toISOString() }; });
470
+ result.receipts++;
471
+ }
472
+ else
473
+ removeEntry(entry.id);
474
+ }
475
+ else if (!entry.interruption) {
476
+ changeEntry(entry.id, (row) => { row.state = 'exited'; });
477
+ result.pending.push(summary(entry));
478
+ }
479
+ else
480
+ changeEntry(entry.id, (row) => { row.state = 'exited'; });
481
+ }
482
+ catch {
483
+ result.unknown.push(summary(entry));
484
+ }
485
+ }
486
+ for (const id of claims)
487
+ result.claimPending.push({ id, surface: 'claim', harness: null, workId: null, pid: null });
488
+ for (const claim of store.rpcClaims ?? []) {
489
+ result.receipts++;
490
+ if (claim.instanceNonce === context?.instanceNonce && liveRpcClaims.has(claim.id))
491
+ result.claimPending.push({ id: claim.id, surface: 'claim', harness: null, workId: null, pid: null });
492
+ }
493
+ result.pending.push(...result.claimPending);
494
+ result.legacyHeldTodoIds = legacyHeldTodoIds();
495
+ assertWorkDeadline(deadline);
496
+ return result;
497
+ }
498
+ /** The caller has already closed admission. Persist every interruption before any signal. */
499
+ export function ownedWorkInstanceNonces() { return [...new Set(readStore().entries.map((row) => row.instanceNonce))]; }
500
+ function assertInterruptionOperation(operationId) {
501
+ if (readOperation()?.id !== operationId)
502
+ throw new Error('The service operation no longer owns the interruption. No further process was changed.');
503
+ }
504
+ export async function markOwnedWorkInterrupted(operationId, instanceNonces = context ? [context.instanceNonce] : []) {
505
+ assertInterruptionOperation(operationId);
506
+ admitting = false;
507
+ forcedOperationId = operationId;
508
+ const store = readStore();
509
+ const interruptedAt = new Date().toISOString();
510
+ const intents = { ...readIntents(), ...Object.fromEntries(store.entries.filter((entry) => !entry.preparationFailure && (entry.interruption || instanceNonces.includes(entry.instanceNonce)))
511
+ .map((entry) => [entry.id, entry.interruption ?? { operationId, interruptedAt }])) };
512
+ for (const nonce of instanceNonces)
513
+ intents[`instance:${nonce}`] ??= { operationId, interruptedAt };
514
+ mkdirSync(lifecycleDir(), { recursive: true, mode: 0o700 });
515
+ const target = join(lifecycleDir(), 'interruption-intent.json');
516
+ const temporary = `${target}.${randomUUID()}.tmp`;
517
+ try {
518
+ writeFileSync(temporary, JSON.stringify(intents), { mode: 0o600, flag: 'wx' });
519
+ renameSync(temporary, target);
520
+ }
521
+ finally {
522
+ if (existsSync(temporary))
523
+ rmSync(temporary);
524
+ }
525
+ }
526
+ export async function interruptOwnedWork(operationId, deadline, instanceNonces = context ? [context.instanceNonce] : []) {
527
+ assertWorkDeadline(deadline);
528
+ await markOwnedWorkInterrupted(operationId, instanceNonces);
529
+ const store = readStore();
530
+ for (const resolve of admissionWaiters)
531
+ resolve();
532
+ admissionWaiters.clear();
533
+ for (const entry of store.entries) {
534
+ assertInterruptionOperation(operationId);
535
+ if (!instanceNonces.includes(entry.instanceNonce) || entry.state === 'exited')
536
+ continue;
537
+ if (!entry.process && entry.spawning)
538
+ await remainingProcesses(entry, deadline);
539
+ if (!entry.process) {
540
+ if (!entry.spawning) {
541
+ if (entry.reference)
542
+ changeEntry(entry.id, (row) => { row.state = 'exited'; });
543
+ else
544
+ removeEntry(entry.id);
545
+ }
546
+ continue;
547
+ }
548
+ await terminateOwnedTree(entry.process, { force: true, deadline, known: entry.descendants });
549
+ assertInterruptionOperation(operationId);
550
+ await confirmOwnedExecutionExited(entry.id, deadline);
551
+ }
552
+ assertInterruptionOperation(operationId);
553
+ const after = await snapshotOwnedWork(deadline);
554
+ assertWorkDeadline(deadline);
555
+ if (after.active.length || after.unknown.length || after.pending.some((row) => !after.claimPending.some((claim) => claim.id === row.id))) {
556
+ throw new Error('Some owned execution or pending claims have not ended. No replacement may start.');
557
+ }
558
+ }
559
+ export function interruptionReceipts(surface) {
560
+ const owner = requireContext();
561
+ return readStore().entries.flatMap((entry) => entry.accountId === owner.accountId && entry.machineId === owner.machineId
562
+ && entry.reference?.surface === surface && entry.interruption && entry.state === 'exited'
563
+ ? [{ id: entry.id, accountId: entry.accountId, machineId: entry.machineId, instanceNonce: entry.instanceNonce, harness: entry.harness,
564
+ reference: entry.reference, process: entry.process, descendants: entry.descendants, ...entry.interruption }]
565
+ : []);
566
+ }
567
+ export function acknowledgeInterruption(id) {
568
+ const owner = requireContext();
569
+ const entry = readStore().entries.find((candidate) => candidate.id === id);
570
+ if (!entry)
571
+ return;
572
+ if (entry.accountId !== owner.accountId || entry.machineId !== owner.machineId || entry.state !== 'exited' || !entry.interruption) {
573
+ throw new Error('Interruption acknowledgement does not match this account and ended execution.');
574
+ }
575
+ removeEntry(id);
576
+ }
577
+ export function failedPanelPreparations() {
578
+ const owner = requireContext();
579
+ return readStore().entries.flatMap((entry) => entry.accountId === owner.accountId && entry.machineId === owner.machineId
580
+ && entry.reference?.surface === 'panel' && entry.preparationFailure && entry.state === 'exited'
581
+ ? [{ id: entry.id, attempt: entry.reference, ...entry.preparationFailure }] : []);
582
+ }
583
+ /** Recovery must consult the durable owner after an account loop is recreated. */
584
+ function heldPanelEntry(attempt) {
585
+ const owner = requireContext();
586
+ return readStore().entries.find((entry) => entry.accountId === owner.accountId && entry.machineId === owner.machineId
587
+ && entry.reference?.surface === 'panel' && entry.reference.runId === attempt.runId
588
+ && entry.reference.processToken === attempt.processToken && entry.reference.startedAt === attempt.startedAt
589
+ && entry.reference.resumedAt === attempt.resumedAt);
590
+ }
591
+ export function ownedPanelAttemptHeld(attempt) {
592
+ return heldPanelEntry(attempt) !== undefined;
593
+ }
594
+ /** Local files remain protected even when another account cannot read the run. */
595
+ export function ownedLocalPanelOwnerHeld(runId) {
596
+ const owner = requireContext();
597
+ return readStore().entries.some((entry) => entry.machineId === owner.machineId
598
+ && entry.reference?.surface === 'panel' && entry.reference.runId === runId);
599
+ }
600
+ export async function stopOwnedPanelAttempt(attempt) {
601
+ const entry = heldPanelEntry(attempt);
602
+ if (!entry || entry.state === 'exited')
603
+ return;
604
+ if (!entry.process)
605
+ throw new Error('This run still has an unverified local execution. Its process record remains protected.');
606
+ const deadline = Date.now() + 30_000;
607
+ await terminateOwnedTree(entry.process, { force: true, deadline, known: entry.descendants });
608
+ await confirmOwnedExecutionExited(entry.id, deadline);
609
+ }
610
+ /** A transport PID can exit before its owned tool processes and settlement. */
611
+ export function ownedWorkerExecutionHeld(workerId) {
612
+ const owner = requireContext();
613
+ return readStore().entries.some((entry) => entry.accountId === owner.accountId && entry.machineId === owner.machineId
614
+ && entry.reference?.surface === 'worker' && entry.reference.workerId === workerId);
615
+ }
616
+ /** A stale worker row cannot identify a later claim on the same request. */
617
+ export function ownedWorkerClaimedAt(workerId) {
618
+ const owner = requireContext();
619
+ const ref = readStore().entries.find((entry) => entry.accountId === owner.accountId && entry.machineId === owner.machineId
620
+ && entry.reference?.surface === 'worker' && entry.reference.workerId === workerId)?.reference;
621
+ return ref?.surface === 'worker' ? ref.claimedAt : null;
622
+ }
623
+ export function acknowledgePreparationFailure(id) {
624
+ const owner = requireContext();
625
+ const entry = readStore().entries.find((candidate) => candidate.id === id);
626
+ if (!entry)
627
+ return;
628
+ if (entry.accountId !== owner.accountId || entry.machineId !== owner.machineId || entry.state !== 'exited' || !entry.preparationFailure) {
629
+ throw new Error('Preparation acknowledgement does not match this account and ended execution.');
630
+ }
631
+ removeEntry(id);
632
+ }
633
+ export function legacySnapshotNeeded() {
634
+ if (!context?.accountId)
635
+ return false;
636
+ const state = readStore().legacy.find((item) => item.accountId === context.accountId && item.machineId === context.machineId);
637
+ return !!state && !state.complete;
638
+ }
639
+ export function saveLegacySnapshot(holds) {
640
+ const owner = requireContext();
641
+ const store = readStore();
642
+ const state = store.legacy.find((item) => item.accountId === owner.accountId && item.machineId === owner.machineId);
643
+ if (!state || state.complete)
644
+ return;
645
+ state.holds = holds;
646
+ state.complete = true;
647
+ writeStore(store);
648
+ }
649
+ export function legacyTodoHolds() {
650
+ if (!context?.accountId)
651
+ return [];
652
+ return readStore().legacy.find((item) => item.accountId === context.accountId && item.machineId === context.machineId)?.holds ?? [];
653
+ }
654
+ export function legacyHeldTodoIds() { return legacyTodoHolds().map((item) => item.todoId); }
655
+ export function releaseLegacyTodoHold(todoId) {
656
+ const owner = requireContext();
657
+ const store = readStore();
658
+ const state = store.legacy.find((item) => item.accountId === owner.accountId && item.machineId === owner.machineId);
659
+ if (!state)
660
+ return;
661
+ state.holds = state.holds.filter((item) => item.todoId !== todoId);
662
+ writeStore(store);
663
+ }
664
+ export function legacyPanelSnapshotNeeded() {
665
+ if (!context?.accountId)
666
+ return false;
667
+ const state = readStore().legacy.find((item) => item.accountId === context.accountId && item.machineId === context.machineId);
668
+ return !!state && !state.panelComplete;
669
+ }
670
+ export function completeLegacyPanelSnapshot() {
671
+ const owner = requireContext();
672
+ const store = readStore();
673
+ const state = store.legacy.find((item) => item.accountId === owner.accountId && item.machineId === owner.machineId);
674
+ if (state) {
675
+ state.panelComplete = true;
676
+ writeStore(store);
677
+ }
678
+ }
679
+ /** A boot-proven old attempt has no surviving local process, but still needs an exact cloud receipt. */
680
+ export function recordExitedInterruption(ref, operationId = randomUUID()) {
681
+ const owner = requireContext();
682
+ const store = readStore();
683
+ if (store.entries.some((row) => row.accountId === owner.accountId && row.machineId === owner.machineId
684
+ && row.reference && JSON.stringify(row.reference) === JSON.stringify(ref)))
685
+ return;
686
+ store.entries.push({ id: randomUUID(), accountId: owner.accountId, machineId: owner.machineId,
687
+ instanceNonce: owner.instanceNonce, harness: null, reference: ref, process: null, descendants: [],
688
+ state: 'exited', interruption: { operationId, interruptedAt: new Date().toISOString() } });
689
+ writeStore(store);
690
+ }
691
+ function sameAttempt(a, b) {
692
+ if (a.surface !== b.surface)
693
+ return false;
694
+ if (a.surface === 'panel' && b.surface === 'panel')
695
+ return a.runId === b.runId && a.processToken === b.processToken
696
+ && a.startedAt === b.startedAt && a.resumedAt === b.resumedAt;
697
+ if (a.surface === 'worker' && b.surface === 'worker')
698
+ return a.todoId === b.todoId && a.claimedAt === b.claimedAt
699
+ && (a.workerId === null || b.workerId === null || a.workerId === b.workerId);
700
+ if (a.surface === 'reply' && b.surface === 'reply')
701
+ return a.todoId === b.todoId
702
+ && a.messageLeases.every((source) => b.messageLeases.some((other) => other.id === source.id && other.taken_at === source.taken_at))
703
+ && a.grantLeases.every((source) => b.grantLeases.some((other) => other.id === source.id && other.resume_taken_at === source.resume_taken_at));
704
+ return false;
705
+ }
706
+ export function beginRpcClaim(surface, action) {
707
+ if (!ownedWorkAllowed())
708
+ throw new Error('This machine is not accepting new work.');
709
+ const owner = requireContext();
710
+ const store = readStore();
711
+ const id = randomUUID();
712
+ liveRpcClaims.add(id);
713
+ (store.rpcClaims ??= []).push({ id, surface, action, accountId: owner.accountId, machineId: owner.machineId, instanceNonce: owner.instanceNonce });
714
+ writeStore(store);
715
+ return id;
716
+ }
717
+ export function pendingRpcClaims(surface) {
718
+ const owner = requireContext();
719
+ return (readStore().rpcClaims ?? []).filter((row) => row.surface === surface && row.accountId === owner.accountId && row.machineId === owner.machineId && !liveRpcClaims.has(row.id));
720
+ }
721
+ export function deferRpcClaim(id) { liveRpcClaims.delete(id); }
722
+ export function recordRpcClaims(id, refs, interrupted = false) {
723
+ const owner = requireContext();
724
+ interrupted ||= forcedOperationId !== null;
725
+ const store = readStore();
726
+ const claim = (store.rpcClaims ?? []).find((row) => row.id === id && row.accountId === owner.accountId && row.machineId === owner.machineId);
727
+ if (!claim) {
728
+ if ((store.rpcClaims ?? []).some((row) => row.id === id))
729
+ throw new Error('The claim receipt does not belong to this account.');
730
+ return;
731
+ }
732
+ for (const ref of refs) {
733
+ if (store.entries.some((row) => row.reference && row.accountId === owner.accountId && sameAttempt(row.reference, ref)))
734
+ continue;
735
+ store.entries.push({ id: randomUUID(), accountId: owner.accountId, machineId: owner.machineId,
736
+ instanceNonce: owner.instanceNonce, harness: null, reference: ref, process: null, descendants: [],
737
+ state: interrupted ? 'exited' : 'pending', interruption: interrupted ? { operationId: id, interruptedAt: new Date().toISOString() } : null,
738
+ rpcId: id, unstarted: true });
739
+ }
740
+ store.rpcClaims = store.rpcClaims?.filter((row) => row.id !== id);
741
+ writeStore(store);
742
+ liveRpcClaims.delete(id);
743
+ }
744
+ export function finishRpcClaims(ids) {
745
+ const owner = requireContext();
746
+ const store = readStore();
747
+ store.entries = store.entries.filter((row) => !row.unstarted || !row.rpcId || !ids.includes(row.rpcId)
748
+ || row.accountId !== owner.accountId || row.machineId !== owner.machineId || row.interruption || row.preparationFailure);
749
+ writeStore(store);
750
+ }
751
+ export function pendingPanelAttempt(runId) {
752
+ const owner = requireContext();
753
+ const row = readStore().entries.find((entry) => entry.instanceNonce === owner.instanceNonce && entry.state === 'pending'
754
+ && entry.reference?.surface === 'panel' && entry.reference.runId === runId);
755
+ return row?.reference?.surface === 'panel' ? row.reference : null;
756
+ }