@ours.network/fleet 0.18.0-nightly.6 → 0.18.1

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.
Files changed (60) hide show
  1. package/README.md +81 -43
  2. package/dist/application/fleet-query-service.js +12 -0
  3. package/dist/application/role-creation-service.js +3 -1
  4. package/dist/application/types.d.ts +11 -0
  5. package/dist/briefing.js +9 -2
  6. package/dist/build-info.json +5 -5
  7. package/dist/cli.js +37 -7
  8. package/dist/config.d.ts +6 -3
  9. package/dist/config.js +28 -14
  10. package/dist/creation.d.ts +14 -15
  11. package/dist/creation.js +19 -13
  12. package/dist/docs.d.ts +1 -1
  13. package/dist/docs.js +92 -33
  14. package/dist/doctor.d.ts +1 -5
  15. package/dist/doctor.js +11 -18
  16. package/dist/fleet-proxy.d.ts +5 -0
  17. package/dist/harness/acp-agent.js +11 -6
  18. package/dist/harness/claude-code.js +200 -11
  19. package/dist/harness/codex.d.ts +4 -1
  20. package/dist/harness/codex.js +70 -12
  21. package/dist/harness/types.d.ts +54 -4
  22. package/dist/loops/manager.d.ts +30 -1
  23. package/dist/loops/manager.js +69 -6
  24. package/dist/loops/state.d.ts +18 -0
  25. package/dist/loops/state.js +4 -0
  26. package/dist/model-env.d.ts +71 -0
  27. package/dist/model-env.js +106 -0
  28. package/dist/monitor.js +1 -1
  29. package/dist/ops.js +1 -1
  30. package/dist/owner-channel/attachments.d.ts +2 -25
  31. package/dist/owner-channel/attachments.js +5 -61
  32. package/dist/owner-channel/channel.d.ts +30 -29
  33. package/dist/owner-channel/channel.js +291 -291
  34. package/dist/owner-channel/mcp.d.ts +24 -0
  35. package/dist/owner-channel/mcp.js +145 -0
  36. package/dist/owner-channel/notices.d.ts +7 -0
  37. package/dist/owner-channel/notices.js +9 -0
  38. package/dist/runner.d.ts +48 -0
  39. package/dist/runner.js +237 -85
  40. package/dist/session/acp.d.ts +104 -0
  41. package/dist/session/acp.js +213 -10
  42. package/dist/session/activity.d.ts +31 -0
  43. package/dist/session/activity.js +48 -0
  44. package/dist/session/conversation-normalizer.d.ts +6 -0
  45. package/dist/session/conversation-normalizer.js +153 -10
  46. package/dist/session/conversation-types.d.ts +23 -4
  47. package/dist/session/types.d.ts +35 -0
  48. package/dist/spawn.js +29 -17
  49. package/dist/supervisor/systemd.js +2 -29
  50. package/dist/watchdog/briefing.js +7 -0
  51. package/dist/web-app/assets/{TerminalView-BAVk1Bot.js → TerminalView-C_G1ID2P.js} +1 -1
  52. package/dist/web-app/assets/{index-C3S-xFRU.js → index-BCBK78hw.js} +5 -5
  53. package/dist/web-app/index.html +1 -1
  54. package/dist/worklog.d.ts +7 -1
  55. package/dist/worklog.js +191 -39
  56. package/package.json +1 -3
  57. package/dist/owner-channel/message-recovery.d.ts +0 -25
  58. package/dist/owner-channel/message-recovery.js +0 -114
  59. package/dist/owner-channel/ours-client.d.ts +0 -148
  60. package/dist/owner-channel/ours-client.js +0 -231
@@ -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/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. */