@ours.network/fleet 0.18.0-nightly.5 → 0.18.0
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 +101 -25
- package/dist/application/fleet-query-service.js +12 -0
- package/dist/application/role-creation-service.js +3 -1
- package/dist/application/types.d.ts +11 -0
- package/dist/briefing.js +9 -2
- package/dist/build-info.json +7 -6
- package/dist/capabilities.d.ts +3 -1
- package/dist/capabilities.js +3 -0
- package/dist/cli.js +83 -7
- package/dist/config.d.ts +11 -3
- package/dist/config.js +40 -15
- package/dist/creation.d.ts +14 -15
- package/dist/creation.js +19 -13
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +113 -27
- package/dist/doctor.d.ts +1 -5
- package/dist/doctor.js +11 -18
- package/dist/fleet-proxy.d.ts +5 -0
- package/dist/harness/acp-agent.js +11 -6
- package/dist/harness/claude-code.js +204 -11
- package/dist/harness/codex.d.ts +4 -1
- package/dist/harness/codex.js +74 -12
- package/dist/harness/types.d.ts +54 -4
- package/dist/harness-plugins.d.ts +48 -0
- package/dist/harness-plugins.js +309 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/loops/manager.d.ts +30 -1
- package/dist/loops/manager.js +69 -6
- package/dist/loops/state.d.ts +18 -0
- package/dist/loops/state.js +4 -0
- package/dist/model-env.d.ts +71 -0
- package/dist/model-env.js +106 -0
- package/dist/monitor.js +1 -1
- package/dist/ops.js +1 -1
- package/dist/owner-channel/attachments.d.ts +2 -25
- package/dist/owner-channel/attachments.js +5 -61
- package/dist/owner-channel/channel.d.ts +28 -17
- package/dist/owner-channel/channel.js +249 -158
- package/dist/owner-channel/mcp.d.ts +24 -0
- package/dist/owner-channel/mcp.js +145 -0
- package/dist/owner-channel/notices.d.ts +7 -0
- package/dist/owner-channel/notices.js +9 -0
- package/dist/resolved-plan.js +1 -0
- package/dist/runner.d.ts +48 -0
- package/dist/runner.js +237 -85
- package/dist/session/acp.d.ts +104 -0
- package/dist/session/acp.js +213 -10
- package/dist/session/activity.d.ts +31 -0
- package/dist/session/activity.js +48 -0
- package/dist/session/conversation-normalizer.d.ts +6 -0
- package/dist/session/conversation-normalizer.js +153 -10
- package/dist/session/conversation-types.d.ts +23 -4
- package/dist/session/types.d.ts +35 -0
- package/dist/spawn.js +29 -17
- package/dist/supervisor/systemd.js +2 -29
- package/dist/watchdog/briefing.js +7 -0
- package/dist/web-app/assets/{TerminalView-BAVk1Bot.js → TerminalView-C_G1ID2P.js} +1 -1
- package/dist/web-app/assets/{index-C3S-xFRU.js → index-BCBK78hw.js} +5 -5
- package/dist/web-app/index.html +1 -1
- package/dist/worklog.d.ts +7 -1
- package/dist/worklog.js +191 -39
- package/package.json +1 -3
- package/dist/owner-channel/ours-client.d.ts +0 -141
- package/dist/owner-channel/ours-client.js +0 -225
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export declare class OursMcpError extends Error {
|
|
2
|
+
}
|
|
3
|
+
export interface OursToolClient {
|
|
4
|
+
start(): Promise<void>;
|
|
5
|
+
callTool(name: string, args?: Record<string, unknown>): Promise<unknown>;
|
|
6
|
+
close(): Promise<void>;
|
|
7
|
+
}
|
|
8
|
+
/** Minimal MCP stdio client. One instance owns exactly one ours identity binding. */
|
|
9
|
+
export declare class OursMcpClient implements OursToolClient {
|
|
10
|
+
private readonly command;
|
|
11
|
+
private readonly env;
|
|
12
|
+
private readonly log;
|
|
13
|
+
private child?;
|
|
14
|
+
private nextId;
|
|
15
|
+
private tail;
|
|
16
|
+
constructor(command?: string, env?: Record<string, string>, log?: (line: string) => void);
|
|
17
|
+
start(): Promise<void>;
|
|
18
|
+
callTool(name: string, args?: Record<string, unknown>): Promise<unknown>;
|
|
19
|
+
close(): Promise<void>;
|
|
20
|
+
private request;
|
|
21
|
+
private requestNow;
|
|
22
|
+
private notify;
|
|
23
|
+
private write;
|
|
24
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { createInterface } from 'node:readline';
|
|
4
|
+
export class OursMcpError extends Error {
|
|
5
|
+
}
|
|
6
|
+
/** Minimal MCP stdio client. One instance owns exactly one ours identity binding. */
|
|
7
|
+
export class OursMcpClient {
|
|
8
|
+
command;
|
|
9
|
+
env;
|
|
10
|
+
log;
|
|
11
|
+
child;
|
|
12
|
+
nextId = 0;
|
|
13
|
+
tail = Promise.resolve();
|
|
14
|
+
constructor(command = 'ours-mcp', env = {}, log = () => undefined) {
|
|
15
|
+
this.command = command;
|
|
16
|
+
this.env = env;
|
|
17
|
+
this.log = log;
|
|
18
|
+
}
|
|
19
|
+
async start() {
|
|
20
|
+
if (this.child && this.child.exitCode === null)
|
|
21
|
+
return;
|
|
22
|
+
// ours-mcp normally records the long-lived client PID so an identity lease
|
|
23
|
+
// survives connector churn. An owner-channel connector has the opposite
|
|
24
|
+
// lifecycle: each supervised attempt owns a fresh connector and must make
|
|
25
|
+
// its lease reclaimable when that connector exits, even though the fleet
|
|
26
|
+
// supervisor itself remains alive. POSIX exec preserves the shell PID as
|
|
27
|
+
// the proxy PID, giving the daemon an exact process-lifetime fence without
|
|
28
|
+
// interpolating the command path into shell text.
|
|
29
|
+
const child = spawn('/bin/sh', [
|
|
30
|
+
'-c', 'OURS_CLIENT_PID=$$; export OURS_CLIENT_PID; exec "$1" "$2"',
|
|
31
|
+
'ours-fleet-owner-proxy', this.command, 'proxy',
|
|
32
|
+
], {
|
|
33
|
+
env: {
|
|
34
|
+
...process.env,
|
|
35
|
+
...this.env,
|
|
36
|
+
// Bindings are keyed by this value. Sharing it would silently rebind a
|
|
37
|
+
// role's normal mailbox or another owner channel.
|
|
38
|
+
CLAUDE_CODE_SESSION_ID: `ours-fleet-owner-${process.pid}-${randomUUID()}`,
|
|
39
|
+
},
|
|
40
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
41
|
+
});
|
|
42
|
+
await new Promise((resolve, reject) => {
|
|
43
|
+
child.once('spawn', resolve);
|
|
44
|
+
child.once('error', reject);
|
|
45
|
+
});
|
|
46
|
+
this.child = child;
|
|
47
|
+
child.once('exit', (code, signal) => {
|
|
48
|
+
this.log(`ours-mcp proxy launcher exited (${code ?? signal ?? 'unknown'})`);
|
|
49
|
+
});
|
|
50
|
+
child.stdin.on('error', error => this.log(`ours-mcp stdin: ${error.message}`));
|
|
51
|
+
createInterface({ input: child.stderr }).on('line', line => this.log(`ours-mcp: ${line}`));
|
|
52
|
+
try {
|
|
53
|
+
await this.request('initialize', {
|
|
54
|
+
protocolVersion: '2025-03-26', capabilities: {},
|
|
55
|
+
clientInfo: { name: 'ours-fleet-owner-channel', version: '1' },
|
|
56
|
+
});
|
|
57
|
+
await this.notify('notifications/initialized', {});
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
await this.close();
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async callTool(name, args = {}) {
|
|
65
|
+
const result = await this.request('tools/call', { name, arguments: args });
|
|
66
|
+
const text = (result.content ?? [])
|
|
67
|
+
.filter(item => item.type === 'text').map(item => item.text ?? '').join('\n').trim();
|
|
68
|
+
if (result.isError)
|
|
69
|
+
throw new OursMcpError(text || `ours tool ${name} failed`);
|
|
70
|
+
if (result.structuredContent !== undefined)
|
|
71
|
+
return result.structuredContent;
|
|
72
|
+
if (!text)
|
|
73
|
+
return {};
|
|
74
|
+
try {
|
|
75
|
+
return JSON.parse(text);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return text;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async close() {
|
|
82
|
+
const child = this.child;
|
|
83
|
+
this.child = undefined;
|
|
84
|
+
if (!child || child.exitCode !== null)
|
|
85
|
+
return;
|
|
86
|
+
// EOF asks the proxy to close normally. Once this exact process exits, the
|
|
87
|
+
// daemon can reclaim its lease even while the supervisor stays alive.
|
|
88
|
+
child.stdin.end();
|
|
89
|
+
const exited = await new Promise(resolve => {
|
|
90
|
+
const timer = setTimeout(() => resolve(false), 1_000);
|
|
91
|
+
child.once('exit', () => { clearTimeout(timer); resolve(true); });
|
|
92
|
+
});
|
|
93
|
+
if (exited || child.exitCode !== null)
|
|
94
|
+
return;
|
|
95
|
+
child.kill('SIGTERM');
|
|
96
|
+
await new Promise(resolve => {
|
|
97
|
+
const timer = setTimeout(() => { child.kill('SIGKILL'); resolve(); }, 5_000);
|
|
98
|
+
child.once('exit', () => { clearTimeout(timer); resolve(); });
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
request(method, params) {
|
|
102
|
+
const run = this.tail.then(() => this.requestNow(method, params));
|
|
103
|
+
this.tail = run.then(() => undefined, () => undefined);
|
|
104
|
+
return run;
|
|
105
|
+
}
|
|
106
|
+
async requestNow(method, params) {
|
|
107
|
+
const child = this.child;
|
|
108
|
+
if (!child || child.exitCode !== null)
|
|
109
|
+
throw new OursMcpError('ours-mcp proxy is not running');
|
|
110
|
+
const id = ++this.nextId;
|
|
111
|
+
await this.write(child, { jsonrpc: '2.0', id, method, params });
|
|
112
|
+
const lines = createInterface({ input: child.stdout, crlfDelay: Infinity });
|
|
113
|
+
try {
|
|
114
|
+
for await (const line of lines) {
|
|
115
|
+
let response;
|
|
116
|
+
try {
|
|
117
|
+
response = JSON.parse(line);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (response.id !== id)
|
|
123
|
+
continue;
|
|
124
|
+
if (response.error !== undefined)
|
|
125
|
+
throw new OursMcpError(JSON.stringify(response.error));
|
|
126
|
+
return response.result ?? {};
|
|
127
|
+
}
|
|
128
|
+
throw new OursMcpError('ours-mcp proxy closed its output');
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
lines.close();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async notify(method, params) {
|
|
135
|
+
const child = this.child;
|
|
136
|
+
if (!child || child.exitCode !== null)
|
|
137
|
+
throw new OursMcpError('ours-mcp proxy is not running');
|
|
138
|
+
await this.write(child, { jsonrpc: '2.0', method, params });
|
|
139
|
+
}
|
|
140
|
+
write(child, value) {
|
|
141
|
+
return new Promise((resolve, reject) => {
|
|
142
|
+
child.stdin.write(JSON.stringify(value) + '\n', error => error ? reject(error) : resolve());
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -22,6 +22,13 @@ export declare const ownerNotices: {
|
|
|
22
22
|
receivedStarted: () => string;
|
|
23
23
|
receivedQueued: (queuedBehind: number) => string;
|
|
24
24
|
receivedInterrupting: () => string;
|
|
25
|
+
/**
|
|
26
|
+
* The honest answer when the agent is mid-task and pre-empting it would have
|
|
27
|
+
* corrupted the conversation. Says "not started yet" rather than borrowing
|
|
28
|
+
* `receivedInterrupting`'s claim that something was cancelled for this
|
|
29
|
+
* request.
|
|
30
|
+
*/
|
|
31
|
+
receivedDeferred: () => string;
|
|
25
32
|
status: (role: string, snapshot: SessionSnapshot) => string;
|
|
26
33
|
interrupted: (role: string) => string;
|
|
27
34
|
/** The turn IS cancelled — say how, without implying the owner must retry. */
|
|
@@ -26,6 +26,15 @@ export const ownerNotices = {
|
|
|
26
26
|
receivedInterrupting: () => "ℹ️ Message received. The agent's previous task was interrupted to prioritize "
|
|
27
27
|
+ 'this request, and it is now working on a response. '
|
|
28
28
|
+ 'The response will arrive in this channel when ready.',
|
|
29
|
+
/**
|
|
30
|
+
* The honest answer when the agent is mid-task and pre-empting it would have
|
|
31
|
+
* corrupted the conversation. Says "not started yet" rather than borrowing
|
|
32
|
+
* `receivedInterrupting`'s claim that something was cancelled for this
|
|
33
|
+
* request.
|
|
34
|
+
*/
|
|
35
|
+
receivedDeferred: () => 'ℹ️ Message received and held. The agent is in the middle of a task that '
|
|
36
|
+
+ 'cannot be interrupted safely; this request starts as soon as that work '
|
|
37
|
+
+ 'reaches a stopping point. The response will arrive in this channel when ready.',
|
|
29
38
|
status: (role, snapshot) => `📊 ${role} status: ${snapshot.readiness}; session is ${snapshot.alive ? 'online' : 'offline'}.`,
|
|
30
39
|
interrupted: (role) => `🛑 Interrupt sent to ${role}'s active turn.`,
|
|
31
40
|
/** The turn IS cancelled — say how, without implying the owner must retry. */
|
package/dist/resolved-plan.js
CHANGED
|
@@ -18,6 +18,7 @@ export function resolvedPlan(cfg) {
|
|
|
18
18
|
schemaVersion: RESOLVED_PLAN_SCHEMA_VERSION,
|
|
19
19
|
sourceFiles: [...cfg.files],
|
|
20
20
|
startStaggerMs: cfg.startStaggerMs,
|
|
21
|
+
harnesses: cfg.harnessPlugins,
|
|
21
22
|
diagnostics: cfg.diagnostics.map(diagnostic => ({ ...diagnostic })),
|
|
22
23
|
roles: cfg.roles.map(resolvedRolePlan),
|
|
23
24
|
loops: cfg.loops.map(loop => sortedObject({
|
package/dist/runner.d.ts
CHANGED
|
@@ -35,6 +35,15 @@ export interface RunnerDeps {
|
|
|
35
35
|
}
|
|
36
36
|
/** Environment injected only into the managed harness process. */
|
|
37
37
|
export declare function managedFleetProxyEnv(role: ResolvedRole, stateDir: string): Record<string, string>;
|
|
38
|
+
/**
|
|
39
|
+
* The environment a managed harness child actually receives, checked at the one
|
|
40
|
+
* point where it is composed. `role.env` deliberately wins over harness prep,
|
|
41
|
+
* which is exactly how a stale fleet-wide model pin used to outrank the model
|
|
42
|
+
* the role was spawned with — so the model pin is verified here rather than
|
|
43
|
+
* trusted, and a disagreement stops the launch instead of being reported as a
|
|
44
|
+
* success (see src/model-env.ts).
|
|
45
|
+
*/
|
|
46
|
+
export declare function harnessChildEnv(role: ResolvedRole, launchEnv: Record<string, string> | undefined, stateDir: string): Record<string, string>;
|
|
38
47
|
/**
|
|
39
48
|
* Record who owns wake delivery for this run. Returning true means a fleet
|
|
40
49
|
* monitor is taking ownership back from a native harness and must start at the
|
|
@@ -61,6 +70,23 @@ export declare function readExitRecord(path: string): ExitRecord | null;
|
|
|
61
70
|
export declare const RESTART_LEDGER_FILE = ".restart-ledger.json";
|
|
62
71
|
/** Consecutive immediate failures tolerated before the agent is held down. */
|
|
63
72
|
export declare const RESTART_FAIL_THRESHOLD = 5;
|
|
73
|
+
/**
|
|
74
|
+
* How the previous supervisor process ended.
|
|
75
|
+
*
|
|
76
|
+
* `abrupt` is the case the ledger used to miss entirely: an OOM-kill or any
|
|
77
|
+
* other external signal takes the supervisor down before it can write anything,
|
|
78
|
+
* the service manager restarts the unit, and every durable indicator still
|
|
79
|
+
* describes the run that died. A health check reading them reported "no
|
|
80
|
+
* restarts" for a role that had died and come back.
|
|
81
|
+
*/
|
|
82
|
+
export interface TerminationRecord {
|
|
83
|
+
class: 'clean' | 'abrupt' | 'unknown';
|
|
84
|
+
detail: string;
|
|
85
|
+
/** When the SURVIVING process observed it, not when it happened. */
|
|
86
|
+
observedAt: string;
|
|
87
|
+
/** Start time of the run that ended, when it was recorded. */
|
|
88
|
+
runStartedAt?: string;
|
|
89
|
+
}
|
|
64
90
|
export interface RestartLedger {
|
|
65
91
|
version: 1;
|
|
66
92
|
consecutiveImmediateFailures: number;
|
|
@@ -72,7 +98,29 @@ export interface RestartLedger {
|
|
|
72
98
|
updatedAt: string;
|
|
73
99
|
/** When the circuit opened, for the held-down status line. */
|
|
74
100
|
openedAt?: string;
|
|
101
|
+
/** How the previous supervisor process ended, including abnormal exits. */
|
|
102
|
+
lastTermination?: TerminationRecord;
|
|
103
|
+
/** Supervisor processes that died without closing their run marker. */
|
|
104
|
+
abruptTerminations?: number;
|
|
105
|
+
/** Start of the supervisor run that owns this state directory now. */
|
|
106
|
+
supervisorStartedAt?: string;
|
|
75
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Carried across a supervisor process's life so its successor can tell an
|
|
110
|
+
* orderly exit from a kill. Present on disk == "a supervisor believed it was
|
|
111
|
+
* running"; the next start finding one that is not its own is proof the
|
|
112
|
+
* previous process died without getting to write anything.
|
|
113
|
+
*/
|
|
114
|
+
export declare const RUN_MARKER_FILE = ".supervisor-run.json";
|
|
115
|
+
/**
|
|
116
|
+
* Claim this state directory for the current supervisor process and report how
|
|
117
|
+
* the previous one ended. Runs BEFORE the first attempt, which is the whole
|
|
118
|
+
* point: after an abrupt kill nothing else writes until an attempt finishes,
|
|
119
|
+
* and an attempt can take minutes.
|
|
120
|
+
*/
|
|
121
|
+
export declare function claimSupervisorRun(dir: string, startedAt: string, pid?: number): TerminationRecord;
|
|
122
|
+
/** Orderly exit: the successor must not read this run as a kill. */
|
|
123
|
+
export declare function releaseSupervisorRun(dir: string): void;
|
|
76
124
|
/** Bounded exponential backoff for the nth consecutive immediate failure. */
|
|
77
125
|
export declare function backoffFor(consecutiveFailures: number): number;
|
|
78
126
|
/** Read a role's restart ledger; a missing or corrupt one starts clean. */
|