@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
@@ -1,8 +1,11 @@
1
1
  import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, } from 'node:fs';
2
+ import { parse } from 'yaml';
2
3
  import { randomUUID } from 'node:crypto';
3
4
  import { basename, join } from 'node:path';
4
5
  import { replaceFileAtomically, withFileLock, withSynchronousFileLock } from './atomic-file.js';
5
6
  import { realExec } from './exec.js';
7
+ import { captureMemberEnvironment, catalogLiveness, memberKey, readMember, registerMember, unregisterMember, unregisterTaskMember } from './supervisor/catalog.js';
8
+ import { ensureFleetParent } from './supervisor/fleet.js';
6
9
  import { stateRoot, tmpRoot } from './paths.js';
7
10
  export const TEMP_SUPERVISOR_FILE = '.temp-supervisor.json';
8
11
  export const TEMP_TERMINATION_FILE = 'termination.jsonl';
@@ -13,26 +16,40 @@ export const TEMP_LAUNCH_GRACE_MS = 60_000;
13
16
  const metadataPath = (dir) => join(dir, TEMP_SUPERVISOR_FILE);
14
17
  export const tempSystemdUnit = (name) => `ours-fleet-temp-${name}.service`;
15
18
  export const tempLaunchdLabel = (name) => `network.ours.fleet.temp.${name}`;
