@ours.network/fleet 0.18.0 → 0.19.0-nightly.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 (69) hide show
  1. package/README.md +57 -121
  2. package/dist/application/fleet-query-service.js +0 -12
  3. package/dist/application/role-creation-service.js +10 -7
  4. package/dist/application/types.d.ts +0 -11
  5. package/dist/briefing.js +6 -15
  6. package/dist/build-info.json +6 -7
  7. package/dist/capabilities.d.ts +1 -3
  8. package/dist/capabilities.js +0 -3
  9. package/dist/cli.js +7 -83
  10. package/dist/config.d.ts +3 -11
  11. package/dist/config.js +15 -40
  12. package/dist/creation.d.ts +38 -22
  13. package/dist/creation.js +111 -24
  14. package/dist/docs.d.ts +1 -1
  15. package/dist/docs.js +46 -120
  16. package/dist/doctor.d.ts +5 -1
  17. package/dist/doctor.js +18 -11
  18. package/dist/fleet-proxy.d.ts +0 -5
  19. package/dist/harness/acp-agent.js +6 -11
  20. package/dist/harness/claude-code.js +11 -204
  21. package/dist/harness/codex.d.ts +1 -4
  22. package/dist/harness/codex.js +12 -74
  23. package/dist/harness/types.d.ts +4 -54
  24. package/dist/index.d.ts +0 -2
  25. package/dist/index.js +0 -1
  26. package/dist/loops/manager.d.ts +1 -30
  27. package/dist/loops/manager.js +6 -69
  28. package/dist/loops/state.d.ts +0 -18
  29. package/dist/loops/state.js +0 -4
  30. package/dist/monitor.js +1 -1
  31. package/dist/ops.d.ts +3 -0
  32. package/dist/ops.js +8 -3
  33. package/dist/owner-channel/attachments.d.ts +25 -2
  34. package/dist/owner-channel/attachments.js +61 -5
  35. package/dist/owner-channel/channel.d.ts +29 -30
  36. package/dist/owner-channel/channel.js +291 -291
  37. package/dist/owner-channel/message-recovery.d.ts +25 -0
  38. package/dist/owner-channel/message-recovery.js +114 -0
  39. package/dist/owner-channel/notices.d.ts +0 -7
  40. package/dist/owner-channel/notices.js +0 -9
  41. package/dist/owner-channel/ours-client.d.ts +148 -0
  42. package/dist/owner-channel/ours-client.js +231 -0
  43. package/dist/resolved-plan.js +0 -1
  44. package/dist/runner.d.ts +0 -48
  45. package/dist/runner.js +94 -236
  46. package/dist/session/acp.d.ts +0 -104
  47. package/dist/session/acp.js +10 -213
  48. package/dist/session/conversation-normalizer.d.ts +0 -6
  49. package/dist/session/conversation-normalizer.js +10 -153
  50. package/dist/session/conversation-types.d.ts +4 -23
  51. package/dist/session/types.d.ts +0 -35
  52. package/dist/spawn.js +26 -33
  53. package/dist/supervisor/systemd.js +29 -2
  54. package/dist/watchdog/briefing.js +0 -7
  55. package/dist/watchdog/run.js +3 -3
  56. package/dist/web-app/assets/{TerminalView-C_G1ID2P.js → TerminalView-BAVk1Bot.js} +1 -1
  57. package/dist/web-app/assets/{index-BCBK78hw.js → index-C3S-xFRU.js} +5 -5
  58. package/dist/web-app/index.html +1 -1
  59. package/dist/worklog.d.ts +1 -7
  60. package/dist/worklog.js +39 -191
  61. package/package.json +3 -1
  62. package/dist/harness-plugins.d.ts +0 -48
  63. package/dist/harness-plugins.js +0 -309
  64. package/dist/model-env.d.ts +0 -71
  65. package/dist/model-env.js +0 -106
  66. package/dist/owner-channel/mcp.d.ts +0 -24
  67. package/dist/owner-channel/mcp.js +0 -145
  68. package/dist/session/activity.d.ts +0 -31
  69. package/dist/session/activity.js +0 -48
