@ours.network/fleet 1.1.0-nightly.8 → 1.1.0-nightly.9

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.
@@ -8,6 +8,8 @@ export declare class FleetCliExit extends Error {
8
8
  }
9
9
  export type FleetAuditPresentation = {
10
10
  kind: 'agent_started';
11
+ eventId: string;
12
+ id: string;
11
13
  name: string;
12
14
  lifetime: 'permanent' | 'temporary';
13
15
  brain: string;
@@ -21,40 +23,65 @@ export type FleetAuditPresentation = {
21
23
  inherited: string[];
22
24
  } | {
23
25
  kind: 'task';
24
- operation: string;
26
+ operation: 'create' | 'start' | 'work' | 'block' | 'unblock' | 'review' | 'done' | 'cancel' | 'finish' | 'delete' | 'settling';
27
+ eventId: string;
25
28
  id: string;
26
29
  title?: string;
27
30
  previousState?: string;
28
31
  newState: string;
29
32
  template?: string;
30
33
  roomId?: string;
34
+ revision?: string;
35
+ list?: string;
31
36
  agents: Array<{
32
37
  name: string;
33
38
  brain?: string;
34
39
  role: string;
40
+ permissions?: string;
35
41
  }>;
36
42
  } | {
37
43
  kind: 'room';
38
- operation: string;
44
+ operation: 'create' | 'activate' | 'close' | 'delete';
45
+ eventId: string;
39
46
  id: string;
40
47
  previousState?: string;
41
48
  newState: string;
49
+ revision?: string;
50
+ name?: string;
42
51
  template?: string;
52
+ taskId?: string;
43
53
  participants: Array<{
44
54
  name: string;
55
+ id?: string;
56
+ brain?: string;
45
57
  role: string;
58
+ permissions?: string;
46
59
  }>;
60
+ } | {
61
+ kind: 'lifecycle_failure';
62
+ eventId: string;
63
+ resource: 'Agent' | 'Task' | 'Room';
64
+ id: string;
65
+ state: string;
66
+ category: 'provision_failed' | 'provision_pending' | 'readiness_failed' | 'settlement_failed' | 'settlement_pending' | 'cleanup_failed' | 'cleanup_pending';
47
67
  };
68
+ /** Commands are Owner-silent unless they record one of these semantic lifecycle kinds. */
69
+ export declare const FLEET_LIFECYCLE_EVENT_KINDS: readonly ["agent_started", "task", "room", "lifecycle_failure"];
70
+ /** Stable semantic dedupe basis: resource kind + stable resource ID + transition/action ID. */
71
+ export declare function lifecycleEventDigestBasis(value: FleetAuditPresentation): string;
48
72
  export declare function beginFleetAuditCollection(): void;
49
73
  export declare function recordFleetAuditResource(kind: 'agent' | 'task' | 'room', id: string | undefined): void;
50
- export declare function recordFleetAuditPresentation(value: FleetAuditPresentation): void;
74
+ type FleetAuditPresentationInput = FleetAuditPresentation extends infer T ? T extends FleetAuditPresentation ? Omit<T, 'eventId'> & {
75
+ eventId?: string;
76
+ } : never : never;
77
+ export declare function recordFleetAuditPresentation(value: FleetAuditPresentationInput): void;
51
78
  export declare function recordFleetAuditFailure(failure: {
52
79
  class: FleetCommandOutcomeClass;
53
80
  effect: 'not_started' | 'completed' | 'unknown';
54
81
  }): void;
55
82
  export declare function consumeFleetAuditCollection(): {
56
83
  resourceIds?: Record<string, string>;
57
- presentation?: FleetAuditPresentation;
84
+ presentations?: FleetAuditPresentation[];
58
85
  failure?: {
59
86
  class: FleetCommandOutcomeClass;
60
87
  effect: 'not_started' | 'completed' | 'unknown';
@@ -81,7 +108,7 @@ export interface FleetAuditAttempt {
81
108
  effect: 'not_started' | 'completed' | 'unknown';
82
109
  delivery: 'sending' | 'delivered' | 'uncertain';
83
110
  resourceIds?: Record<string, string>;
84
- presentation?: FleetAuditPresentation;
111
+ presentations?: FleetAuditPresentation[];
85
112
  };
86
113
  }
87
114
  export declare const fleetProxyCommandInventory: Readonly<{
@@ -90,6 +117,7 @@ export declare const fleetProxyCommandInventory: Readonly<{
90
117
  export declare const fleetProxyTopLevelInventory: Readonly<{
91
118
  safeRead: string[];
92
119
  agent: string[];
120
+ public: string[];
93
121
  denied: string[];
94
122
  hidden: string[];
95
123
  aliases: string[];
@@ -125,7 +153,8 @@ export declare function validateFleetAuditFinish(value: unknown): asserts value
125
153
  exitCode?: number;
126
154
  effect: 'not_started' | 'completed' | 'unknown';
127
155
  resourceIds?: Record<string, string>;
128
- presentation?: FleetAuditPresentation;
156
+ presentations?: FleetAuditPresentation[];
129
157
  };
130
- export declare function renderFleetAuditInvocation(attempt: FleetAuditAttempt): string;
131
- export declare function renderFleetAuditOutcome(attempt: FleetAuditAttempt): string;
158
+ /** Compact Owner presentation. It intentionally has no command, argv, environment, or correlation data. */
159
+ export declare function renderFleetLifecycleEvent(value: FleetAuditPresentation): string;
160
+ export {};
@@ -13,15 +13,35 @@ export class FleetCliExit extends Error {
13
13
  this.effect = effect;
14
14
  }
15
15
  }
16
+ /** Commands are Owner-silent unless they record one of these semantic lifecycle kinds. */
17
+ export const FLEET_LIFECYCLE_EVENT_KINDS = Object.freeze([
18
+ 'agent_started', 'task', 'room', 'lifecycle_failure',
19
+ ]);
20
+ /** Stable semantic dedupe basis: resource kind + stable resource ID + transition/action ID. */
21
+ export function lifecycleEventDigestBasis(value) {
22
+ const resourceKind = value.kind === 'lifecycle_failure' ? value.resource : value.kind;
23
+ return `${resourceKind}\0${value.id}\0${value.eventId}`;
24
+ }
16
25
  let collection;
17
- export function beginFleetAuditCollection() { collection = { resourceIds: {} }; }
26
+ export function beginFleetAuditCollection() { collection = { resourceIds: {}, presentations: [] }; }
18
27
  export function recordFleetAuditResource(kind, id) {
19
28
  if (id && collection)
20
29
  collection.resourceIds[kind] = id;
21
30
  }
22
31
  export function recordFleetAuditPresentation(value) {
23
- if (collection)
24
- collection.presentation = structuredClone(value);
32
+ if (collection && FLEET_LIFECYCLE_EVENT_KINDS.includes(value.kind)) {
33
+ const basis = value.eventId ?? (value.kind === 'agent_started' ? value.actionId
34
+ : value.kind === 'task' || value.kind === 'room'
35
+ ? `${value.operation}:${value.revision ?? `${value.previousState ?? 'none'}:${value.newState}`}`
36
+ : `${value.category}:${value.state}`);
37
+ collection.presentations.push(structuredClone({ ...value, eventId: basis }));
38
+ if (value.kind === 'task')
39
+ collection.resourceIds.task = value.id;
40
+ if (value.kind === 'room')
41
+ collection.resourceIds.room = value.id;
42
+ if (value.kind === 'agent_started')
43
+ collection.resourceIds.agent = value.id;
44
+ }
25
45
  }
26
46
  export function recordFleetAuditFailure(failure) { if (collection)
27
47
  collection.failure = failure; }
@@ -30,7 +50,7 @@ export function consumeFleetAuditCollection() {
30
50
  collection = undefined;
31
51
  return {
32
52
  ...(current && Object.keys(current.resourceIds).length ? { resourceIds: current.resourceIds } : {}),
33
- ...(current?.presentation ? { presentation: current.presentation } : {}),
53
+ ...(current?.presentations.length ? { presentations: current.presentations } : {}),
34
54
  ...(current?.failure ? { failure: current.failure } : {}),
35
55
  };
36
56
  }
@@ -43,12 +63,11 @@ const AGENT_SURFACES = {
43
63
  room: new Set(['create', 'list', 'show', 'open', 'members', 'delete', 'close', 'recover']),
44
64
  };
45
65
  export const fleetProxyCommandInventory = Object.freeze(Object.fromEntries(Object.entries(AGENT_SURFACES).map(([surface, commands]) => [surface, [...commands]])));
46
- const DENIED = new Set([
47
- 'up', 'down', 'restart', 'force-restart', 'attach', 'send', 'loops', 'owner-channel',
48
- 'watchdog-run', 'watchdog-report', 'rm', 'init', 'web',
49
- ]);
50
66
  export const fleetProxyTopLevelInventory = Object.freeze({
51
- safeRead: [...SAFE_READ], agent: Object.keys(AGENT_SURFACES), denied: [...DENIED],
67
+ safeRead: [...SAFE_READ], agent: Object.keys(AGENT_SURFACES), public: [
68
+ 'up', 'down', 'restart', 'force-restart', 'attach', 'send', 'loops', 'owner-channel',
69
+ 'watchdog-run', 'watchdog-report', 'rm', 'init', 'web',
70
+ ], denied: [],
52
71
  hidden: ['_run', '_run-temp', '_run-watchdog', '_run-watchdogs'], aliases: ['man'],
53
72
  });
54
73
  const globalValueOptions = new Set(['-c', '--configuration']);
@@ -92,13 +111,14 @@ export function classifyFleetArgv(argv) {
92
111
  if (sub.startsWith('_'))
93
112
  return { command: `${command} ${sub}`, route: 'supervisor-proxy/internal', decision: 'deny' };
94
113
  if (command !== 'spawn' && !AGENT_SURFACES[command].has(sub))
95
- return { command: `${command} ${sub}`, route: 'supervisor-proxy/unsupported', decision: 'unsupported' };
114
+ return { command: `${command} ${sub}`, route: `supervisor-proxy/${command}`, decision: 'allow' };
96
115
  return { command: sub.startsWith('-') ? command : `${command} ${sub}`,
97
116
  route: `supervisor-proxy/${command}`, decision: 'allow' };
98
117
  }
99
- if (DENIED.has(command))
100
- return { command, route: 'supervisor-proxy/permission-denied', decision: 'deny' };
101
- return { command, route: 'supervisor-proxy/unsupported', decision: 'unsupported' };
118
+ // The proxy is attribution and routing, not a product permission boundary.
119
+ // Commander validates every public command. Hidden process entry points above
120
+ // remain guarded because recursively invoking them breaks the supervisor protocol.
121
+ return { command, route: 'supervisor-proxy/public', decision: 'allow' };
102
122
  }
103
123
  const marker = (value) => value === '' ? '[REDACTED:empty]' : '[REDACTED:value]';
104
124
  const sensitiveValueFlags = new Set([
@@ -251,7 +271,7 @@ export class FleetCommandAuditStore {
251
271
  const prior = { class: attempt.outcome.class, effect: attempt.outcome.effect,
252
272
  ...(attempt.outcome.exitCode === undefined ? {} : { exitCode: attempt.outcome.exitCode }),
253
273
  ...(attempt.outcome.resourceIds ? { resourceIds: attempt.outcome.resourceIds } : {}),
254
- ...(attempt.outcome.presentation ? { presentation: attempt.outcome.presentation } : {}) };
274
+ ...(attempt.outcome.presentations ? { presentations: attempt.outcome.presentations } : {}) };
255
275
  if (JSON.stringify(prior) !== JSON.stringify(outcome))
256
276
  throw new Error('conflicting fleet command outcome for existing correlation');
257
277
  }
@@ -293,7 +313,7 @@ export function validateFleetAuditBegin(value) {
293
313
  }
294
314
  export function validateFleetAuditFinish(value) {
295
315
  const input = value;
296
- const allowed = ['correlationId', 'class', 'exitCode', 'effect', 'resourceIds', 'presentation'];
316
+ const allowed = ['correlationId', 'class', 'exitCode', 'effect', 'resourceIds', 'presentations'];
297
317
  const resources = input?.resourceIds;
298
318
  if (!input || Object.keys(input).some(key => !allowed.includes(key))
299
319
  || typeof input.correlationId !== 'string' || !UUID.test(input.correlationId)
@@ -302,7 +322,8 @@ export function validateFleetAuditFinish(value) {
302
322
  || (resources !== undefined && (!resources || typeof resources !== 'object' || Array.isArray(resources)
303
323
  || Object.entries(resources).some(([key, id]) => !['agent', 'task', 'room'].includes(key)
304
324
  || typeof id !== 'string' || !SAFE_RESOURCE_ID.test(id))))
305
- || (input.presentation !== undefined && !validPresentation(input.presentation)))
325
+ || (input.presentations !== undefined && (!Array.isArray(input.presentations)
326
+ || input.presentations.length > 128 || !input.presentations.every(validPresentation))))
306
327
  throw new Error('invalid fleet audit finish fields');
307
328
  }
308
329
  function validPresentation(value) {
@@ -312,64 +333,101 @@ function validPresentation(value) {
312
333
  const safe = (v) => typeof v === 'string' && SAFE_RESOURCE_ID.test(v);
313
334
  const text = (v) => typeof v === 'string' && v.length <= 256
314
335
  && !/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/u.test(v);
336
+ const exact = (record, keys) => Object.keys(record).every(key => keys.includes(key));
337
+ const transition = (allowed) => typeof p.operation === 'string' && typeof p.newState === 'string'
338
+ && allowed[p.operation]?.includes(`${String(p.previousState ?? 'none')}→${p.newState}`);
315
339
  if (p.kind === 'agent_started')
316
- return safe(p.name) && ['permanent', 'temporary'].includes(String(p.lifetime))
340
+ return exact(p, ['kind', 'eventId', 'id', 'name', 'lifetime', 'brain', 'role',
341
+ 'harness', 'session', 'model', 'permissions', 'parent', 'actionId', 'inherited'])
342
+ && safe(p.eventId) && safe(p.id) && safe(p.name) && ['permanent', 'temporary'].includes(String(p.lifetime))
317
343
  && text(p.brain) && text(p.role) && safe(p.harness) && p.session === 'acp' && safe(p.parent) && safe(p.actionId)
318
344
  && (p.model === undefined || text(p.model)) && (p.permissions === undefined || text(p.permissions))
319
345
  && Array.isArray(p.inherited) && p.inherited.every(text);
320
346
  if (p.kind === 'task')
321
- return safe(p.operation) && safe(p.id) && safe(p.newState)
347
+ return exact(p, ['kind', 'operation', 'eventId', 'id', 'title', 'previousState',
348
+ 'newState', 'template', 'roomId', 'revision', 'list', 'agents'])
349
+ && ['create', 'start', 'work', 'block', 'unblock', 'review', 'done',
350
+ 'cancel', 'finish', 'delete', 'settling'].includes(String(p.operation)) && safe(p.eventId) && safe(p.id) && safe(p.newState)
351
+ && transition({ create: ['none→backlog', 'none→provisioning', 'none→active'],
352
+ start: ['backlog→provisioning'], work: ['provisioning→active'],
353
+ block: ['backlog→backlog', 'provisioning→provisioning', 'active→active', 'review→review'],
354
+ unblock: ['backlog→backlog', 'provisioning→provisioning', 'active→active', 'review→review'],
355
+ review: ['active→review'], done: ['review→done'], finish: ['active→done', 'review→done'],
356
+ cancel: ['backlog→cancelled', 'provisioning→cancelled', 'active→cancelled', 'review→cancelled'],
357
+ delete: ['done→deleted'], settling: ['active→active', 'review→review', 'backlog→backlog',
358
+ 'provisioning→provisioning'] })
322
359
  && (p.title === undefined || text(p.title)) && (p.previousState === undefined || safe(p.previousState))
323
360
  && (p.template === undefined || text(p.template)) && (p.roomId === undefined || safe(p.roomId))
361
+ && (p.list === undefined || text(p.list)) && (p.revision === undefined || text(p.revision))
324
362
  && Array.isArray(p.agents) && p.agents.length <= 64
325
- && p.agents.every(a => a && typeof a === 'object' && safe(a.name)
363
+ && p.agents.every(a => a && typeof a === 'object'
364
+ && exact(a, ['name', 'brain', 'role', 'permissions'])
365
+ && safe(a.name)
326
366
  && text(a.role)
327
- && (a.brain === undefined || text(a.brain)));
367
+ && (a.brain === undefined || text(a.brain))
368
+ && (a.permissions === undefined || text(a.permissions)));
328
369
  if (p.kind === 'room')
329
- return safe(p.operation) && safe(p.id) && safe(p.newState)
370
+ return exact(p, ['kind', 'operation', 'eventId', 'id', 'previousState', 'newState',
371
+ 'revision', 'name', 'template', 'taskId', 'participants'])
372
+ && ['create', 'activate', 'close', 'delete'].includes(String(p.operation))
373
+ && safe(p.eventId) && safe(p.id) && safe(p.newState)
374
+ && transition({ create: ['none→provisioning'], activate: ['provisioning→active'],
375
+ close: ['active→closing', 'provisioning→closing'],
376
+ delete: ['closing→deleted'] })
330
377
  && (p.previousState === undefined || safe(p.previousState))
331
- && (p.template === undefined || text(p.template))
378
+ && (p.name === undefined || text(p.name)) && (p.template === undefined || text(p.template))
379
+ && (p.taskId === undefined || safe(p.taskId)) && (p.revision === undefined || text(p.revision))
332
380
  && Array.isArray(p.participants) && p.participants.length <= 64
333
- && p.participants.every(a => a && typeof a === 'object' && safe(a.name)
334
- && text(a.role));
381
+ && p.participants.every(a => a && typeof a === 'object'
382
+ && exact(a, ['name', 'id', 'brain', 'role', 'permissions'])
383
+ && safe(a.name)
384
+ && text(a.role)
385
+ && (a.id === undefined || safe(a.id))
386
+ && (a.brain === undefined || text(a.brain))
387
+ && (a.permissions === undefined || text(a.permissions)));
388
+ if (p.kind === 'lifecycle_failure')
389
+ return exact(p, ['kind', 'eventId', 'resource', 'id', 'state', 'category'])
390
+ && ['Agent', 'Task', 'Room'].includes(String(p.resource))
391
+ && safe(p.eventId) && safe(p.id) && safe(p.state) && ['provision_failed', 'provision_pending', 'readiness_failed', 'settlement_failed',
392
+ 'settlement_pending', 'cleanup_failed', 'cleanup_pending'].includes(String(p.category));
335
393
  return false;
336
394
  }
337
- export function renderFleetAuditInvocation(attempt) {
338
- return ['🧾 Fleet command invoked', `Correlation: ${attempt.correlationId}`, `Agent: ${attempt.caller}`,
339
- `Route: ${attempt.classification.route}`, `Fleet action: ${attempt.classification.command}`,
340
- `Decision: ${attempt.classification.decision}`, `Invoked: ${attempt.invokedAt}`,
341
- `Raw argv (redacted): ${JSON.stringify(attempt.argv)}`].join('\n');
342
- }
343
- export function renderFleetAuditOutcome(attempt) {
344
- if (!attempt.outcome)
345
- throw new Error('fleet command audit outcome is absent');
346
- return ['🧾 Fleet command outcome', `Correlation: ${attempt.correlationId}`, `Agent: ${attempt.caller}`,
347
- `Route: ${attempt.classification.route}`, `Fleet action: ${attempt.classification.command}`,
348
- `Result: ${attempt.outcome.class}`, `Effect: ${attempt.outcome.effect}`,
349
- ...(attempt.outcome.exitCode === undefined ? [] : [`Exit: ${attempt.outcome.exitCode}`]),
350
- ...Object.entries(attempt.outcome.resourceIds ?? {}).sort(([a], [b]) => a.localeCompare(b))
351
- .map(([kind, id]) => `${kind[0]?.toUpperCase()}${kind.slice(1)} ID: ${id}`),
352
- `Completed: ${attempt.outcome.completedAt}`, `Raw argv (redacted): ${JSON.stringify(attempt.argv)}`,
353
- ...renderPresentation(attempt.outcome.presentation)].join('\n');
354
- }
355
- function renderPresentation(value) {
356
- if (!value)
357
- return [];
358
- if (value.kind === 'agent_started')
359
- return ['Structured result: Agent started', `Agent: ${value.name}`,
360
- `Brain: ${value.brain}`, `Role: ${value.role}`,
361
- `Runtime: ${value.harness}/${value.session}${value.model ? ` model=${value.model}` : ''}`,
362
- ...(value.permissions ? [`Permissions: ${value.permissions}`] : []), `Parent: ${value.parent}`,
363
- `Agent action ID: ${value.actionId}`,
364
- `Inheritance: ${value.inherited.length ? value.inherited.join(', ') : 'none'}`];
365
- if (value.kind === 'task')
366
- return ['Structured result: Task', `Operation: ${value.operation}`,
367
- `Task: ${value.title ? `${value.title} (` : ''}${value.id}${value.title ? ')' : ''}`,
368
- `Status: ${value.previousState ?? 'unresolved'} -> ${value.newState}`,
369
- ...(value.template ? [`Template: ${value.template}`] : []), ...(value.roomId ? [`Room: ${value.roomId}`] : []),
370
- `Responsible Agents: ${value.agents.length ? value.agents.map(a => `${a.name} [Brain ${a.brain ?? 'unresolved'}; Role ${a.role}]`).join('; ') : 'none'}`];
371
- return ['Structured result: Room', `Operation: ${value.operation}`, `Room: ${value.id}`,
372
- `Status: ${value.previousState ?? 'unresolved'} -> ${value.newState}`,
373
- ...(value.template ? [`Template: ${value.template}`] : []),
374
- `Participants: ${value.participants.length ? value.participants.map(a => `${a.name} [Role ${a.role}]`).join('; ') : 'none'}`];
395
+ /** Compact Owner presentation. It intentionally has no command, argv, environment, or correlation data. */
396
+ export function renderFleetLifecycleEvent(value) {
397
+ if (value.kind === 'lifecycle_failure') {
398
+ const actions = {
399
+ provision_failed: 'Inspect Fleet service logs, correct configuration, then recover the resource.',
400
+ provision_pending: 'Run Task or Room recover after checking member readiness.',
401
+ readiness_failed: 'Inspect the Agent log, correct its configuration, then retry creation.',
402
+ settlement_failed: 'Inspect Fleet service logs, then run Task recover.',
403
+ settlement_pending: 'Run Task recover to continue settlement.',
404
+ cleanup_failed: 'Inspect Fleet service logs, then run Room recover.',
405
+ cleanup_pending: 'Run Room recover to continue cleanup.',
406
+ };
407
+ const lifecycle = value.category.endsWith('_pending') ? 'lifecycle pending' : 'lifecycle failure';
408
+ return `⚠️ ${value.resource} ${value.id} ${lifecycle} (${value.category}); `
409
+ + `state ${value.state}. Action: ${actions[value.category]}`;
410
+ }
411
+ if (value.kind === 'agent_started') {
412
+ const permission = value.permissions ? `; permissions ${value.permissions}` : '';
413
+ return `🧑‍💻 ${value.parent} spawned ${value.lifetime} Agent ${value.name} (${value.id}) — ready. `
414
+ + `Brain ${value.brain ?? 'unresolved'}; Role ${value.role ?? 'unresolved'}${permission}.`;
415
+ }
416
+ if (value.kind === 'task') {
417
+ const title = value.title ? ` “${value.title}”` : '';
418
+ const context = [value.list ? `List ${value.list}` : undefined,
419
+ value.template ? `template ${value.template}` : undefined,
420
+ value.roomId ? `Room ${value.roomId}` : undefined].filter(Boolean).join('; ');
421
+ const agents = value.agents.length ? ` Agents: ${value.agents.map(agent => `${agent.name} [Brain ${agent.brain ?? 'unresolved'}; Role ${agent.role}`
422
+ + `${agent.permissions ? `; permissions ${agent.permissions}` : ''}]`).join('; ')}.` : '';
423
+ return `📋 Task${title} (${value.id}) ${value.operation}: `
424
+ + `${value.previousState ?? 'none'} → ${value.newState}.${context ? ` ${context}.` : ''}${agents}`;
425
+ }
426
+ const context = [value.template ? `template ${value.template}` : undefined,
427
+ value.taskId ? `Task ${value.taskId}` : undefined].filter(Boolean).join('; ');
428
+ const participants = value.participants.length ? ` Participants: ${value.participants.map(participant => `${participant.name}${participant.id ? ` (${participant.id})` : ''} [Role ${participant.role}`
429
+ + `${participant.brain ? `; Brain ${participant.brain}` : ''}`
430
+ + `${participant.permissions ? `; permissions ${participant.permissions}` : ''}]`).join('; ')}.` : '';
431
+ return `🏠 Room${value.name ? ` “${value.name}”` : ''} (${value.id}) ${value.operation}: `
432
+ + `${value.previousState ?? 'none'} → ${value.newState}.${context ? ` ${context}.` : ''}${participants}`;
375
433
  }
@@ -53,7 +53,7 @@ export interface OwnerChannelHandle {
53
53
  exitCode?: number;
54
54
  effect: 'not_started' | 'completed' | 'unknown';
55
55
  resourceIds?: Record<string, string>;
56
- presentation?: FleetAuditPresentation;
56
+ presentations?: FleetAuditPresentation[];
57
57
  }): Promise<FleetAuditAttempt>;
58
58
  }
59
59
  export declare const OWNER_RECOVERY_QUEUE_CAPACITY = 32;
@@ -223,7 +223,7 @@ export declare class OwnerChannel implements OwnerChannelHandle {
223
223
  exitCode?: number;
224
224
  effect: 'not_started' | 'completed' | 'unknown';
225
225
  resourceIds?: Record<string, string>;
226
- presentation?: FleetAuditPresentation;
226
+ presentations?: FleetAuditPresentation[];
227
227
  }): Promise<FleetAuditAttempt>;
228
228
  recover(epoch: string): Promise<void>;
229
229
  private recoveryStage;
@@ -15,7 +15,7 @@ import { ACP_CANCEL_DEADLINE_EXCEEDED, SessionControlError, } from '../session/t
15
15
  import { VERSION } from '../version.js';
16
16
  import { renderMarkdownFailure, renderMarkdownResult, roomStatus, taskStatus, } from '../rooms-tasks/markdown.js';
17
17
  import { dispatchOwnerCommand, fleetCliOps, isOwnerCommandText, } from './commands.js';
18
- import { FleetCommandAuditStore, renderFleetAuditInvocation, renderFleetAuditOutcome, } from '../fleet-command-audit.js';
18
+ import { FleetCommandAuditStore, lifecycleEventDigestBasis, renderFleetLifecycleEvent, } from '../fleet-command-audit.js';
19
19
  import { OURS_BOUND_ELSEWHERE, OursSdkClient, oursErrorCode, } from './ours-client.js';
20
20
  import { ownerNotices, } from './notices.js';
21
21
  import { DuplicateSendError, OwnerAuthorizationState, OwnerChannelState, OwnerConversationState, } from './state.js';
@@ -263,18 +263,14 @@ export class OwnerChannel {
263
263
  return this.queueManagement(async () => {
264
264
  if (!this.ready || this.stopping)
265
265
  throw new Error('owner-channel MCP client is unavailable');
266
- const model = event.model ? `, model ${event.model}` : '';
267
- const monitorPolicy = event.monitor.interrupt === true
268
- ? ' with interruption'
269
- : event.monitor.interrupt === 'after_tool' ? ' with after-tool steering' : '';
270
- const monitor = `${event.monitor.mode} monitor${monitorPolicy}`;
271
266
  const permission = event.permissionMode
272
- ? `; permission ${event.permissionMode.fleetMode}, native ${event.permissionMode.nativeMode}`
273
- : '';
274
- const inherited = event.inherited.length
275
- ? ` Supervisor inherited omitted defaults: ${event.inherited.join(', ')}.` : '';
276
- await this.sendProactiveMessage(`🧑‍💻 ${event.caller} spawned ${event.lifetime} agent ${event.role} `
277
- + `(${event.harness}/${event.session}${model}; ${monitor}${permission}).${inherited}`, `fleet-spawn\0${event.creationActionId}`, 0);
267
+ ? `${event.permissionMode.fleetMode}/${event.permissionMode.nativeMode}` : undefined;
268
+ await this.sendProactiveMessage(renderFleetLifecycleEvent({ kind: 'agent_started', eventId: event.creationActionId,
269
+ id: event.role, name: event.role,
270
+ lifetime: event.lifetime, brain: event.brainSummary, role: event.roleSummary,
271
+ harness: event.harness, session: event.session, model: event.model,
272
+ permissions: permission, parent: event.caller, actionId: event.creationActionId,
273
+ inherited: event.inherited }), `fleet-spawn\0${event.creationActionId}`, 0);
278
274
  });
279
275
  }
280
276
  beginFleetCommandAudit(requestId, argv) {
@@ -283,20 +279,9 @@ export class OwnerChannel {
283
279
  if (attempt.invocation === 'delivered')
284
280
  return attempt;
285
281
  if (attempt.invocation === 'uncertain')
286
- throw new Error('fleet command invocation delivery is uncertain; execution is denied');
287
- if (!this.ready || this.stopping) {
288
- this.commandAudits.invocation(attempt.correlationId, this.options.role, 'uncertain');
289
- this.options.log(`[${this.options.role}] fleet command invocation ${attempt.correlationId} `
290
- + 'delivery unavailable; execution denied');
291
- throw new Error('owner-channel MCP client is unavailable; invocation recorded uncertain');
292
- }
293
- try {
294
- await this.sendProactiveMessage(renderFleetAuditInvocation(attempt), `fleet-command-invocation\0${attempt.correlationId}`, 0);
295
- }
296
- catch (error) {
297
- this.commandAudits.invocation(attempt.correlationId, this.options.role, 'uncertain');
298
- throw error;
299
- }
282
+ return attempt;
283
+ this.options.log(`[${this.options.role}] fleet proxy command ${attempt.correlationId} `
284
+ + `route=${attempt.classification.route} decision=${attempt.classification.decision}`);
300
285
  attempt = this.commandAudits.invocation(attempt.correlationId, this.options.role, 'delivered');
301
286
  return attempt;
302
287
  });
@@ -309,26 +294,34 @@ export class OwnerChannel {
309
294
  class: input.class, effect: input.effect,
310
295
  ...(input.exitCode === undefined ? {} : { exitCode: input.exitCode }),
311
296
  ...(input.resourceIds ? { resourceIds: input.resourceIds } : {}),
312
- ...(input.presentation ? { presentation: input.presentation } : {}),
297
+ ...(input.presentations ? { presentations: input.presentations } : {}),
313
298
  });
314
299
  if (attempt.outcome?.delivery === 'delivered')
315
300
  return attempt;
316
301
  if (attempt.outcome?.delivery === 'uncertain')
317
- throw new Error(`fleet command outcome delivery is uncertain after execution; effect status=${attempt.outcome.effect}`);
318
- if (!this.ready || this.stopping) {
319
- this.commandAudits.outcome(attempt.correlationId, this.options.role, 'uncertain');
320
- this.options.log(`[${this.options.role}] fleet command outcome ${attempt.correlationId} `
321
- + `delivery unavailable after execution; effect status=${attempt.outcome?.effect}`);
322
- throw new Error(`owner-channel unavailable after execution; effect status=${attempt.outcome?.effect}`);
323
- }
302
+ return attempt;
303
+ let uncertain = false;
324
304
  try {
325
- await this.sendProactiveMessage(renderFleetAuditOutcome(attempt), `fleet-command-outcome\0${attempt.correlationId}`, 0);
305
+ for (const event of attempt.outcome?.presentations ?? []) {
306
+ try {
307
+ await this.sendProactiveMessage(renderFleetLifecycleEvent(event), `fleet-lifecycle\0${lifecycleEventDigestBasis(event)}`, 0);
308
+ }
309
+ catch (error) {
310
+ if (error instanceof DuplicateSendError) {
311
+ if (error.status !== 'delivered')
312
+ uncertain = true;
313
+ this.options.log(`[${this.options.role}] duplicate Fleet lifecycle event suppressed`);
314
+ continue;
315
+ }
316
+ throw error;
317
+ }
318
+ }
326
319
  }
327
320
  catch (error) {
328
- this.commandAudits.outcome(attempt.correlationId, this.options.role, 'uncertain');
329
- throw new Error(`fleet command outcome delivery is uncertain after execution; effect status=${attempt.outcome?.effect}`, { cause: error });
321
+ uncertain = true;
322
+ this.logError(`fleet lifecycle delivery uncertain correlation=${attempt.correlationId}`, error);
330
323
  }
331
- attempt = this.commandAudits.outcome(attempt.correlationId, this.options.role, 'delivered');
324
+ attempt = this.commandAudits.outcome(attempt.correlationId, this.options.role, uncertain ? 'uncertain' : 'delivered');
332
325
  return attempt;
333
326
  });