16
- export function prepareTempSupervisor(dir, role) {
19
+ export function prepareTempSupervisor(dir, role, taskOwner) {
17
20
  const record = {
18
- version: 1, role, launchId: randomUUID(), createdAt: new Date().toISOString(), phase: 'launching',
21
+ version: 1, role, launchId: randomUUID(), createdAt: new Date().toISOString(), phase: 'launching', ...(taskOwner ? { taskOwner } : {}),
19
22
  };
20
23
  replaceFileAtomically(metadataPath(dir), JSON.stringify(record, null, 2) + '\n');
21
24
  return record;
22
25
  }
23
26
  export function readTempSupervisor(dir) {
27
+ let value;
24
28
  try {
25
- const value = JSON.parse(readFileSync(metadataPath(dir), 'utf8'));
26
- if (value.version !== 1 || typeof value.role !== 'string' || typeof value.launchId !== 'string')
27
- return undefined;
28
- return value;
29
+ value = JSON.parse(readFileSync(metadataPath(dir), 'utf8'));
29
30
  }
30
31
  catch {
31
32
  return undefined;
32
33
  }
34
+ if (!value || typeof value !== 'object' || value.version !== 1 || typeof value.role !== 'string' || typeof value.launchId !== 'string')
35
+ return undefined;
36
+ if (value.kind !== undefined && !['fleet-managed', 'systemd-transient', 'launchd-transient', 'detached'].includes(value.kind))
37
+ throw Error('TEMP_SUPERVISOR_KIND_UNSUPPORTED');
38
+ return value;
39
+ }
40
+ /** Broad scans retain unsupported entries instead of blocking other members. */
41
+ export function readTempSupervisorForScan(dir) {
42
+ try {
43
+ return readTempSupervisor(dir);
44
+ }
45
+ catch (error) {
46
+ if (error instanceof Error && error.message === 'TEMP_SUPERVISOR_KIND_UNSUPPORTED')
47
+ return undefined;
48
+ throw error;
49
+ }
33
50
  }
34
51
  const metadataLockPath = (dir) => join(stateRoot(), 'locks', 'temp-supervisors', encodeURIComponent(basename(dir)));
35
- async function updateTempSupervisor(dir, update) {
52
+ export async function updateTempSupervisor(dir, update) {
36
53
  // The launcher and the just-started supervisor are separate processes. Lock
37
54
  // their read/merge/write updates so neither can discard the other's kind,
38
55
  // target, pid or phase. The lock lives outside the role directory so an
@@ -48,54 +65,22 @@ async function updateTempSupervisor(dir, update) {
48
65
  return next;
49
66
  });
50
67
  }
51
- /**
52
- * Launch a temp supervisor outside the caller's service-manager ownership
53
- * boundary. `detached: true` creates a new process group but does not escape a
54
- * systemd cgroup; a transient unit does, and is not enabled across reboot.
55
- */
68
+ /** Register managed members under Fleet; explicit manual mode uses a detached child. */
56
69
  export function makeTempSupervisorLauncher(options = {}) {
57
70
  const exec = options.exec ?? realExec;
58
71
  const platform = options.platform ?? process.platform;
59
72
  const supervisor = options.supervisor ?? process.env.OURS_FLEET_SUPERVISOR;
60
73
  return async (binPath, args, dir) => {
61
- const inherited = [
62
- 'HOME', 'PATH', 'XDG_RUNTIME_DIR', 'OURS_FLEET_HOME', 'OURS_FLEET_SOCKET_ROOT', 'CODEX_HOME',
63
- // Preserve runtime selection across the service-manager boundary, including
64
- // an explicit empty value which selects the bundle over manager defaults.
65
- 'CODEX_PATH',
66
- // The child supervisor performs daemon identity and wake probes itself;
67
- // it must resolve the same ours profile as the spawning supervisor.
68
- 'OURS_PORT', 'OURS_STATE_DIR', 'OURS_API_TOKEN', 'OURS_CONFIG',
69
- ]
70
- .flatMap(key => process.env[key] !== undefined ? [`${key}=${process.env[key]}`] : []);
71
74
  const role = args.at(-1);
72
75
  if (!role)
73
76
  throw new Error('temporary supervisor launch requires a role name');
74
- const log = join(dir, 'supervisor.log');
75
- if (supervisor !== 'none' && platform === 'linux') {
76
- const target = tempSystemdUnit(role);
77
- await updateTempSupervisor(dir, { phase: 'launching', kind: 'systemd-transient', target, binPath });
78
- const result = await exec('systemd-run', [
79
- '--user', '--quiet', '--collect', `--unit=${target}`,
80
- '--property=Type=exec', '--property=KillMode=control-group', '--property=TimeoutStopSec=15s',
81
- `--property=StandardOutput=append:${log}`, `--property=StandardError=append:${log}`,
82
- ...inherited.map(value => `--setenv=${value}`),
83
- process.execPath, binPath, ...args,
84
- ]);
85
- if (result.code !== 0)
86
- throw new Error(`systemd-run ${target} failed: ${result.stderr.trim() || `exit ${result.code}`}`);
87
- await updateTempSupervisor(dir, { phase: 'active' });
88
- return;
89
- }
90
- if (supervisor !== 'none' && platform === 'darwin') {
91
- const target = tempLaunchdLabel(role);
92
- await updateTempSupervisor(dir, { phase: 'launching', kind: 'launchd-transient', target, binPath });
93
- const result = await exec('launchctl', [
94
- 'submit', '-l', target, '-o', log, '-e', log, '--',
95
- '/usr/bin/env', ...inherited, process.execPath, binPath, ...args,
96
- ]);
97
- if (result.code !== 0)
98
- throw new Error(`launchctl submit ${target} failed: ${result.stderr.trim() || `exit ${result.code}`}`);
77
+ const metadata = readTempSupervisor(dir);
78
+ if (supervisor !== 'none') {
79
+ const kind = metadata?.taskOwner ? 'task' : 'temporary';
80
+ await updateTempSupervisor(dir, { phase: 'launching', kind: 'fleet-managed', target: memberKey(role, kind), binPath });
81
+ await ensureFleetParent(exec, platform);
82
+ await registerMember({ name: role, kind, dir, environment: await captureMemberEnvironment(),
83
+ ...(metadata?.taskOwner ? { taskOwner: metadata.taskOwner, launchId: metadata.launchId } : {}) });
99
84
  await updateTempSupervisor(dir, { phase: 'active' });
100
85
  return;
101
86
  }
@@ -211,7 +196,7 @@ export function archiveTempState(role, reason, outcome, detail, now = new Date()
211
196
  return paths.target;
212
197
  }
213
198
  function isArchiveForLaunch(path, role, launchId) {
214
- const supervisor = readTempSupervisor(path);
199
+ const supervisor = readTempSupervisorForScan(path);
215
200
  if (supervisor?.role !== role || supervisor.launchId !== launchId)
216
201
  return false;
217
202
  try {
@@ -256,7 +241,7 @@ export function tempArchiveForCreationAction(role, creationActionId) {
256
241
  return undefined;
257
242
  }
258
243
  const matches = entries.flatMap(path => {
259
- const supervisor = readTempSupervisor(path);
244
+ const supervisor = readTempSupervisorForScan(path);
260
245
  if (!supervisor || supervisor.role !== role || !isArchiveForLaunch(path, role, supervisor.launchId))
261
246
  return [];
262
247
  try {
@@ -317,7 +302,15 @@ function recoverInterruptedArchives(now) {
317
302
  const recovered = [];
318
303
  for (const name of names) {
319
304
  const source = join(archiveRoot(), name);
320
- const supervisor = readTempSupervisor(source);
305
+ let supervisor;
306
+ try {
307
+ supervisor = readTempSupervisor(source);
308
+ }
309
+ catch (error) {
310
+ if (error instanceof Error && error.message === 'TEMP_SUPERVISOR_KIND_UNSUPPORTED')
311
+ continue;
312
+ throw error;
313
+ }
321
314
  let line;
322
315
  try {
323
316
  line = readFileSync(join(source, TEMP_TERMINATION_FILE), 'utf8')
@@ -393,11 +386,13 @@ export async function tempSupervisorLiveness(dir, deps = {}) {
393
386
  // A concurrent spawn writes the target before systemd-run/launchctl has
394
387
  // registered it. A not-yet-created unit looks exactly like an inactive one,
395
388
  // so launch phase plus its bounded grace is not cleanup authority.
396
- if (record.phase === 'launching'
389
+ if (record.phase === 'launching' && !requestedTempStopReason(dir)
397
390
  && (!Number.isFinite(age) || age < TEMP_LAUNCH_GRACE_MS))
398
391
  return 'unknown';
399
392
  // Older writers could lose `kind` in an unlocked metadata RMW while retaining
400
393
  // the supervisor pid. Exact argv ownership is stronger than the missing tag.
394
+ if (record.kind === 'fleet-managed')
395
+ return (await catalogLiveness(record.target ?? memberKey(record.role, record.taskOwner ? 'task' : 'temporary'), exec)).state;
401
396
  if (record.pid && (!record.kind || record.kind === 'detached'))
402
397
  return detachedProcessLiveness(record, deps);
403
398
  if (record.kind === 'systemd-transient' && record.target) {
@@ -461,6 +456,13 @@ export async function stopTempSupervisor(role, deps = {}) {
461
456
  replaceFileAtomically(join(dir, TEMP_STOP_REQUEST_FILE), JSON.stringify({
462
457
  version: 1, role, reason: 'operator-stop', requestedAt: new Date().toISOString(),
463
458
  }) + '\n');
459
+ if (record.kind === 'fleet-managed') {
460
+ if (record.taskOwner)
461
+ await unregisterTaskMember(role, { ...record.taskOwner, launchId: record.launchId }, { exec, sleep: deps.sleep });
462
+ else
463
+ await unregisterMember(record.target ?? memberKey(role, 'temporary'), { exec, sleep: deps.sleep });
464
+ return 'stopped';
465
+ }
464
466
  if (record.kind === 'systemd-transient' && record.target) {
465
467
  const result = await exec('systemctl', ['--user', 'stop', record.target]);
466
468
  if (result.code !== 0
@@ -511,6 +513,26 @@ export async function stopTempSupervisor(role, deps = {}) {
511
513
  (deps.kill ?? process.kill)(record.pid, 'SIGTERM');
512
514
  return 'stopped';
513
515
  }
516
+ /** Legacy task snapshots remain recovery evidence even before explicit migration. */
517
+ function hasRetainedTaskReference(dir) {
518
+ try {
519
+ if (basename(dir).startsWith('layout-')) {
520
+ const provenance = JSON.parse(readFileSync(join(dir, 'creation.json'), 'utf8'));
521
+ if (typeof provenance.creationActionId === 'string' && provenance.creationActionId.startsWith('task-'))
522
+ return true;
523
+ }
524
+ const startup = parse(readFileSync(join(dir, 'role.yaml'), 'utf8'))?.roomMemberStartup;
525
+ if (!startup?.room_id || !/^[A-Za-z0-9_-]{1,120}$/.test(startup.room_id))
526
+ return false;
527
+ const room = JSON.parse(readFileSync(join(stateRoot(), 'rooms', `${startup.room_id}.json`), 'utf8'));
528
+ return typeof room.task_id === 'string'
529
+ && room.member_seats.some((seat) => seat.role_name === basename(dir));
530
+ }
531
+ catch (error) {
532
+ // An unreadable role/room is uncertain ownership, never permission to reclaim.
533
+ return error.code !== 'ENOENT';
534
+ }
535
+ }
514
536
  /**
515
537
  * Move a bounded batch of definitely-dead temp state into the evidence archive.
516
538
  * Unknown/legacy/live entries are preserved; absence of proof is never cleanup authority.
@@ -532,8 +554,18 @@ export async function reclaimStaleTempState(deps = {}) {
532
554
  const archived = [...recovered];
533
555
  for (const entry of entries) {
534
556
  const dir = join(tmpRoot(), entry.name);
535
- if (!readTempSupervisor(dir))
536
- continue; // legacy evidence has no safe ownership proof
557
+ const supervisor = readTempSupervisorForScan(dir);
558
+ if (!supervisor || supervisor.taskOwner || hasRetainedTaskReference(dir))
559
+ continue;
560
+ if (supervisor?.kind === 'fleet-managed' && supervisor.target) {
561
+ try {
562
+ if (readMember(supervisor.target)?.desired === 'running')
563
+ continue;
564
+ }
565
+ catch {
566
+ continue;
567
+ } // uncertain catalog intent is never reclamation authority
568
+ }
537
569
  if (await tempSupervisorLiveness(dir, deps) !== 'stopped')
538
570
  continue;
539
571
  const target = archiveTempState(entry.name, 'stale-supervisor', 'reclaimed', 'supervisor is definitively stopped; state moved from the live roster without deletion', now);
@@ -1,3 +1,3 @@
1
1
  export declare const TEMP_RECYCLE_EXIT = 75;
2
2
  /** Keep the service's main process alive while replacing a PID-scoped worker. */
3
- export declare function runTempSupervisor(name: string, entrypoint: string, initial?: () => Promise<void>): Promise<void>;
3
+ export declare function runTempSupervisor(name: string, entrypoint: string, initial?: () => Promise<void>, suspendOnStop?: boolean): Promise<void>;
@@ -2,7 +2,7 @@ import { spawn } from 'node:child_process';
2
2
  import { runTemp, SupervisorRecycleRequiredError } from './runner.js';
3
3
  export const TEMP_RECYCLE_EXIT = 75;
4
4
  /** Keep the service's main process alive while replacing a PID-scoped worker. */
5
- export async function runTempSupervisor(name, entrypoint, initial = () => runTemp(name)) {
5
+ export async function runTempSupervisor(name, entrypoint, initial = () => runTemp(name), suspendOnStop = false) {
6
6
  try {
7
7
  await initial();
8
8
  return;
@@ -12,7 +12,7 @@ export async function runTempSupervisor(name, entrypoint, initial = () => runTem
12
12
  throw error;
13
13
  }
14
14
  for (let attempt = 0; attempt < 3; attempt++) {
15
- const child = spawn(process.execPath, [entrypoint, '_run-temp-worker', name], {
15
+ const child = spawn(process.execPath, [entrypoint, '_run-temp-worker', name, ...(suspendOnStop ? ['--suspend-on-stop'] : [])], {
16
16
  stdio: 'inherit',
17
17
  env: process.env,
18
18
  });
@@ -0,0 +1,54 @@
1
+ # Upgrade to one Fleet service
2
+
3
+ Permanent and task agents share one systemd user service on Linux or one
4
+ LaunchAgent on macOS. Install the package and explicitly run `ours-fleet init`;
5
+ package installation alone does not migrate services. New task/temporary launches
6
+ refuse `FLEET_SERVICE_NOT_INSTALLED` until init succeeds. Native supervision uses
7
+ the OS user's home; a custom `OURS_FLEET_HOME` is refused. Manual/isolated fleets
8
+ can use `OURS_FLEET_SUPERVISOR=none` without boot recovery.
9
+
10
+ Before init, back up Fleet configuration, private member/runtime/session state,
11
+ task/room records and native units/plists. Preserve operator readiness commands,
12
+ timeout/restart settings and environment in the common parent's explicit
13
+ systemd drop-ins (`~/.config/systemd/user/ours-fleet.service.d/`). Conflicting
14
+ per-agent settings need operator resolution. Migration does not copy scripts or
15
+ silently discard hooks. Resolve `OPERATOR_DROPIN_REQUIRED` or `UNSUPPORTED_DROPIN`,
16
+ reload the user manager, verify its effective settings, then retry.
17
+
18
+ Init preflights configured permanent agents, retires only their proven old native
19
+ services and registers them after exact old-process absence. `up NAME` migrates
20
+ only NAME. Existing identity and conversation files remain in place. On Linux,
21
+ enabled legacy roles transfer running boot intent, including enabled inactive
22
+ roles; disabled inactive roles remain untouched. macOS stopped roles stay stopped.
23
+ Interrupted transfers retain receipts; retry with the original state instead of
24
+ deleting proof or starting old and new supervisors together.
25
+
26
+ The parent starts existing agent runners. Adding a member does not restart it.
27
+ Restarting the parent affects every managed agent. Permanent agents resume their
28
+ conversation with their existing readiness/continuity prompts and may receive a
29
+ new private runtime instance after orderly release. Task agents retain their
30
+ original identity, runtime owner and conversation. Managed stop persists across
31
+ parent restart/reboot. Non-resumable temporary agents retire on parent shutdown.
32
+
33
+ ## Existing tasks
34
+
35
+ Running pre-upgrade transient task members remain untouched and report
36
+ `migration_pending`; init does not make them durable. Clean legacy shutdown or
37
+ explicit temporary stop releases/archives them. After abrupt process loss,
38
+ `ours-fleet task recover-members TASK_ID` can adopt a stopped single-room member
39
+ only with its original owner, identity, launch, workspace, conversation and room
40
+ admission intact and exact process absence proven. Missing/mismatched evidence
41
+ and unmarked legacy layouts require reconciliation. There is no automatic live
42
+ handover: let old work retire normally, or preserve and inspect its original
43
+ state before a controlled transition. Do not create replacement identities,
44
+ redeem invitations again or fabricate ownership records to force recovery.
45
+
46
+ ## Rollback
47
+
48
+ Stop the common parent and prove all managed process groups absent. Preserve an
49
+ offline backup of catalog, receipts, private state and operator drop-ins. Restore
50
+ the prior Fleet release and reviewed legacy units/plists, enabling only intended
51
+ running agents. Keep the common parent disabled while legacy supervision runs.
52
+ Re-entering central mode requires retained-proof reconciliation and old-process
53
+ absence. Physical reboot/boot ordering, real macOS launchd and live host migration
54
+ have not been qualified by the isolated process and injected native tests.
@@ -159,3 +159,29 @@ claiming complete erasure. An identity with no recoverable authenticated binding
159
159
  requires restoring its actual ownership evidence; a name-only force removal is
160
160
  not supported. Room deletion removes the linked Task's dead room/member links and preserves its workspace; use explicit
161
161
  Task deletion when the request includes the associated Task and its artifacts.
162
+
163
+
164
+ ## Retained task agents
165
+
166
+ Task agents use the common Fleet service and existing runners while remaining
167
+ outside the standalone agent roster. Recovery retains their original identity,
168
+ private runtime owner, launch, working directory, admitted room and supported
169
+ backend conversation. `ours-fleet task recover-members TASK_ID` resumes proven
170
+ stopped members or resets their held restart circuit; it never creates identities
171
+ or invitations. See [upgrade and rollback](fleet-migration.md) for legacy tasks.
172
+
173
+ Recovery delivers an undispatched assignment once, continues interrupted work,
174
+ and sends no readiness/work prompt to a completed idle conversation. An uncertain
175
+ dispatch gets a continuation instead of replaying the assignment. First-session
176
+ retry requires proof that no readiness/work was submitted; established session
177
+ loss requires restoration of the original context. Task agents require a resumable
178
+ harness. Missing ownership, workspace or room evidence keeps recovery held.
179
+
180
+ Parent shutdown suspends task agents. Explicit member stop terminally releases
181
+ the exact saved owner and archives its launch; it does not complete the task.
182
+ Finish/cancel/review retain their existing semantics. Task deletion stops and
183
+ unregisters only owned members, verifies identity absence and retries cleanup
184
+ before erasing task/room/workspace resources. Permanent siblings remain intact.
185
+ Standalone CLI/web removal refuses `TASK_MEMBER_IS_TASK_OWNED`; use the owning
186
+ task lifecycle. Lost/corrupt task or launch records require restoring original
187
+ backup evidence or explicit ownership reconciliation before deletion can finish.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ours.network/fleet",
3
- "version": "1.2.0-nightly.54",
3
+ "version": "1.2.0-nightly.56",
4
4
  "description": "Harness-agnostic fleet of persistent, identity-bound AI agents. Declarative fleet.yaml, managed native/ACP sessions, supervision, and ours.network messaging.",
5
5
  "type": "module",
6
6
  "license": "FSL-1.1-Apache-2.0",
@@ -22,6 +22,7 @@
22
22
  "docs/live-voice.md",
23
23
  "docs/room-layouts.md",
24
24
  "docs/validation/managed-cli-permissions.md",
25
+ "docs/fleet-migration.md",
25
26
  "LICENSE",
26
27
  "docs/acp-files.md"
27
28
  ],
@@ -38,6 +39,8 @@
38
39
  "test:pack": "vitest run --config vitest.integration.config.ts",
39
40
  "prepack": "npm run build",
40
41
  "prepublishOnly": "npm run build && npm test",
42
+ "test:task-recovery": "node test/task-runtime-recovery.integration.mjs",
43
+ "test:fleet-recovery": "node test/fleet-manager-recovery.integration.mjs",
41
44
  "test:v1-recovery": "node test/fleet-v1-recovery.integration.mjs retry && node test/fleet-v1-recovery.integration.mjs terminal-after-failure && node test/fleet-v1-recovery.integration.mjs terminal-release-failure"
42
45
  },
43
46
  "dependencies": {