@ours.network/fleet 1.2.0-nightly.54 → 1.2.0-nightly.55
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.
- package/README.md +20 -12
- package/dist/agent-ours/service.d.ts +4 -1
- package/dist/agent-ours/service.js +31 -7
- package/dist/application/log-service.js +8 -8
- package/dist/application/role-creation-service.js +2 -1
- package/dist/application/role-removal-service.js +7 -1
- package/dist/application/role-repository.js +17 -0
- package/dist/application/task-layouts.js +12 -2
- package/dist/application/task-room-service.js +19 -17
- package/dist/application/types.d.ts +2 -0
- package/dist/build-info.json +4 -4
- package/dist/cli.js +31 -3
- package/dist/config.d.ts +2 -0
- package/dist/harness/agent-session.d.ts +2 -0
- package/dist/harness/claude-code-session.js +1 -1
- package/dist/harness/codex-session.js +2 -2
- package/dist/managed-recovery.d.ts +13 -0
- package/dist/managed-recovery.js +31 -0
- package/dist/ops.d.ts +2 -0
- package/dist/ops.js +15 -2
- package/dist/rooms-tasks/cli.js +21 -0
- package/dist/rooms-tasks/close.d.ts +2 -0
- package/dist/rooms-tasks/close.js +66 -1
- package/dist/rooms-tasks/deletion.js +10 -0
- package/dist/rooms-tasks/erasure.js +4 -2
- package/dist/rooms-tasks/layout-member-retirement.d.ts +10 -0
- package/dist/rooms-tasks/layout-member-retirement.js +90 -0
- package/dist/rooms-tasks/layout-service.d.ts +1 -0
- package/dist/rooms-tasks/layout-service.js +31 -13
- package/dist/rooms-tasks/layout.d.ts +7 -0
- package/dist/rooms-tasks/layout.js +35 -0
- package/dist/rooms-tasks/legacy-task-member.d.ts +19 -0
- package/dist/rooms-tasks/legacy-task-member.js +116 -0
- package/dist/rooms-tasks/member-ownership.d.ts +5 -0
- package/dist/rooms-tasks/member-ownership.js +40 -0
- package/dist/rooms-tasks/provision.js +26 -1
- package/dist/rooms-tasks/recovery.d.ts +19 -0
- package/dist/rooms-tasks/recovery.js +110 -0
- package/dist/rooms-tasks/task-service-retirement.d.ts +11 -0
- package/dist/rooms-tasks/task-service-retirement.js +21 -0
- package/dist/rooms-tasks/task-state.d.ts +1 -0
- package/dist/rooms-tasks/task-state.js +16 -1
- package/dist/rooms-tasks/types.d.ts +3 -0
- package/dist/rooms-tasks/workspace.d.ts +2 -0
- package/dist/rooms-tasks/workspace.js +5 -0
- package/dist/runner.d.ts +6 -0
- package/dist/runner.js +160 -22
- package/dist/session/acp.d.ts +1 -0
- package/dist/session/acp.js +7 -0
- package/dist/session/arbiter.d.ts +5 -0
- package/dist/session/arbiter.js +16 -2
- package/dist/session/codex-app-server.d.ts +1 -0
- package/dist/session/codex-app-server.js +5 -0
- package/dist/session/types.d.ts +2 -0
- package/dist/spawn.d.ts +5 -1
- package/dist/spawn.js +32 -3
- package/dist/startup-readiness.d.ts +1 -1
- package/dist/startup-readiness.js +3 -3
- package/dist/supervisor/adoption.d.ts +8 -0
- package/dist/supervisor/adoption.js +78 -0
- package/dist/supervisor/catalog.d.ts +62 -0
- package/dist/supervisor/catalog.js +254 -0
- package/dist/supervisor/fleet.d.ts +8 -0
- package/dist/supervisor/fleet.js +89 -0
- package/dist/supervisor/index.js +3 -6
- package/dist/supervisor/launchd.d.ts +1 -1
- package/dist/supervisor/launchd.js +24 -12
- package/dist/supervisor/legacy.d.ts +35 -0
- package/dist/supervisor/legacy.js +524 -0
- package/dist/supervisor/manager.d.ts +15 -0
- package/dist/supervisor/manager.js +258 -0
- package/dist/supervisor/scope.d.ts +4 -0
- package/dist/supervisor/scope.js +10 -0
- package/dist/supervisor/systemd.d.ts +2 -2
- package/dist/supervisor/systemd.js +14 -14
- package/dist/task-supervision.d.ts +9 -0
- package/dist/task-supervision.js +145 -0
- package/dist/temp-lifecycle.d.ts +17 -7
- package/dist/temp-lifecycle.js +85 -53
- package/dist/temp-supervisor-recovery.d.ts +1 -1
- package/dist/temp-supervisor-recovery.js +2 -2
- package/docs/fleet-migration.md +54 -0
- package/docs/task-workspaces.md +26 -0
- package/package.json +4 -1
package/dist/temp-lifecycle.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
75
|
-
if (supervisor !== 'none'
|
|
76
|
-
const
|
|
77
|
-
await updateTempSupervisor(dir, { phase: 'launching', kind: '
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
536
|
-
|
|
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
|
|
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.
|
package/docs/task-workspaces.md
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "1.2.0-nightly.55",
|
|
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": {
|