@ours.network/fleet 1.1.0-nightly.9 → 1.1.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.
- package/README.md +285 -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 +8 -1
- 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 +562 -289
- package/dist/briefing.js +79 -22
- 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 +129 -47
- package/dist/config.d.ts +42 -3
- package/dist/config.js +202 -29
- package/dist/creation.d.ts +1 -1
- package/dist/creation.js +7 -4
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +195 -41
- package/dist/doctor.d.ts +2 -0
- package/dist/doctor.js +40 -4
- package/dist/fleet-command-audit.d.ts +22 -1
- package/dist/fleet-command-audit.js +355 -50
- package/dist/fleet-proxy.d.ts +5 -2
- package/dist/harness/claude-code-session.js +5 -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 +21 -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 +19 -3
- package/dist/owner-channel/channel.js +301 -120
- 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/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 +361 -285
- 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 -83
- 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 +97 -47
- package/dist/session/acp.d.ts +27 -1
- package/dist/session/acp.js +307 -17
- 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 +1 -1
- package/dist/session/control.js +17 -1
- package/dist/session/conversation-types.d.ts +4 -4
- package/dist/session/stall-watchdog.d.ts +60 -0
- package/dist/session/stall-watchdog.js +210 -0
- package/dist/session/types.d.ts +29 -8
- 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, lifecycleEventDigestBasis, renderFleetLifecycleEvent, } from '../fleet-command-audit.js';
|
|
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.
|
|
@@ -270,9 +280,41 @@ export class OwnerChannel {
|
|
|
270
280
|
lifetime: event.lifetime, brain: event.brainSummary, role: event.roleSummary,
|
|
271
281
|
harness: event.harness, session: event.session, model: event.model,
|
|
272
282
|
permissions: permission, parent: event.caller, actionId: event.creationActionId,
|
|
273
|
-
inherited: event.inherited
|
|
283
|
+
inherited: event.inherited,
|
|
284
|
+
...(event.configuration ? { configuration: event.configuration } : {}) }), `fleet-spawn\0${event.creationActionId}`, 0);
|
|
274
285
|
});
|
|
275
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
|
+
}
|
|
276
318
|
beginFleetCommandAudit(requestId, argv) {
|
|
277
319
|
const run = this.managementTail.then(async () => {
|
|
278
320
|
let attempt = this.commandAudits.begin(requestId, this.options.role, argv);
|
|
@@ -300,33 +342,50 @@ export class OwnerChannel {
|
|
|
300
342
|
return attempt;
|
|
301
343
|
if (attempt.outcome?.delivery === 'uncertain')
|
|
302
344
|
return attempt;
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
this.options.log(`[${this.options.role}] duplicate Fleet lifecycle event suppressed`);
|
|
314
|
-
continue;
|
|
315
|
-
}
|
|
316
|
-
throw error;
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
catch (error) {
|
|
321
|
-
uncertain = true;
|
|
322
|
-
this.logError(`fleet lifecycle delivery uncertain correlation=${attempt.correlationId}`, error);
|
|
323
|
-
}
|
|
324
|
-
attempt = this.commandAudits.outcome(attempt.correlationId, this.options.role, uncertain ? 'uncertain' : 'delivered');
|
|
325
|
-
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);
|
|
326
355
|
});
|
|
327
356
|
this.managementTail = run.then(() => undefined, () => undefined);
|
|
328
357
|
return run;
|
|
329
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
|
+
}
|
|
330
389
|
recover(epoch) {
|
|
331
390
|
if (this.stopping)
|
|
332
391
|
return Promise.reject(new Error('owner channel is stopping'));
|
|
@@ -368,6 +427,9 @@ export class OwnerChannel {
|
|
|
368
427
|
if (superseded())
|
|
369
428
|
throw new Error('owner recovery epoch superseded');
|
|
370
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);
|
|
371
433
|
if (superseded())
|
|
372
434
|
throw new Error('owner recovery epoch superseded');
|
|
373
435
|
// Journal-aware drain is the only operation allowed to mark owner mail
|
|
@@ -377,6 +439,7 @@ export class OwnerChannel {
|
|
|
377
439
|
if (superseded())
|
|
378
440
|
throw new Error('owner recovery epoch superseded');
|
|
379
441
|
this.ready = true;
|
|
442
|
+
await this.flushPendingFleetLifecycle();
|
|
380
443
|
this.watchTask = this.watchLoop(watchGeneration);
|
|
381
444
|
}
|
|
382
445
|
catch (error) {
|
|
@@ -827,27 +890,61 @@ export class OwnerChannel {
|
|
|
827
890
|
throw new Error('the ours daemon returned invalid unread message metadata');
|
|
828
891
|
const preflight = listed.slice(0, OWNER_MESSAGE_BATCH_LIMIT);
|
|
829
892
|
const now = Date.now();
|
|
830
|
-
const
|
|
831
|
-
|
|
832
|
-
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)
|
|
833
907
|
throw new Error('the ours daemon returned duplicate unread message metadata');
|
|
834
908
|
this.messageRecovery.claim(claims);
|
|
835
909
|
let fresh = [];
|
|
836
910
|
let remaining = 0;
|
|
837
911
|
// SDK batchLimit rejects zero. An empty preflight is a read-only drain.
|
|
838
|
-
if (
|
|
839
|
-
const payload = await this.client.getMessages(
|
|
840
|
-
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
|
|
841
916
|
|| !Number.isSafeInteger(payload.remaining) || payload.remaining < 0)
|
|
842
917
|
throw new Error('the ours daemon returned an invalid claimed message batch');
|
|
843
918
|
fresh = payload.messages.map(message => this.historyMessage(message));
|
|
844
919
|
remaining = payload.remaining;
|
|
845
|
-
const expected =
|
|
920
|
+
const expected = textClaimKeys;
|
|
846
921
|
const actual = new Set(fresh.map(item => `${item.seq}\0${item.wire_id}`));
|
|
847
|
-
if (fresh.length !==
|
|
922
|
+
if (fresh.length !== textClaims.length || actual.size !== fresh.length
|
|
848
923
|
|| actual.size !== expected.size
|
|
849
924
|
|| [...expected].some(item => !actual.has(item)))
|
|
850
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);
|
|
851
948
|
}
|
|
852
949
|
const merged = new Map();
|
|
853
950
|
for (const message of [...recovered, ...fresh]) {
|
|
@@ -1103,6 +1200,8 @@ export class OwnerChannel {
|
|
|
1103
1200
|
const wireId = this.wireId(message);
|
|
1104
1201
|
if (!wireId || this.state.has(wireId) || this.inFlight.has(wireId))
|
|
1105
1202
|
return false;
|
|
1203
|
+
if (message.message_kind === 'command')
|
|
1204
|
+
return this.handleRecoveredTypedCommand(message, wireId);
|
|
1106
1205
|
const sender = this.sender(message);
|
|
1107
1206
|
if (this.isAgentSender(sender.id)) {
|
|
1108
1207
|
try {
|
|
@@ -1170,12 +1269,13 @@ export class OwnerChannel {
|
|
|
1170
1269
|
catch (error) {
|
|
1171
1270
|
await rm(outbox, { recursive: true, force: true });
|
|
1172
1271
|
if (error instanceof SessionControlError
|
|
1173
|
-
&& error.reasonCode === ACP_CANCEL_DEADLINE_EXCEEDED
|
|
1272
|
+
&& (error.reasonCode === ACP_CANCEL_DEADLINE_EXCEEDED
|
|
1273
|
+
|| error.reasonCode === CODEX_APP_SERVER_CANCEL_DEADLINE_EXCEEDED)) {
|
|
1174
1274
|
// drainAll journaled this authenticated message before delivery. The
|
|
1175
1275
|
// adapter generation is terminating, so leave the wire unhandled and
|
|
1176
1276
|
// body-free: the resumed owner channel recovers it exactly once.
|
|
1177
1277
|
this.options.log(`[${this.options.role}] owner request ${requestId.slice(0, 12)} `
|
|
1178
|
-
+ `held for adapter resume reason=${
|
|
1278
|
+
+ `held for adapter resume reason=${error.reasonCode}`);
|
|
1179
1279
|
return false;
|
|
1180
1280
|
}
|
|
1181
1281
|
this.logError('request delivery failed', error);
|
|
@@ -1215,7 +1315,21 @@ export class OwnerChannel {
|
|
|
1215
1315
|
* agent itself can execute /force-restart, /model, or any other command.
|
|
1216
1316
|
*/
|
|
1217
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
|
+
};
|
|
1218
1331
|
const ctx = {
|
|
1332
|
+
authenticatedCid: sender.id,
|
|
1219
1333
|
role: this.options.role,
|
|
1220
1334
|
harness: this.options.harness,
|
|
1221
1335
|
version: VERSION,
|
|
@@ -1233,16 +1347,36 @@ export class OwnerChannel {
|
|
|
1233
1347
|
},
|
|
1234
1348
|
fleetList: () => this.fleetOps.list(),
|
|
1235
1349
|
closeRoom: roomId => this.closeRoomFromOwner(sender, roomId, wireId),
|
|
1236
|
-
recoverRoom: roomId => this.recoverRoomFromOwner(sender, roomId, wireId),
|
|
1237
1350
|
terminalTask: (taskId, kind, outcome) => this.terminalTaskFromOwner(sender, taskId, kind, outcome, wireId),
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
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;
|
|
1242
1363
|
}),
|
|
1243
|
-
startTask: taskId =>
|
|
1244
|
-
|
|
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;
|
|
1245
1378
|
}),
|
|
1379
|
+
taskProvisioningOutcome: taskId => new TaskRoomApplicationService(this.options.configPath).taskProvisioningOutcome(taskId),
|
|
1246
1380
|
listTasks: filter => new TaskRoomApplicationService(this.options.configPath).listTasks(filter),
|
|
1247
1381
|
groupedTasks: filter => new TaskRoomApplicationService(this.options.configPath).groupedTasks(filter),
|
|
1248
1382
|
listTaskLists: () => new TaskRoomApplicationService(this.options.configPath).listTaskLists(),
|
|
@@ -1268,19 +1402,29 @@ export class OwnerChannel {
|
|
|
1268
1402
|
reviewTask: taskId => new TaskRoomApplicationService(this.options.configPath).reviewTask({
|
|
1269
1403
|
actor: { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id }, taskId,
|
|
1270
1404
|
}),
|
|
1271
|
-
deleteTask: taskId =>
|
|
1272
|
-
actor: { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id }, taskId,
|
|
1273
|
-
}),
|
|
1405
|
+
deleteTask: taskId => this.deleteTaskFromOwner(sender, taskId, wireId),
|
|
1274
1406
|
listRoomQueries: filter => new TaskRoomApplicationService(this.options.configPath).listRooms(filter),
|
|
1275
1407
|
getRoomQuery: id => new TaskRoomApplicationService(this.options.configPath).getRoomDetail(id),
|
|
1276
1408
|
listTemplateQueries: () => new TaskRoomApplicationService(this.options.configPath).listTemplates(),
|
|
1277
1409
|
getTemplateQuery: name => new TaskRoomApplicationService(this.options.configPath).getTemplate(name),
|
|
1278
|
-
createRoom: input => new TaskRoomApplicationService(this.options.configPath).createRoom({
|
|
1410
|
+
createRoom: input => provisioningCommand(() => new TaskRoomApplicationService(this.options.configPath).createRoom({
|
|
1279
1411
|
...input, actor: { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id },
|
|
1280
|
-
}),
|
|
1412
|
+
})),
|
|
1281
1413
|
recentEvents: limit => this.options.session.eventsSince(0).slice(-limit),
|
|
1282
1414
|
readWorklogTail: maxChars => this.readWorklogTail(maxChars),
|
|
1283
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
|
+
},
|
|
1284
1428
|
};
|
|
1285
1429
|
try {
|
|
1286
1430
|
await dispatchOwnerCommand(text, ctx);
|
|
@@ -1293,6 +1437,84 @@ export class OwnerChannel {
|
|
|
1293
1437
|
if (!this.inFlight.has(wireId))
|
|
1294
1438
|
this.state.remember(wireId);
|
|
1295
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
|
+
}
|
|
1296
1518
|
/** Queue raw slash text to the harness and report the turn's outcome. */
|
|
1297
1519
|
async runHarnessCommand(sender, command, wireId) {
|
|
1298
1520
|
const requestId = this.requestId(wireId);
|
|
@@ -1340,7 +1562,7 @@ export class OwnerChannel {
|
|
|
1340
1562
|
await this.send(sender.id, renderMarkdownFailure({
|
|
1341
1563
|
kind: 'pending', subject: `/room delete ${roomId} ${roomId}`,
|
|
1342
1564
|
detail: 'The deletion request was accepted and is still being settled.',
|
|
1343
|
-
action: `Run /room
|
|
1565
|
+
action: `Run /room delete ${roomId} ${roomId} again if deletion remains pending.`,
|
|
1344
1566
|
}), wireId);
|
|
1345
1567
|
this.state.remember(wireId);
|
|
1346
1568
|
try {
|
|
@@ -1353,78 +1575,35 @@ export class OwnerChannel {
|
|
|
1353
1575
|
throw error;
|
|
1354
1576
|
}
|
|
1355
1577
|
}
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
const actor = { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id };
|
|
1359
|
-
const result = await app.recoverRoom({ actor, roomId });
|
|
1360
|
-
if (result.kind === 'deletion_worker_required') {
|
|
1361
|
-
await this.send(sender.id, renderMarkdownFailure({ kind: 'pending',
|
|
1362
|
-
subject: `/room recover ${roomId}`,
|
|
1363
|
-
detail: 'The deletion recovery is still being settled.',
|
|
1364
|
-
action: `Run /room recover ${roomId} if deletion remains pending.` }), wireId);
|
|
1365
|
-
this.state.remember(wireId);
|
|
1366
|
-
try {
|
|
1367
|
-
await this.fleetOps.closeRoom(roomId);
|
|
1368
|
-
}
|
|
1369
|
-
catch (error) {
|
|
1370
|
-
await app.recordRoomSettlementError({ actor, roomId,
|
|
1371
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1372
|
-
recoveryHint: `External delete worker failed to start. Retry /room delete ${roomId} ${roomId}.` });
|
|
1373
|
-
throw error;
|
|
1374
|
-
}
|
|
1375
|
-
return;
|
|
1376
|
-
}
|
|
1377
|
-
const r = result.orchestration;
|
|
1378
|
-
await this.send(sender.id, renderMarkdownResult({ icon: '🛟', title: 'Room recovery',
|
|
1379
|
-
fields: [{ label: 'Room', value: result.room.room_id, kind: 'code' },
|
|
1380
|
-
{ label: 'Status', value: roomStatus(result.room.state), kind: 'markdown' },
|
|
1381
|
-
...(r ? [{ label: 'Saga', value: r.saga.phase, kind: 'code' }] : [])],
|
|
1382
|
-
sections: result.issues.length ? [{ heading: 'Next steps', items: result.issues }]
|
|
1383
|
-
: [{ heading: 'Result', items: ['No recovery action is needed.'] }] }), wireId);
|
|
1384
|
-
this.state.remember(wireId);
|
|
1385
|
-
}
|
|
1386
|
-
/** Carry a task terminal request through a worker that survives this role. */
|
|
1387
|
-
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) {
|
|
1388
1580
|
const app = new TaskRoomApplicationService(this.options.configPath);
|
|
1389
1581
|
const actor = { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id };
|
|
1390
|
-
const
|
|
1391
|
-
if (
|
|
1392
|
-
await this.send(sender.id,
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
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' }],
|
|
1396
1587
|
}), wireId);
|
|
1397
1588
|
this.state.remember(wireId);
|
|
1398
|
-
try {
|
|
1399
|
-
await this.fleetOps.recoverTask(taskId);
|
|
1400
|
-
}
|
|
1401
|
-
catch (error) {
|
|
1402
|
-
await app.recordSettlementError({ actor, taskId,
|
|
1403
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1404
|
-
recoveryHint: `External settle worker failed to start. Retry /task recover ${taskId}.` });
|
|
1405
|
-
throw error;
|
|
1406
|
-
}
|
|
1407
1589
|
return;
|
|
1408
1590
|
}
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
:
|
|
1412
|
-
|
|
1413
|
-
: issue.code === 'member_failed' ? `Member failed at step ${issue.stepIndex}`
|
|
1414
|
-
: issue.code === 'resume_failed' ? `Resume failed: ${issue.error}`
|
|
1415
|
-
: issue.code === 'provisioning_resumed' ? 'Provisioning resumed successfully'
|
|
1416
|
-
: issue.code);
|
|
1417
|
-
await this.send(sender.id, renderMarkdownResult({
|
|
1418
|
-
icon: '🛟', title: 'Task recovery',
|
|
1419
|
-
fields: [{ label: 'Task', value: task.task_id, kind: 'code' },
|
|
1420
|
-
{ label: 'Status', value: taskStatus(task.state), kind: 'markdown' },
|
|
1421
|
-
...(room ? [{ label: 'Room', value: room.room_id, kind: 'code' },
|
|
1422
|
-
{ label: 'Room status', value: roomStatus(room.state), kind: 'markdown' },
|
|
1423
|
-
{ label: 'Saga', value: room.saga.phase, kind: 'code' }] : [])],
|
|
1424
|
-
sections: hints.length ? [{ heading: 'Next steps', items: hints }]
|
|
1425
|
-
: [{ 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.`,
|
|
1426
1595
|
}), wireId);
|
|
1427
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
|
+
}
|
|
1428
1607
|
}
|
|
1429
1608
|
async terminalTaskFromOwner(sender, taskId, kind, outcome, wireId) {
|
|
1430
1609
|
const app = new TaskRoomApplicationService(this.options.configPath);
|
|
@@ -1446,7 +1625,7 @@ export class OwnerChannel {
|
|
|
1446
1625
|
await this.send(sender.id, renderMarkdownFailure({
|
|
1447
1626
|
kind: 'pending', subject: `/task ${kind === 'done' ? 'done' : 'cancel'} ${taskId}`,
|
|
1448
1627
|
detail: 'The terminal request was accepted and is still being settled.',
|
|
1449
|
-
action: `
|
|
1628
|
+
action: `Repeat /task ${kind === 'done' ? 'done' : 'cancel'} ${taskId} if it remains pending.`,
|
|
1450
1629
|
}), wireId);
|
|
1451
1630
|
this.state.remember(wireId);
|
|
1452
1631
|
try {
|
|
@@ -1455,7 +1634,7 @@ export class OwnerChannel {
|
|
|
1455
1634
|
catch (error) {
|
|
1456
1635
|
await app.recordSettlementError({
|
|
1457
1636
|
actor, taskId, error: error instanceof Error ? error.message : String(error),
|
|
1458
|
-
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.`,
|
|
1459
1638
|
});
|
|
1460
1639
|
throw error;
|
|
1461
1640
|
}
|
|
@@ -1913,7 +2092,8 @@ export class OwnerChannel {
|
|
|
1913
2092
|
startProgress(event);
|
|
1914
2093
|
// Automatic commentary is an ACP phase extension. Other backends and
|
|
1915
2094
|
// older adapters retain their established final-only behavior.
|
|
1916
|
-
if (this.options.session.
|
|
2095
|
+
if (this.options.session.capabilities?.messagePhases
|
|
2096
|
+
?? this.options.session.backend === 'acp')
|
|
1917
2097
|
acceptCommentary(event);
|
|
1918
2098
|
})
|
|
1919
2099
|
: () => undefined;
|
|
@@ -1955,7 +2135,8 @@ export class OwnerChannel {
|
|
|
1955
2135
|
enabled: this.commentsEnabled,
|
|
1956
2136
|
baseline: this.commentsBaseline,
|
|
1957
2137
|
// Only the ACP backend emits the phase marker commentary relaying needs.
|
|
1958
|
-
supported: this.options.session.
|
|
2138
|
+
supported: this.options.session.capabilities?.messagePhases
|
|
2139
|
+
?? this.options.session.backend === 'acp',
|
|
1959
2140
|
};
|
|
1960
2141
|
}
|
|
1961
2142
|
/** Model-authored commentary only; raw protocol/tool data never reaches here. */
|