@ours.network/fleet 0.12.0 → 0.13.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 (40) hide show
  1. package/README.md +184 -0
  2. package/dist/briefing.js +10 -0
  3. package/dist/cli.js +404 -1
  4. package/dist/config.d.ts +18 -2
  5. package/dist/config.js +67 -3
  6. package/dist/docs.d.ts +1 -1
  7. package/dist/docs.js +111 -0
  8. package/dist/duration.js +7 -3
  9. package/dist/harness/claude-code.js +5 -0
  10. package/dist/harness/types.d.ts +6 -0
  11. package/dist/loops/config.d.ts +30 -0
  12. package/dist/loops/config.js +135 -0
  13. package/dist/loops/manager.d.ts +48 -0
  14. package/dist/loops/manager.js +237 -0
  15. package/dist/loops/state.d.ts +54 -0
  16. package/dist/loops/state.js +148 -0
  17. package/dist/monitor.js +26 -2
  18. package/dist/owner-channel/attachments.d.ts +74 -0
  19. package/dist/owner-channel/attachments.js +378 -0
  20. package/dist/owner-channel/channel.d.ts +114 -2
  21. package/dist/owner-channel/channel.js +622 -43
  22. package/dist/owner-channel/notices.d.ts +21 -0
  23. package/dist/owner-channel/notices.js +66 -0
  24. package/dist/owner-channel/state.d.ts +34 -0
  25. package/dist/owner-channel/state.js +148 -1
  26. package/dist/owner-channel/tasks.d.ts +62 -0
  27. package/dist/owner-channel/tasks.js +246 -0
  28. package/dist/resolved-plan.js +11 -0
  29. package/dist/runner.js +87 -7
  30. package/dist/session/acp.d.ts +5 -2
  31. package/dist/session/acp.js +83 -25
  32. package/dist/session/arbiter.d.ts +42 -0
  33. package/dist/session/arbiter.js +72 -0
  34. package/dist/session/control.d.ts +12 -1
  35. package/dist/session/control.js +56 -3
  36. package/dist/session/types.d.ts +26 -2
  37. package/dist/session/types.js +5 -2
  38. package/dist/spawn.js +1 -0
  39. package/dist/supervisor/systemd.js +12 -2
  40. package/package.json +1 -1
