@ours.network/fleet 1.1.0-nightly.6 → 1.1.0-nightly.8
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 +23 -4
- package/dist/agent-recovery-gate.d.ts +17 -0
- package/dist/agent-recovery-gate.js +135 -0
- package/dist/application/fleet-query-service.js +13 -1
- package/dist/application/task-room-service.d.ts +1 -1
- package/dist/build-info.json +4 -4
- package/dist/cli.js +6 -2
- package/dist/config-yaml.d.ts +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +49 -2
- package/dist/daemon-recovery.d.ts +93 -0
- package/dist/daemon-recovery.js +328 -0
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +26 -3
- package/dist/doctor.js +14 -0
- package/dist/init-guidance.d.ts +1 -1
- package/dist/init-guidance.js +4 -3
- package/dist/owner-channel/channel.d.ts +40 -0
- package/dist/owner-channel/channel.js +210 -16
- package/dist/owner-channel/commands.js +3 -9
- package/dist/preset-bootstrap.d.ts +10 -0
- package/dist/preset-bootstrap.js +85 -0
- package/dist/rooms-tasks/cli.js +4 -4
- package/dist/rooms-tasks/config.js +0 -4
- package/dist/rooms-tasks/templates.d.ts +0 -1
- package/dist/rooms-tasks/templates.js +9 -76
- package/dist/rooms-tasks/types.d.ts +2 -1
- package/dist/runner.d.ts +8 -1
- package/dist/runner.js +74 -3
- package/dist/temp-lifecycle.d.ts +1 -1
- package/dist/web/fleet-config-service.js +4 -3
- package/package.json +2 -1
- package/presets/fleet/agents/Agent.yaml +3 -0
- package/presets/fleet/agents/Architect.yaml +3 -0
- package/presets/fleet/agents/Critic.yaml +3 -0
- package/presets/fleet/agents/Developer.yaml +3 -0
- package/presets/fleet/agents/Secretary.yaml +3 -0
- package/presets/fleet/agents/Tester.yaml +3 -0
- package/presets/fleet/brains/claude-default.yaml +5 -0
- package/presets/fleet/roles/Agent.yaml +6 -0
- package/presets/fleet/roles/Architect.yaml +6 -0
- package/presets/fleet/roles/Critic.yaml +6 -0
- package/presets/fleet/roles/Developer.yaml +6 -0
- package/presets/fleet/roles/Secretary.yaml +6 -0
- package/presets/fleet/roles/Tester.yaml +6 -0
- package/presets/fleet/room_templates/pair.yaml +11 -0
- package/presets/fleet/room_templates/single.yaml +8 -0
- package/presets/fleet/room_templates/team.yaml +12 -0
- package/presets/fleet.yaml +7 -0
- package/presets/manifest.json +5 -0
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@ brain: { inline: { harness: codex, session: acp } }
|
|
|
43
43
|
## How it works
|
|
44
44
|
|
|
45
45
|
```
|
|
46
|
-
~/fleet.yaml + ~/fleet/{agents,roles,brains}/*.yaml your declaration
|
|
46
|
+
~/fleet.yaml + ~/fleet/{agents,roles,brains,room_templates}/*.yaml your declaration
|
|
47
47
|
│ ours-fleet up
|
|
48
48
|
▼
|
|
49
49
|
briefing.md per role ──► agent session adapter ──► ACP client/session ──► ACP agent
|
|
@@ -91,7 +91,7 @@ equivalent); logs land in `~/.ours-fleet/logs/`.
|
|
|
91
91
|
|
|
92
92
|
```sh
|
|
93
93
|
npm i -g @ours.network/fleet
|
|
94
|
-
ours-fleet init # units/dirs/linger
|
|
94
|
+
ours-fleet init # units/dirs/linger + missing standard presets
|
|
95
95
|
ours-fleet doctor # verifies everything above, with actionable messages
|
|
96
96
|
```
|
|
97
97
|
|
|
@@ -107,8 +107,8 @@ become that account and repeat.
|
|
|
107
107
|
## Quickstart
|
|
108
108
|
|
|
109
109
|
```sh
|
|
110
|
-
|
|
111
|
-
|
|
110
|
+
ours-fleet init # safe to repeat: creates missing files, never replaces edits
|
|
111
|
+
ours-fleet config # validates Agents, Roles, Brains, and Room templates
|
|
112
112
|
$EDITOR ~/fleet/agents/*.yaml # compose Role + Brain and operational settings
|
|
113
113
|
ours-fleet up # boot the fleet (staggered)
|
|
114
114
|
ours-fleet ls # running consoles
|
|
@@ -442,6 +442,25 @@ installer/setup flows remain responsible for starting it.
|
|
|
442
442
|
|
|
443
443
|
### Rooms and tasks
|
|
444
444
|
|
|
445
|
+
Init installs editable `single`, `pair`, and `team` definitions under
|
|
446
|
+
`~/fleet/room_templates/`, plus every exact-cased Agent, Role, and Brain they
|
|
447
|
+
reference. Inspect them with `ours-fleet template list` and
|
|
448
|
+
`ours-fleet template show team`. After configuring the authenticated owner below:
|
|
449
|
+
|
|
450
|
+
```sh
|
|
451
|
+
ours-fleet task create --title "Solo task" --template single
|
|
452
|
+
ours-fleet task create --title "Reviewed change" --template pair
|
|
453
|
+
ours-fleet task create --title "Phased delivery" --template team
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
`ours-fleet init -c /path/custom.yaml` seeds `/path/custom/` instead. Init reports
|
|
457
|
+
the packaged preset revision and source directory and only seeds missing files.
|
|
458
|
+
It never upgrades an edited preset. To adopt a newer packaged file explicitly,
|
|
459
|
+
copy the reported source file beside the existing target as `.new-default`, review
|
|
460
|
+
`diff -u`, then replace the target yourself. This is the sole adoption operation;
|
|
461
|
+
rerunning init is not an update. Users upgrading from hardcoded templates should
|
|
462
|
+
run init once (with the same `-c` selection they normally use).
|
|
463
|
+
|
|
445
464
|
Rooms always use `ours-cowork`; there is no room-provider selector. Configure
|
|
446
465
|
the cowork daemon connection and the room owner directly:
|
|
447
466
|
|
|
@@ -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'
|
|
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',
|
|
@@ -179,7 +179,7 @@ export declare class TaskRoomApplicationService {
|
|
|
179
179
|
name: string;
|
|
180
180
|
version: number;
|
|
181
181
|
description: string;
|
|
182
|
-
|
|
182
|
+
sourceFile?: string;
|
|
183
183
|
room?: import("../rooms-tasks/types.js").TemplateRoomConfig;
|
|
184
184
|
contract?: string;
|
|
185
185
|
members: import("../rooms-tasks/types.js").TemplateMemberSlot[];
|
package/dist/build-info.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.1.0-nightly.
|
|
3
|
-
"buildId": "
|
|
4
|
-
"commit": "
|
|
2
|
+
"version": "1.1.0-nightly.8",
|
|
3
|
+
"buildId": "871d3c2bae20",
|
|
4
|
+
"commit": "cfd589d27e88eb95710df1f037a4c13e22eedb79",
|
|
5
5
|
"dirty": true,
|
|
6
|
-
"builtAt": "2026-08-
|
|
6
|
+
"builtAt": "2026-08-31T09:40:52.301Z",
|
|
7
7
|
"capabilities": [
|
|
8
8
|
"monitor.interrupt.after_tool"
|
|
9
9
|
]
|
package/dist/cli.js
CHANGED
|
@@ -8,6 +8,7 @@ import { createInterface } from 'node:readline';
|
|
|
8
8
|
import { Command } from 'commander';
|
|
9
9
|
import { VERSION } from './version.js';
|
|
10
10
|
import { INIT_COMPLETION_GUIDANCE } from './init-guidance.js';
|
|
11
|
+
import { bootstrapPresets } from './preset-bootstrap.js';
|
|
11
12
|
import { analyzeInstalls, buildInfo, buildLabel, discoverInstalls, runningLabel, } from './provenance.js';
|
|
12
13
|
import { agentDir, agentsRoot, tmpRoot, logsRoot, deriveXdgRuntimeDir } from './paths.js';
|
|
13
14
|
import { findRole, loadConfig, ROLE_NAME_RE } from './config.js';
|
|
@@ -1264,12 +1265,15 @@ cOpt(program.command('doctor').description('prerequisite report'))
|
|
|
1264
1265
|
if (!rep.ok)
|
|
1265
1266
|
throw new FleetCliExit(1);
|
|
1266
1267
|
});
|
|
1267
|
-
program.command('init').description('
|
|
1268
|
-
.action(async () => {
|
|
1268
|
+
cOpt(program.command('init').description('idempotent host setup plus missing packaged presets'))
|
|
1269
|
+
.action(async (opts) => {
|
|
1269
1270
|
for (const d of [agentsRoot(), tmpRoot(), logsRoot()])
|
|
1270
1271
|
mkdirSync(d, { recursive: true });
|
|
1271
1272
|
for (const m of await pickBackend().init(binPath))
|
|
1272
1273
|
console.log(m);
|
|
1274
|
+
const seeded = bootstrapPresets(opts.configuration);
|
|
1275
|
+
console.log(`Packaged preset revision ${seeded.revision}: ${seeded.sourceRoot}`);
|
|
1276
|
+
console.log(`Created ${seeded.created.length}; preserved ${seeded.preserved.length} existing file(s).`);
|
|
1273
1277
|
console.log(INIT_COMPLETION_GUIDANCE);
|
|
1274
1278
|
});
|
|
1275
1279
|
const webCommand = cOpt(program.command('web').description('start or open the secure localhost fleet web console'))
|
package/dist/config-yaml.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type YamlMode = 'compat' | 'strict';
|
|
2
|
-
export type ConfigDiagnosticKind = 'anchor' | 'alias' | 'explicit-tag' | 'non-scalar-key' | 'multiple-documents';
|
|
2
|
+
export type ConfigDiagnosticKind = 'anchor' | 'alias' | 'explicit-tag' | 'non-scalar-key' | 'multiple-documents' | 'deprecated-field';
|
|
3
3
|
export interface ConfigDiagnostic {
|
|
4
4
|
severity: 'warning';
|
|
5
5
|
kind: ConfigDiagnosticKind;
|
package/dist/config.d.ts
CHANGED
|
@@ -220,7 +220,7 @@ export interface FleetConfig {
|
|
|
220
220
|
files: string[];
|
|
221
221
|
configMode?: 'split-v2';
|
|
222
222
|
sourceDocuments?: Array<{
|
|
223
|
-
kind: 'Manifest' | 'Agent' | 'Role' | 'Brain';
|
|
223
|
+
kind: 'Manifest' | 'Agent' | 'Role' | 'Brain' | 'RoomTemplate';
|
|
224
224
|
id?: string;
|
|
225
225
|
path: string;
|
|
226
226
|
}>;
|
package/dist/config.js
CHANGED
|
@@ -265,6 +265,35 @@ function readKindDirectory(root, kind, required, yamlMode, diagnostics, files) {
|
|
|
265
265
|
}
|
|
266
266
|
return result;
|
|
267
267
|
}
|
|
268
|
+
function readRoomTemplateDirectory(root, yamlMode, diagnostics, files) {
|
|
269
|
+
if (!existsSync(root))
|
|
270
|
+
return {};
|
|
271
|
+
assertTrustedPath(root, 'directory');
|
|
272
|
+
const result = {};
|
|
273
|
+
const sources = new Map();
|
|
274
|
+
const names = readdirSync(root)
|
|
275
|
+
.filter(name => ['.yaml', '.yml'].includes(extname(name).toLowerCase())).sort();
|
|
276
|
+
for (const filename of names) {
|
|
277
|
+
const file = join(root, filename);
|
|
278
|
+
assertTrustedPath(file, 'file');
|
|
279
|
+
const id = basename(filename, extname(filename));
|
|
280
|
+
const previous = sources.get(id);
|
|
281
|
+
if (previous)
|
|
282
|
+
throw new ConfigError(`E_DUPLICATE_ID: room template '${id}' defined by both ${previous} and ${file}`);
|
|
283
|
+
const parsed = parseFleetDocument(file, readFileSync(file, 'utf8'), yamlMode);
|
|
284
|
+
diagnostics.push(...parsed.diagnostics);
|
|
285
|
+
if (parsed.value.override_builtin === true)
|
|
286
|
+
diagnostics.push({
|
|
287
|
+
severity: 'warning', kind: 'deprecated-field', file, line: 1, column: 1,
|
|
288
|
+
message: `${file}: override_builtin is deprecated and ignored for a file-backed Room template`,
|
|
289
|
+
});
|
|
290
|
+
files.push(file);
|
|
291
|
+
const validated = validateRoomTemplatesConfig({ [id]: parsed.value }, file)[id];
|
|
292
|
+
result[id] = { ...validated, sourceFile: file };
|
|
293
|
+
sources.set(id, file);
|
|
294
|
+
}
|
|
295
|
+
return result;
|
|
296
|
+
}
|
|
268
297
|
function selection(raw, kind, agentFile, presets, allowed) {
|
|
269
298
|
if (!isPlainObject(raw))
|
|
270
299
|
schemaError(agentFile, `/${kind}`, 'must be { ref } or { inline }');
|
|
@@ -586,6 +615,9 @@ export function loadConfig(configPath, options = {}) {
|
|
|
586
615
|
let ownerInvite;
|
|
587
616
|
let roomTemplates;
|
|
588
617
|
let tasks;
|
|
618
|
+
const fileTemplates = readRoomTemplateDirectory(join(splitRootFor(base), 'room_templates'), options.yamlMode ?? 'compat', diagnostics, files);
|
|
619
|
+
if (Object.keys(fileTemplates).length)
|
|
620
|
+
roomTemplates = fileTemplates;
|
|
589
621
|
for (const { file, doc } of docs) {
|
|
590
622
|
if (doc.rooms !== undefined) {
|
|
591
623
|
if (rooms)
|
|
@@ -598,7 +630,21 @@ export function loadConfig(configPath, options = {}) {
|
|
|
598
630
|
}
|
|
599
631
|
if (doc.room_templates !== undefined) {
|
|
600
632
|
const validated = validateRoomTemplatesConfig(deepSub(doc.room_templates, vars), file);
|
|
601
|
-
|
|
633
|
+
const raw = doc.room_templates;
|
|
634
|
+
for (const [name, template] of Object.entries(validated)) {
|
|
635
|
+
const shadowed = roomTemplates?.[name];
|
|
636
|
+
const marker = raw[name]?.override_builtin;
|
|
637
|
+
if (shadowed && (marker !== true || template.version <= shadowed.version))
|
|
638
|
+
throw new ConfigError(`${file}: room_templates.${name} shadows file preset ${shadowed.sourceFile}; `
|
|
639
|
+
+ 'set override_builtin: true and use a higher version');
|
|
640
|
+
if (marker === true)
|
|
641
|
+
diagnostics.push({
|
|
642
|
+
severity: 'warning', kind: 'deprecated-field', file, line: 1, column: 1,
|
|
643
|
+
message: `${file}: room_templates.${name}.override_builtin is deprecated; `
|
|
644
|
+
+ 'it is retained only as an explicit manifest-over-file migration marker',
|
|
645
|
+
});
|
|
646
|
+
roomTemplates = { ...(roomTemplates ?? {}), [name]: { ...template, sourceFile: file } };
|
|
647
|
+
}
|
|
602
648
|
}
|
|
603
649
|
if (doc.tasks !== undefined) {
|
|
604
650
|
if (tasks)
|
|
@@ -616,7 +662,8 @@ export function loadConfig(configPath, options = {}) {
|
|
|
616
662
|
return { kind: 'Manifest', path };
|
|
617
663
|
const parent = basename(dirname(path));
|
|
618
664
|
const kind = parent === 'agents' ? 'Agent'
|
|
619
|
-
: parent === 'roles' ? 'Role'
|
|
665
|
+
: parent === 'roles' ? 'Role'
|
|
666
|
+
: parent === 'room_templates' ? 'RoomTemplate' : 'Brain';
|
|
620
667
|
return { kind, id: basename(path, extname(path)), path };
|
|
621
668
|
}),
|
|
622
669
|
};
|
|
@@ -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 {};
|