@ours.network/fleet 1.1.0-nightly.5 → 1.1.0-nightly.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,17 @@
1
+ import type { ConversationEventV1 } from './session/conversation-types.js';
2
+ import type { AgentSession } from './session/types.js';
3
+ export interface AgentRecoveryEvidence {
4
+ ok: boolean;
5
+ reason: 'RECOVERY_TOOLS_VERIFIED' | 'RECOVERY_PROMPT_MISSING' | 'RECOVERY_TURN_INCOMPLETE' | 'RECOVERY_TURN_FAILED' | 'RECOVERY_CHOOSE_MISSING' | 'RECOVERY_CURRENT_MISSING' | 'RECOVERY_GET_MESSAGES_MISSING' | 'RECOVERY_TOOL_ORDER_INVALID';
6
+ chooseIdentity: boolean;
7
+ currentIdentity: boolean;
8
+ getMessages: boolean;
9
+ turnCompleted: boolean;
10
+ }
11
+ /**
12
+ * Verify one exact recovery turn from its durable conversation ledger. Tool
13
+ * arguments/results are inspected only to derive these booleans and are never
14
+ * returned or persisted by this gate.
15
+ */
16
+ export declare function evaluateAgentRecovery(events: readonly ConversationEventV1[], promptId: string, identity: string): AgentRecoveryEvidence;
17
+ export declare function recoverAgentIdentity(session: AgentSession, identity: string): Promise<AgentRecoveryEvidence>;
@@ -0,0 +1,135 @@
1
+ const TOOL_NAMES = {
2
+ choose: new Set(['choose_identity', 'ours.choose_identity', 'mcp__ours__choose_identity']),
3
+ current: new Set(['current_identity', 'ours.current_identity', 'mcp__ours__current_identity']),
4
+ messages: new Set(['get_messages', 'ours.get_messages', 'mcp__ours__get_messages']),
5
+ };
6
+ function recordFor(records, event) {
7
+ if (event.kind !== 'tool.upsert' || !event.toolCallId)
8
+ return undefined;
9
+ const payload = event.payload;
10
+ const previous = records.get(event.toolCallId) ?? {};
11
+ const next = {
12
+ ...previous,
13
+ ...(payload.title !== undefined ? { title: payload.title } : {}),
14
+ ...(payload.status !== undefined ? { status: payload.status } : {}),
15
+ ...(payload.rawInput !== undefined ? { rawInput: payload.rawInput } : {}),
16
+ ...(payload.status === 'completed' ? { completedSeq: event.seq } : {}),
17
+ };
18
+ records.set(event.toolCallId, next);
19
+ return next;
20
+ }
21
+ function objectInput(input) {
22
+ if (!input || input.truncated || input.redacted || !input.json
23
+ || typeof input.json !== 'object' || Array.isArray(input.json))
24
+ return undefined;
25
+ return input.json;
26
+ }
27
+ function safeChoose(record, identity) {
28
+ if (!record.title || !TOOL_NAMES.choose.has(record.title) || record.status !== 'completed')
29
+ return false;
30
+ const input = objectInput(record.rawInput);
31
+ if (!input)
32
+ return false;
33
+ const keys = Object.keys(input).sort();
34
+ if (keys.some(key => key !== 'force' && key !== 'name'))
35
+ return false;
36
+ return input.name === identity && (input.force === undefined || input.force === false);
37
+ }
38
+ function safeNoOrBoundedInput(record, names, allowed) {
39
+ if (!record.title || !names.has(record.title) || record.status !== 'completed')
40
+ return false;
41
+ if (!record.rawInput)
42
+ return allowed({});
43
+ const input = objectInput(record.rawInput);
44
+ return input !== undefined && allowed(input);
45
+ }
46
+ function safeCurrent(record) {
47
+ return safeNoOrBoundedInput(record, TOOL_NAMES.current, input => Object.keys(input).length === 0);
48
+ }
49
+ function safeMessages(record) {
50
+ return safeNoOrBoundedInput(record, TOOL_NAMES.messages, input => {
51
+ const keys = Object.keys(input);
52
+ if (keys.some(key => key !== 'limit'))
53
+ return false;
54
+ return input.limit === undefined
55
+ || (Number.isSafeInteger(input.limit) && input.limit >= 1 && input.limit <= 200);
56
+ });
57
+ }
58
+ /**
59
+ * Verify one exact recovery turn from its durable conversation ledger. Tool
60
+ * arguments/results are inspected only to derive these booleans and are never
61
+ * returned or persisted by this gate.
62
+ */
63
+ export function evaluateAgentRecovery(events, promptId, identity) {
64
+ const ordered = [...events].sort((a, b) => a.seq - b.seq);
65
+ const admitted = ordered.find(event => event.kind === 'prompt.admitted' && event.promptId === promptId);
66
+ const empty = (reason) => ({
67
+ ok: false, reason, chooseIdentity: false, currentIdentity: false,
68
+ getMessages: false, turnCompleted: false,
69
+ });
70
+ if (!admitted)
71
+ return empty('RECOVERY_PROMPT_MISSING');
72
+ const terminal = ordered.find(event => event.seq > admitted.seq
73
+ && event.kind === 'turn.completed'
74
+ && event.promptId === promptId
75
+ && event.sessionGeneration === admitted.sessionGeneration);
76
+ if (!terminal)
77
+ return empty('RECOVERY_TURN_INCOMPLETE');
78
+ const outcome = terminal.payload.outcome;
79
+ if (outcome !== 'completed')
80
+ return empty('RECOVERY_TURN_FAILED');
81
+ const records = new Map();
82
+ for (const event of ordered) {
83
+ if (event.seq <= admitted.seq || event.seq >= terminal.seq
84
+ || event.promptId !== promptId
85
+ || event.sessionGeneration !== admitted.sessionGeneration)
86
+ continue;
87
+ recordFor(records, event);
88
+ }
89
+ const values = [...records.values()];
90
+ const chooses = values.filter(record => safeChoose(record, identity));
91
+ const currents = values.filter(safeCurrent);
92
+ const messages = values.filter(safeMessages);
93
+ const chooseIdentity = chooses.length > 0;
94
+ const currentIdentity = currents.length > 0;
95
+ const getMessages = messages.length > 0;
96
+ const base = { chooseIdentity, currentIdentity, getMessages, turnCompleted: true };
97
+ if (!chooseIdentity)
98
+ return { ok: false, reason: 'RECOVERY_CHOOSE_MISSING', ...base };
99
+ if (!currentIdentity)
100
+ return { ok: false, reason: 'RECOVERY_CURRENT_MISSING', ...base };
101
+ if (!getMessages)
102
+ return { ok: false, reason: 'RECOVERY_GET_MESSAGES_MISSING', ...base };
103
+ const orderedChain = chooses.some(choose => currents.some(current => messages.some(message => choose.completedSeq !== undefined && current.completedSeq !== undefined
104
+ && message.completedSeq !== undefined
105
+ && choose.completedSeq < current.completedSeq
106
+ && current.completedSeq < message.completedSeq)));
107
+ if (!orderedChain)
108
+ return { ok: false, reason: 'RECOVERY_TOOL_ORDER_INVALID', ...base };
109
+ return { ok: true, reason: 'RECOVERY_TOOLS_VERIFIED', ...base };
110
+ }
111
+ export async function recoverAgentIdentity(session, identity) {
112
+ if (!session.subscribeConversation)
113
+ return {
114
+ ok: false, reason: 'RECOVERY_PROMPT_MISSING', chooseIdentity: false,
115
+ currentIdentity: false, getMessages: false, turnCompleted: false,
116
+ };
117
+ const events = [];
118
+ const unsubscribe = session.subscribeConversation(event => events.push(event));
119
+ try {
120
+ const queued = await session.queuePrompt([
121
+ '[fleet-recovery] The shared ours daemon restarted.',
122
+ `Call ours choose_identity with name ${JSON.stringify(identity)} and force false.`,
123
+ 'Then call current_identity, then get_messages. Complete all three in that order.',
124
+ 'Do not create/delete identities, force-bind, interrupt, or restart any service/session.',
125
+ ].join('\n'), { origin: { kind: 'fleet-monitor' } });
126
+ await queued.completion;
127
+ // Conversation publication is synchronous with terminal settlement in the
128
+ // in-tree ACP store; filtering by exact promptId/sessionGeneration remains
129
+ // the authority even if unrelated events arrived concurrently.
130
+ return evaluateAgentRecovery(events, queued.promptId, identity);
131
+ }
132
+ finally {
133
+ unsubscribe();
134
+ }
135
+ }
@@ -4,6 +4,7 @@ import { classifyActivity } from '../session/activity.js';
4
4
  import { controlRequest } from '../session/control.js';
