@ours.network/fleet 1.2.0-nightly.54 → 1.2.0-nightly.56

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 (86) hide show
  1. package/README.md +20 -12
  2. package/dist/agent-ours/service.d.ts +4 -1
  3. package/dist/agent-ours/service.js +31 -7
  4. package/dist/application/log-service.js +8 -8
  5. package/dist/application/role-creation-service.js +2 -1
  6. package/dist/application/role-removal-service.js +7 -1
  7. package/dist/application/role-repository.js +17 -0
  8. package/dist/application/task-layouts.js +12 -2
  9. package/dist/application/task-room-service.js +19 -17
  10. package/dist/application/types.d.ts +2 -0
  11. package/dist/build-info.json +4 -4
  12. package/dist/cli.js +31 -3
  13. package/dist/config.d.ts +2 -0
  14. package/dist/creation.d.ts +1 -1
  15. package/dist/creation.js +2 -2
  16. package/dist/harness/agent-session.d.ts +2 -0
  17. package/dist/harness/claude-code-session.js +1 -1
  18. package/dist/harness/codex-session.js +2 -2
  19. package/dist/managed-recovery.d.ts +13 -0
  20. package/dist/managed-recovery.js +31 -0
  21. package/dist/ops.d.ts +2 -0
  22. package/dist/ops.js +15 -2
  23. package/dist/rooms-tasks/cli.js +21 -0
  24. package/dist/rooms-tasks/close.d.ts +2 -0
  25. package/dist/rooms-tasks/close.js +66 -1
  26. package/dist/rooms-tasks/deletion.js +10 -0
  27. package/dist/rooms-tasks/erasure.js +4 -2
  28. package/dist/rooms-tasks/layout-member-retirement.d.ts +10 -0
  29. package/dist/rooms-tasks/layout-member-retirement.js +90 -0
  30. package/dist/rooms-tasks/layout-service.d.ts +1 -0
  31. package/dist/rooms-tasks/layout-service.js +31 -13
  32. package/dist/rooms-tasks/layout.d.ts +7 -0
  33. package/dist/rooms-tasks/layout.js +35 -0
  34. package/dist/rooms-tasks/legacy-task-member.d.ts +19 -0
  35. package/dist/rooms-tasks/legacy-task-member.js +116 -0
  36. package/dist/rooms-tasks/member-ownership.d.ts +5 -0
  37. package/dist/rooms-tasks/member-ownership.js +40 -0
  38. package/dist/rooms-tasks/provision.js +26 -1
  39. package/dist/rooms-tasks/recovery.d.ts +19 -0
  40. package/dist/rooms-tasks/recovery.js +110 -0
  41. package/dist/rooms-tasks/task-service-retirement.d.ts +11 -0
  42. package/dist/rooms-tasks/task-service-retirement.js +21 -0
  43. package/dist/rooms-tasks/task-state.d.ts +1 -0
  44. package/dist/rooms-tasks/task-state.js +16 -1
  45. package/dist/rooms-tasks/types.d.ts +3 -0
  46. package/dist/rooms-tasks/workspace.d.ts +2 -0
  47. package/dist/rooms-tasks/workspace.js +5 -0
  48. package/dist/runner.d.ts +6 -0
  49. package/dist/runner.js +160 -22
  50. package/dist/session/acp.d.ts +1 -0
  51. package/dist/session/acp.js +7 -0
  52. package/dist/session/arbiter.d.ts +5 -0
  53. package/dist/session/arbiter.js +16 -2
  54. package/dist/session/codex-app-server.d.ts +1 -0
  55. package/dist/session/codex-app-server.js +5 -0
  56. package/dist/session/types.d.ts +2 -0
  57. package/dist/spawn.d.ts +5 -1
  58. package/dist/spawn.js +32 -3
  59. package/dist/startup-readiness.d.ts +1 -1
  60. package/dist/startup-readiness.js +3 -3
  61. package/dist/supervisor/adoption.d.ts +8 -0
  62. package/dist/supervisor/adoption.js +78 -0
  63. package/dist/supervisor/catalog.d.ts +62 -0
  64. package/dist/supervisor/catalog.js +254 -0
  65. package/dist/supervisor/fleet.d.ts +8 -0
  66. package/dist/supervisor/fleet.js +89 -0
  67. package/dist/supervisor/index.js +3 -6
  68. package/dist/supervisor/launchd.d.ts +1 -1
  69. package/dist/supervisor/launchd.js +24 -12
  70. package/dist/supervisor/legacy.d.ts +35 -0
  71. package/dist/supervisor/legacy.js +524 -0
  72. package/dist/supervisor/manager.d.ts +15 -0
  73. package/dist/supervisor/manager.js +258 -0
  74. package/dist/supervisor/scope.d.ts +4 -0
  75. package/dist/supervisor/scope.js +10 -0
  76. package/dist/supervisor/systemd.d.ts +2 -2
  77. package/dist/supervisor/systemd.js +14 -14
  78. package/dist/task-supervision.d.ts +9 -0
  79. package/dist/task-supervision.js +145 -0
  80. package/dist/temp-lifecycle.d.ts +17 -7
  81. package/dist/temp-lifecycle.js +85 -53
  82. package/dist/temp-supervisor-recovery.d.ts +1 -1
  83. package/dist/temp-supervisor-recovery.js +2 -2
  84. package/docs/fleet-migration.md +54 -0
  85. package/docs/task-workspaces.md +26 -0
  86. package/package.json +4 -1