@@ -0,0 +1,25 @@
1
+ export interface PendingMessageClaim {
2
+ wireId: string;
3
+ seq: number;
4
+ claimedAt: number;
5
+ }
6
+ /**
7
+ * Body-free crash journal for the SQLite getMessages read boundary.
8
+ *
9
+ * A claim lands before getMessages marks its exact oldest-first batch read.
10
+ * After a crash, getHistoryItem can therefore recover the body by wire ID
11
+ * without fleet ever duplicating message plaintext in its own state.
12
+ */
13
+ export declare class MessageRecoveryState {
14
+ private readonly path;
15
+ private readonly limit;
16
+ private pending;
17
+ private corrupt;
18
+ constructor(path: string, limit?: number);
19
+ integrity(): boolean;
20
+ list(): PendingMessageClaim[];
21
+ claim(items: PendingMessageClaim[]): void;
22
+ pruneHandled(handled: (wireId: string) => boolean): number;
23
+ private assertHealthy;
24
+ private persist;
25
+ }
@@ -0,0 +1,114 @@
1
+ import { chmodSync, existsSync, readFileSync } from 'node:fs';
2
+ import { replaceFileAtomically } from '../atomic-file.js';
3
+ const MAX_PENDING_MESSAGES = 5_000;
4
+ const MAX_WIRE_ID_CHARS = 1_024;
5
+ /**
6
+ * Body-free crash journal for the SQLite getMessages read boundary.
7
+ *
8
+ * A claim lands before getMessages marks its exact oldest-first batch read.
9
+ * After a crash, getHistoryItem can therefore recover the body by wire ID
10
+ * without fleet ever duplicating message plaintext in its own state.
11
+ */
12
+ export class MessageRecoveryState {
13
+ path;
14
+ limit;
15
+ pending = [];
16
+ corrupt = false;
17
+ constructor(path, limit = MAX_PENDING_MESSAGES) {
18
+ this.path = path;
19
+ this.limit = limit;
20
+ if (!existsSync(path))
21
+ return;
22
+ try {
23
+ const raw = JSON.parse(readFileSync(path, 'utf8'));
24
+ if (raw.version !== 1 || !Array.isArray(raw.pending)
25
+ || raw.pending.length > limit || !raw.pending.every(validClaim)
26
+ || new Set(raw.pending.map(item => item.wireId)).size !== raw.pending.length)
27
+ throw new Error('invalid message recovery state');
28
+ this.pending = raw.pending.map(item => ({ ...item }));
29
+ chmodSync(path, 0o600);
30
+ }
31
+ catch {
32
+ this.corrupt = true;
33
+ this.pending = [];
34
+ try {
35
+ chmodSync(path, 0o600);
36
+ }
37
+ catch { /* retain the evidence */ }
38
+ }
39
+ }
40
+ integrity() { return !this.corrupt; }
41
+ list() {
42
+ this.assertHealthy();
43
+ return this.pending.map(item => ({ ...item }));
44
+ }
45
+ claim(items) {
46
+ this.assertHealthy();
47
+ if (!items.length)
48
+ return;
49
+ if (!items.every(validClaim))
50
+ throw new Error('invalid message recovery claim');
51
+ const next = this.pending.map(item => ({ ...item }));
52
+ const byWire = new Map(next.map(item => [item.wireId, item]));
53
+ for (const item of items) {
54
+ const existing = byWire.get(item.wireId);
55
+ if (existing) {
56
+ if (existing.seq !== item.seq)
57
+ throw new Error('message recovery wire ID changed sequence');
58
+ continue;
59
+ }
60
+ if (next.length >= this.limit)
61
+ throw new Error('too many pending message recoveries');
62
+ const copy = { ...item };
63
+ next.push(copy);
64
+ byWire.set(copy.wireId, copy);
65
+ }
66
+ if (next.length === this.pending.length)
67
+ return;
68
+ const previous = this.pending;
69
+ this.pending = next;
70
+ try {
71
+ this.persist();
72
+ }
73
+ catch (error) {
74
+ this.pending = previous;
75
+ throw error;
76
+ }
77
+ }
78
+ pruneHandled(handled) {
79
+ this.assertHealthy();
80
+ const next = this.pending.filter(item => !handled(item.wireId));
81
+ const removed = this.pending.length - next.length;
82
+ if (!removed)
83
+ return 0;
84
+ const previous = this.pending;
85
+ this.pending = next;
86
+ try {
87
+ this.persist();
88
+ }
89
+ catch (error) {
90
+ this.pending = previous;
91
+ throw error;
92
+ }
93
+ return removed;
94
+ }
95
+ assertHealthy() {
96
+ if (this.corrupt)
97
+ throw new Error('message recovery state is corrupt');
98
+ }
99
+ persist() {
100
+ replaceFileAtomically(this.path, JSON.stringify({
101
+ version: 1, pending: this.pending,
102
+ }) + '\n', 0o600);
103
+ chmodSync(this.path, 0o600);
104
+ }
105
+ }
106
+ function validClaim(value) {
107
+ if (!value || typeof value !== 'object')
108
+ return false;
109
+ const item = value;
110
+ return typeof item.wireId === 'string' && item.wireId.length >= 1
111
+ && Array.from(item.wireId).length <= MAX_WIRE_ID_CHARS
112
+ && Number.isSafeInteger(item.seq) && item.seq >= 1
113
+ && Number.isSafeInteger(item.claimedAt) && item.claimedAt >= 0;
114
+ }
@@ -22,13 +22,6 @@ 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;
32
25
  status: (role: string, snapshot: SessionSnapshot) => string;
