@ours.network/fleet 1.1.0-nightly.8 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +231 -46
- package/dist/application/capabilities.js +18 -6
- package/dist/application/fleet-query-service.js +7 -4
- package/dist/application/role-command-service.js +9 -0
- package/dist/application/role-creation-service.d.ts +4 -4
- package/dist/application/role-creation-service.js +9 -2
- package/dist/application/role-repository.d.ts +4 -2
- package/dist/application/role-repository.js +6 -3
- package/dist/application/session-control.d.ts +3 -1
- package/dist/application/session-control.js +4 -2
- package/dist/application/task-room-service.d.ts +92 -39
- package/dist/application/task-room-service.js +579 -173
- package/dist/briefing.js +85 -28
- package/dist/build-info.json +5 -5
- package/dist/canonical-json.d.ts +2 -0
- package/dist/canonical-json.js +10 -0
- package/dist/cli.js +130 -55
- package/dist/config.d.ts +38 -3
- package/dist/config.js +193 -28
- package/dist/creation.d.ts +1 -1
- package/dist/creation.js +7 -4
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +193 -55
- package/dist/doctor.d.ts +2 -0
- package/dist/doctor.js +40 -4
- package/dist/fleet-command-audit.d.ts +59 -9
- package/dist/fleet-command-audit.js +426 -63
- package/dist/fleet-proxy.d.ts +5 -2
- package/dist/harness/claude-code-session.js +2 -1
- package/dist/harness/codex-app-server-proxy.d.ts +40 -0
- package/dist/harness/codex-app-server-proxy.js +348 -14
- package/dist/harness/codex-session.d.ts +9 -2
- package/dist/harness/codex-session.js +18 -2
- package/dist/harness/codex.d.ts +5 -1
- package/dist/harness/codex.js +104 -10
- package/dist/harness/types.d.ts +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +3 -2
- package/dist/init-guidance.d.ts +1 -1
- package/dist/init-guidance.js +1 -1
- package/dist/init-wizard.d.ts +98 -0
- package/dist/init-wizard.js +631 -0
- package/dist/lifecycle-summary.d.ts +134 -0
- package/dist/lifecycle-summary.js +179 -0
- package/dist/loops/config.d.ts +10 -1
- package/dist/loops/config.js +38 -5
- package/dist/model-recovery.d.ts +1 -1
- package/dist/ops.js +2 -17
- package/dist/owner-channel/channel.d.ts +21 -5
- package/dist/owner-channel/channel.js +312 -138
- package/dist/owner-channel/commands.d.ts +28 -6
- package/dist/owner-channel/commands.js +109 -27
- package/dist/owner-channel/lifecycle-outbox.d.ts +24 -0
- package/dist/owner-channel/lifecycle-outbox.js +87 -0
- package/dist/owner-channel/ours-client.d.ts +19 -0
- package/dist/owner-channel/ours-client.js +3 -0
- package/dist/owner-channel/state.d.ts +2 -0
- package/dist/owner-channel/state.js +8 -2
- package/dist/permissions.js +1 -1
- package/dist/preset-migration.d.ts +42 -0
- package/dist/preset-migration.js +450 -0
- package/dist/reports/artifact.d.ts +10 -0
- package/dist/reports/artifact.js +32 -0
- package/dist/reports/index.d.ts +5 -0
- package/dist/reports/index.js +5 -0
- package/dist/reports/render.d.ts +6 -0
- package/dist/reports/render.js +240 -0
- package/dist/reports/service.d.ts +42 -0
- package/dist/reports/service.js +57 -0
- package/dist/reports/tasks.d.ts +52 -0
- package/dist/reports/tasks.js +177 -0
- package/dist/reports/types.d.ts +315 -0
- package/dist/reports/types.js +6 -0
- package/dist/resolved-plan.js +13 -1
- package/dist/rooms-tasks/cli.d.ts +2 -0
- package/dist/rooms-tasks/cli.js +434 -279
- package/dist/rooms-tasks/close.d.ts +9 -1
- package/dist/rooms-tasks/close.js +23 -7
- package/dist/rooms-tasks/config.js +16 -25
- package/dist/rooms-tasks/cowork-adapter.d.ts +5 -0
- package/dist/rooms-tasks/cowork-adapter.js +13 -0
- package/dist/rooms-tasks/deletion.d.ts +51 -0
- package/dist/rooms-tasks/deletion.js +217 -0
- package/dist/rooms-tasks/external-worker.d.ts +8 -0
- package/dist/rooms-tasks/external-worker.js +38 -0
- package/dist/rooms-tasks/launch-snapshot.d.ts +19 -0
- package/dist/rooms-tasks/launch-snapshot.js +181 -0
- package/dist/rooms-tasks/member-overrides.d.ts +30 -0
- package/dist/rooms-tasks/member-overrides.js +204 -0
- package/dist/rooms-tasks/member-startup.d.ts +1 -0
- package/dist/rooms-tasks/member-startup.js +9 -2
- package/dist/rooms-tasks/provision.js +146 -56
- package/dist/rooms-tasks/room-state.d.ts +1 -0
- package/dist/rooms-tasks/room-state.js +12 -1
- package/dist/rooms-tasks/task-room-name.d.ts +12 -0
- package/dist/rooms-tasks/task-room-name.js +61 -0
- package/dist/rooms-tasks/task-state.d.ts +115 -3
- package/dist/rooms-tasks/task-state.js +361 -11
- package/dist/rooms-tasks/templates.d.ts +4 -1
- package/dist/rooms-tasks/templates.js +34 -3
- package/dist/rooms-tasks/terminal.d.ts +3 -0
- package/dist/rooms-tasks/terminal.js +10 -4
- package/dist/rooms-tasks/types.d.ts +92 -7
- package/dist/rooms-tasks/types.js +14 -1
- package/dist/runner.d.ts +3 -0
- package/dist/runner.js +92 -33
- package/dist/session/acp.d.ts +4 -1
- package/dist/session/acp.js +29 -13
- package/dist/session/arbiter.d.ts +2 -1
- package/dist/session/arbiter.js +2 -0
- package/dist/session/codex-app-server-transport.d.ts +53 -0
- package/dist/session/codex-app-server-transport.js +184 -0
- package/dist/session/codex-app-server.d.ts +122 -0
- package/dist/session/codex-app-server.js +1031 -0
- package/dist/session/control.d.ts +2 -2
- package/dist/session/control.js +18 -2
- package/dist/session/conversation-types.d.ts +4 -4
- package/dist/session/types.d.ts +24 -6
- package/dist/session/types.js +28 -0
- package/dist/spawn.d.ts +9 -0
- package/dist/spawn.js +77 -9
- package/dist/web/fleet-config-service.d.ts +1 -0
- package/dist/web/fleet-config-service.js +25 -7
- package/dist/web/runtime.js +11 -6
- package/dist/web/server.js +28 -1
- package/dist/web-app/assets/{index-BbTc5KtZ.js → index-C0h3ALvs.js} +8 -8
- package/dist/web-app/index.html +1 -1
- package/package.json +4 -4
- package/presets/brain-catalog.json +17 -0
- package/presets/fleet/{agents → agent_templates}/Critic.yaml +2 -0
- package/presets/fleet/{agents/Agent.yaml → agent_templates/Developer.yaml} +3 -1
- package/presets/fleet/agent_templates/LocalCoordinator.yaml +50 -0
- package/presets/fleet/agents/{Developer.yaml → FleetCoordinator.yaml} +1 -1
- package/presets/fleet/brains/claude-fable-5-high.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-low.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-max.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-medium.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-xhigh.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-high.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-low.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-max.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-medium.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-xhigh.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-high.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-low.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-max.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-medium.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-mini-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-mini-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-mini-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-mini-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-5-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-5-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-5-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-5-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-ultra.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-ultra.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-astra-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-astra-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-astra-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-astra-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-astra-ultra.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-astra-xhigh.yaml +5 -0
- package/presets/fleet/roles/Coordinator.yaml +37 -0
- package/presets/fleet/roles/Critic.yaml +23 -1
- package/presets/fleet/roles/Developer.yaml +27 -5
- package/presets/fleet/roles/LocalCoordinator.yaml +35 -0
- package/presets/fleet/room_templates/pair.yaml +9 -7
- package/presets/fleet/room_templates/single.yaml +5 -4
- package/presets/fleet/room_templates/team.yaml +12 -8
- package/presets/manifest.json +2 -1
- package/presets/fleet/agents/Architect.yaml +0 -3
- package/presets/fleet/agents/Secretary.yaml +0 -3
- package/presets/fleet/agents/Tester.yaml +0 -3
- package/presets/fleet/roles/Agent.yaml +0 -6
- package/presets/fleet/roles/Architect.yaml +0 -6
- package/presets/fleet/roles/Secretary.yaml +0 -6
- package/presets/fleet/roles/Tester.yaml +0 -6
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
-
import { mkdir, readFile, readdir, rm } from 'node:fs/promises';
|
|
3
|
+
import { mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { canonicalCid } from '../config.js';
|
|
6
6
|
import { DAEMON_RECOVERY_DEADLINE_MS } from '../daemon-recovery.js';
|
|
@@ -11,14 +11,15 @@ import { RoleRepository } from '../application/role-repository.js';
|
|
|
11
11
|
import { FleetQueryService } from '../application/fleet-query-service.js';
|
|
12
12
|
import { interruptSession, queueSessionPrompt } from '../application/session-mutations.js';
|
|
13
13
|
import { pickBackend } from '../supervisor/index.js';
|
|
14
|
-
import { ACP_CANCEL_DEADLINE_EXCEEDED, SessionControlError, } from '../session/types.js';
|
|
14
|
+
import { ACP_CANCEL_DEADLINE_EXCEEDED, CODEX_APP_SERVER_CANCEL_DEADLINE_EXCEEDED, SessionControlError, } from '../session/types.js';
|
|
15
15
|
import { VERSION } from '../version.js';
|
|
16
|
-
import { renderMarkdownFailure, renderMarkdownResult,
|
|
17
|
-
import { dispatchOwnerCommand, fleetCliOps, isOwnerCommandText, } from './commands.js';
|
|
18
|
-
import { FleetCommandAuditStore,
|
|
16
|
+
import { renderMarkdownFailure, renderMarkdownResult, taskStatus, } from '../rooms-tasks/markdown.js';
|
|
17
|
+
import { dispatchOwnerCommand, fleetCliOps, isOwnerCommandText, ownerTypedCommandCatalog, ownerTypedCommandText, } from './commands.js';
|
|
18
|
+
import { beginFleetAuditCollection, consumeFleetAuditCollection, FleetCommandAuditStore, lifecycleEventDigestBasis, renderFleetLifecycleEvent, setFleetAuditLifecycleCheckpoint, } 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';
|
|
22
|
+
import { FleetLifecycleOutbox } from './lifecycle-outbox.js';
|
|
22
23
|
import { OwnerTaskState, ownerTaskAuditId, ownerTaskDigest, } from './tasks.js';
|
|
23
24
|
import { AttachmentRecoveryState, admitAttachments, cleanupAttachmentRoot, parseIncomingAttachments, parseRetrievedAttachments, prepareAttachmentDirectory, recoveredAttachment, removeRequestDirectory, safeField, validateAttachmentSelection, validateAttachmentRelaySelection, writeRecoveredAttachment, } from './attachments.js';
|
|
24
25
|
import { acquireOwnerBinderLease, OWNER_BIND_HANDOFF_TIMEOUT_MS, } from './binder.js';
|
|
@@ -78,6 +79,8 @@ export class OwnerChannel {
|
|
|
78
79
|
* (a crash must replay them) but must not be queued twice while live.
|
|
79
80
|
*/
|
|
80
81
|
inFlight = new Set();
|
|
82
|
+
/** SDK handlers entered during the current getMessages call. */
|
|
83
|
+
typedHandlersEntered = new Set();
|
|
81
84
|
/** Wires already NACKed to the managed agent, so a history replay stays quiet. */
|
|
82
85
|
relayNacks = new Set();
|
|
83
86
|
/**
|
|
@@ -112,6 +115,7 @@ export class OwnerChannel {
|
|
|
112
115
|
fleetOps;
|
|
113
116
|
prepareRestart;
|
|
114
117
|
commandAudits;
|
|
118
|
+
lifecycleOutbox;
|
|
115
119
|
constructor(options) {
|
|
116
120
|
this.options = options;
|
|
117
121
|
this.client = options.client ?? new OursSdkClient(options.env, line => options.log(`[${options.role}] owner channel ${line}`));
|
|
@@ -128,6 +132,7 @@ export class OwnerChannel {
|
|
|
128
132
|
});
|
|
129
133
|
this.state = new OwnerChannelState(join(options.stateDir, '.owner-channel-state.json'));
|
|
130
134
|
this.commandAudits = new FleetCommandAuditStore(join(options.stateDir, '.owner-channel-command-audit.json'));
|
|
135
|
+
this.lifecycleOutbox = new FleetLifecycleOutbox(join(options.stateDir, '.owner-channel-lifecycle-outbox.json'));
|
|
131
136
|
this.authorizations = new OwnerAuthorizationState(join(options.stateDir, '.owner-channel-owners.json'), options.config.owners);
|
|
132
137
|
this.conversations = new OwnerConversationState(join(options.stateDir, '.owner-channel-conversations.json'));
|
|
133
138
|
this.tasks = new OwnerTaskState(join(options.stateDir, '.owner-channel-tasks.json'));
|
|
@@ -153,6 +158,8 @@ export class OwnerChannel {
|
|
|
153
158
|
options.log(`[${options.role}] owner message recovery state corrupt; message intake disabled`);
|
|
154
159
|
if (!this.attachmentRecovery.integrity())
|
|
155
160
|
options.log(`[${options.role}] owner attachment recovery state corrupt; attachments disabled`);
|
|
161
|
+
if (!this.lifecycleOutbox.integrity().ok)
|
|
162
|
+
options.log(`[${options.role}] owner lifecycle outbox corrupt; lifecycle delivery disabled`);
|
|
156
163
|
}
|
|
157
164
|
async start() {
|
|
158
165
|
this.stopping = false;
|
|
@@ -168,6 +175,7 @@ export class OwnerChannel {
|
|
|
168
175
|
for (;;) {
|
|
169
176
|
try {
|
|
170
177
|
await this.client.bindIdentity(this.options.config.identity);
|
|
178
|
+
await this.registerTypedCommands();
|
|
171
179
|
break;
|
|
172
180
|
}
|
|
173
181
|
catch (error) {
|
|
@@ -199,6 +207,8 @@ export class OwnerChannel {
|
|
|
199
207
|
}
|
|
200
208
|
this.startedOnce = true;
|
|
201
209
|
this.ready = true;
|
|
210
|
+
await this.flushPendingFleetCommandAudits();
|
|
211
|
+
await this.flushPendingFleetLifecycle();
|
|
202
212
|
// Do not make role startup wait for an old owner request to finish a turn.
|
|
203
213
|
// watchLoop itself drains before every establishment, including this first
|
|
204
214
|
// one, so there is no drain-to-tip race.
|
|
@@ -263,40 +273,57 @@ export class OwnerChannel {
|
|
|
263
273
|
return this.queueManagement(async () => {
|
|
264
274
|
if (!this.ready || this.stopping)
|
|
265
275
|
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
276
|
const permission = event.permissionMode
|
|
272
|
-
?
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
277
|
+
? `${event.permissionMode.fleetMode}/${event.permissionMode.nativeMode}` : undefined;
|
|
278
|
+
await this.sendProactiveMessage(renderFleetLifecycleEvent({ kind: 'agent_started', eventId: event.creationActionId,
|
|
279
|
+
id: event.role, name: event.role,
|
|
280
|
+
lifetime: event.lifetime, brain: event.brainSummary, role: event.roleSummary,
|
|
281
|
+
harness: event.harness, session: event.session, model: event.model,
|
|
282
|
+
permissions: permission, parent: event.caller, actionId: event.creationActionId,
|
|
283
|
+
inherited: event.inherited,
|
|
284
|
+
...(event.configuration ? { configuration: event.configuration } : {}) }), `fleet-spawn\0${event.creationActionId}`, 0);
|
|
278
285
|
});
|
|
279
286
|
}
|
|
287
|
+
notifyFleetLifecycle(presentations) {
|
|
288
|
+
const run = this.managementTail.then(async () => {
|
|
289
|
+
// Acceptance is durable before delivery. A detached worker may exit
|
|
290
|
+
// while the Owner channel is reconnecting; startup/recovery flushes the
|
|
291
|
+
// pending event through the existing exactly-once send ledger.
|
|
292
|
+
this.lifecycleOutbox.enqueue(presentations);
|
|
293
|
+
if (this.ready && !this.stopping)
|
|
294
|
+
await this.flushPendingFleetLifecycle();
|
|
295
|
+
});
|
|
296
|
+
this.managementTail = run.then(() => undefined, () => undefined);
|
|
297
|
+
return run;
|
|
298
|
+
}
|
|
299
|
+
async flushPendingFleetLifecycle() {
|
|
300
|
+
for (const entry of this.lifecycleOutbox.pending()) {
|
|
301
|
+
try {
|
|
302
|
+
await this.sendProactiveMessage(renderFleetLifecycleEvent(entry.presentation), `fleet-lifecycle\0${lifecycleEventDigestBasis(entry.presentation)}`, 0);
|
|
303
|
+
this.lifecycleOutbox.finish(entry.digest, 'delivered');
|
|
304
|
+
}
|
|
305
|
+
catch (error) {
|
|
306
|
+
if (error instanceof DuplicateSendError) {
|
|
307
|
+
this.lifecycleOutbox.finish(entry.digest, error.status === 'delivered' ? 'delivered' : 'uncertain');
|
|
308
|
+
this.options.log(`[${this.options.role}] duplicate Fleet lifecycle event suppressed`);
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
// If the send crossed the uncertain boundary, the proactive ledger
|
|
312
|
+
// will refuse it on the next flush. A pre-send outage stays pending.
|
|
313
|
+
this.logError('pending Fleet lifecycle delivery deferred', error);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
280
318
|
beginFleetCommandAudit(requestId, argv) {
|
|
281
319
|
const run = this.managementTail.then(async () => {
|
|
282
320
|
let attempt = this.commandAudits.begin(requestId, this.options.role, argv);
|
|
283
321
|
if (attempt.invocation === 'delivered')
|
|
284
322
|
return attempt;
|
|
285
323
|
if (attempt.invocation === 'uncertain')
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
}
|
|
324
|
+
return attempt;
|
|
325
|
+
this.options.log(`[${this.options.role}] fleet proxy command ${attempt.correlationId} `
|
|
326
|
+
+ `route=${attempt.classification.route} decision=${attempt.classification.decision}`);
|
|
300
327
|
attempt = this.commandAudits.invocation(attempt.correlationId, this.options.role, 'delivered');
|
|
301
328
|
return attempt;
|
|
302
329
|
});
|
|
@@ -309,31 +336,56 @@ export class OwnerChannel {
|
|
|
309
336
|
class: input.class, effect: input.effect,
|
|
310
337
|
...(input.exitCode === undefined ? {} : { exitCode: input.exitCode }),
|
|
311
338
|
...(input.resourceIds ? { resourceIds: input.resourceIds } : {}),
|
|
312
|
-
...(input.
|
|
339
|
+
...(input.presentations ? { presentations: input.presentations } : {}),
|
|
313
340
|
});
|
|
314
341
|
if (attempt.outcome?.delivery === 'delivered')
|
|
315
342
|
return attempt;
|
|
316
343
|
if (attempt.outcome?.delivery === 'uncertain')
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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 });
|
|
330
|
-
}
|
|
331
|
-
attempt = this.commandAudits.outcome(attempt.correlationId, this.options.role, 'delivered');
|
|
332
|
-
return attempt;
|
|
344
|
+
return attempt;
|
|
345
|
+
// Before the Owner sink is attached, ordinary commands have nothing to
|
|
346
|
+
// deliver. Lifecycle presentations remain in the never-attempted
|
|
347
|
+
// `sending` state and are flushed exactly once when start() makes the
|
|
348
|
+
// authenticated sink ready. A restart converts them to `uncertain`, so
|
|
349
|
+
// effects are never replayed after an unproven delivery boundary.
|
|
350
|
+
if (!attempt.outcome?.presentations?.length)
|
|
351
|
+
return this.commandAudits.outcome(attempt.correlationId, this.options.role, 'delivered');
|
|
352
|
+
if (!this.ready)
|
|
353
|
+
return attempt;
|
|
354
|
+
return this.deliverFleetCommandOutcome(attempt);
|
|
333
355
|
});
|
|
334
356
|
this.managementTail = run.then(() => undefined, () => undefined);
|
|
335
357
|
return run;
|
|
336
358
|
}
|
|
359
|
+
async flushPendingFleetCommandAudits() {
|
|
360
|
+
for (const attempt of this.commandAudits.list())
|
|
361
|
+
if (attempt.outcome?.delivery === 'sending')
|
|
362
|
+
await this.deliverFleetCommandOutcome(attempt);
|
|
363
|
+
}
|
|
364
|
+
async deliverFleetCommandOutcome(attempt) {
|
|
365
|
+
let uncertain = false;
|
|
366
|
+
try {
|
|
367
|
+
for (const event of attempt.outcome?.presentations ?? []) {
|
|
368
|
+
try {
|
|
369
|
+
await this.sendProactiveMessage(renderFleetLifecycleEvent(event), `fleet-lifecycle\0${lifecycleEventDigestBasis(event)}`, 0);
|
|
370
|
+
}
|
|
371
|
+
catch (error) {
|
|
372
|
+
if (error instanceof DuplicateSendError) {
|
|
373
|
+
if (error.status !== 'delivered')
|
|
374
|
+
uncertain = true;
|
|
375
|
+
this.options.log(`[${this.options.role}] duplicate Fleet lifecycle event suppressed`);
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
throw error;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
catch (error) {
|
|
383
|
+
uncertain = true;
|
|
384
|
+
this.logError(`fleet lifecycle delivery uncertain correlation=${attempt.correlationId}`, error);
|
|
385
|
+
}
|
|
386
|
+
attempt = this.commandAudits.outcome(attempt.correlationId, this.options.role, uncertain ? 'uncertain' : 'delivered');
|
|
387
|
+
return attempt;
|
|
388
|
+
}
|
|
337
389
|
recover(epoch) {
|
|
338
390
|
if (this.stopping)
|
|
339
391
|
return Promise.reject(new Error('owner channel is stopping'));
|
|
@@ -375,6 +427,9 @@ export class OwnerChannel {
|
|
|
375
427
|
if (superseded())
|
|
376
428
|
throw new Error('owner recovery epoch superseded');
|
|
377
429
|
await this.recoveryStage('bind', this.client.bindIdentity(this.options.config.identity), deadlineAt);
|
|
430
|
+
if (superseded())
|
|
431
|
+
throw new Error('owner recovery epoch superseded');
|
|
432
|
+
await this.recoveryStage('register_commands', this.registerTypedCommands(), deadlineAt);
|
|
378
433
|
if (superseded())
|
|
379
434
|
throw new Error('owner recovery epoch superseded');
|
|
380
435
|
// Journal-aware drain is the only operation allowed to mark owner mail
|
|
@@ -384,6 +439,7 @@ export class OwnerChannel {
|
|
|
384
439
|
if (superseded())
|
|
385
440
|
throw new Error('owner recovery epoch superseded');
|
|
386
441
|
this.ready = true;
|
|
442
|
+
await this.flushPendingFleetLifecycle();
|
|
387
443
|
this.watchTask = this.watchLoop(watchGeneration);
|
|
388
444
|
}
|
|
389
445
|
catch (error) {
|
|
@@ -834,27 +890,61 @@ export class OwnerChannel {
|
|
|
834
890
|
throw new Error('the ours daemon returned invalid unread message metadata');
|
|
835
891
|
const preflight = listed.slice(0, OWNER_MESSAGE_BATCH_LIMIT);
|
|
836
892
|
const now = Date.now();
|
|
837
|
-
const
|
|
838
|
-
|
|
839
|
-
if (
|
|
893
|
+
const metadata = preflight.map(item => ({ item, claim: this.messageClaim(item, now),
|
|
894
|
+
kind: item.message_kind }));
|
|
895
|
+
if (metadata.some(item => !['text', 'command', 'command_result'].includes(item.kind)))
|
|
896
|
+
throw new Error('the ours daemon returned an unknown unread message kind');
|
|
897
|
+
// The SDK marks the entire batch read before invoking typed handlers. Claim
|
|
898
|
+
// every text AND command row first: if an earlier lifecycle handler exits
|
|
899
|
+
// this process, a later command is recovered from persistent history even
|
|
900
|
+
// though its SDK handler was never entered. Command-result rows are inert.
|
|
901
|
+
const recoverable = metadata.filter(item => item.kind !== 'command_result');
|
|
902
|
+
const claims = recoverable.map(item => item.claim);
|
|
903
|
+
const textClaims = metadata.filter(item => item.kind === 'text').map(item => item.claim);
|
|
904
|
+
const textClaimKeys = new Set(textClaims.map(item => `${item.seq}\0${item.wireId}`));
|
|
905
|
+
const preflightKeys = new Set(metadata.map(item => `${item.claim.seq}\0${item.claim.wireId}`));
|
|
906
|
+
if (preflightKeys.size !== metadata.length)
|
|
840
907
|
throw new Error('the ours daemon returned duplicate unread message metadata');
|
|
841
908
|
this.messageRecovery.claim(claims);
|
|
842
909
|
let fresh = [];
|
|
843
910
|
let remaining = 0;
|
|
844
911
|
// SDK batchLimit rejects zero. An empty preflight is a read-only drain.
|
|
845
|
-
if (
|
|
846
|
-
const payload = await this.client.getMessages(
|
|
847
|
-
if (!payload || !Array.isArray(payload.messages)
|
|
912
|
+
if (preflight.length) {
|
|
913
|
+
const payload = await this.client.getMessages(preflight.length);
|
|
914
|
+
if (!payload || !Array.isArray(payload.messages) || !Array.isArray(payload.command_results)
|
|
915
|
+
|| !Number.isSafeInteger(payload.commands_handled) || payload.commands_handled < 0
|
|
848
916
|
|| !Number.isSafeInteger(payload.remaining) || payload.remaining < 0)
|
|
849
917
|
throw new Error('the ours daemon returned an invalid claimed message batch');
|
|
850
918
|
fresh = payload.messages.map(message => this.historyMessage(message));
|
|
851
919
|
remaining = payload.remaining;
|
|
852
|
-
const expected =
|
|
920
|
+
const expected = textClaimKeys;
|
|
853
921
|
const actual = new Set(fresh.map(item => `${item.seq}\0${item.wire_id}`));
|
|
854
|
-
if (fresh.length !==
|
|
922
|
+
if (fresh.length !== textClaims.length || actual.size !== fresh.length
|
|
855
923
|
|| actual.size !== expected.size
|
|
856
924
|
|| [...expected].some(item => !actual.has(item)))
|
|
857
925
|
throw new Error('the ours daemon claimed a different message batch than fleet journaled');
|
|
926
|
+
const expectedCommands = metadata.filter(item => item.kind === 'command');
|
|
927
|
+
if (payload.commands_handled !== expectedCommands.length)
|
|
928
|
+
throw new Error('the ours daemon handled a different typed-command batch than fleet inspected');
|
|
929
|
+
const expectedResults = new Set(metadata.filter(item => item.kind === 'command_result')
|
|
930
|
+
.map(item => `${item.claim.seq}\0${item.claim.wireId}`));
|
|
931
|
+
const actualResults = new Set(payload.command_results
|
|
932
|
+
.map(item => `${item.seq}\0${item.wire_id}`));
|
|
933
|
+
if (actualResults.size !== payload.command_results.length
|
|
934
|
+
|| actualResults.size !== expectedResults.size
|
|
935
|
+
|| [...expectedResults].some(item => !actualResults.has(item)))
|
|
936
|
+
throw new Error('the ours daemon returned a different typed-command result batch than fleet inspected');
|
|
937
|
+
for (const item of expectedCommands) {
|
|
938
|
+
// Invalid envelopes and unregistered commands never enter a handler;
|
|
939
|
+
// the SDK has already returned handler_failed, so consume those now.
|
|
940
|
+
// Entered harness commands keep their claim until async completion.
|
|
941
|
+
if (!this.typedHandlersEntered.has(item.claim.wireId))
|
|
942
|
+
this.state.remember(item.claim.wireId);
|
|
943
|
+
this.typedHandlersEntered.delete(item.claim.wireId);
|
|
944
|
+
}
|
|
945
|
+
for (const item of metadata)
|
|
946
|
+
if (item.kind === 'command_result')
|
|
947
|
+
this.state.remember(item.claim.wireId);
|
|
858
948
|
}
|
|
859
949
|
const merged = new Map();
|
|
860
950
|
for (const message of [...recovered, ...fresh]) {
|
|
@@ -1110,6 +1200,8 @@ export class OwnerChannel {
|
|
|
1110
1200
|
const wireId = this.wireId(message);
|
|
1111
1201
|
if (!wireId || this.state.has(wireId) || this.inFlight.has(wireId))
|
|
1112
1202
|
return false;
|
|
1203
|
+
if (message.message_kind === 'command')
|
|
1204
|
+
return this.handleRecoveredTypedCommand(message, wireId);
|
|
1113
1205
|
const sender = this.sender(message);
|
|
1114
1206
|
if (this.isAgentSender(sender.id)) {
|
|
1115
1207
|
try {
|
|
@@ -1177,12 +1269,13 @@ export class OwnerChannel {
|
|
|
1177
1269
|
catch (error) {
|
|
1178
1270
|
await rm(outbox, { recursive: true, force: true });
|
|
1179
1271
|
if (error instanceof SessionControlError
|
|
1180
|
-
&& error.reasonCode === ACP_CANCEL_DEADLINE_EXCEEDED
|
|
1272
|
+
&& (error.reasonCode === ACP_CANCEL_DEADLINE_EXCEEDED
|
|
1273
|
+
|| error.reasonCode === CODEX_APP_SERVER_CANCEL_DEADLINE_EXCEEDED)) {
|
|
1181
1274
|
// drainAll journaled this authenticated message before delivery. The
|
|
1182
1275
|
// adapter generation is terminating, so leave the wire unhandled and
|
|
1183
1276
|
// body-free: the resumed owner channel recovers it exactly once.
|
|
1184
1277
|
this.options.log(`[${this.options.role}] owner request ${requestId.slice(0, 12)} `
|
|
1185
|
-
+ `held for adapter resume reason=${
|
|
1278
|
+
+ `held for adapter resume reason=${error.reasonCode}`);
|
|
1186
1279
|
return false;
|
|
1187
1280
|
}
|
|
1188
1281
|
this.logError('request delivery failed', error);
|
|
@@ -1222,7 +1315,21 @@ export class OwnerChannel {
|
|
|
1222
1315
|
* agent itself can execute /force-restart, /model, or any other command.
|
|
1223
1316
|
*/
|
|
1224
1317
|
async handleCommand(sender, text, wireId) {
|
|
1318
|
+
const provisioningCommand = async (run) => {
|
|
1319
|
+
beginFleetAuditCollection();
|
|
1320
|
+
setFleetAuditLifecycleCheckpoint(events => this.notifyFleetLifecycle(events));
|
|
1321
|
+
try {
|
|
1322
|
+
return await run();
|
|
1323
|
+
}
|
|
1324
|
+
finally {
|
|
1325
|
+
setFleetAuditLifecycleCheckpoint(undefined);
|
|
1326
|
+
const remaining = consumeFleetAuditCollection().presentations;
|
|
1327
|
+
if (remaining?.length)
|
|
1328
|
+
await this.notifyFleetLifecycle(remaining);
|
|
1329
|
+
}
|
|
1330
|
+
};
|
|
1225
1331
|
const ctx = {
|
|
1332
|
+
authenticatedCid: sender.id,
|
|
1226
1333
|
role: this.options.role,
|
|
1227
1334
|
harness: this.options.harness,
|
|
1228
1335
|
version: VERSION,
|
|
@@ -1240,16 +1347,36 @@ export class OwnerChannel {
|
|
|
1240
1347
|
},
|
|
1241
1348
|
fleetList: () => this.fleetOps.list(),
|
|
1242
1349
|
closeRoom: roomId => this.closeRoomFromOwner(sender, roomId, wireId),
|
|
1243
|
-
recoverRoom: roomId => this.recoverRoomFromOwner(sender, roomId, wireId),
|
|
1244
1350
|
terminalTask: (taskId, kind, outcome) => this.terminalTaskFromOwner(sender, taskId, kind, outcome, wireId),
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1351
|
+
createTask: input => provisioningCommand(async () => {
|
|
1352
|
+
const service = new TaskRoomApplicationService(this.options.configPath);
|
|
1353
|
+
const task = await service.createTask({
|
|
1354
|
+
...input,
|
|
1355
|
+
actor: { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id },
|
|
1356
|
+
});
|
|
1357
|
+
if (!input.backlog && !input.noRoom && task.room_id) {
|
|
1358
|
+
const outcome = service.taskProvisioningOutcome(task.task_id);
|
|
1359
|
+
if (outcome.kind === 'in_progress' && !outcome.next_action)
|
|
1360
|
+
await this.fleetOps.provisionTask(task.task_id);
|
|
1361
|
+
}
|
|
1362
|
+
return task;
|
|
1249
1363
|
}),
|
|
1250
|
-
startTask: taskId =>
|
|
1251
|
-
|
|
1364
|
+
startTask: taskId => provisioningCommand(async () => {
|
|
1365
|
+
const service = new TaskRoomApplicationService(this.options.configPath);
|
|
1366
|
+
const task = await service.startTask({
|
|
1367
|
+
actor: { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id }, taskId,
|
|
1368
|
+
});
|
|
1369
|
+
// A zero-duration observation repairs a ready notice after a crash or
|
|
1370
|
+
// duplicate start without extending the start command's existing wait.
|
|
1371
|
+
const outcome = await service.awaitTaskProvisioning({
|
|
1372
|
+
actor: { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id },
|
|
1373
|
+
taskId: task.task_id, waitMs: 0,
|
|
1374
|
+
});
|
|
1375
|
+
if (outcome.kind === 'in_progress' && !outcome.next_action)
|
|
1376
|
+
await this.fleetOps.provisionTask(task.task_id);
|
|
1377
|
+
return outcome;
|
|
1252
1378
|
}),
|
|
1379
|
+
taskProvisioningOutcome: taskId => new TaskRoomApplicationService(this.options.configPath).taskProvisioningOutcome(taskId),
|
|
1253
1380
|
listTasks: filter => new TaskRoomApplicationService(this.options.configPath).listTasks(filter),
|
|
1254
1381
|
groupedTasks: filter => new TaskRoomApplicationService(this.options.configPath).groupedTasks(filter),
|
|
1255
1382
|
listTaskLists: () => new TaskRoomApplicationService(this.options.configPath).listTaskLists(),
|
|
@@ -1275,19 +1402,29 @@ export class OwnerChannel {
|
|
|
1275
1402
|
reviewTask: taskId => new TaskRoomApplicationService(this.options.configPath).reviewTask({
|
|
1276
1403
|
actor: { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id }, taskId,
|
|
1277
1404
|
}),
|
|
1278
|
-
deleteTask: taskId =>
|
|
1279
|
-
actor: { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id }, taskId,
|
|
1280
|
-
}),
|
|
1405
|
+
deleteTask: taskId => this.deleteTaskFromOwner(sender, taskId, wireId),
|
|
1281
1406
|
listRoomQueries: filter => new TaskRoomApplicationService(this.options.configPath).listRooms(filter),
|
|
1282
1407
|
getRoomQuery: id => new TaskRoomApplicationService(this.options.configPath).getRoomDetail(id),
|
|
1283
1408
|
listTemplateQueries: () => new TaskRoomApplicationService(this.options.configPath).listTemplates(),
|
|
1284
1409
|
getTemplateQuery: name => new TaskRoomApplicationService(this.options.configPath).getTemplate(name),
|
|
1285
|
-
createRoom: input => new TaskRoomApplicationService(this.options.configPath).createRoom({
|
|
1410
|
+
createRoom: input => provisioningCommand(() => new TaskRoomApplicationService(this.options.configPath).createRoom({
|
|
1286
1411
|
...input, actor: { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id },
|
|
1287
|
-
}),
|
|
1412
|
+
})),
|
|
1288
1413
|
recentEvents: limit => this.options.session.eventsSince(0).slice(-limit),
|
|
1289
1414
|
readWorklogTail: maxChars => this.readWorklogTail(maxChars),
|
|
1290
1415
|
reply: async (replyText) => { await this.send(sender.id, replyText, wireId); },
|
|
1416
|
+
replyHtml: async (filename, html) => {
|
|
1417
|
+
const directory = join(this.options.stateDir, '.owner-channel-report-outbox', this.requestId(wireId));
|
|
1418
|
+
const path = join(directory, filename);
|
|
1419
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
1420
|
+
try {
|
|
1421
|
+
await writeFile(path, html, { flag: 'wx', mode: 0o600 });
|
|
1422
|
+
await this.client.sendFile({ contact: sender.id, path, filename, replyToWireId: wireId });
|
|
1423
|
+
}
|
|
1424
|
+
finally {
|
|
1425
|
+
await rm(directory, { recursive: true, force: true });
|
|
1426
|
+
}
|
|
1427
|
+
},
|
|
1291
1428
|
};
|
|
1292
1429
|
try {
|
|
1293
1430
|
await dispatchOwnerCommand(text, ctx);
|
|
@@ -1300,6 +1437,84 @@ export class OwnerChannel {
|
|
|
1300
1437
|
if (!this.inFlight.has(wireId))
|
|
1301
1438
|
this.state.remember(wireId);
|
|
1302
1439
|
}
|
|
1440
|
+
/**
|
|
1441
|
+
* Register one typed adapter per primary slash command. The adapter performs
|
|
1442
|
+
* the live CID check before constructing the same slash text and entering the
|
|
1443
|
+
* existing dispatcher; its null SDK result avoids duplicating the ordinary
|
|
1444
|
+
* owner-channel replies that remain the command's result contract.
|
|
1445
|
+
*/
|
|
1446
|
+
registerTypedCommands() {
|
|
1447
|
+
const commands = ownerTypedCommandCatalog().map(definition => ({
|
|
1448
|
+
name: definition.name,
|
|
1449
|
+
description: definition.description,
|
|
1450
|
+
input_schema: definition.input_schema,
|
|
1451
|
+
handler: (input, context) => {
|
|
1452
|
+
this.typedHandlersEntered.add(context.request_wire_id);
|
|
1453
|
+
return this.handleTypedCommand(definition.name, input, context);
|
|
1454
|
+
},
|
|
1455
|
+
}));
|
|
1456
|
+
return this.client.registerCommands(commands);
|
|
1457
|
+
}
|
|
1458
|
+
async handleRecoveredTypedCommand(message, wireId) {
|
|
1459
|
+
let payload;
|
|
1460
|
+
try {
|
|
1461
|
+
const parsed = JSON.parse(String(message.body ?? message.text ?? ''));
|
|
1462
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)
|
|
1463
|
+
|| Object.keys(parsed).length !== 2
|
|
1464
|
+
|| typeof parsed.command !== 'string'
|
|
1465
|
+
|| !('arguments' in parsed))
|
|
1466
|
+
throw new Error('invalid command payload');
|
|
1467
|
+
payload = parsed;
|
|
1468
|
+
}
|
|
1469
|
+
catch {
|
|
1470
|
+
// The original SDK attempt would have emitted handler_failed without
|
|
1471
|
+
// entering Fleet. Recovery mirrors that terminal no-effect outcome.
|
|
1472
|
+
this.state.remember(wireId);
|
|
1473
|
+
return true;
|
|
1474
|
+
}
|
|
1475
|
+
const sender = this.sender(message);
|
|
1476
|
+
try {
|
|
1477
|
+
await this.handleTypedCommand(payload.command, payload.arguments, {
|
|
1478
|
+
sender_cid: sender.id, sender_name: sender.name, request_wire_id: wireId,
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1481
|
+
catch (error) {
|
|
1482
|
+
// Expected handler failures (unauthorized/invalid typed input) are
|
|
1483
|
+
// terminal and already recorded, just as the SDK swallows them.
|
|
1484
|
+
if (!this.state.has(wireId))
|
|
1485
|
+
throw error;
|
|
1486
|
+
}
|
|
1487
|
+
return true;
|
|
1488
|
+
}
|
|
1489
|
+
async handleTypedCommand(name, input, context) {
|
|
1490
|
+
const sender = { id: context.sender_cid, name: context.sender_name };
|
|
1491
|
+
const wireId = context.request_wire_id;
|
|
1492
|
+
if (!this.isEffectiveOwner(sender.id)) {
|
|
1493
|
+
this.options.log(`[${this.options.role}] owner channel ignored unauthorized typed-command sender `
|
|
1494
|
+
+ `${sender.id || '<unknown>'}`);
|
|
1495
|
+
await this.warnOwnerOfUnauthorizedSender(sender.id);
|
|
1496
|
+
if (wireId)
|
|
1497
|
+
this.state.remember(wireId);
|
|
1498
|
+
throw new Error('typed owner command denied');
|
|
1499
|
+
}
|
|
1500
|
+
try {
|
|
1501
|
+
try {
|
|
1502
|
+
this.conversations.recordInbound(sender.id, wireId);
|
|
1503
|
+
}
|
|
1504
|
+
catch (error) {
|
|
1505
|
+
this.logError('owner conversation route update failed', error);
|
|
1506
|
+
}
|
|
1507
|
+
await this.handleCommand(sender, ownerTypedCommandText(name, input), wireId);
|
|
1508
|
+
return null;
|
|
1509
|
+
}
|
|
1510
|
+
catch (error) {
|
|
1511
|
+
// Structural typed-input failures are terminal handler_failed outcomes,
|
|
1512
|
+
// not indefinitely replayable work.
|
|
1513
|
+
if (wireId)
|
|
1514
|
+
this.state.remember(wireId);
|
|
1515
|
+
throw error;
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1303
1518
|
/** Queue raw slash text to the harness and report the turn's outcome. */
|
|
1304
1519
|
async runHarnessCommand(sender, command, wireId) {
|
|
1305
1520
|
const requestId = this.requestId(wireId);
|
|
@@ -1347,7 +1562,7 @@ export class OwnerChannel {
|
|
|
1347
1562
|
await this.send(sender.id, renderMarkdownFailure({
|
|
1348
1563
|
kind: 'pending', subject: `/room delete ${roomId} ${roomId}`,
|
|
1349
1564
|
detail: 'The deletion request was accepted and is still being settled.',
|
|
1350
|
-
action: `Run /room
|
|
1565
|
+
action: `Run /room delete ${roomId} ${roomId} again if deletion remains pending.`,
|
|
1351
1566
|
}), wireId);
|
|
1352
1567
|
this.state.remember(wireId);
|
|
1353
1568
|
try {
|
|
@@ -1360,78 +1575,35 @@ export class OwnerChannel {
|
|
|
1360
1575
|
throw error;
|
|
1361
1576
|
}
|
|
1362
1577
|
}
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
const actor = { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id };
|
|
1366
|
-
const result = await app.recoverRoom({ actor, roomId });
|
|
1367
|
-
if (result.kind === 'deletion_worker_required') {
|
|
1368
|
-
await this.send(sender.id, renderMarkdownFailure({ kind: 'pending',
|
|
1369
|
-
subject: `/room recover ${roomId}`,
|
|
1370
|
-
detail: 'The deletion recovery is still being settled.',
|
|
1371
|
-
action: `Run /room recover ${roomId} if deletion remains pending.` }), wireId);
|
|
1372
|
-
this.state.remember(wireId);
|
|
1373
|
-
try {
|
|
1374
|
-
await this.fleetOps.closeRoom(roomId);
|
|
1375
|
-
}
|
|
1376
|
-
catch (error) {
|
|
1377
|
-
await app.recordRoomSettlementError({ actor, roomId,
|
|
1378
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1379
|
-
recoveryHint: `External delete worker failed to start. Retry /room delete ${roomId} ${roomId}.` });
|
|
1380
|
-
throw error;
|
|
1381
|
-
}
|
|
1382
|
-
return;
|
|
1383
|
-
}
|
|
1384
|
-
const r = result.orchestration;
|
|
1385
|
-
await this.send(sender.id, renderMarkdownResult({ icon: '🛟', title: 'Room recovery',
|
|
1386
|
-
fields: [{ label: 'Room', value: result.room.room_id, kind: 'code' },
|
|
1387
|
-
{ label: 'Status', value: roomStatus(result.room.state), kind: 'markdown' },
|
|
1388
|
-
...(r ? [{ label: 'Saga', value: r.saga.phase, kind: 'code' }] : [])],
|
|
1389
|
-
sections: result.issues.length ? [{ heading: 'Next steps', items: result.issues }]
|
|
1390
|
-
: [{ heading: 'Result', items: ['No recovery action is needed.'] }] }), wireId);
|
|
1391
|
-
this.state.remember(wireId);
|
|
1392
|
-
}
|
|
1393
|
-
/** Carry a task terminal request through a worker that survives this role. */
|
|
1394
|
-
async recoverTaskFromOwner(sender, taskId, wireId) {
|
|
1578
|
+
/** Accept a permanent any-state deletion, acknowledge it, then hand cleanup to a durable worker. */
|
|
1579
|
+
async deleteTaskFromOwner(sender, taskId, wireId) {
|
|
1395
1580
|
const app = new TaskRoomApplicationService(this.options.configPath);
|
|
1396
1581
|
const actor = { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id };
|
|
1397
|
-
const
|
|
1398
|
-
if (
|
|
1399
|
-
await this.send(sender.id,
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
action: `Run /task recover ${taskId} if it remains pending.`,
|
|
1582
|
+
const accepted = await app.requestTaskDeletion({ actor, taskId });
|
|
1583
|
+
if (accepted.status === 'already_absent') {
|
|
1584
|
+
await this.send(sender.id, renderMarkdownResult({
|
|
1585
|
+
icon: '🗑️', title: 'Task already absent',
|
|
1586
|
+
fields: [{ label: 'ID', value: taskId, kind: 'code' }],
|
|
1403
1587
|
}), wireId);
|
|
1404
1588
|
this.state.remember(wireId);
|
|
1405
|
-
try {
|
|
1406
|
-
await this.fleetOps.recoverTask(taskId);
|
|
1407
|
-
}
|
|
1408
|
-
catch (error) {
|
|
1409
|
-
await app.recordSettlementError({ actor, taskId,
|
|
1410
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1411
|
-
recoveryHint: `External settle worker failed to start. Retry /task recover ${taskId}.` });
|
|
1412
|
-
throw error;
|
|
1413
|
-
}
|
|
1414
1589
|
return;
|
|
1415
1590
|
}
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
:
|
|
1419
|
-
|
|
1420
|
-
: issue.code === 'member_failed' ? `Member failed at step ${issue.stepIndex}`
|
|
1421
|
-
: issue.code === 'resume_failed' ? `Resume failed: ${issue.error}`
|
|
1422
|
-
: issue.code === 'provisioning_resumed' ? 'Provisioning resumed successfully'
|
|
1423
|
-
: issue.code);
|
|
1424
|
-
await this.send(sender.id, renderMarkdownResult({
|
|
1425
|
-
icon: '🛟', title: 'Task recovery',
|
|
1426
|
-
fields: [{ label: 'Task', value: task.task_id, kind: 'code' },
|
|
1427
|
-
{ label: 'Status', value: taskStatus(task.state), kind: 'markdown' },
|
|
1428
|
-
...(room ? [{ label: 'Room', value: room.room_id, kind: 'code' },
|
|
1429
|
-
{ label: 'Room status', value: roomStatus(room.state), kind: 'markdown' },
|
|
1430
|
-
{ label: 'Saga', value: room.saga.phase, kind: 'code' }] : [])],
|
|
1431
|
-
sections: hints.length ? [{ heading: 'Next steps', items: hints }]
|
|
1432
|
-
: [{ heading: 'Result', items: ['No automated recovery action is available.'] }],
|
|
1591
|
+
await this.send(sender.id, renderMarkdownFailure({
|
|
1592
|
+
kind: 'pending', subject: `/task delete ${taskId} ${taskId}`,
|
|
1593
|
+
detail: 'The deletion was accepted; cleanup is settling in the background.',
|
|
1594
|
+
action: `Repeat /task delete ${taskId} ${taskId} if it remains pending.`,
|
|
1433
1595
|
}), wireId);
|
|
1434
1596
|
this.state.remember(wireId);
|
|
1597
|
+
try {
|
|
1598
|
+
await this.fleetOps.settleTaskDeletion(taskId);
|
|
1599
|
+
}
|
|
1600
|
+
catch (error) {
|
|
1601
|
+
await app.recordDeletionError({
|
|
1602
|
+
actor, taskId, error: error instanceof Error ? error.message : String(error),
|
|
1603
|
+
recoveryHint: `External delete worker failed to start. Retry /task delete ${taskId} ${taskId}.`,
|
|
1604
|
+
});
|
|
1605
|
+
throw error;
|
|
1606
|
+
}
|
|
1435
1607
|
}
|
|
1436
1608
|
async terminalTaskFromOwner(sender, taskId, kind, outcome, wireId) {
|
|
1437
1609
|
const app = new TaskRoomApplicationService(this.options.configPath);
|
|
@@ -1453,7 +1625,7 @@ export class OwnerChannel {
|
|
|
1453
1625
|
await this.send(sender.id, renderMarkdownFailure({
|
|
1454
1626
|
kind: 'pending', subject: `/task ${kind === 'done' ? 'done' : 'cancel'} ${taskId}`,
|
|
1455
1627
|
detail: 'The terminal request was accepted and is still being settled.',
|
|
1456
|
-
action: `
|
|
1628
|
+
action: `Repeat /task ${kind === 'done' ? 'done' : 'cancel'} ${taskId} if it remains pending.`,
|
|
1457
1629
|
}), wireId);
|
|
1458
1630
|
this.state.remember(wireId);
|
|
1459
1631
|
try {
|
|
@@ -1462,7 +1634,7 @@ export class OwnerChannel {
|
|
|
1462
1634
|
catch (error) {
|
|
1463
1635
|
await app.recordSettlementError({
|
|
1464
1636
|
actor, taskId, error: error instanceof Error ? error.message : String(error),
|
|
1465
|
-
recoveryHint: `External settle worker failed to start. Retry the identical task command
|
|
1637
|
+
recoveryHint: `External settle worker failed to start. Retry the identical task command.`,
|
|
1466
1638
|
});
|
|
1467
1639
|
throw error;
|
|
1468
1640
|
}
|
|
@@ -1920,7 +2092,8 @@ export class OwnerChannel {
|
|
|
1920
2092
|
startProgress(event);
|
|
1921
2093
|
// Automatic commentary is an ACP phase extension. Other backends and
|
|
1922
2094
|
// older adapters retain their established final-only behavior.
|
|
1923
|
-
if (this.options.session.
|
|
2095
|
+
if (this.options.session.capabilities?.messagePhases
|
|
2096
|
+
?? this.options.session.backend === 'acp')
|
|
1924
2097
|
acceptCommentary(event);
|
|
1925
2098
|
})
|
|
1926
2099
|
: () => undefined;
|
|
@@ -1962,7 +2135,8 @@ export class OwnerChannel {
|
|
|
1962
2135
|
enabled: this.commentsEnabled,
|
|
1963
2136
|
baseline: this.commentsBaseline,
|
|
1964
2137
|
// Only the ACP backend emits the phase marker commentary relaying needs.
|
|
1965
|
-
supported: this.options.session.
|
|
2138
|
+
supported: this.options.session.capabilities?.messagePhases
|
|
2139
|
+
?? this.options.session.backend === 'acp',
|
|
1966
2140
|
};
|
|
1967
2141
|
}
|
|
1968
2142
|
/** Model-authored commentary only; raw protocol/tool data never reaches here. */
|