@@ -0,0 +1,40 @@
1
+ import { existsSync, lstatSync, readFileSync, readdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { agentDir, agentsRoot } from '../paths.js';
4
+ /** A room seat is never authority to retire a standalone persistent agent.
5
+ * Check both role-name collisions and a permanent role using the seat's
6
+ * identity name. Missing legacy task state must not bypass this fence.
7
+ */
8
+ export function assertMemberNotPermanent(name) {
9
+ if (!/^[a-zA-Z0-9_-]{1,120}$/.test(name))
10
+ throw new Error('Invalid retirement member name');
11
+ if (existsSync(agentDir(name)))
12
+ throw new Error(`room member '${name}' conflicts with permanent agent state; refusing retirement`);
13
+ let entries;
14
+ try {
15
+ entries = readdirSync(agentsRoot(), { withFileTypes: true });
16
+ }
17
+ catch (error) {
18
+ if (error.code === 'ENOENT')
19
+ return;
20
+ throw error;
21
+ }
22
+ for (const entry of entries) {
23
+ if (!entry.isDirectory() || entry.isSymbolicLink())
24
+ continue;
25
+ const path = join(agentsRoot(), entry.name, '.identity');
26
+ let stat;
27
+ try {
28
+ stat = lstatSync(path);
29
+ }
30
+ catch (error) {
31
+ if (error.code === 'ENOENT')
32
+ continue;
33
+ throw error;
34
+ }
35
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1024)
36
+ throw new Error('Permanent identity ownership proof is unsafe; refusing member retirement');
37
+ if (readFileSync(path, 'utf8').trim() === name)
38
+ throw new Error(`room member '${name}' identity belongs to permanent agent '${entry.name}'; refusing retirement`);
39
+ }
40
+ }
@@ -7,7 +7,8 @@ import { parse } from 'yaml';
7
7
  import { advanceSaga, setSagaError, updateMemberSeats, updateMemberStartup, activateRoom, getRoomRecord, } from './room-state.js';
8
8
  import { activateTask, updateTaskMembers, blockTask, unblockTask, getTask, } from './task-state.js';
9
9
  import { storedRoomLaunchPolicy } from './types.js';
10
- import { spawnTemp } from '../spawn.js';
10
+ import { spawnTemp, resumeTaskSupervisor } from '../spawn.js';
11
+ import { adoptLegacyTaskMember } from './legacy-task-member.js';
11
12
  import { effectivePermissionMode } from '../permissions.js';
12
13
  import { selectionOrigin, summarizeResolvedLaunch, } from '../lifecycle-summary.js';
13
14
  import { canonicalJson } from '../canonical-json.js';
@@ -207,6 +208,22 @@ async function retainRunningLaunch(input) {
207
208
  } });
208
209
  return true;
209
210
  }