33
26
  interrupted: (role: string) => string;
34
27
  /** The turn IS cancelled — say how, without implying the owner must retry. */
@@ -26,15 +26,6 @@ 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.',
38
29
  status: (role, snapshot) => `📊 ${role} status: ${snapshot.readiness}; session is ${snapshot.alive ? 'online' : 'offline'}.`,
39
30
  interrupted: (role) => `🛑 Interrupt sent to ${role}'s active turn.`,
40
31
  /** The turn IS cancelled — say how, without implying the owner must retry. */
@@ -0,0 +1,148 @@
1
+ import { OursClient, type AttachOursClientOptions, type NotificationEvent } from '@ours.network/sdk/client';
2
+ /** Any failure of a daemon operation. Never carries a message body or a token. */
3
+ export declare class OursDaemonError extends Error {
4
+ }
5
+ /**
6
+ * The daemon accepted the call and answered "not sent". The MCP surface reported
7
+ * exactly these verdicts as tool errors, so they must keep throwing here: the
8
+ * owner channel books a resolved send as delivered, and a silently-swallowed
9
+ * refusal would be recorded as a delivered message that never left the host.
10
+ */
11
+ export declare class OursSendRefusedError extends OursDaemonError {
12
+ }
13
+ type Res<M extends keyof OursClient> = OursClient[M] extends (...args: never[]) => infer R ? Awaited<R> : never;
14
+ export type OursContactsView = Res<'listContacts'>;
15
+ export type OursInviteResult = Res<'generateInvite'>;
16
+ export type OursAddContactResult = Res<'addContact'>;
17
+ export type OursIncomingMessage = Res<'listIncomingMessages'>[number];
18
+ export type OursMessagesPayload = Res<'getMessages'>;
19
+ export type OursInboundMessage = OursMessagesPayload['messages'][number];
20
+ export type OursHistoryMessage = NonNullable<Res<'getHistoryItem'>>;
21
+ export type OursIncomingFile = Res<'listIncomingFiles'>[number];
22
+ export type OursRetrievedFiles = Res<'getFiles'>;
23
+ export type OursRetrievedFile = OursRetrievedFiles['files'][number];
24
+ export type OursHistoryFile = NonNullable<Res<'getFileInfo'>>;
25
+ export type OursNotificationEvent = NotificationEvent;
26
+ export declare class OursWatchDeadlineError extends OursDaemonError {
27
+ }
28
+ /**
29
+ * The daemon operations the owner channel needs, one typed method each.
30
+ *
31
+ * This interface deliberately has no generic `callTool(name, args): unknown`
32
+ * escape hatch. The legacy MCP connector answered every
33
+ * tool with `{content:[{type:'text',...}]}` and no `structuredContent`, the
34
+ * transport fell back to returning the daemon's English sentence — which the
35
+ * channel then pattern-matched (an invite blob sliced out of a prose sentence,
36
+ * a bind conflict detected with /currently bound to another live session/i).
37
+ * With no untyped result there is nothing left to pattern-match.
38
+ */
39
+ export interface OursOps {
40
+ /** Prepare the transport. Must be called before any operation. */
41
+ start(): Promise<void>;
42
+ /** Bind this session's identity. Throws `BOUND_ELSEWHERE` when it is held live. */
43
+ bindIdentity(name: string): Promise<void>;
44
+ listContacts(): Promise<OursContactsView>;
45
+ generateInvite(name?: string): Promise<OursInviteResult>;
46
+ addContact(a: {
47
+ invite: string;
48
+ name?: string;
49
+ }): Promise<OursAddContactResult>;
50
+ listIncomingMessages(): Promise<OursIncomingMessage[]>;
51
+ getMessages(limit: number): Promise<OursMessagesPayload>;
52
+ getHistoryItem(wireId: string): Promise<OursHistoryMessage | null>;
53
+ watchNotifications(identity: string, options?: {
54
+ since?: number | 'tip';
55
+ signal?: AbortSignal;
56
+ }): AsyncGenerator<OursNotificationEvent, void, undefined>;
57
+ listIncomingFiles(): Promise<OursIncomingFile[]>;
58
+ getFileInfo(wireId: string): Promise<OursHistoryFile | null>;
59
+ getFiles(wireIds: string[]): Promise<OursRetrievedFiles>;
60
+ /**
61
+ * The bytes of an already-retrieved file. Transport only — the caller owns
62
+ * where they land, so path safety stays with the attachment code that already
63
+ * enforces it (`writeRecoveredAttachment`).
64
+ */
65
+ fetchFile(wireId: string): Promise<Uint8Array>;
66
+ sendMessage(a: {
67
+ contact: string;
68
+ text: string;
69
+ replyToWireId?: string;
70
+ }): Promise<void>;
71
+ sendFile(a: {
72
+ contact: string;
73
+ path: string;
74
+ filename: string;
75
+ replyToWireId?: string;
76
+ }): Promise<void>;
77
+ /** Release the daemon lease and stop. Never throws. */
78
+ close(): Promise<void>;
79
+ }
80
+ /**
81
+ * The typed error code of a daemon operation, or undefined when the failure was
82
+ * not one (transport, abort, programming error). `instanceof` is checked first;
83
+ * the structural fallback keeps a duplicated SDK copy in a consumer's tree from
84
+ * silently demoting a real daemon verdict to "unknown transport failure".
85
+ */
86
+ export declare function oursErrorCode(error: unknown): string | undefined;
87
+ /** The identity is bound by another live session; a predecessor may still be releasing it. */
88
+ export declare const OURS_BOUND_ELSEWHERE = "BOUND_ELSEWHERE";
89
+ export interface OursSdkClientDeps {
90
+ /** Test seam; production uses the SDK's coherence-checking application attach path. */
91
+ attachClient?(options: AttachOursClientOptions): OursClient | Promise<OursClient>;
92
+ /** Underlying transport and deadline seams for deterministic half-open tests. */
93
+ fetch?: typeof globalThis.fetch;
94
+ notificationRequestDeadlineMs?: number;
95
+ readFile?(path: string): Promise<Uint8Array>;
96
+ }
97
+ /**
98
+ * The owner-channel's daemon client: one `OursClient` over the local ours HTTP
99
+ * API, owning exactly one identity binding.
100
+ *
101
+ * Lease lifetime. The lease token IS the session, so each channel instance mints
102
+ * its own and hands it back in `close()`. That replaces the connector proxy's
103
+ * shell-PID fence, which existed because a supervised attempt had to make its
104
+ * lease reclaimable while the supervisor itself stayed alive: an explicit
105
+ * release does that deterministically, and `clientPid` still covers the case
106
+ * where the whole supervisor dies without unwinding.
107
+ */
108
+ export declare class OursSdkClient implements OursOps {
109
+ private readonly env;
110
+ private readonly log;
111
+ private readonly deps;
112
+ private client?;
113
+ private readonly leaseToken;
114
+ constructor(env?: Record<string, string>, log?: (line: string) => void, deps?: OursSdkClientDeps);
115
+ start(): Promise<void>;
116
+ bindIdentity(name: string): Promise<void>;
117
+ listContacts(): Promise<OursContactsView>;
118
+ generateInvite(name?: string): Promise<OursInviteResult>;
119
+ addContact(a: {
120
+ invite: string;
121
+ name?: string;
122
+ }): Promise<OursAddContactResult>;
123
+ listIncomingMessages(): Promise<OursIncomingMessage[]>;
124
+ getMessages(limit: number): Promise<OursMessagesPayload>;
125
+ getHistoryItem(wireId: string): Promise<OursHistoryMessage | null>;
126
+ watchNotifications(identity: string, options?: {
127
+ since?: number | 'tip';
128
+ signal?: AbortSignal;
129
+ }): AsyncGenerator<OursNotificationEvent, void, undefined>;
130
+ listIncomingFiles(): Promise<OursIncomingFile[]>;
131
+ getFileInfo(wireId: string): Promise<OursHistoryFile | null>;
132
+ getFiles(wireIds: string[]): Promise<OursRetrievedFiles>;
133
+ fetchFile(wireId: string): Promise<Uint8Array>;
134
+ sendMessage(a: {
135
+ contact: string;
136
+ text: string;
137
+ replyToWireId?: string;
138
+ }): Promise<void>;
139
+ sendFile(a: {
140
+ contact: string;
141
+ path: string;
142
+ filename: string;
143
+ replyToWireId?: string;
144
+ }): Promise<void>;
145
+ close(): Promise<void>;
146
+ private ops;
147
+ }
148
+ export {};
@@ -0,0 +1,231 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { extname } from 'node:path';
4
+ import { OursError, attachOursClient, } from '@ours.network/sdk/client';
5
+ /** Any failure of a daemon operation. Never carries a message body or a token. */
6
+ export class OursDaemonError extends Error {
7
+ }
8
+ /**
9
+ * The daemon accepted the call and answered "not sent". The MCP surface reported
10
+ * exactly these verdicts as tool errors, so they must keep throwing here: the
11
+ * owner channel books a resolved send as delivered, and a silently-swallowed
12
+ * refusal would be recorded as a delivered message that never left the host.
13
+ */
14
+ export class OursSendRefusedError extends OursDaemonError {
15
+ }
16
+ // The daemon normally returns a quiet long-poll within 25 seconds. Keep the
17
+ // client-side fence comfortably above it so ordinary quiet periods do not
18
+ // recycle a healthy stream, while a half-open socket still heals on its own.
19
+ const NOTIFICATION_REQUEST_DEADLINE_MS = 120_000;
20
+ // SDK sendFile({path}) inferred these common types before reading the path in
21
+ // the daemon process. Staged uploads move that read into fleet, so preserve the
22
+ // same advertised MIME rather than silently turning every attachment into an
23
+ // octet stream. Importing the SDK root just for its helper would also pull the
24
+ // daemon runtime into this client-only process.
25
+ const FILE_MIME_BY_EXTENSION = {
26
+ '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif',
27
+ '.webp': 'image/webp', '.svg': 'image/svg+xml', '.bmp': 'image/bmp', '.ico': 'image/x-icon',
28
+ '.pdf': 'application/pdf', '.txt': 'text/plain', '.md': 'text/markdown',
29
+ '.json': 'application/json', '.csv': 'text/csv', '.html': 'text/html',
30
+ '.xml': 'application/xml', '.zip': 'application/zip', '.gz': 'application/gzip',
31
+ '.tar': 'application/x-tar', '.mp3': 'audio/mpeg', '.wav': 'audio/wav',
32
+ '.mp4': 'video/mp4', '.mov': 'video/quicktime',
33
+ };
34
+ function mimeFromFilename(filename) {
35
+ return FILE_MIME_BY_EXTENSION[extname(filename).toLowerCase()] ?? 'application/octet-stream';
36
+ }
37
+ export class OursWatchDeadlineError extends OursDaemonError {
38
+ }
39
+ function notificationRequest(input) {
40
+ const url = typeof input === 'string' || input instanceof URL ? String(input) : input.url;
41
+ return /\/identities\/[^/]+\/notifications(?:\?|$)/.test(url);
42
+ }
43
+ function notificationDeadlineFetch(fetchImpl, deadlineMs) {
44
+ return async (input, init) => {
45
+ if (!notificationRequest(input))
46
+ return fetchImpl(input, init);
47
+ const ctrl = new AbortController();
48
+ const upstream = init?.signal;
49
+ let rejectFence;
50
+ const fence = new Promise((_resolve, reject) => { rejectFence = reject; });
51
+ const abortUpstream = () => {
52
+ const reason = upstream?.reason
53
+ ?? new DOMException('ours notification request aborted', 'AbortError');
54
+ ctrl.abort(reason);
55
+ // Shutdown must not wait for the deadline when a custom fetch ignores
56
+ // AbortSignal either.
57
+ rejectFence(reason);
58
+ };
59
+ if (upstream?.aborted)
60
+ abortUpstream();
61
+ else
62
+ upstream?.addEventListener('abort', abortUpstream, { once: true });
63
+ const timer = setTimeout(() => {
64
+ const error = new OursWatchDeadlineError('ours notification request deadline exceeded');
65
+ ctrl.abort(error);
66
+ // Do not rely on a custom or half-open fetch implementation to honor
67
+ // AbortSignal: the fence itself must always settle the request.
68
+ rejectFence(error);
69
+ }, deadlineMs);
70
+ timer.unref?.();
71
+ try {
72
+ return await Promise.race([
73
+ fetchImpl(input, { ...init, signal: ctrl.signal }),
74
+ fence,
75
+ ]);
76
+ }
77
+ finally {
78
+ clearTimeout(timer);
79
+ upstream?.removeEventListener('abort', abortUpstream);
80
+ }
81
+ };
82
+ }
83
+ /**
84
+ * The typed error code of a daemon operation, or undefined when the failure was
85
+ * not one (transport, abort, programming error). `instanceof` is checked first;
86
+ * the structural fallback keeps a duplicated SDK copy in a consumer's tree from
87
+ * silently demoting a real daemon verdict to "unknown transport failure".
88
+ */
89
+ export function oursErrorCode(error) {
90
+ if (error instanceof OursError)
91
+ return error.code;
92
+ if (error instanceof Error && error.name === 'OursError') {
93
+ const code = error.code;
94
+ if (typeof code === 'string')
95
+ return code;
96
+ }
97
+ return undefined;
98
+ }
99
+ /** The identity is bound by another live session; a predecessor may still be releasing it. */
100
+ export const OURS_BOUND_ELSEWHERE = 'BOUND_ELSEWHERE';
101
+ /**
102
+ * The owner-channel's daemon client: one `OursClient` over the local ours HTTP
103
+ * API, owning exactly one identity binding.
104
+ *
105
+ * Lease lifetime. The lease token IS the session, so each channel instance mints
106
+ * its own and hands it back in `close()`. That replaces the connector proxy's
107
+ * shell-PID fence, which existed because a supervised attempt had to make its
108
+ * lease reclaimable while the supervisor itself stayed alive: an explicit
109
+ * release does that deterministically, and `clientPid` still covers the case
110
+ * where the whole supervisor dies without unwinding.
111
+ */
112
+ export class OursSdkClient {
113
+ env;
114
+ log;
115
+ deps;
116
+ client;
117
+ leaseToken = `ours-fleet-owner-${process.pid}-${randomUUID()}`;
118
+ constructor(env = {}, log = () => undefined, deps = {}) {
119
+ this.env = env;
120
+ this.log = log;
121
+ this.deps = deps;
122
+ }
123
+ async start() {
124
+ if (this.client)
125
+ return;
126
+ const environment = { ...process.env, ...this.env };
127
+ // SDK 2's supported application path resolves endpoint, state root, and
128
+ // token as one coherent selection, proves the daemon's state root before
129
+ // sending credentials, and only then constructs the client.
130
+ const options = {
131
+ env: environment,
132
+ leaseToken: this.leaseToken,
133
+ clientPid: process.pid,
134
+ fetch: notificationDeadlineFetch(this.deps.fetch ?? globalThis.fetch, this.deps.notificationRequestDeadlineMs ?? NOTIFICATION_REQUEST_DEADLINE_MS),
135
+ };
136
+ this.client = await (this.deps.attachClient?.(options) ?? attachOursClient(options));
137
+ }
138
+ async bindIdentity(name) {
139
+ // force is pinned off: the owner channel never evicts another live session
140
+ // from an identity, it waits for the bounded handoff window and then fails.
141
+ await this.ops().chooseIdentity({ name, force: false });
142
+ }
143
+ async listContacts() {
144
+ return this.ops().listContacts();
145
+ }
146
+ async generateInvite(name) {
147
+ return this.ops().generateInvite(name ? { name } : {});
148
+ }
149
+ async addContact(a) {
150
+ return this.ops().addContact({ invite: a.invite, ...(a.name ? { name: a.name } : {}) });
151
+ }
152
+ async listIncomingMessages() {
153
+ return this.ops().listIncomingMessages();
154
+ }
155
+ async getMessages(limit) {
156
+ return this.ops().getMessages({ limit });
157
+ }
158
+ async getHistoryItem(wireId) {
159
+ return this.ops().getHistoryItem({ wire_id: wireId });
160
+ }
161
+ watchNotifications(identity, options) {
162
+ return this.ops().watchNotifications(identity, options);
163
+ }
164
+ async listIncomingFiles() {
165
+ return this.ops().listIncomingFiles();
166
+ }
167
+ async getFileInfo(wireId) {
168
+ return this.ops().getFileInfo({ wire_id: wireId });
169
+ }
170
+ async getFiles(wireIds) {
171
+ return this.ops().getFiles({ wire_ids: wireIds });
172
+ }
173
+ async fetchFile(wireId) {
174
+ // save_file has no SDK operation on purpose: that daemon route only reports
175
+ // that a too-old connector reached it. The bytes of a retrieved file are
176
+ // already on disk, so read them back and let the caller write them as this
177
+ // process's own OS user.
178
+ return this.ops().fetchFile(wireId);
179
+ }
180
+ async sendMessage(a) {
181
+ const verdict = await this.ops().sendMessage({
182
+ contact: a.contact, text: a.text,
183
+ ...(a.replyToWireId ? { reply_to_wire_id: a.replyToWireId } : {}),
184
+ });
185
+ // Legacy parity: only `refused` was a tool error. `migrating`,
186
+ // `deferred` and `e2e` are accepted-and-queued outcomes that it reported as
187
+ // success, so they must not become failures here.
188
+ if (verdict.kind === 'refused')
189
+ throw new OursSendRefusedError('the daemon refused the message: the contact\'s end-to-end session must be '
190
+ + 're-established after an upgrade; it was not sent and not downgraded');
191
+ }
192
+ async sendFile(a) {
193
+ // The shared daemon may be owned by a different OS process/user and must
194
+ // never be expected to open fleet-private outbox paths. SDK 2 stages bytes
195
+ // read by THIS process, then sends only the opaque upload receipt.
196
+ const bytes = await (this.deps.readFile?.(a.path) ?? readFile(a.path));
197
+ const staged = await this.ops().uploadFile(bytes, {
198
+ filename: a.filename, mime: mimeFromFilename(a.filename),
199
+ });
200
+ const verdict = await this.ops().sendFile({
201
+ contact: a.contact, upload_id: staged.upload_id, filename: a.filename,
202
+ ...(a.replyToWireId ? { reply_to_wire_id: a.replyToWireId } : {}),
203
+ });
204
+ // Legacy parity treated `migrating` as an error for
205
+ // files and as success for messages: files are not auto-queued behind a
206
+ // migration, so "queued" would be a false delivery claim.
207
+ if (verdict.kind === 'refused' || verdict.kind === 'migrating')
208
+ throw new OursSendRefusedError(`the daemon did not send the file (${verdict.kind}): the contact's end-to-end `
209
+ + 'session must be re-established after an upgrade; files are not queued');
210
+ }
211
+ async close() {
212
+ const client = this.client;
213
+ this.client = undefined;
214
+ if (!client)
215
+ return;
216
+ // Handing the lease back is what lets a successor bind this identity without
217
+ // waiting for the supervisor to exit. A failure here is not fatal — the
218
+ // daemon still reclaims the lease when this process dies.
219
+ try {
220
+ await client.releaseLease();
221
+ }
222
+ catch (error) {
223
+ this.log(`lease release failed: ${error?.message ?? String(error)}`);
224
+ }
225
+ }
226
+ ops() {
227
+ if (!this.client)
228
+ throw new OursDaemonError('ours daemon client is not started');
229
+ return this.client;
230
+ }
231
+ }
@@ -18,7 +18,6 @@ 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,
22
21
  diagnostics: cfg.diagnostics.map(diagnostic => ({ ...diagnostic })),
23
22
  roles: cfg.roles.map(resolvedRolePlan),
24
23
  loops: cfg.loops.map(loop => sortedObject({
package/dist/runner.d.ts CHANGED
@@ -35,15 +35,6 @@ 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>;
47
38
  /**
48
39
  * Record who owns wake delivery for this run. Returning true means a fleet
49
40
  * monitor is taking ownership back from a native harness and must start at the
@@ -70,23 +61,6 @@ export declare function readExitRecord(path: string): ExitRecord | null;
70
61
  export declare const RESTART_LEDGER_FILE = ".restart-ledger.json";
71
62
  /** Consecutive immediate failures tolerated before the agent is held down. */
72
63
  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
- }
90
64
  export interface RestartLedger {
91
65
  version: 1;
92
66
  consecutiveImmediateFailures: number;
@@ -98,29 +72,7 @@ export interface RestartLedger {
98
72
  updatedAt: string;
99
73
  /** When the circuit opened, for the held-down status line. */
100
74
  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;
107
75
  }
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;
124
76
  /** Bounded exponential backoff for the nth consecutive immediate failure. */
125
77
  export declare function backoffFor(consecutiveFailures: number): number;
126
78
  /** Read a role's restart ledger; a missing or corrupt one starts clean. */