@@ -0,0 +1,378 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { chmodSync, constants, existsSync, lstatSync, mkdirSync, readFileSync, } from 'node:fs';
3
+ import { chmod, link, lstat, mkdir, open, readdir, rm } from 'node:fs/promises';
4
+ import { basename, join } from 'node:path';
5
+ import { replaceFileAtomically } from '../atomic-file.js';
6
+ const WIRE = /^[A-Fa-f0-9]{64}$/;
7
+ const CID = /^[A-Fa-f0-9]{64}$/;
8
+ const MAX_PENDING_REQUESTS = 32;
9
+ export function parseIncomingAttachments(raw) {
10
+ const values = raw?.files;
11
+ if (!Array.isArray(values))
12
+ return [];
13
+ const out = [];
14
+ for (const value of values) {
15
+ if (!value || typeof value !== 'object')
16
+ continue;
17
+ const file = value;
18
+ const from = file.from;
19
+ const reply = file.reply_to;
20
+ const wireId = String(file.wire_id ?? '');
21
+ const senderId = String(from?.id ?? '');
22
+ const size = Number(file.size);
23
+ const fileId = Number(file.file_id);
24
+ if (!WIRE.test(wireId) || !CID.test(senderId) || !Number.isSafeInteger(size) || size < 0
25
+ || !Number.isSafeInteger(fileId) || fileId < 1)
26
+ continue;
27
+ const replyWire = String(reply?.wire_id ?? '');
28
+ out.push({
29
+ fileId, wireId, senderId, senderName: safeField(from?.name, 160),
30
+ filename: safeField(file.filename, 255), mime: String(file.mime ?? '').toLowerCase(),
31
+ size, status: String(file.status ?? ''), date: safeField(file.date, 80),
32
+ kind: file.kind === 'voice_message' ? 'voice_message' : 'file',
33
+ replyTo: WIRE.test(replyWire) ? { wire_id: replyWire,
34
+ ...(Number.isSafeInteger(reply?.sentence) && Number(reply?.sentence) > 0
35
+ ? { sentence: Number(reply?.sentence) } : {}) } : null,
36
+ });
37
+ }
38
+ return out;
39
+ }
40
+ export function parseRetrievedAttachments(raw, expected, recovered = false) {
41
+ const values = raw?.files;
42
+ if (!Array.isArray(values) || values.length !== expected.length)
43
+ throw new Error('ours-mcp returned an incomplete selected attachment set');
44
+ const byWire = new Map(expected.map(file => [file.wireId, file]));
45
+ const out = [];
46
+ for (const value of values) {
47
+ if (!value || typeof value !== 'object')
48
+ throw new Error('ours-mcp returned invalid attachment metadata');
49
+ const file = value;
50
+ const wireId = String(file.wire_id ?? '');
51
+ const listed = byWire.get(wireId);
52
+ const from = file.from;
53
+ const size = Number(file.size);
54
+ const sha256 = String(file.sha256 ?? '');
55
+ const mime = String(file.mime ?? '').toLowerCase();
56
+ const kind = file.kind === 'voice_message' ? 'voice_message' : 'file';
57
+ if (!listed || String(from?.id ?? '') !== listed.senderId
58
+ || !Number.isSafeInteger(size) || size !== listed.size || mime !== listed.mime
59
+ || kind !== listed.kind || !/^[a-f0-9]{64}$/.test(sha256)
60
+ || typeof file.path !== 'string' || !file.path)
61
+ throw new Error('ours-mcp selected attachment provenance or integrity metadata mismatched');
62
+ out.push({
63
+ ...listed, filename: safeField(file.filename, 255), mime, size, path: file.path, sha256, kind,
64
+ ...(recovered ? {} : parseTranscription(file.transcription, wireId)),
65
+ });
66
+ byWire.delete(wireId);
67
+ }
68
+ if (byWire.size)
69
+ throw new Error('ours-mcp omitted a selected attachment');
70
+ return out;
71
+ }
72
+ function parseTranscription(value, wireId) {
73
+ if (!value || typeof value !== 'object')
74
+ return {};
75
+ const item = value;
76
+ if (!['succeeded', 'failed', 'unavailable'].includes(String(item.status ?? ''))
77
+ || String(item.file_wire_id ?? '') !== wireId)
78
+ return {};
79
+ const status = item.status;
80
+ return { transcription: {
81
+ configured: item.configured === true, attempted: item.attempted === true, status,
82
+ provider: typeof item.provider === 'string' ? safeField(item.provider, 80) : null,
83
+ text: status === 'succeeded' && typeof item.text === 'string'
84
+ ? safeField(item.text, 16_000) : null,
85
+ errorCategory: typeof item.error_category === 'string'
86
+ ? safeField(item.error_category, 80) : null,
87
+ audioPath: typeof item.audio_path === 'string' ? item.audio_path : '',
88
+ fileWireId: wireId,
89
+ } };
90
+ }
91
+ export function validateAttachmentSelection(files, config) {
92
+ if (!config.enabled)
93
+ return 'attachments are disabled for this owner channel';
94
+ if (files.length > config.max_files_per_request)
95
+ return `the request exceeds the ${config.max_files_per_request}-file limit`;
96
+ let total = 0;
97
+ for (const file of files) {
98
+ if (!config.allowed_mime.includes(file.mime))
99
+ return `MIME type ${file.mime || '(missing)'} is not allowed`;
100
+ if (file.size > config.max_file_bytes)
101
+ return `a file exceeds the ${config.max_file_bytes}-byte limit`;
102
+ total += file.size;
103
+ if (!Number.isSafeInteger(total) || total > config.max_request_bytes)
104
+ return `the request exceeds the ${config.max_request_bytes}-byte total limit`;
105
+ }
106
+ return undefined;
107
+ }
108
+ export async function prepareAttachmentDirectory(root, requestId) {
109
+ mkdirSync(root, { recursive: true, mode: 0o700 });
110
+ const rootStat = lstatSync(root);
111
+ if (!rootStat.isDirectory() || rootStat.isSymbolicLink())
112
+ throw new Error('attachment root is not a safe directory');
113
+ chmodSync(root, 0o700);
114
+ const dir = join(root, requestId);
115
+ if (existsSync(dir))
116
+ await removeRequestDirectory(dir);
117
+ await mkdir(dir, { mode: 0o700 });
118
+ const stat = await lstat(dir);
119
+ if (!stat.isDirectory() || stat.isSymbolicLink())
120
+ throw new Error('attachment request path is not a safe directory');
121
+ await chmod(dir, 0o700);
122
+ return dir;
123
+ }
124
+ export async function admitAttachments(files, dir, config) {
125
+ const admitted = [];
126
+ let total = 0;
127
+ for (let index = 0; index < files.length; index++) {
128
+ const file = files[index];
129
+ const sourceStat = await lstat(file.path);
130
+ if (!sourceStat.isFile() || sourceStat.isSymbolicLink())
131
+ throw new Error('retrieved attachment path is not a regular file');
132
+ if (sourceStat.size > config.max_file_bytes)
133
+ throw new Error('retrieved attachment exceeds its size limit');
134
+ const handle = await open(file.path, constants.O_RDONLY | constants.O_NOFOLLOW);
135
+ let bytes;
136
+ try {
137
+ const opened = await handle.stat();
138
+ if (!opened.isFile() || opened.dev !== sourceStat.dev || opened.ino !== sourceStat.ino)
139
+ throw new Error('retrieved attachment changed during admission');
140
+ bytes = await handle.readFile();
141
+ }
142
+ finally {
143
+ await handle.close();
144
+ }
145
+ const digest = createHash('sha256').update(bytes).digest('hex');
146
+ if (bytes.length !== file.size || digest !== file.sha256)
147
+ throw new Error('retrieved attachment size or hash mismatched structured metadata');
148
+ total += bytes.length;
149
+ if (total > config.max_request_bytes)
150
+ throw new Error('retrieved attachments exceed the request size limit');
151
+ const detectedMime = detectMime(bytes, file.mime);
152
+ if (!mimeCompatible(file.mime, detectedMime))
153
+ throw new Error(`retrieved attachment content does not match declared MIME ${file.mime}`);
154
+ const filename = sanitizeFilename(file.filename);
155
+ const finalPath = join(dir, `${index + 1}-${file.wireId.slice(0, 12)}-${filename}`);
156
+ const tmp = join(dir, `.${basename(finalPath)}.${randomUUID()}.tmp`);
157
+ const output = await open(tmp, 'wx', 0o600);
158
+ try {
159
+ await output.writeFile(bytes);
160
+ await output.sync();
161
+ }
162
+ catch (error) {
163
+ await output.close().catch(() => { });
164
+ await rm(tmp, { force: true });
165
+ throw error;
166
+ }
167
+ await output.close();
168
+ try {
169
+ await link(tmp, finalPath);
170
+ }
171
+ catch (error) {
172
+ await rm(tmp, { force: true });
173
+ throw error;
174
+ }
175
+ await rm(tmp, { force: true });
176
+ await chmod(finalPath, 0o600);
177
+ admitted.push({
178
+ wireId: file.wireId, filename, path: finalPath, declaredMime: file.mime,
179
+ detectedMime, size: bytes.length, sha256: digest, kind: file.kind,
180
+ ...(file.transcription ? { transcription: {
181
+ configured: file.transcription.configured, attempted: file.transcription.attempted,
182
+ status: file.transcription.status, provider: file.transcription.provider,
183
+ text: file.transcription.text, errorCategory: file.transcription.errorCategory,
184
+ fileWireId: file.transcription.fileWireId,
185
+ } } : {}),
186
+ });
187
+ }
188
+ return admitted;
189
+ }
190
+ export async function recoveredAttachment(file, path) {
191
+ const stat = await lstat(path);
192
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size !== file.size)
193
+ throw new Error('recovered attachment is not the expected regular file');
194
+ await chmod(path, 0o600);
195
+ const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
196
+ let bytes;
197
+ try {
198
+ bytes = await handle.readFile();
199
+ }
200
+ finally {
201
+ await handle.close();
202
+ }
203
+ return {
204
+ ...file, path, size: bytes.length,
205
+ sha256: createHash('sha256').update(bytes).digest('hex'),
206
+ ...(file.kind === 'voice_message' ? { transcription: {
207
+ configured: false, attempted: false, status: 'unavailable',
208
+ provider: null, text: null, errorCategory: 'restart_recovery',
209
+ audioPath: path, fileWireId: file.wireId,
210
+ } } : {}),
211
+ };
212
+ }
213
+ export async function removeRequestDirectory(path) {
214
+ let stat;
215
+ try {
216
+ stat = await lstat(path);
217
+ }
218
+ catch {
219
+ return;
220
+ }
221
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
222
+ await rm(path, { force: true });
223
+ return;
224
+ }
225
+ await rm(path, { recursive: true, force: true });
226
+ }
227
+ export async function cleanupAttachmentRoot(root, now, retentionMs, limit = 256) {
228
+ if (!existsSync(root))
229
+ return 0;
230
+ const rootStat = lstatSync(root);
231
+ if (!rootStat.isDirectory() || rootStat.isSymbolicLink())
232
+ throw new Error('attachment root is unsafe');
233
+ let removed = 0;
234
+ for (const name of (await readdir(root)).slice(0, limit)) {
235
+ const path = join(root, name);
236
+ const stat = await lstat(path).catch(() => undefined);
237
+ if (!stat || now - stat.mtimeMs < retentionMs)
238
+ continue;
239
+ await removeRequestDirectory(path);
240
+ removed++;
241
+ }
242
+ return removed;
243
+ }
244
+ export class AttachmentRecoveryState {
245
+ path;
246
+ pending = [];
247
+ corrupt = false;
248
+ constructor(path) {
249
+ this.path = path;
250
+ if (!existsSync(path))
251
+ return;
252
+ try {
253
+ const raw = JSON.parse(readFileSync(path, 'utf8'));
254
+ if (raw.version !== 1 || !Array.isArray(raw.pending) || raw.pending.length > MAX_PENDING_REQUESTS
255
+ || !raw.pending.every(item => validPending(item)))
256
+ throw new Error('invalid recovery state');
257
+ this.pending = raw.pending.map(item => ({ ...item, fileWireIds: [...item.fileWireIds] }));
258
+ chmodSync(path, 0o600);
259
+ }
260
+ catch {
261
+ this.corrupt = true;
262
+ this.pending = [];
263
+ try {
264
+ chmodSync(path, 0o600);
265
+ }
266
+ catch { }
267
+ }
268
+ }
269
+ integrity() { return !this.corrupt; }
270
+ list() {
271
+ this.assertHealthy();
272
+ return this.pending.map(item => ({ ...item, fileWireIds: [...item.fileWireIds] }));
273
+ }
274
+ add(item) {
275
+ this.assertHealthy();
276
+ if (!validPending(item))
277
+ throw new Error('invalid attachment recovery route');
278
+ if (this.pending.some(value => value.id === item.id))
279
+ return;
280
+ if (this.pending.length >= MAX_PENDING_REQUESTS)
281
+ throw new Error('too many pending attachment recoveries');
282
+ this.pending.push({ ...item, fileWireIds: [...item.fileWireIds] });
283
+ this.persist();
284
+ }
285
+ remove(id) {
286
+ this.assertHealthy();
287
+ const next = this.pending.filter(item => item.id !== id);
288
+ if (next.length === this.pending.length)
289
+ return;
290
+ this.pending = next;
291
+ this.persist();
292
+ }
293
+ cleanup(now, retentionMs) {
294
+ this.assertHealthy();
295
+ const old = this.pending.length;
296
+ this.pending = this.pending.filter(item => now - item.createdAt <= retentionMs);
297
+ if (this.pending.length !== old)
298
+ this.persist();
299
+ return old - this.pending.length;
300
+ }
301
+ assertHealthy() { if (this.corrupt)
302
+ throw new Error('attachment recovery state is corrupt'); }
303
+ persist() {
304
+ replaceFileAtomically(this.path, JSON.stringify({ version: 1, pending: this.pending }) + '\n', 0o600);
305
+ chmodSync(this.path, 0o600);
306
+ }
307
+ }
308
+ function validPending(value) {
309
+ if (!value || typeof value !== 'object')
310
+ return false;
311
+ const item = value;
312
+ return /^[a-f0-9]{64}$/.test(item.id) && CID.test(item.contact) && WIRE.test(item.originWireId)
313
+ && Array.isArray(item.fileWireIds) && item.fileWireIds.length >= 1 && item.fileWireIds.length <= 32
314
+ && item.fileWireIds.every(wire => WIRE.test(wire))
315
+ && new Set(item.fileWireIds).size === item.fileWireIds.length
316
+ && Number.isSafeInteger(item.createdAt) && item.createdAt >= 0;
317
+ }
318
+ export function safeField(value, max) {
319
+ return Array.from(String(value ?? '').normalize('NFC')
320
+ .replace(/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/gu, ' '))
321
+ .slice(0, max).join('').trim();
322
+ }
323
+ export function sanitizeFilename(value) {
324
+ const clean = safeField(basename(value.replace(/\\/g, '/')), 180)
325
+ .replace(/[^\p{L}\p{N}._ -]+/gu, '_').replace(/^\.+/, '').trim();
326
+ return clean || 'attachment.bin';
327
+ }
328
+ function detectMime(bytes, declared) {
329
+ if (bytes.subarray(0, 5).toString() === '%PDF-')
330
+ return 'application/pdf';
331
+ if (bytes.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])))
332
+ return 'image/png';
333
+ if (bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff)
334
+ return 'image/jpeg';
335
+ if (/^GIF8[79]a/.test(bytes.subarray(0, 6).toString()))
336
+ return 'image/gif';
337
+ if (bytes.subarray(0, 4).toString() === 'RIFF' && bytes.subarray(8, 12).toString() === 'WEBP')
338
+ return 'image/webp';
339
+ if (bytes.subarray(0, 4).toString() === 'OggS')
340
+ return 'audio/ogg';
341
+ if (bytes.subarray(0, 4).toString() === 'RIFF' && bytes.subarray(8, 12).toString() === 'WAVE')
342
+ return 'audio/wav';
343
+ if (bytes.subarray(0, 3).toString() === 'ID3' || (bytes[0] === 0xff && (bytes[1] & 0xe0) === 0xe0))
344
+ return 'audio/mpeg';
345
+ if (bytes.subarray(4, 8).toString() === 'ftyp')
346
+ return declared === 'audio/mp4' ? 'audio/mp4' : 'application/mp4';
347
+ if (bytes.subarray(0, 4).equals(Buffer.from([0x1a, 0x45, 0xdf, 0xa3])))
348
+ return declared === 'audio/webm' ? 'audio/webm' : 'video/webm';
349
+ if (bytes.subarray(0, 4).equals(Buffer.from([0x50, 0x4b, 0x03, 0x04])))
350
+ return 'application/zip';
351
+ if (bytes.subarray(0, 8).equals(Buffer.from([0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1])))
352
+ return 'application/x-cfb';
353
+ const text = bytes.toString('utf8');
354
+ if (!text.includes('\ufffd') && !/[\u0000-\u0008\u000b\u000c\u000e-\u001f]/u.test(text)) {
355
+ if (declared === 'application/json') {
356
+ try {
357
+ JSON.parse(text);
358
+ return 'application/json';
359
+ }
360
+ catch { }
361
+ }
362
+ return 'text/plain';
363
+ }
364
+ return 'application/octet-stream';
365
+ }
366
+ function mimeCompatible(declared, detected) {
367
+ if (declared === detected)
368
+ return true;
369
+ if (['audio/wav', 'audio/x-wav'].includes(declared) && detected === 'audio/wav')
370
+ return true;
371
+ if (detected === 'application/zip' && declared.startsWith('application/vnd.openxmlformats-officedocument.'))
372
+ return true;
373
+ if (detected === 'application/x-cfb' && [
374
+ 'application/msword', 'application/vnd.ms-excel', 'application/vnd.ms-powerpoint',
375
+ ].includes(declared))
376
+ return true;
377
+ return false;
378
+ }
@@ -1,7 +1,10 @@
1
1
  import { type ChildProcessWithoutNullStreams } from 'node:child_process';