5
5
  import { SessionControlError } from '../session/types.js';
6
6
  import { readExitRecord, readRestartLedger } from '../runner.js';
7
+ import { readDaemonRecoveryStatus } from '../daemon-recovery.js';
7
8
  import { roleCapabilities } from './capabilities.js';
8
9
  import { FleetError } from './errors.js';
9
10
  const clean = (value, max = 512) => value.replace(/[\0-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '').trim().slice(0, max);
@@ -46,7 +47,8 @@ function readIsolation(dir) {
46
47
  function sessionOverall(supervisor, session, restart, monitor, isolation, problems) {
47
48
  if (restart.circuit === 'open' || monitor.health === 'failed' || monitor.health === 'degraded'
48
49
  || isolation.degraded
49
- || problems.some(problem => problem.severity === 'error' || problem.source === 'watchdog')
50
+ || problems.some(problem => problem.severity === 'error'
51
+ || problem.source === 'watchdog' || problem.source === 'daemon-recovery')
50
52
  || session.readiness === 'failed')
51
53
  return 'attention';
52
54
  // A reachable agent session is the user's live interaction surface. Its
@@ -103,6 +105,16 @@ export class FleetQueryService {
103
105
  const lastExit = dir ? readExitRecord(join(dir, '.exit-status')) ?? undefined : undefined;
104
106
  const session = await this.session(role, dir, live.state);
105
107
  const problems = [...role.problems];
108
+ const recovery = dir ? readDaemonRecoveryStatus(dir) : undefined;
109
+ if (recovery && recovery.state !== 'recovered') {
110
+ const paths = ['agent', 'owner']
111
+ .filter(name => recovery.paths[name].state !== 'recovered')
112
+ .map(name => `${name}:${recovery.paths[name].state}`);
113
+ problems.push({
114
+ code: 'daemon_recovery', severity: 'warning', source: 'daemon-recovery',
115
+ detail: `${recovery.state}; ${paths.join(', ') || 'no degraded path'}; epoch ${recovery.epoch || 'unavailable'}`,
116
+ });
117
+ }
106
118
  if (live.state === 'running' && session.reachability !== 'online')
107
119
  problems.push({
108
120
  code: 'supervisor_session_disagreement', severity: 'warning',
@@ -61,6 +61,16 @@ export class RoleCreationService {
61
61
  const creationActionId = randomUUID();
62
62
  plan.options.creationActionId = creationActionId;
63
63
  const statePath = await this.launchSync(plan.options);
64
+ const selectionSummary = (kind) => {
65
+ const value = plan.options[kind];
66
+ const provenance = plan.inherited.includes(kind) ? 'inherited' : 'explicit';
67
+ if (!value)
68
+ return `unresolved (${provenance})`;
69
+ if ('ref' in value)
70
+ return `ref:${value.ref} (${provenance})`;
71
+ const fingerprint = createHash('sha256').update(JSON.stringify(value.inline)).digest('hex').slice(0, 16);
72
+ return `inline:sha256:${fingerprint} (${provenance})`;
73
+ };
64
74
  return {
65
75
  caller: plan.caller, role: plan.options.name,
66
76
  lifetime: plan.options.temp ? 'temporary' : 'permanent', statePath,
@@ -69,6 +79,7 @@ export class RoleCreationService {
69
79
  monitor: { mode: plan.preview.resolvedRole.monitor.mode, interrupt: plan.preview.resolvedRole.monitor.interrupt },
70
80
  permissionMode: effectivePermissionMode(plan.preview.resolvedRole), inherited: plan.inherited,
71
81
  creationActionId,
82
+ brainSummary: selectionSummary('brain'), roleSummary: selectionSummary('role'),
72
83
  };
73
84
  }
74
85
  launchSync(options, creation) {
package/dist/briefing.js CHANGED
@@ -172,12 +172,16 @@ export function generateBriefing(role, v, opts) {
172
172
  if (role.session === 'acp') {
173
173
  L.push('', '### Managed fleet commands');
174
174
  L.push('This ACP role has a supervisor-scoped ours-fleet proxy. Use the ordinary');
175
- L.push('`ours-fleet spawn` command; the CLI routes it through your live supervisor, which');
176
- L.push('records you as the caller and reports successful creation to your owner channel.');
175
+ L.push('`ours-fleet` command; the CLI opens an authenticated audit attempt with your live');
176
+ L.push('supervisor before parsing or side effects. Your existing OS sandbox remains the executor.');
177
+ L.push('Every allowed attempt writes a redacted raw invocation and correlated outcome to your');
178
+ L.push('Owner-visible channel; denied, invalid, failed, and read-only attempts are visible too.');
177
179
  L.push('A minimal call is `ours-fleet spawn DeveloperName --temp`.');
178
180
  L.push('For omitted settings, the supervisor inherits your canonical Brain and Role selections,');
179
181
  L.push('working directory, neutral permissions, coordinator, and fleet monitor policy. Every');
180
182
  L.push('explicit option wins; identity/session-local and secret material never inherit.');
183
+ L.push('Operator lifecycle/control and hidden worker commands are denied through this route.');
184
+ L.push('Invocation delivery failure denies execution; outcome uncertainty is reported without retrying effects.');
181
185
  L.push('This proxy is attribution and convenience, not a security boundary for unisolated roles.');
182
186
  }
183
187
  if (role.coordinator) {
@@ -1,9 +1,9 @@
1
1
  {
2
- "version": "1.1.0-nightly.5",
3
- "buildId": "7a0d408032b9",
4
- "commit": "f5fd6cbade0e4f19e4c65d76b4ac170388a13665",
2
+ "version": "1.1.0-nightly.7",
3
+ "buildId": "a09aa98e92aa",
4
+ "commit": "d79160ae66847305a109bed339d8c8830a20bba7",
5
5
  "dirty": true,
6
- "builtAt": "2026-08-30T19:55:15.112Z",
6
+ "builtAt": "2026-08-31T07:32:26.852Z",
7
7
  "capabilities": [
8
8
  "monitor.interrupt.after_tool"
9
9
  ]
package/dist/cli.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn as spawnChild } from 'node:child_process';
3
+ import { randomUUID } from 'node:crypto';
3
4
  import { existsSync, mkdirSync, readFileSync, readdirSync, statSync } from 'node:fs';
4
5
  import { realpathSync } from 'node:fs';
5
6
  import { join as joinPath, resolve as resolvePath } from 'node:path';
@@ -37,6 +38,7 @@ import { requestWebControl } from './web/control.js';
37
38
  import { WebServiceManager } from './web/service.js';
38
39
  import { WebAccessStore, passwordAccess, validatePublicOrigin } from './web/access.js';
39
40
  import { FLEET_PROXY_CALLER_ENV, FLEET_PROXY_STATE_DIR_ENV, } from './fleet-proxy.js';
41
+ import { beginFleetAuditCollection, consumeFleetAuditCollection, FleetCliExit, recordFleetAuditPresentation, recordFleetAuditResource, } from './fleet-command-audit.js';
40
42
  import './harness/claude-code.js'; // registers the claude-code adapter
41
43
  import './harness/codex.js'; // registers the codex adapter
42
44
  import { registerTemplateCommands, registerTaskCommands, registerRoomCommands } from './rooms-tasks/cli.js';
@@ -66,7 +68,10 @@ const roleLifecycle = (configPath, operationDeps) => {
66
68
  return new RoleLifecycleService({ repository, ops: operationDeps, configPath,
67
69
  status: async (roleId) => (await query.detail(roleId)).status });
68
70
  };
69
- const die = (e) => { console.error(String(e instanceof Error ? e.message : e)); process.exit(1); };
71
+ const die = (e) => {
72
+ console.error(String(e instanceof Error ? e.message : e));
73
+ throw new FleetCliExit(1);
74
+ };
70
75
  /** Exec a child with our stdio (logs/attach). */
71
76
  const passthrough = (cmd, args) => new Promise(resolve => {
72
77
  const c = spawnChild(cmd, args, { stdio: 'inherit' });
@@ -76,6 +81,7 @@ const program = new Command()
76
81
  .name('ours-fleet')
77
82
  .description('Fleet of persistent, identity-bound AI agents — canonical Brain + Role definitions over ACP sessions.')
78
83
  .enablePositionalOptions()
84
+ .exitOverride()
79
85
  .version(VERSION);
80
86
  const cOpt = (cmd) => cmd.option('-c, --configuration <file>', 'manifest (default: ~/fleet.yaml; documents under ~/fleet/)');
81
87
  const collect = (value, previous) => [...previous, value];
@@ -1192,6 +1198,13 @@ cOpt(program.command('spawn [name]').description('spawn a new agent (permanent b
1192
1198
  if (!response.ok)
1193
1199
  throw new SessionControlError(response.kind ?? 'backend', response.error ?? 'managed spawn failed');
1194
1200
  const result = response.result;
1201
+ recordFleetAuditResource('agent', result.role);
1202
+ recordFleetAuditPresentation({ kind: 'agent_started', name: result.role,
1203
+ lifetime: result.lifetime, brain: result.brainSummary, role: result.roleSummary,
1204
+ harness: result.harness, session: result.session, model: result.model,
1205
+ permissions: result.permissionMode
1206
+ ? `${result.permissionMode.fleetMode}/${result.permissionMode.nativeMode}` : undefined,
1207
+ parent: result.caller, actionId: result.creationActionId, inherited: result.inherited });
1195
1208
  const expectedCaller = process.env[FLEET_PROXY_CALLER_ENV];
1196
1209
  if (expectedCaller && result.caller !== expectedCaller)
1197
1210
  throw new Error(`fleet proxy caller mismatch: expected '${expectedCaller}', got '${result.caller}'`);
@@ -1248,7 +1261,8 @@ cOpt(program.command('doctor').description('prerequisite report'))
1248
1261
  });
1249
1262
  for (const c of rep.checks)
1250
1263
  console.log(`${c.ok ? 'ok ' : 'MISS'} ${c.name.padEnd(22)} ${c.detail}`);
1251
- process.exit(rep.ok ? 0 : 1);
1264
+ if (!rep.ok)
1265
+ throw new FleetCliExit(1);
1252
1266
  });
1253
1267
  program.command('init').description('one-time host setup (units, dirs, linger)')
1254
1268
  .action(async () => {
@@ -1497,11 +1511,84 @@ cOpt(program.command('_run-watchdogs', { hidden: true }))
1497
1511
  die(e);
1498
1512
  }
1499
1513
  });
1500
- const spawnIndex = process.argv.indexOf('spawn');
1501
- if (spawnIndex >= 0 && process.argv.slice(spawnIndex + 1).some(arg => arg === '--harness' || arg.startsWith('--harness=') || arg === '--model' || arg.startsWith('--model='))) {
1502
- console.error('error: --harness and --model were removed; select a Brain with --brain');
1503
- process.exitCode = 1;
1514
+ async function parseFleetCli() {
1515
+ const spawnIndex = process.argv.indexOf('spawn');
1516
+ if (spawnIndex >= 0 && process.argv.slice(spawnIndex + 1).some(arg => arg === '--harness' || arg.startsWith('--harness=') || arg === '--model' || arg.startsWith('--model='))) {
1517
+ console.error('error: --harness and --model were removed; select a Brain with --brain');
1518
+ throw new FleetCliExit(1);
1519
+ }
1520
+ try {
1521
+ await program.parseAsync(process.argv);
1522
+ }
1523
+ catch (error) {
1524
+ const commander = error;
1525
+ if (commander.exitCode === 0)
1526
+ return;
1527
+ if (typeof commander.exitCode === 'number')
1528
+ throw new FleetCliExit(commander.exitCode, 'validation', 'not_started');
1529
+ throw error;
1530
+ }
1504
1531
  }
1505
- else {
1506
- program.parseAsync(process.argv);
1532
+ async function runFleetCli() {
1533
+ const stateDir = process.env[FLEET_PROXY_STATE_DIR_ENV];
1534
+ const caller = process.env[FLEET_PROXY_CALLER_ENV];
1535
+ if (!stateDir || !caller) {
1536
+ await parseFleetCli();
1537
+ return;
1538
+ }
1539
+ const requestId = randomUUID();
1540
+ let attempt;
1541
+ const begun = await controlRequest(stateDir, {
1542
+ command: 'fleet_audit_begin', audit: { requestId, argv: process.argv.slice(2) },
1543
+ });
1544
+ if (!begun.ok)
1545
+ throw new SessionControlError(begun.kind ?? 'rejected', begun.error ?? 'fleet command audit refused');
1546
+ attempt = begun.result;
1547
+ if (attempt.caller !== caller)
1548
+ throw new SessionControlError('rejected', `fleet audit caller mismatch: expected '${caller}', got '${attempt.caller}'`);
1549
+ let exitCode = 0;
1550
+ let outcomeClass = 'success';
1551
+ let effect = 'completed';
1552
+ beginFleetAuditCollection();
1553
+ if (attempt.classification.decision !== 'allow') {
1554
+ exitCode = 1;
1555
+ outcomeClass = 'denied';
1556
+ effect = 'not_started';
1557
+ console.error(`fleet supervisor proxy ${attempt.classification.decision}: ${attempt.classification.command}`);
1558
+ }
1559
+ else {
1560
+ const originalExit = process.exit;
1561
+ process.exit = ((code) => { throw new FleetCliExit(code ?? 0); });
1562
+ try {
1563
+ await parseFleetCli();
1564
+ }
1565
+ catch (error) {
1566
+ exitCode = error instanceof FleetCliExit ? error.exitCode : 1;
1567
+ outcomeClass = error instanceof FleetCliExit ? error.outcomeClass : 'runtime';
1568
+ effect = error instanceof FleetCliExit ? error.effect : 'unknown';
1569
+ if (!(error instanceof FleetCliExit))
1570
+ console.error(error instanceof Error ? error.message : String(error));
1571
+ }
1572
+ finally {
1573
+ process.exit = originalExit;
1574
+ }
1575
+ }
1576
+ const metadata = consumeFleetAuditCollection();
1577
+ if (metadata.failure) {
1578
+ outcomeClass = metadata.failure.class;
1579
+ effect = metadata.failure.effect;
1580
+ }
1581
+ const finished = await controlRequest(stateDir, { command: 'fleet_audit_finish', audit: {
1582
+ correlationId: attempt.correlationId, class: outcomeClass, exitCode, effect,
1583
+ ...(metadata.resourceIds ? { resourceIds: metadata.resourceIds } : {}),
1584
+ ...(metadata.presentation ? { presentation: metadata.presentation } : {}),
1585
+ } });
1586
+ if (!finished.ok)
1587
+ throw new SessionControlError(finished.kind ?? 'backend', finished.error ?? `audit delivery failed after execution; effect status=${effect}`);
1588
+ process.exitCode = exitCode;
1507
1589
  }
1590
+ void runFleetCli().catch(error => {
1591
+ if (!(error instanceof FleetCliExit))
1592
+ console.error(error instanceof Error ? error.message : String(error));
1593
+ process.exitCode = error instanceof FleetCliExit ? error.exitCode : 1;
1594
+ });
@@ -0,0 +1,93 @@
1
+ import { type FetchLike } from './monitor.js';
2
+ export declare const DAEMON_RECOVERY_MAX_ATTEMPTS = 6;
3
+ export declare const DAEMON_RECOVERY_INITIAL_BACKOFF_MS = 1000;
4
+ export declare const DAEMON_RECOVERY_MAX_BACKOFF_MS = 5000;
5
+ export declare const DAEMON_RECOVERY_DEADLINE_MS = 60000;
6
+ export interface DaemonGeneration {
7
+ bootId: string;
8
+ pid: number;
9
+ startedAt: number;
10
+ stateDir: string;
11
+ }
12
+ export type DaemonGenerationProbe = {
13
+ state: 'ready';
14
+ generation: DaemonGeneration;
15
+ } | {
16
+ state: 'unavailable';
17
+ reason: string;
18
+ };
19
+ export type DaemonGenerationObservation = {
20
+ kind: 'baseline' | 'stable' | 'available';
21
+ generation: DaemonGeneration;
22
+ } | {
23
+ kind: 'changed';
24
+ previous: DaemonGeneration;
25
+ generation: DaemonGeneration;
26
+ } | {
27
+ kind: 'lost' | 'unavailable';
28
+ previous?: DaemonGeneration;
29
+ reason: string;
30
+ };
31
+ interface GenerationProbeDeps {
32
+ readText?(path: string): string;
33
+ canonicalize?(path: string): string;
34
+ }
35
+ /**
36
+ * Corroborate the loopback daemon's unauthenticated `/info`, its credentialed
37
+ * identity-index readiness route, and its local boot-generation record. The
38
+ * index enforces auth when the daemon visibility requires it; open visibility
39
+ * deliberately does not. No source is sufficient
40
+ * alone: `/info` has no boot id, the identity index has no generation, and a
41
+ * stale `ready` file can outlive the process that wrote it.
42
+ */
43
+ export declare function probeDaemonGeneration(fetch: FetchLike, env: NodeJS.ProcessEnv, deps?: GenerationProbeDeps): Promise<DaemonGenerationProbe>;
44
+ export declare class DaemonGenerationObserver {
45
+ private current?;
46
+ private unavailable;
47
+ observe(probe: DaemonGenerationProbe): DaemonGenerationObservation;
48
+ }
49
+ export declare function daemonRecoveryBackoff(attempt: number): number;
50
+ export type RecoveryPath = 'agent' | 'owner';
51
+ export type RecoveryPathResult = {
52
+ ok: true;
53
+ } | {
54
+ ok: false;
55
+ reason: string;
56
+ };
57
+ export interface RoleRecoveryControllerOptions {
58
+ role: string;
59
+ identity: string;
60
+ stateDir: string;
61
+ now(): number;
62
+ sleep(ms: number): Promise<void>;
63
+ recoverAgent(epoch: string): Promise<RecoveryPathResult>;
64
+ recoverOwner(epoch: string): Promise<RecoveryPathResult>;
65
+ log(line: string): void;
66
+ }
67
+ export interface RecoveryStatus {
68
+ version: 1;
69
+ identity: string;
70
+ epoch: string;
71
+ state: 'recovering' | 'recovered' | 'degraded' | 'cancelled';
72
+ paths: Record<RecoveryPath, {
73
+ state: 'pending' | 'recovered' | 'degraded';
74
+ attempts: number;
75
+ reason?: string;
76
+ }>;
77
+ updatedAt: string;
78
+ }
79
+ export declare function readDaemonRecoveryStatus(dir: string): RecoveryStatus | undefined;
80
+ /** Per-role, per-generation bounded recovery with path-level fault isolation. */
81
+ export declare class RoleRecoveryController {
82
+ private readonly options;
83
+ private token;
84
+ private activeEpoch?;
85
+ private active?;
86
+ private status?;
87
+ constructor(options: RoleRecoveryControllerOptions);
88
+ recover(generation: DaemonGeneration): Promise<RecoveryStatus>;
89
+ cancel(): void;
90
+ noteLoss(reason: string): void;
91
+ private write;
92
+ }
93
+ export {};