334
327
  this.managementTail = run.then(() => undefined, () => undefined);
@@ -1,5 +1,7 @@
1
1
  /** A send whose (dedupe-scoped) digest was already recorded: it must not repeat. */
2
2
  export declare class DuplicateSendError extends Error {
3
+ readonly status: OwnerProactiveSend['status'];
4
+ constructor(status: OwnerProactiveSend['status']);
3
5
  }
4
6
  /** Durable bounded dedupe containing wire IDs only — never message or reply plaintext. */
5
7
  export declare class OwnerChannelState {
@@ -5,6 +5,11 @@ import { replaceFileAtomically } from '../atomic-file.js';
5
5
  import { canonicalCid } from '../config.js';
6
6
  /** A send whose (dedupe-scoped) digest was already recorded: it must not repeat. */
7
7
  export class DuplicateSendError extends Error {
8
+ status;
9
+ constructor(status) {
10
+ super('duplicate proactive owner message refused');
11
+ this.status = status;
12
+ }
8
13
  }
9
14
  /** Durable bounded dedupe containing wire IDs only — never message or reply plaintext. */
10
15
  export class OwnerChannelState {
@@ -184,8 +189,9 @@ export class OwnerConversationState {
184
189
  // 'all' scope serves wire-keyed idempotency: a crash replay must not
185
190
  // deliver the same wire to a second owner after the route moved.
186
191
  const scope = dedupe === 'all' ? this.sends : recent;
187
- if (scope.some(send => send.digest === digest))
188
- throw new DuplicateSendError('duplicate proactive owner message refused');
192
+ const duplicate = scope.find(send => send.digest === digest);
193
+ if (duplicate)
194
+ throw new DuplicateSendError(duplicate.status);
189
195
  const last = recent.at(-1);
190
196
  if (last && now - last.at < minIntervalMs)
191
197
  throw new Error(`proactive owner messages are rate-limited to one every ${minIntervalMs}ms`);