@ours.network/fleet 0.17.0 → 0.17.2
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 +38 -2
- package/dist/application/role-removal-service.js +1 -1
- package/dist/application/session-control.d.ts +14 -10
- package/dist/application/session-control.js +14 -3
- package/dist/atomic-file.d.ts +7 -1
- package/dist/atomic-file.js +33 -5
- package/dist/build-info.json +10 -0
- package/dist/capabilities.d.ts +20 -0
- package/dist/capabilities.js +21 -0
- package/dist/cli.js +98 -10
- package/dist/config.d.ts +9 -2
- package/dist/config.js +16 -2
- package/dist/creation.d.ts +16 -0
- package/dist/creation.js +28 -0
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +70 -4
- package/dist/doctor.d.ts +5 -0
- package/dist/doctor.js +87 -2
- package/dist/fleet-proxy.js +2 -2
- package/dist/harness/acp-agent.d.ts +3 -0
- package/dist/harness/acp-agent.js +4 -1
- package/dist/harness/codex-app-server-proxy.d.ts +4 -0
- package/dist/harness/codex-app-server-proxy.js +133 -0
- package/dist/harness/codex.js +116 -11
- package/dist/harness/types.d.ts +2 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/loops/manager.d.ts +42 -1
- package/dist/loops/manager.js +115 -16
- package/dist/loops/state.d.ts +46 -2
- package/dist/loops/state.js +81 -3
- package/dist/monitor.d.ts +21 -0
- package/dist/monitor.js +42 -0
- package/dist/ops.d.ts +6 -0
- package/dist/ops.js +46 -1
- package/dist/owner-channel/channel.d.ts +18 -2
- package/dist/owner-channel/channel.js +146 -2
- package/dist/owner-channel/commands.d.ts +2 -2
- package/dist/owner-channel/commands.js +7 -2
- package/dist/owner-channel/notices.d.ts +2 -0
- package/dist/owner-channel/notices.js +3 -0
- package/dist/permissions.d.ts +2 -0
- package/dist/permissions.js +5 -0
- package/dist/provenance.d.ts +77 -0
- package/dist/provenance.js +283 -0
- package/dist/runner.d.ts +7 -1
- package/dist/runner.js +100 -14
- package/dist/session/acp.d.ts +40 -4
- package/dist/session/acp.js +272 -37
- package/dist/session/arbiter.d.ts +28 -2
- package/dist/session/arbiter.js +75 -4
- package/dist/session/control.js +12 -6
- package/dist/session/event-log.d.ts +109 -0
- package/dist/session/event-log.js +247 -0
- package/dist/session/events.d.ts +21 -0
- package/dist/session/events.js +105 -26
- package/dist/session/tmux.d.ts +3 -2
- package/dist/session/tmux.js +2 -0
- package/dist/session/types.d.ts +39 -2
- package/dist/session/types.js +11 -1
- package/dist/spawn.d.ts +3 -3
- package/dist/spawn.js +40 -14
- package/dist/temp-lifecycle.d.ts +62 -0
- package/dist/temp-lifecycle.js +437 -0
- package/package.json +5 -3
package/dist/session/types.d.ts
CHANGED
|
@@ -62,10 +62,38 @@ export interface TurnResult {
|
|
|
62
62
|
* session is gone, and `timeout` explicitly does NOT say the prompt was lost.
|
|
63
63
|
*/
|
|
64
64
|
export type ControlFailureKind = 'offline' | 'control-unavailable' | 'timeout' | 'rejected' | 'backend';
|
|
65
|
+
/** Stable body-free reason shared by ACP recovery and durable ingress. */
|
|
66
|
+
export declare const ACP_CANCEL_DEADLINE_EXCEEDED = "ACP_CANCEL_DEADLINE_EXCEEDED";
|
|
65
67
|
export declare class SessionControlError extends Error {
|
|
66
68
|
readonly kind: ControlFailureKind;
|
|
67
|
-
|
|
69
|
+
/** Stable body-free machine reason for recovery/audit decisions. */
|
|
70
|
+
readonly reasonCode?: string | undefined;
|
|
71
|
+
constructor(kind: ControlFailureKind, message: string,
|
|
72
|
+
/** Stable body-free machine reason for recovery/audit decisions. */
|
|
73
|
+
reasonCode?: string | undefined);
|
|
68
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* How an explicit cancellation ended. Forced recovery is a SUCCESS: the turn is
|
|
77
|
+
* over and the session is being reclaimed. Reporting it as a failed interrupt is
|
|
78
|
+
* what made owners, the control plane and the web console retry an operation
|
|
79
|
+
* that had already done exactly what was asked.
|
|
80
|
+
*/
|
|
81
|
+
export interface InterruptOutcome {
|
|
82
|
+
/** `settled` — the turn (or nothing) ended cooperatively. `forced` — the adapter ignored the cancel and was restarted. */
|
|
83
|
+
state: 'settled' | 'forced';
|
|
84
|
+
/** Stable body-free reason present only for a forced recovery. */
|
|
85
|
+
reasonCode?: string;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* What a `SessionHandle.interrupt` implementation may resolve. Before 0.17.1 the
|
|
89
|
+
* contract was `Promise<void>`, and resolving at all meant the cancellation had
|
|
90
|
+
* taken effect cooperatively — so an implementation written against that
|
|
91
|
+
* contract stays valid and keeps its exact meaning. Only in-tree consumers read
|
|
92
|
+
* the richer outcome, and they normalize through `interruptOutcome` first.
|
|
93
|
+
*/
|
|
94
|
+
export type InterruptResult = InterruptOutcome | void;
|
|
95
|
+
/** The one place the legacy `void` reply is given its meaning: `settled`. */
|
|
96
|
+
export declare function interruptOutcome(result: InterruptResult): InterruptOutcome;
|
|
69
97
|
/**
|
|
70
98
|
* A prompt the live session has taken responsibility for. Interactive callers
|
|
71
99
|
* stop here: the session has the prompt, and waiting for the turn to finish is
|
|
@@ -218,7 +246,16 @@ export interface SessionHandle {
|
|
|
218
246
|
submitPrompt(text: string, options?: SubmitPromptOptions): Promise<TurnResult>;
|
|
219
247
|
/** Monitor-only ACP safe-boundary delivery. Never implies human/control cancellation. */
|
|
220
248
|
submitPromptAfterTool?(text: string, options?: SubmitPromptOptions): Promise<TurnResult>;
|
|
221
|
-
|
|
249
|
+
/**
|
|
250
|
+
* Cancel the active turn. Resolves when the cancellation has taken effect —
|
|
251
|
+
* cooperatively (`settled`) or through bounded forced recovery (`forced`).
|
|
252
|
+
* It rejects only when the cancellation itself could not be delivered.
|
|
253
|
+
*
|
|
254
|
+
* The return type is widened to `InterruptResult` for one reason: an
|
|
255
|
+
* implementation written against the pre-0.17.1 `Promise<void>` contract must
|
|
256
|
+
* keep compiling. Read it through `interruptOutcome`, never directly.
|
|
257
|
+
*/
|
|
258
|
+
interrupt(source?: TurnCancellationSource): Promise<InterruptResult>;
|
|
222
259
|
respondPermission(permissionId: string, optionId: string): boolean;
|
|
223
260
|
/** Generation-bound browser decision; stale/settled/invalid all fail closed. */
|
|
224
261
|
respondPermissionV2?(permissionId: string, optionId: string, sessionGeneration: string): 'accepted' | 'stale';
|
package/dist/session/types.js
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
|
+
/** Stable body-free reason shared by ACP recovery and durable ingress. */
|
|
2
|
+
export const ACP_CANCEL_DEADLINE_EXCEEDED = 'ACP_CANCEL_DEADLINE_EXCEEDED';
|
|
1
3
|
export class SessionControlError extends Error {
|
|
2
4
|
kind;
|
|
3
|
-
|
|
5
|
+
reasonCode;
|
|
6
|
+
constructor(kind, message,
|
|
7
|
+
/** Stable body-free machine reason for recovery/audit decisions. */
|
|
8
|
+
reasonCode) {
|
|
4
9
|
super(message);
|
|
5
10
|
this.kind = kind;
|
|
11
|
+
this.reasonCode = reasonCode;
|
|
6
12
|
this.name = 'SessionControlError';
|
|
7
13
|
}
|
|
8
14
|
}
|
|
15
|
+
/** The one place the legacy `void` reply is given its meaning: `settled`. */
|
|
16
|
+
export function interruptOutcome(result) {
|
|
17
|
+
return result ?? { state: 'settled' };
|
|
18
|
+
}
|
|
9
19
|
/**
|
|
10
20
|
* Classify a shell `$?`. Above 128 the shell is reporting 128+signal — the only
|
|
11
21
|
* signal evidence a pane wrapper can give us.
|
package/dist/spawn.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { type OpsDeps } from './ops.js';
|
|
|
4
4
|
import { type CreationDeps, type CreationProvenance } from './creation.js';
|
|
5
5
|
import './harness/claude-code.js';
|
|
6
6
|
import './harness/codex.js';
|
|
7
|
+
import { type SupervisorLauncher } from './temp-lifecycle.js';
|
|
7
8
|
/**
|
|
8
9
|
* The provenance record written by the most recent spawn in this process, so
|
|
9
10
|
* the CLI can print the same summary it persisted rather than rebuilding it.
|
|
@@ -94,7 +95,6 @@ export interface SpawnDryRun {
|
|
|
94
95
|
export declare function spawnDryRun(o: SpawnOpts): SpawnDryRun;
|
|
95
96
|
/** Permanent spawn: persist to ~/fleet.d/<Name>.yaml, then bring it up. */
|
|
96
97
|
export declare function spawnPermanent(o: SpawnOpts, deps: OpsDeps, creation?: CreationDeps): Promise<string>;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
/** Temp spawn: state under ~/.ours-fleet/tmp, plain tmux, auto-clean on exit. */
|
|
98
|
+
export type { SupervisorLauncher } from './temp-lifecycle.js';
|
|
99
|
+
/** Temp spawn: live state under ~/.ours-fleet/tmp, independent transient supervision. */
|
|
100
100
|
export declare function spawnTemp(o: SpawnOpts, binPath: string, launch?: SupervisorLauncher, creation?: CreationDeps): Promise<string>;
|
package/dist/spawn.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn as spawnChild } from 'node:child_process';
|
|
2
|
-
import { existsSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { parse, stringify } from 'yaml';
|
|
5
5
|
import { agentDir, fleetDDir } from './paths.js';
|
|
@@ -12,6 +12,7 @@ import { VERSION } from './version.js';
|
|
|
12
12
|
import './harness/claude-code.js';
|
|
13
13
|
import './harness/codex.js';
|
|
14
14
|
import { getAdapter } from './harness/registry.js';
|
|
15
|
+
import { archiveTempState, makeTempSupervisorLauncher, prepareTempSupervisor, reclaimStaleTempState, } from './temp-lifecycle.js';
|
|
15
16
|
/**
|
|
16
17
|
* The provenance record written by the most recent spawn in this process, so
|
|
17
18
|
* the CLI can print the same summary it persisted rather than rebuilding it.
|
|
@@ -341,22 +342,37 @@ export async function spawnPermanent(o, deps, creation = {}) {
|
|
|
341
342
|
return file;
|
|
342
343
|
}, creation);
|
|
343
344
|
}
|
|
344
|
-
|
|
345
|
-
|
|
345
|
+
/** Fallback used only when service-manager supervision is explicitly disabled. */
|
|
346
|
+
const spawnDetached = (binPath, args, dir) => {
|
|
347
|
+
// Log to the temp dir; the child fd stays valid when retirement moves the
|
|
348
|
+
// directory into the evidence archive.
|
|
346
349
|
const out = openSync(join(dir, 'supervisor.log'), 'a');
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
350
|
+
let child;
|
|
351
|
+
try {
|
|
352
|
+
child = spawnChild(process.execPath, [binPath, ...args], {
|
|
353
|
+
detached: true,
|
|
354
|
+
stdio: ['ignore', out, out],
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
finally {
|
|
358
|
+
closeSync(out);
|
|
359
|
+
}
|
|
351
360
|
child.unref();
|
|
361
|
+
if (!child.pid)
|
|
362
|
+
throw new Error('detached temporary supervisor did not report a pid');
|
|
363
|
+
return child.pid;
|
|
352
364
|
};
|
|
353
|
-
|
|
354
|
-
|
|
365
|
+
const independentSupervisor = makeTempSupervisorLauncher({ spawnDetached });
|
|
366
|
+
/** Temp spawn: live state under ~/.ours-fleet/tmp, independent transient supervision. */
|
|
367
|
+
export async function spawnTemp(o, binPath, launch = independentSupervisor, creation = {}) {
|
|
355
368
|
validateSpawnOpts(o);
|
|
356
369
|
if (o.isolationFile)
|
|
357
370
|
readIsolationFile(o.isolationFile); // fail before reserving
|
|
358
371
|
if (o.missionFile)
|
|
359
372
|
readMissionFile(o.missionFile); // fail before reserving
|
|
373
|
+
// Retire only supervisors whose recorded owner is definitively stopped. This
|
|
374
|
+
// bounded pass keeps the active roster clean without deleting old evidence.
|
|
375
|
+
await reclaimStaleTempState();
|
|
360
376
|
// Temporary roles go through the SAME reservation boundary as permanent ones
|
|
361
377
|
// (6.4): a temp agent competes for the same names.
|
|
362
378
|
creation.onStage?.('reserving');
|
|
@@ -425,19 +441,29 @@ async function spawnTempInner(o, binPath, launch, tx, guarantee, onStage) {
|
|
|
425
441
|
});
|
|
426
442
|
writeProvenance(dir, provenance);
|
|
427
443
|
lastProvenance = provenance;
|
|
428
|
-
tx.record({
|
|
444
|
+
tx.record({
|
|
445
|
+
stage: `temp state dir ${dir}`,
|
|
446
|
+
undo: () => {
|
|
447
|
+
// A failed launch is still lifecycle evidence: briefing, provenance,
|
|
448
|
+
// metadata and supervisor output explain what happened. Remove it from
|
|
449
|
+
// the live roster by atomic archive, never recursive deletion.
|
|
450
|
+
archiveTempState(o.name, 'startup-failure', 'failed', 'temporary creation rolled back after launch/setup failure; evidence preserved');
|
|
451
|
+
},
|
|
452
|
+
});
|
|
429
453
|
writeFileSync(join(dir, 'role.yaml'), stringify(role));
|
|
430
454
|
// Snapshot the fleet start-stagger so the detached temp supervisor (no config path
|
|
431
455
|
// threaded through it) honors the same launch gate — a burst of temp spawns spaces
|
|
432
456
|
// out; a lone temp spawn still waits zero (time-based gate).
|
|
433
457
|
if (cfg.startStaggerMs > 0)
|
|
434
458
|
writeFileSync(join(dir, START_STAGGER_FILE), String(cfg.startStaggerMs));
|
|
435
|
-
|
|
459
|
+
prepareTempSupervisor(dir, o.name);
|
|
460
|
+
// Run the supervisor independently — NOT inside a tmux session named <name>.
|
|
436
461
|
// `_run-temp` -> runOnce() creates AND kills the tmux session <name> for the
|
|
437
462
|
// agent itself; a supervisor sharing that session name would SIGHUP its own
|
|
438
|
-
// process before the agent ever launches.
|
|
439
|
-
//
|
|
463
|
+
// process before the agent ever launches. On a service-managed host the temp
|
|
464
|
+
// runner gets its own transient unit/job, so stopping the coordinator's unit
|
|
465
|
+
// cannot kill a live worker in the coordinator's cgroup.
|
|
440
466
|
onStage?.('starting_temp');
|
|
441
|
-
launch(binPath, ['_run-temp', o.name], dir);
|
|
467
|
+
await launch(binPath, ['_run-temp', o.name], dir);
|
|
442
468
|
return dir;
|
|
443
469
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type Exec } from './exec.js';
|
|
2
|
+
export declare const TEMP_SUPERVISOR_FILE = ".temp-supervisor.json";
|
|
3
|
+
export declare const TEMP_TERMINATION_FILE = "termination.jsonl";
|
|
4
|
+
export declare const TEMP_STOP_REQUEST_FILE = ".temp-stop-request.json";
|
|
5
|
+
export declare const TEMP_RECLAIM_BATCH = 32;
|
|
6
|
+
export declare const TEMP_LAUNCH_GRACE_MS = 60000;
|
|
7
|
+
export type TempSupervisorKind = 'systemd-transient' | 'launchd-transient' | 'detached';
|
|
8
|
+
export type TempTerminationReason = 'identity-closed' | 'session-ended' | 'operator-stop' | 'supervisor-signal' | 'startup-failure' | 'stale-supervisor';
|
|
9
|
+
export interface TempSupervisorRecord {
|
|
10
|
+
version: 1;
|
|
11
|
+
role: string;
|
|
12
|
+
launchId: string;
|
|
13
|
+
createdAt: string;
|
|
14
|
+
phase: 'launching' | 'active';
|
|
15
|
+
kind?: TempSupervisorKind;
|
|
16
|
+
target?: string;
|
|
17
|
+
pid?: number;
|
|
18
|
+
binPath?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface TempTerminationRecord {
|
|
21
|
+
version: 1;
|
|
22
|
+
role: string;
|
|
23
|
+
launchId?: string;
|
|
24
|
+
at: string;
|
|
25
|
+
reason: TempTerminationReason;
|
|
26
|
+
outcome: 'retired' | 'reclaimed' | 'failed';
|
|
27
|
+
detail: string;
|
|
28
|
+
}
|
|
29
|
+
export type SupervisorLauncher = (binPath: string, args: string[], dir: string) => void | Promise<void>;
|
|
30
|
+
export declare const tempSystemdUnit: (name: string) => string;
|
|
31
|
+
export declare const tempLaunchdLabel: (name: string) => string;
|
|
32
|
+
export declare function prepareTempSupervisor(dir: string, role: string): TempSupervisorRecord;
|
|
33
|
+
export declare function readTempSupervisor(dir: string): TempSupervisorRecord | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* Launch a temp supervisor outside the caller's service-manager ownership
|
|
36
|
+
* boundary. `detached: true` creates a new process group but does not escape a
|
|
37
|
+
* systemd cgroup; a transient unit does, and is not enabled across reboot.
|
|
38
|
+
*/
|
|
39
|
+
export declare function makeTempSupervisorLauncher(options?: {
|
|
40
|
+
exec?: Exec;
|
|
41
|
+
platform?: NodeJS.Platform;
|
|
42
|
+
supervisor?: string;
|
|
43
|
+
spawnDetached?: (binPath: string, args: string[], dir: string) => number;
|
|
44
|
+
}): SupervisorLauncher;
|
|
45
|
+
export declare function markTempSupervisorActive(dir: string, pid?: number): Promise<void>;
|
|
46
|
+
export declare function requestedTempStopReason(dir: string): 'operator-stop' | undefined;
|
|
47
|
+
/** Move retired state out of the live roster without deleting any evidence. */
|
|
48
|
+
export declare function archiveTempState(role: string, reason: TempTerminationReason, outcome: TempTerminationRecord['outcome'], detail: string, now?: Date): string | undefined;
|
|
49
|
+
interface TempLifecycleDeps {
|
|
50
|
+
exec?: Exec;
|
|
51
|
+
now?(): number;
|
|
52
|
+
kill?(pid: number, signal: NodeJS.Signals | 0): void;
|
|
53
|
+
log?(line: string): void;
|
|
54
|
+
}
|
|
55
|
+
export declare function tempSupervisorLiveness(dir: string, deps?: TempLifecycleDeps): Promise<'running' | 'stopped' | 'unknown'>;
|
|
56
|
+
export declare function stopTempSupervisor(role: string, deps?: TempLifecycleDeps): Promise<'stopped' | 'already-stopped'>;
|
|
57
|
+
/**
|
|
58
|
+
* Move a bounded batch of definitely-dead temp state into the evidence archive.
|
|
59
|
+
* Unknown/legacy/live entries are preserved; absence of proof is never cleanup authority.
|
|
60
|
+
*/
|
|
61
|
+
export declare function reclaimStaleTempState(deps?: TempLifecycleDeps): Promise<string[]>;
|
|
62
|
+
export {};
|