211
+ if (supervisor.taskOwner) {
212
+ await resumeTaskSupervisor(member.name, provision.binPath);
213
+ updateMemberStartup(provision.roomId, member.name, { launch: {
214
+ ...retainedLaunch, state: 'launched', launch_id: supervisor.launchId,
215
+ presentation: retainedLaunch.presentation ?? presentationFromStatePath(dir, settings, member.coworkRole),
216
+ updated_at: new Date().toISOString(),
217
+ } });
218
+ return true;
219
+ }
220
+ if (provision.taskId) {
221
+ const adopted = await adoptLegacyTaskMember({ taskId: provision.taskId, roomId: provision.roomId,
222
+ roomIdentityCid, creationActionId: retainedLaunch.action_id }, member.name);
223
+ if (adopted !== 'running-legacy')
224
+ await resumeTaskSupervisor(member.name, provision.binPath);
225
+ return true;
226
+ }
210
227
  await secureStoppedTempArchive(member.name, supervisor.launchId);
211
228
  updateMemberStartup(provision.roomId, member.name, { launch: {
212
229
  ...retainedLaunch, state: 'stopped', launch_id: supervisor.launchId,
@@ -215,6 +232,8 @@ async function retainRunningLaunch(input) {
215
232
  return false;
216
233
  }
217
234
  if (seat.launch?.state === 'launched' && !existsSync(dir)) {
235
+ if (provision.taskId)
236
+ throw Error(`retained task member ${member.name} is missing; explicit reconciliation required`);
218
237
  if (!seat.launch.launch_id || !seat.launch.action_id)
219
238
  throw new Error(`missing durable launch identity for disappeared ${member.name}`);
220
239
  const archive = await secureStoppedTempArchive(member.name, seat.launch.launch_id);
@@ -249,6 +268,7 @@ async function launchMember(input) {
249
268
  const actionId = randomUUID();
250
269
  let effectiveActionId = actionId;
251
270
  const attempt = (seat.launch?.attempt ?? 0) + 1;
271
+ const taskSupervised = Boolean(provision.taskId) && process.env.OURS_FLEET_SUPERVISOR !== 'none';
252
272
  const taskSha = sha256Text(startup.task);
253
273
  const effectiveAgentDefinition = structuredClone(settings.definition);
254
274
  const { projection: agentDefinition, fingerprint: agentFingerprint } = launchDefinition(effectiveAgentDefinition);
@@ -256,6 +276,7 @@ async function launchMember(input) {
256
276
  ? process.env[FLEET_PROXY_CALLER_ENV] : undefined;
257
277
  updateMemberStartup(provision.roomId, member.name, { launch: {
258
278
  state: 'intent', attempt, action_id: actionId, mission_sha256: taskSha,
279
+ ...(taskSupervised ? { task_supervised: true } : {}),
259
280
  agent_definition: agentDefinition, agent_fingerprint: agentFingerprint,
260
281
  agent_template: settings.template, agent_template_hash: settings.templateHash,
261
282
  ...(proxyCaller ? { caller_role: proxyCaller } : {}),
@@ -280,6 +301,7 @@ async function launchMember(input) {
280
301
  if (launched.creationActionId !== actionId) {
281
302
  updateMemberStartup(provision.roomId, member.name, { launch: {
282
303
  state: 'intent', attempt, action_id: launched.creationActionId,
304
+ ...(taskSupervised ? { task_supervised: true } : {}),
283
305
  mission_sha256: taskSha, agent_definition: agentDefinition,
284
306
  agent_fingerprint: agentFingerprint,
285
307
  agent_template: settings.template, agent_template_hash: settings.templateHash,
@@ -297,6 +319,7 @@ async function launchMember(input) {
297
319
  : presentationFromStatePath(launchedDir, settings, member.coworkRole);
298
320
  updateMemberStartup(provision.roomId, member.name, { launch: {
299
321
  state: 'launched', attempt, action_id: launched.creationActionId, mission_sha256: taskSha,
322
+ ...(taskSupervised ? { task_supervised: true } : {}),
300
323
  agent_definition: agentDefinition, agent_fingerprint: agentFingerprint,
301
324
  agent_template: settings.template, agent_template_hash: settings.templateHash,
302
325
  presentation,
@@ -307,6 +330,7 @@ async function launchMember(input) {
307
330
  catch (error) {
308
331
  updateMemberStartup(provision.roomId, member.name, { launch: {
309
332
  state: 'failed', attempt, action_id: effectiveActionId, mission_sha256: taskSha,
333
+ ...(taskSupervised ? { task_supervised: true } : {}),
310
334
  agent_definition: agentDefinition, agent_fingerprint: agentFingerprint,
311
335
  agent_template: settings.template, agent_template_hash: settings.templateHash,
312
336
  ...(proxyCaller ? { caller_role: proxyCaller } : {}),
@@ -503,6 +527,7 @@ async function provisionMembersUnlocked(input) {
503
527
  member,
504
528
  settings: settings.get(member.name),
505
529
  startup: {
530
+ ...(taskId ? { task_id: taskId } : {}),
506
531
  workspace: existing.workspace,
507
532
  room_id: roomId,
508
533
  room_identity_cid: roomIdentityCid,
@@ -0,0 +1,19 @@
1
+ import { tempSupervisorLiveness } from '../temp-lifecycle.js';
2
+ import { resumeTaskSupervisor } from '../spawn.js';
3
+ import type { CoworkAdapter } from './cowork-adapter.js';
4
+ export type MemberRecovery = {
5
+ name: string;
6
+ status: 'running' | 'resumed' | 'migration_pending' | 'recovery_reset';
7
+ };
8
+ export interface TaskMemberRecoveryDeps {
9
+ liveness?: typeof tempSupervisorLiveness;
10
+ resume?: typeof resumeTaskSupervisor;
11
+ adopt?(taskId: string, roomId: string, name: string): Promise<void>;
12
+ }
13
+ /** Explicit supported recovery; never creates an identity, seat, invite, or session ID. */
14
+ export declare function recoverTaskMembers(input: {
15
+ taskId: string;
16
+ binPath: string;
17
+ cowork?: Pick<CoworkAdapter, 'getRoom'>;
18
+ deps?: TaskMemberRecoveryDeps;
19
+ }): Promise<MemberRecovery[]>;
@@ -0,0 +1,110 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { parse } from 'yaml';
4
+ import { withFileLock } from '../atomic-file.js';
5
+ import { agentDir, stateRoot } from '../paths.js';
6
+ import { readProvenance } from '../creation.js';
7
+ import { readTempSupervisor, tempSupervisorLiveness, requestedTempStopReason } from '../temp-lifecycle.js';
8
+ import { taskSupervisorMayRun } from '../task-supervision.js';
9
+ import { resumeTaskSupervisor } from '../spawn.js';
10
+ import { resetRestartLedger, readRestartLedger } from '../runner.js';
11
+ import { readRoomReadiness } from '../agent-ours/service.js';
12
+ import { getTask } from './task-state.js';
13
+ import { getRoomRecord } from './room-state.js';
14
+ import { roomCloseLockPath, CLOSE_LOCK_STALE_MS } from './close.js';
15
+ import { taskOperationLockPath, TASK_OPERATION_LOCK_STALE_MS } from './terminal.js';
16
+ import { adoptLegacyTaskMember } from './legacy-task-member.js';
17
+ /** Explicit supported recovery; never creates an identity, seat, invite, or session ID. */
18
+ export async function recoverTaskMembers(input) {
19
+ const deps = input.deps ?? {};
20
+ return withFileLock(taskOperationLockPath(input.taskId), async () => {
21
+ const task = getTask(input.taskId);
22
+ if (!['provisioning', 'active', 'review'].includes(task.state) || task.deletion || task.terminal_intent)
23
+ throw Error('Task is not open for member recovery');
24
+ const recover = async (name, roomId) => {
25
+ const dir = agentDir(name, true), metadata = readTempSupervisor(dir);
26
+ if (!metadata || metadata.role !== name)
27
+ throw Error('Exact supervisor state missing; explicit reconciliation required');
28
+ if (requestedTempStopReason(dir))
29
+ throw Error('Task member has an explicit retirement request');
30
+ const live = await (deps.liveness ?? tempSupervisorLiveness)(dir);
31
+ if (live === 'unknown')
32
+ throw Error('Supervisor liveness unknown; refusing recovery');
33
+ if (!metadata.taskOwner) {
34
+ if (live === 'running')
35
+ return { name, status: 'migration_pending' };
36
+ if (!roomId)
37
+ throw Error('Legacy layout member requires explicit reconciliation');
38
+ if (deps.adopt)
39
+ await deps.adopt(input.taskId, roomId, name);
40
+ else {
41
+ const room = getRoomRecord(roomId);
42
+ const seat = room.member_seats.find(member => member.role_name === name);
43
+ const adopted = await adoptLegacyTaskMember({ taskId: input.taskId, roomId,
44
+ roomIdentityCid: room.room_identity_cid, creationActionId: seat.launch.action_id }, name);
45
+ if (adopted === 'running-legacy')
46
+ return { name, status: 'migration_pending' };
47
+ }
48
+ }
49
+ if (!taskSupervisorMayRun(name))
50
+ throw Error('Task member is retired');
51
+ const reset = readRestartLedger(dir).circuit === 'open';
52
+ if (reset)
53
+ resetRestartLedger(dir);
54
+ if (live === 'running') {
55
+ return { name, status: reset ? 'recovery_reset' : 'running' };
56
+ }
57
+ await (deps.resume ?? resumeTaskSupervisor)(name, input.binPath);
58
+ return { name, status: 'resumed' };
59
+ };
60
+ if (task.layout) {
61
+ const path = join(stateRoot(), 'layouts', `${task.layout.run_id}.json`);
62
+ return withFileLock(path + '.lock', async () => {
63
+ const state = JSON.parse(readFileSync(path, 'utf8'));
64
+ if (state.closed || state.closing || state.uncertain)
65
+ throw Error('Layout requires reconciliation before recovery');
66
+ const result = [];
67
+ for (const member of Object.values(state.participants)) {
68
+ if (!member.owned || member.retired)
69
+ continue;
70
+ if (!member.instance?.agent || member.instance.remote)
71
+ throw Error('Layout owned instance evidence missing');
72
+ result.push(await recover(member.instance.agent));
73
+ }
74
+ return result;
75
+ });
76
+ }
77
+ if (!task.room_id || !input.cowork)
78
+ throw Error('Task room required for member recovery');
79
+ return withFileLock(roomCloseLockPath(task.room_id), async () => {
80
+ const room = getRoomRecord(task.room_id);
81
+ if (!room || room.task_id !== task.task_id || room.close || !['active', 'provisioning'].includes(room.state)
82
+ || !room.room_identity_cid || room.room_identity_cid !== task.room_identity_cid)
83
+ throw Error('Exact open task room required');
84
+ const remote = await input.cowork.getRoom(room.room_id);
85
+ if (!remote || remote.identity_cid !== room.room_identity_cid || remote.state !== 'active')
86
+ throw Error('Exact active Cowork room required');
87
+ // Validate the complete admitted roster before any member is resumed.
88
+ for (const seat of room.member_seats) {
89
+ const dir = agentDir(seat.role_name, true), metadata = readTempSupervisor(dir), provenance = readProvenance(dir);
90
+ const role = parse(readFileSync(join(dir, 'role.yaml'), 'utf8')), startup = role.roomMemberStartup;
91
+ const found = remote.seats.filter(item => item.display_name === seat.role_name && item.seat_state !== 'removed');
92
+ const ready = readRoomReadiness(room.room_identity_cid, seat.role_name);
93
+ if (seat.retirement || !seat.identity_cid || !seat.invite_id || !seat.launch?.action_id || !seat.launch.launch_id
94
+ || metadata?.role !== seat.role_name || metadata.launchId !== seat.launch.launch_id
95
+ || provenance?.role !== seat.role_name || provenance.creationActionId !== seat.launch.action_id
96
+ || role.identity !== seat.role_name || startup?.identity_name !== seat.role_name
97
+ || startup.room_id !== room.room_id || startup.room_identity_cid !== room.room_identity_cid
98
+ || startup.invite_id !== seat.invite_id || startup.role !== seat.cowork_role
99
+ || !ready || ready.cid !== seat.identity_cid || ready.room !== room.room_id || ready.invite !== seat.invite_id
100
+ || found.length !== 1 || found[0].identity_cid !== seat.identity_cid || found[0].invite_id !== seat.invite_id
101
+ || found[0].role !== seat.cowork_role || found[0].seat_state !== 'active')
102
+ throw Error(`Exact admitted member proof missing: ${seat.role_name}`);
103
+ }
104
+ const result = [];
105
+ for (const seat of room.member_seats)
106
+ result.push(await recover(seat.role_name, room.room_id));
107
+ return result;
108
+ }, {}, CLOSE_LOCK_STALE_MS);
109
+ }, {}, TASK_OPERATION_LOCK_STALE_MS);
110
+ }
@@ -0,0 +1,11 @@
1
+ import type { Exec } from '../exec.js';
2
+ /** Registration retirement precedes every identity-absence shortcut. A proven
3
+ * legacy transient/detached launch never owned a retained registration.
4
+ * Missing state still requires exact catalog ownership before unregistering.
5
+ */
6
+ export declare function retireTaskMemberService(name: string, expected: {
7
+ taskId: string;
8
+ creationActionId?: string;
9
+ launchId?: string;
10
+ taskSupervised?: boolean;
11
+ }, exec?: Exec): Promise<void>;
@@ -0,0 +1,21 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { agentDir } from '../paths.js';
3
+ import { readTempSupervisor, tempArchiveForCreationAction, tempArchiveForLaunch } from '../temp-lifecycle.js';
4
+ import { unregisterTaskMember, hasTaskMemberRegistration } from '../supervisor/catalog.js';
5
+ import { assertMemberNotPermanent } from './member-ownership.js';
6
+ /** Registration retirement precedes every identity-absence shortcut. A proven
7
+ * legacy transient/detached launch never owned a retained registration.
8
+ * Missing state still requires exact catalog ownership before unregistering.
9
+ */
10
+ export async function retireTaskMemberService(name, expected, exec) {
11
+ assertMemberNotPermanent(name);
12
+ const archive = expected.creationActionId ? tempArchiveForCreationAction(name, expected.creationActionId)
13
+ : expected.launchId ? { path: tempArchiveForLaunch(name, expected.launchId) } : undefined;
14
+ const source = existsSync(agentDir(name, true)) ? agentDir(name, true) : archive?.path;
15
+ const metadata = source ? readTempSupervisor(source) : undefined;
16
+ const hasServiceEvidence = hasTaskMemberRegistration(name);
17
+ const durableMetadata = metadata?.taskOwner && metadata.kind === 'fleet-managed';
18
+ if (!hasServiceEvidence && !expected.taskSupervised && !durableMetadata)
19
+ return;
20
+ await unregisterTaskMember(name, expected, { exec });
21
+ }
@@ -130,6 +130,7 @@ interface SeatEvidence {
130
130
  launch?: {
131
131
  launch_id?: string;
132
132
  action_id?: string;
133
+ task_supervised?: boolean;
133
134
  };
134
135
  retirement?: {
135
136
  absence_verified?: boolean;
@@ -664,6 +664,8 @@ function findCursorForSeat(id, stored, seat) {
664
664
  if (cursor && seat.identity_cid
665
665
  && cursor.identity_cid.toLowerCase() !== seat.identity_cid.toLowerCase())
666
666
  throw new TaskStateError(`task ${id} deletion member '${seat.role_name}' identity CID mismatch between cursor and room seat`);
667
+ if (cursor?.action_id && seat.launch?.action_id && cursor.action_id !== seat.launch.action_id)
668
+ throw new TaskStateError(`task ${id} deletion member '${seat.role_name}' creation ownership proof changed`);
667
669
  return cursor;
668
670
  }
669
671
  /**
@@ -684,11 +686,20 @@ export function upsertTaskDeletionMembersFromSeats(id, seats) {
684
686
  for (const seat of seats) {
685
687
  if (!seat.identity_cid)
686
688
  continue;
687
- if (!findCursorForSeat(id, stored, seat)) {
689
+ const cursor = findCursorForSeat(id, stored, seat);
690
+ if (!cursor) {
688
691
  stored.deletion.members.push({
689
692
  name: seat.role_name, identity_cid: seat.identity_cid, phase: 'pending', updated_at: now,
693
+ ...(seat.launch?.action_id ? { action_id: seat.launch.action_id } : {}),
694
+ ...(seat.launch?.task_supervised ? { task_supervised: true } : {}),
690
695
  });
691
696
  }
697
+ else {
698
+ if (seat.launch?.action_id)
699
+ cursor.action_id ??= seat.launch.action_id;
700
+ if (seat.launch?.task_supervised)
701
+ cursor.task_supervised = true;
702
+ }
692
703
  }
693
704
  writeTask(stored);
694
705
  return presentTaskLenient(stored);
@@ -747,6 +758,10 @@ export function importTaskDeletionRetirementEvidence(id, seats, proofs = []) {
747
758
  cursor = { name: seat.role_name, identity_cid: seat.identity_cid, phase: 'pending', updated_at: now };
748
759
  stored.deletion.members.push(cursor);
749
760
  }
761
+ if (seat.launch?.action_id)
762
+ cursor.action_id ??= seat.launch.action_id;
763
+ if (seat.launch?.task_supervised)
764
+ cursor.task_supervised = true;
750
765
  if (DELETION_MEMBER_PHASE_ORDER[evidence.phase] < DELETION_MEMBER_PHASE_ORDER[cursor.phase])
751
766
  continue;
752
767
  if (cursor.launch_id !== undefined && cursor.launch_id !== evidence.launch_id)
@@ -62,6 +62,8 @@ export type TaskDeletionMemberPhase = 'pending' | 'stop_requested' | 'liveness_a
62
62
  export interface TaskDeletionMemberCursor {
63
63
  name: string;
64
64
  identity_cid: string;
65
+ action_id?: string;
66
+ task_supervised?: boolean;
65
67
  phase: TaskDeletionMemberPhase;
66
68
  launch_id?: string;
67
69
  archive_path?: string;
@@ -170,6 +172,7 @@ export interface RoomRoleBriefingDefinition {
170
172
  last_error?: string;
171
173
  }
172
174
  export interface RoomMemberLaunchState {
175
+ task_supervised?: boolean;
173
176
  state: 'pending' | 'intent' | 'launched' | 'stopped' | 'failed';
174
177
  attempt: number;
175
178
  action_id?: string;
@@ -11,6 +11,8 @@ export declare class WorkspaceError extends Error {
11
11
  export declare function assertSafeAncestors(path: string): void;
12
12
  export declare function planWorkspace(owner: OwnedWorkspace['owner'], id: string): OwnedWorkspace;
13
13
  export declare function validateWorkspace(w: OwnedWorkspace, owner?: "task" | "room", id?: string): void;
14
+ /** Read-only recovery proof; never allocate a replacement for a missing workspace. */
15
+ export declare function assertWorkspacePresent(w: OwnedWorkspace, owner: OwnedWorkspace['owner'], id: string): void;
14
16
  /** Caller must first prove the exact workspace marker or retired archive
15
17
  * provenance, and hold its lifecycle lock with all managed writers retired.
16
18
  * Directories need owner rwx for recursive removal; ordinary files do not.
@@ -54,6 +54,11 @@ function marked(path, w) {
54
54
  if (JSON.stringify(actual) !== JSON.stringify(w))
55
55
  fail('ownership marker mismatch');
56
56
  }
57
+ /** Read-only recovery proof; never allocate a replacement for a missing workspace. */
58
+ export function assertWorkspacePresent(w, owner, id) {
59
+ validateWorkspace(w, owner, id);
60
+ marked(w.path, w);
61
+ }
57
62
  function staging(w, phase) {
58
63
  return join(dirname(w.path), `.${w.id}.${w.token}.${phase}`);
59
64
  }
package/dist/runner.d.ts CHANGED
@@ -39,6 +39,8 @@ export interface RunnerDeps {
39
39
  reportOwnerStartupFailure(stateDir: string): Promise<'delivered' | 'duplicate'>;
40
40
  /** Lets a test (or a shutdown path) end the supervised restart loop. */
41
41
  shouldStop?(): boolean;
42
+ /** Parent shutdown suspends a managed transient member; explicit stop still retires. */
43
+ suspendOnStop?: boolean;
42
44
  }
43
45
  export declare const SUPERVISOR_RECYCLE_REQUIRED = "OWNER_CHANNEL_SUPERVISOR_RECYCLE_REQUIRED";
44
46
  export declare class SupervisorRecycleRequiredError extends Error {
@@ -201,9 +203,13 @@ export declare function runOnce(name: string, opts?: {
201
203
  */
202
204
  export declare function runSupervised(name: string, opts?: {
203
205
  configPath?: string;
206
+ temp?: boolean;
204
207
  }, partialDeps?: Partial<RunnerDeps>, attempt?: (n: string, o: {
205
208
  configPath?: string;
206
209
  allowResumeRotation?: boolean;
210
+ temp?: boolean;
207
211
  }, d: Partial<RunnerDeps>) => Promise<AttemptResult>): Promise<RestartLedger>;
208
212
  /** Temp-agent entrypoint: run once, journal why it ended, then archive its evidence. */
213
+ /** The parent uses the same adapter-relative sustained-uptime reset boundary. */
214
+ export declare function stableSupervisorWindow(name: string, temp: boolean, configPath?: string): number;
209
215
  export declare function runTemp(name: string, deps?: Partial<RunnerDeps>, attempt?: typeof runOnce): Promise<void>;