2
- import type { OwnerChannelConfig } from '../config.js';
2
+ import { type OwnerChannelConfig } from '../config.js';
3
3
  import type { SessionHandle } from '../session/types.js';
4
4
  import { type OursToolClient } from './mcp.js';
5
+ import { type OwnerUpdatePhase } from './notices.js';
6
+ import { type OwnerEntry } from './state.js';
7
+ import { type OwnerTaskPhase } from './tasks.js';
5
8
  export interface OwnerChannelOptions {
6
9
  role: string;
7
10
  config: OwnerChannelConfig;
@@ -18,6 +21,84 @@ export interface OwnerChannelHandle {
18
21
  start(): Promise<void>;
19
22
  drain(): Promise<void>;
20
23
  close(): Promise<void>;
24
+ manage(request: OwnerChannelManagementRequest): Promise<OwnerChannelManagementResult>;
25
+ }
26
+ export type OwnerChannelManagementRequest = {
27
+ action: 'contact_list';
28
+ } | {
29
+ action: 'contact_invite';
30
+ name?: string;
31
+ } | {
32
+ action: 'contact_add';
33
+ invite: string;
34
+ name?: string;
35
+ } | {
36
+ action: 'owner_list';
37
+ } | {
38
+ action: 'owner_authorize';
39
+ cid: string;
40
+ } | {
41
+ action: 'owner_revoke';
42
+ cid: string;
43
+ } | {
44
+ action: 'request_update';
45
+ requestId: string;
46
+ phase: OwnerUpdatePhase;
47
+ message: string;
48
+ } | {
49
+ action: 'task_open';
50
+ requestId: string;
51
+ } | {
52
+ action: 'task_report';
53
+ taskId: string;
54
+ phase: OwnerTaskPhase;
55
+ message: string;
56
+ };
57
+ export type OwnerChannelManagementResult = {
58
+ action: 'contact_list';
59
+ contacts: OwnerContact[];
60
+ } | {
61
+ action: 'contact_invite';
62
+ invite: string;
63
+ } | {
64
+ action: 'contact_add';
65
+ status: 'pending';
66
+ contact?: OwnerContact;
67
+ } | {
68
+ action: 'owner_list';
69
+ integrity: {
70
+ ok: boolean;
71
+ error?: string;
72
+ };
73
+ owners: OwnerEntry[];
74
+ } | {
75
+ action: 'owner_authorize' | 'owner_revoke';
76
+ owner: OwnerEntry;
77
+ } | {
78
+ action: 'request_update';
79
+ requestId: string;
80
+ sequence: number;
81
+ } | {
82
+ action: 'task_open';
83
+ taskId: string;
84
+ expiresAt: string;
85
+ } | {
86
+ action: 'task_report';
87
+ taskId: string;
88
+ phase: OwnerTaskPhase;
89
+ sequence: number;
90
+ state: 'open' | 'closed';
91
+ };
92
+ export type { OwnerUpdatePhase } from './notices.js';
93
+ export interface OwnerContact {
94
+ cid: string;
95
+ name: string;
96
+ status: string;
97
+ kind?: string;
98
+ human?: {
99
+ cid?: string;
100
+ name?: string;
101
+ };
21
102
  }
22
103
  /**
23
104
  * Fleet-owned trusted ingress. The agent never binds this identity and never
@@ -27,28 +108,59 @@ export declare class OwnerChannel implements OwnerChannelHandle {
27
108
  private readonly options;
28
109
  private readonly client;
29
110
  private readonly state;
111
+ private readonly authorizations;
112
+ private readonly tasks;
113
+ private readonly attachmentRecovery;
114
+ private readonly attachmentConfig;
115
+ private readonly attachmentRoot;
116
+ /**
117
+ * Wire IDs whose turn is still running. They stay OUT of the durable state
118
+ * (a crash must replay them) but must not be queued twice while live.
119
+ */
120
+ private readonly inFlight;
30
121
  private stopping;
31
122
  private watchProcess?;
32
123
  private watchTask?;
33
124
  private drainTask?;
34
125
  private drainRequested;
35
- private readonly inFlight;
36
126
  private readonly completionTasks;
127
+ private readonly activeRequests;
128
+ private managementTail;
129
+ private ready;
37
130
  constructor(options: OwnerChannelOptions);
38
131
  start(): Promise<void>;
39
132
  drain(): Promise<void>;
40
133
  close(): Promise<void>;
134
+ manage(request: OwnerChannelManagementRequest): Promise<OwnerChannelManagementResult>;
135
+ private manageNow;
136
+ private contacts;
137
+ private contact;
138
+ private assertCid;
139
+ private assertLabel;
140
+ private safeMetadata;
141
+ private sendOwnerUpdate;
142
+ private openOwnerTask;
143
+ private sendOwnerTaskReport;
144
+ private safeOwnerUpdate;
145
+ private safeTaskReport;
41
146
  private drainAll;
147
+ private attachmentGroups;
148
+ private handleAttachmentGroup;
42
149
  private handle;
43
150
  private complete;
151
+ private ownerAttachmentPrompt;
44
152
  private ownerPrompt;
45
153
  private outboxDir;
154
+ private requestId;
46
155
  private send;
47
156
  private sendAttachments;
48
157
  /** Bound message size without splitting Unicode code points. */
49
158
  private sendFinal;
50
159
  private wireId;
51
160
  private sender;
161
+ private latestEventSeq;
162
+ /** Map only event shape and allowlisted status to owner-safe phase text. */
163
+ private progressPhase;
52
164
  private watchLoop;
53
165
  private errorText;
54
166
  private logError;