@ours.network/fleet 1.0.3 → 1.0.4
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/dist/application/role-command-service.d.ts +29 -1
- package/dist/application/role-command-service.js +41 -2
- package/dist/application/role-creation-service.d.ts +32 -1
- package/dist/application/role-creation-service.js +56 -10
- package/dist/application/role-removal-service.d.ts +19 -0
- package/dist/application/role-removal-service.js +13 -3
- package/dist/application/session-mutations.d.ts +7 -0
- package/dist/application/session-mutations.js +8 -0
- package/dist/application/task-room-service.d.ts +227 -0
- package/dist/application/task-room-service.js +529 -0
- package/dist/build-info.json +4 -4
- package/dist/cli.js +39 -15
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/owner-channel/channel.d.ts +5 -0
- package/dist/owner-channel/channel.js +143 -18
- package/dist/owner-channel/commands.d.ts +26 -1
- package/dist/owner-channel/commands.js +66 -128
- package/dist/rooms-tasks/cli.js +181 -462
- package/dist/runner.js +10 -33
- package/dist/session/control.d.ts +10 -1
- package/dist/session/control.js +22 -21
- package/dist/watchdog/query.d.ts +2 -0
- package/dist/watchdog/query.js +7 -3
- package/dist/web/server.js +2 -2
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -7,20 +7,21 @@ import { createInterface } from 'node:readline';
|
|
|
7
7
|
import { Command } from 'commander';
|
|
8
8
|
import { VERSION } from './version.js';
|
|
9
9
|
import { analyzeInstalls, buildInfo, buildLabel, discoverInstalls, runningLabel, } from './provenance.js';
|
|
10
|
-
import { agentDir, agentsRoot, tmpRoot, logsRoot, deriveXdgRuntimeDir
|
|
11
|
-
import { findRole, loadConfig
|
|
10
|
+
import { agentDir, agentsRoot, tmpRoot, logsRoot, deriveXdgRuntimeDir } from './paths.js';
|
|
11
|
+
import { findRole, loadConfig } from './config.js';
|
|
12
12
|
import { formatDuration } from './duration.js';
|
|
13
13
|
import { resolvedPlan } from './resolved-plan.js';
|
|
14
14
|
import { Tmux, tmuxArgs } from './tmux.js';
|
|
15
15
|
import { pickBackend } from './supervisor/index.js';
|
|
16
|
-
import { up, down
|
|
16
|
+
import { up, down } from './ops.js';
|
|
17
17
|
import { readRestartLedger, runSupervised, runTemp } from './runner.js';
|
|
18
18
|
import { executeWatchdogRun, runWatchdogAgent } from './watchdog/run.js';
|
|
19
19
|
import { readSchedulerState, resetSchedulerState, runScheduler } from './watchdog/scheduler.js';
|
|
20
20
|
import { partitionRestartNames } from './watchdog/config.js';
|
|
21
21
|
import { WatchdogServiceManager } from './watchdog/service.js';
|
|
22
22
|
import { acquireRunLock, latestReport, listRuns, readReport, releaseRunLock, reportsDir, } from './watchdog/store.js';
|
|
23
|
-
import {
|
|
23
|
+
import { watchdogAddressable } from './watchdog/query.js';
|
|
24
|
+
import { lastProvenance } from './spawn.js';
|
|
24
25
|
import { stringify } from 'yaml';
|
|
25
26
|
import { resolvedRolePlan } from './resolved-plan.js';
|
|
26
27
|
import { creationBuildNote, formatProvenance, readProvenance } from './creation.js';
|
|
@@ -39,6 +40,11 @@ import { FLEET_PROXY_CALLER_ENV, FLEET_PROXY_STATE_DIR_ENV, } from './fleet-prox
|
|
|
39
40
|
import './harness/claude-code.js'; // registers the claude-code adapter
|
|
40
41
|
import './harness/codex.js'; // registers the codex adapter
|
|
41
42
|
import { registerTemplateCommands, registerTaskCommands, registerRoomCommands } from './rooms-tasks/cli.js';
|
|
43
|
+
import { RoleCreationService } from './application/role-creation-service.js';
|
|
44
|
+
import { RoleRemovalService } from './application/role-removal-service.js';
|
|
45
|
+
import { RoleRepository } from './application/role-repository.js';
|
|
46
|
+
import { FleetQueryService } from './application/fleet-query-service.js';
|
|
47
|
+
import { executeRestartBatch, RoleLifecycleService, } from './application/role-command-service.js';
|
|
42
48
|
// sudo/su shells lack XDG_RUNTIME_DIR, breaking every systemctl/journalctl
|
|
43
49
|
// --user child (supervisor commands, logs, doctor). Derive it before dispatch. (#9)
|
|
44
50
|
deriveXdgRuntimeDir();
|
|
@@ -54,6 +60,12 @@ const deps = () => ({
|
|
|
54
60
|
log: l => console.log(l),
|
|
55
61
|
watchdogService: new WatchdogServiceManager(),
|
|
56
62
|
});
|
|
63
|
+
const roleLifecycle = (configPath, operationDeps) => {
|
|
64
|
+
const repository = new RoleRepository({ configPath });
|
|
65
|
+
const query = new FleetQueryService({ repository, supervisor: operationDeps.backend });
|
|
66
|
+
return new RoleLifecycleService({ repository, ops: operationDeps, configPath,
|
|
67
|
+
status: async (roleId) => (await query.detail(roleId)).status });
|
|
68
|
+
};
|
|
57
69
|
const die = (e) => { console.error(String(e instanceof Error ? e.message : e)); process.exit(1); };
|
|
58
70
|
/** Exec a child with our stdio (logs/attach). */
|
|
59
71
|
const passthrough = (cmd, args) => new Promise(resolve => {
|
|
@@ -333,8 +345,11 @@ cOpt(program.command('restart [names...]').description('re-sync config + bounce,
|
|
|
333
345
|
}
|
|
334
346
|
// Bare `restart` (no names) restarts every role — that meaning must
|
|
335
347
|
// survive even though filtering an empty array also yields [].
|
|
336
|
-
if (names.length === 0 || roleNames.length > 0)
|
|
337
|
-
|
|
348
|
+
if (names.length === 0 || roleNames.length > 0) {
|
|
349
|
+
const operationDeps = deps();
|
|
350
|
+
const lifecycle = roleLifecycle(opts.configuration, operationDeps);
|
|
351
|
+
await executeRestartBatch(lifecycle, { roleIds: roleNames, mode: 'keep', config: cfg });
|
|
352
|
+
}
|
|
338
353
|
}
|
|
339
354
|
catch (e) {
|
|
340
355
|
die(e);
|
|
@@ -343,7 +358,10 @@ cOpt(program.command('restart [names...]').description('re-sync config + bounce,
|
|
|
343
358
|
cOpt(program.command('force-restart [names...]').description('re-sync + bounce FRESH (context wiped)'))
|
|
344
359
|
.action(async (names, opts) => {
|
|
345
360
|
try {
|
|
346
|
-
|
|
361
|
+
const cfg = loadConfig(opts.configuration);
|
|
362
|
+
const operationDeps = deps();
|
|
363
|
+
const lifecycle = roleLifecycle(opts.configuration, operationDeps);
|
|
364
|
+
await executeRestartBatch(lifecycle, { roleIds: names, mode: 'fresh', config: cfg });
|
|
347
365
|
}
|
|
348
366
|
catch (e) {
|
|
349
367
|
die(e);
|
|
@@ -963,12 +981,12 @@ cOpt(ownerAuthorizationCommand.command('revoke <Role> <contact-cid>')
|
|
|
963
981
|
* store.ts's own `watchdogDir` choke-point guard (defense in depth).
|
|
964
982
|
*/
|
|
965
983
|
function watchdogKnown(name, configPath) {
|
|
984
|
+
let configured = [];
|
|
966
985
|
try {
|
|
967
|
-
|
|
968
|
-
return true;
|
|
986
|
+
configured = loadConfig(configPath).watchdogs.map(w => w.name);
|
|
969
987
|
}
|
|
970
988
|
catch { /* config missing/broken: fall through to the store check */ }
|
|
971
|
-
return
|
|
989
|
+
return watchdogAddressable(name, configured);
|
|
972
990
|
}
|
|
973
991
|
function renderHeldDownLine(state) {
|
|
974
992
|
if (!state.heldDown)
|
|
@@ -1096,7 +1114,9 @@ cOpt(program.command('watchdog-report <name> [runId]')
|
|
|
1096
1114
|
cOpt(program.command('rm <name>').description('stop + remove a role (temporary evidence is archived)'))
|
|
1097
1115
|
.action(async (name, opts) => {
|
|
1098
1116
|
try {
|
|
1099
|
-
await
|
|
1117
|
+
await new RoleRemovalService({ configPath: opts.configuration, ops: deps() }).removeDirect({
|
|
1118
|
+
actor: { kind: 'local_control', surface: 'cli' }, role: name,
|
|
1119
|
+
});
|
|
1100
1120
|
}
|
|
1101
1121
|
catch (e) {
|
|
1102
1122
|
die(e);
|
|
@@ -1152,8 +1172,11 @@ cOpt(program.command('spawn [name]').description('spawn a new agent (permanent b
|
|
|
1152
1172
|
};
|
|
1153
1173
|
if (o.json && !o.dryRun)
|
|
1154
1174
|
throw new Error('--json is currently valid only with --dry-run');
|
|
1175
|
+
const creationDeps = deps();
|
|
1176
|
+
const creation = new RoleCreationService({ configPath: opts.configuration,
|
|
1177
|
+
ops: creationDeps, binPath, journal: false });
|
|
1155
1178
|
if (o.dryRun) {
|
|
1156
|
-
const result =
|
|
1179
|
+
const result = creation.previewSpawn({ origin: 'direct', options: o }).preview;
|
|
1157
1180
|
if (o.json) {
|
|
1158
1181
|
process.stdout.write(`${JSON.stringify({
|
|
1159
1182
|
schemaVersion: result.schemaVersion,
|
|
@@ -1196,13 +1219,14 @@ cOpt(program.command('spawn [name]').description('spawn a new agent (permanent b
|
|
|
1196
1219
|
console.log(`→ watch it: ours-fleet peek ${result.role} | attach: ours-fleet attach ${result.role}`);
|
|
1197
1220
|
return;
|
|
1198
1221
|
}
|
|
1199
|
-
const
|
|
1222
|
+
const created = await creation.createDirect(o);
|
|
1223
|
+
const plannedPermissionMode = effectivePermissionMode(created.plan.preview.resolvedRole);
|
|
1200
1224
|
if (o.temp) {
|
|
1201
|
-
const dir =
|
|
1225
|
+
const dir = created.statePath;
|
|
1202
1226
|
console.log(`spawned temp agent '${roleName}' (state: ${dir}; gone on exit/reboot)`);
|
|
1203
1227
|
}
|
|
1204
1228
|
else {
|
|
1205
|
-
const file =
|
|
1229
|
+
const file = created.statePath;
|
|
1206
1230
|
console.log(`spawned '${roleName}' (config: ${file})`);
|
|
1207
1231
|
}
|
|
1208
1232
|
// The same provenance that was persisted, so what the operator reads now
|
package/dist/index.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ export { RoleRepository } from './application/role-repository.js';
|
|
|
19
19
|
export { FleetQueryService } from './application/fleet-query-service.js';
|
|
20
20
|
export { AcpRoleSessionAdapter, TmuxRoleSessionAdapter } from './application/session-control.js';
|
|
21
21
|
export { RoleCreationService } from './application/role-creation-service.js';
|
|
22
|
-
export { RoleCommandService } from './application/role-command-service.js';
|
|
22
|
+
export { RoleCommandService, RoleLifecycleService } from './application/role-command-service.js';
|
|
23
23
|
export { StructuredLogService } from './application/log-service.js';
|
|
24
24
|
export { roleCapabilities } from './application/capabilities.js';
|
|
25
25
|
export { FleetError, normalizeError } from './application/errors.js';
|
package/dist/index.js
CHANGED
|
@@ -15,7 +15,7 @@ export { RoleRepository } from './application/role-repository.js';
|
|
|
15
15
|
export { FleetQueryService } from './application/fleet-query-service.js';
|
|
16
16
|
export { AcpRoleSessionAdapter, TmuxRoleSessionAdapter } from './application/session-control.js';
|
|
17
17
|
export { RoleCreationService } from './application/role-creation-service.js';
|
|
18
|
-
export { RoleCommandService } from './application/role-command-service.js';
|
|
18
|
+
export { RoleCommandService, RoleLifecycleService } from './application/role-command-service.js';
|
|
19
19
|
export { StructuredLogService } from './application/log-service.js';
|
|
20
20
|
export { roleCapabilities } from './application/capabilities.js';
|
|
21
21
|
export { FleetError, normalizeError } from './application/errors.js';
|
|
@@ -19,6 +19,8 @@ export interface OwnerChannelOptions {
|
|
|
19
19
|
client?: OursOps;
|
|
20
20
|
/** Test seam; production uses the detached ours-fleet CLI (`fleetCliOps`). */
|
|
21
21
|
fleet?: OwnerFleetOps;
|
|
22
|
+
/** Read-only restart validation; production uses the shared lifecycle service. */
|
|
23
|
+
prepareRestart?: (role: string, mode: 'keep' | 'fresh') => Promise<void>;
|
|
22
24
|
/** Forwarded to fleet CLI invocations spawned for owner commands. */
|
|
23
25
|
configPath?: string;
|
|
24
26
|
/** Deterministic clock/process seams for binder handoff tests. */
|
|
@@ -164,6 +166,7 @@ export declare class OwnerChannel implements OwnerChannelHandle {
|
|
|
164
166
|
private binder?;
|
|
165
167
|
private binderOwnedInternally;
|
|
166
168
|
private readonly fleetOps;
|
|
169
|
+
private readonly prepareRestart;
|
|
167
170
|
constructor(options: OwnerChannelOptions);
|
|
168
171
|
start(): Promise<void>;
|
|
169
172
|
drain(): Promise<void>;
|
|
@@ -222,7 +225,9 @@ export declare class OwnerChannel implements OwnerChannelHandle {
|
|
|
222
225
|
* before an external worker starts a saga that can retire this process.
|
|
223
226
|
*/
|
|
224
227
|
private closeRoomFromOwner;
|
|
228
|
+
private recoverRoomFromOwner;
|
|
225
229
|
/** Carry a task terminal request through a worker that survives this role. */
|
|
230
|
+
private recoverTaskFromOwner;
|
|
226
231
|
private terminalTaskFromOwner;
|
|
227
232
|
/** Code-point-safe tail of the worklog, or undefined when there is none. */
|
|
228
233
|
private readWorklogTail;
|
|
@@ -4,9 +4,15 @@ import { mkdir, readFile, readdir, rm } from 'node:fs/promises';
|
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { DEFAULT_OWNER_ATTACHMENT_MIME, canonicalCid, } from '../config.js';
|
|
6
6
|
import { replaceFileAtomically } from '../atomic-file.js';
|
|
7
|
-
import {
|
|
7
|
+
import { TaskRoomApplicationService } from '../application/task-room-service.js';
|
|
8
|
+
import { RoleLifecycleService } from '../application/role-command-service.js';
|
|
9
|
+
import { RoleRepository } from '../application/role-repository.js';
|
|
10
|
+
import { FleetQueryService } from '../application/fleet-query-service.js';
|
|
11
|
+
import { interruptSession, queueSessionPrompt } from '../application/session-mutations.js';
|
|
12
|
+
import { pickBackend } from '../supervisor/index.js';
|
|
13
|
+
import { ACP_CANCEL_DEADLINE_EXCEEDED, SessionControlError, } from '../session/types.js';
|
|
8
14
|
import { VERSION } from '../version.js';
|
|
9
|
-
import { renderMarkdownFailure, renderMarkdownResult, taskStatus, } from '../rooms-tasks/markdown.js';
|
|
15
|
+
import { renderMarkdownFailure, renderMarkdownResult, roomStatus, taskStatus, } from '../rooms-tasks/markdown.js';
|
|
10
16
|
import { dispatchOwnerCommand, fleetCliOps, isOwnerCommandText, } from './commands.js';
|
|
11
17
|
import { OURS_BOUND_ELSEWHERE, OursSdkClient, oursErrorCode, } from './ours-client.js';
|
|
12
18
|
import { ownerNotices, } from './notices.js';
|
|
@@ -74,10 +80,21 @@ export class OwnerChannel {
|
|
|
74
80
|
binder;
|
|
75
81
|
binderOwnedInternally = false;
|
|
76
82
|
fleetOps;
|
|
83
|
+
prepareRestart;
|
|
77
84
|
constructor(options) {
|
|
78
85
|
this.options = options;
|
|
79
86
|
this.client = options.client ?? new OursSdkClient(options.env, line => options.log(`[${options.role}] owner channel ${line}`));
|
|
80
87
|
this.fleetOps = options.fleet ?? fleetCliOps(options.role, options.configPath);
|
|
88
|
+
this.prepareRestart = options.prepareRestart ?? (async (role, mode) => {
|
|
89
|
+
const backend = pickBackend();
|
|
90
|
+
const repository = new RoleRepository({ configPath: options.configPath });
|
|
91
|
+
const query = new FleetQueryService({ repository, supervisor: backend });
|
|
92
|
+
const lifecycle = new RoleLifecycleService({ repository,
|
|
93
|
+
ops: { backend, binPath: process.argv[1], log: options.log },
|
|
94
|
+
configPath: options.configPath,
|
|
95
|
+
status: async (roleId) => (await query.detail(roleId)).status });
|
|
96
|
+
await lifecycle.prepareRestart({ roleIds: [role], mode });
|
|
97
|
+
});
|
|
81
98
|
this.state = new OwnerChannelState(join(options.stateDir, '.owner-channel-state.json'));
|
|
82
99
|
this.authorizations = new OwnerAuthorizationState(join(options.stateDir, '.owner-channel-owners.json'), options.config.owners);
|
|
83
100
|
this.conversations = new OwnerConversationState(join(options.stateDir, '.owner-channel-conversations.json'));
|
|
@@ -793,7 +810,7 @@ export class OwnerChannel {
|
|
|
793
810
|
outbox = this.outboxDir(originWireId);
|
|
794
811
|
await mkdir(outbox, { recursive: true, mode: 0o700 });
|
|
795
812
|
const activityCursor = this.latestEventSeq(this.options.session.eventsSince(0));
|
|
796
|
-
const queued = await this.options.session
|
|
813
|
+
const queued = await queueSessionPrompt(this.options.session, this.ownerAttachmentPrompt(sender, originWireId, requestId, admitted, group.caption), {
|
|
797
814
|
interrupt: this.options.config.interrupt,
|
|
798
815
|
...(this.options.config.interrupt ? { interruptSource: 'owner' } : {}),
|
|
799
816
|
origin: { kind: 'owner', requestId,
|
|
@@ -918,7 +935,7 @@ export class OwnerChannel {
|
|
|
918
935
|
let queued;
|
|
919
936
|
const activityCursor = this.latestEventSeq(this.options.session.eventsSince(0));
|
|
920
937
|
try {
|
|
921
|
-
queued = await this.options.session
|
|
938
|
+
queued = await queueSessionPrompt(this.options.session, this.ownerPrompt(sender, text, wireId), {
|
|
922
939
|
interrupt: this.options.config.interrupt,
|
|
923
940
|
...(this.options.config.interrupt ? { interruptSource: 'owner' } : {}),
|
|
924
941
|
origin: { kind: 'owner', requestId, displayText: text },
|
|
@@ -977,7 +994,7 @@ export class OwnerChannel {
|
|
|
977
994
|
harness: this.options.harness,
|
|
978
995
|
version: VERSION,
|
|
979
996
|
snapshot: () => this.options.session.snapshot(),
|
|
980
|
-
interrupt:
|
|
997
|
+
interrupt: () => interruptSession(this.options.session, 'owner'),
|
|
981
998
|
runHarnessCommand: command => this.runHarnessCommand(sender, command, wireId),
|
|
982
999
|
restart: mode => this.restartSelf(sender, mode, wireId),
|
|
983
1000
|
comments: () => this.commentsState(),
|
|
@@ -990,7 +1007,37 @@ export class OwnerChannel {
|
|
|
990
1007
|
},
|
|
991
1008
|
fleetList: () => this.fleetOps.list(),
|
|
992
1009
|
closeRoom: roomId => this.closeRoomFromOwner(sender, roomId, wireId),
|
|
1010
|
+
recoverRoom: roomId => this.recoverRoomFromOwner(sender, roomId, wireId),
|
|
993
1011
|
terminalTask: (taskId, kind, outcome) => this.terminalTaskFromOwner(sender, taskId, kind, outcome, wireId),
|
|
1012
|
+
recoverTask: taskId => this.recoverTaskFromOwner(sender, taskId, wireId),
|
|
1013
|
+
createTask: input => new TaskRoomApplicationService(this.options.configPath).createTask({
|
|
1014
|
+
...input,
|
|
1015
|
+
actor: { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id },
|
|
1016
|
+
}),
|
|
1017
|
+
startTask: taskId => new TaskRoomApplicationService(this.options.configPath).startTask({
|
|
1018
|
+
actor: { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id }, taskId,
|
|
1019
|
+
}),
|
|
1020
|
+
listTasks: filter => new TaskRoomApplicationService(this.options.configPath).listTasks(filter),
|
|
1021
|
+
getTask: taskId => new TaskRoomApplicationService(this.options.configPath).getTask(taskId),
|
|
1022
|
+
blockTask: (taskId, reason) => new TaskRoomApplicationService(this.options.configPath).blockTask({
|
|
1023
|
+
actor: { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id }, taskId, reason,
|
|
1024
|
+
}),
|
|
1025
|
+
unblockTask: taskId => new TaskRoomApplicationService(this.options.configPath).unblockTask({
|
|
1026
|
+
actor: { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id }, taskId,
|
|
1027
|
+
}),
|
|
1028
|
+
reviewTask: taskId => new TaskRoomApplicationService(this.options.configPath).reviewTask({
|
|
1029
|
+
actor: { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id }, taskId,
|
|
1030
|
+
}),
|
|
1031
|
+
deleteTask: taskId => new TaskRoomApplicationService(this.options.configPath).deleteTask({
|
|
1032
|
+
actor: { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id }, taskId,
|
|
1033
|
+
}),
|
|
1034
|
+
listRoomQueries: filter => new TaskRoomApplicationService(this.options.configPath).listRooms(filter),
|
|
1035
|
+
getRoomQuery: id => new TaskRoomApplicationService(this.options.configPath).getRoomDetail(id),
|
|
1036
|
+
listTemplateQueries: () => new TaskRoomApplicationService(this.options.configPath).listTemplates(),
|
|
1037
|
+
getTemplateQuery: name => new TaskRoomApplicationService(this.options.configPath).getTemplate(name),
|
|
1038
|
+
createRoom: input => new TaskRoomApplicationService(this.options.configPath).createRoom({
|
|
1039
|
+
...input, actor: { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id },
|
|
1040
|
+
}),
|
|
994
1041
|
recentEvents: limit => this.options.session.eventsSince(0).slice(-limit),
|
|
995
1042
|
readWorklogTail: maxChars => this.readWorklogTail(maxChars),
|
|
996
1043
|
reply: async (replyText) => { await this.send(sender.id, replyText, wireId); },
|
|
@@ -1009,7 +1056,7 @@ export class OwnerChannel {
|
|
|
1009
1056
|
/** Queue raw slash text to the harness and report the turn's outcome. */
|
|
1010
1057
|
async runHarnessCommand(sender, command, wireId) {
|
|
1011
1058
|
const requestId = this.requestId(wireId);
|
|
1012
|
-
const queued = await this.options.session
|
|
1059
|
+
const queued = await queueSessionPrompt(this.options.session, command, {
|
|
1013
1060
|
origin: { kind: 'owner', requestId },
|
|
1014
1061
|
});
|
|
1015
1062
|
this.inFlight.add(wireId);
|
|
@@ -1036,6 +1083,7 @@ export class OwnerChannel {
|
|
|
1036
1083
|
*/
|
|
1037
1084
|
async restartSelf(sender, mode, wireId) {
|
|
1038
1085
|
const command = mode === 'fresh' ? '/force-restart' : '/restart';
|
|
1086
|
+
await this.prepareRestart(this.options.role, mode);
|
|
1039
1087
|
await this.send(sender.id, ownerNotices.restarting(this.options.role, command, mode), wireId);
|
|
1040
1088
|
this.state.remember(wireId);
|
|
1041
1089
|
this.options.log(`[${this.options.role}] owner requested ${command}`);
|
|
@@ -1046,8 +1094,9 @@ export class OwnerChannel {
|
|
|
1046
1094
|
* before an external worker starts a saga that can retire this process.
|
|
1047
1095
|
*/
|
|
1048
1096
|
async closeRoomFromOwner(sender, roomId, wireId) {
|
|
1049
|
-
const
|
|
1050
|
-
|
|
1097
|
+
const app = new TaskRoomApplicationService(this.options.configPath);
|
|
1098
|
+
const actor = { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id };
|
|
1099
|
+
await app.requestRoomDeletion({ actor, roomId });
|
|
1051
1100
|
await this.send(sender.id, renderMarkdownFailure({
|
|
1052
1101
|
kind: 'pending', subject: `/room delete ${roomId} ${roomId}`,
|
|
1053
1102
|
detail: 'The deletion request was accepted and is still being settled.',
|
|
@@ -1058,24 +1107,97 @@ export class OwnerChannel {
|
|
|
1058
1107
|
await this.fleetOps.closeRoom(roomId);
|
|
1059
1108
|
}
|
|
1060
1109
|
catch (error) {
|
|
1061
|
-
await
|
|
1110
|
+
await app.recordRoomSettlementError({ actor, roomId,
|
|
1111
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1112
|
+
recoveryHint: `External delete worker failed to start. Retry /room delete ${roomId} ${roomId}.` });
|
|
1062
1113
|
throw error;
|
|
1063
1114
|
}
|
|
1064
1115
|
}
|
|
1116
|
+
async recoverRoomFromOwner(sender, roomId, wireId) {
|
|
1117
|
+
const app = new TaskRoomApplicationService(this.options.configPath);
|
|
1118
|
+
const actor = { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id };
|
|
1119
|
+
const result = await app.recoverRoom({ actor, roomId });
|
|
1120
|
+
if (result.kind === 'deletion_worker_required') {
|
|
1121
|
+
await this.send(sender.id, renderMarkdownFailure({ kind: 'pending',
|
|
1122
|
+
subject: `/room recover ${roomId}`,
|
|
1123
|
+
detail: 'The deletion recovery is still being settled.',
|
|
1124
|
+
action: `Run /room recover ${roomId} if deletion remains pending.` }), wireId);
|
|
1125
|
+
this.state.remember(wireId);
|
|
1126
|
+
try {
|
|
1127
|
+
await this.fleetOps.closeRoom(roomId);
|
|
1128
|
+
}
|
|
1129
|
+
catch (error) {
|
|
1130
|
+
await app.recordRoomSettlementError({ actor, roomId,
|
|
1131
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1132
|
+
recoveryHint: `External delete worker failed to start. Retry /room delete ${roomId} ${roomId}.` });
|
|
1133
|
+
throw error;
|
|
1134
|
+
}
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
const r = result.orchestration;
|
|
1138
|
+
await this.send(sender.id, renderMarkdownResult({ icon: '🛟', title: 'Room recovery',
|
|
1139
|
+
fields: [{ label: 'Room', value: result.room.room_id, kind: 'code' },
|
|
1140
|
+
{ label: 'Status', value: roomStatus(result.room.state), kind: 'markdown' },
|
|
1141
|
+
...(r ? [{ label: 'Saga', value: r.saga.phase, kind: 'code' }] : [])],
|
|
1142
|
+
sections: result.issues.length ? [{ heading: 'Next steps', items: result.issues }]
|
|
1143
|
+
: [{ heading: 'Result', items: ['No recovery action is needed.'] }] }), wireId);
|
|
1144
|
+
this.state.remember(wireId);
|
|
1145
|
+
}
|
|
1065
1146
|
/** Carry a task terminal request through a worker that survives this role. */
|
|
1147
|
+
async recoverTaskFromOwner(sender, taskId, wireId) {
|
|
1148
|
+
const app = new TaskRoomApplicationService(this.options.configPath);
|
|
1149
|
+
const actor = { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id };
|
|
1150
|
+
const begin = await app.beginTaskRecovery({ actor, taskId });
|
|
1151
|
+
if (begin.kind === 'terminal_worker_required') {
|
|
1152
|
+
await this.send(sender.id, renderMarkdownFailure({
|
|
1153
|
+
kind: 'pending', subject: `/task recover ${taskId}`,
|
|
1154
|
+
detail: 'The recovery request was accepted and is still being settled.',
|
|
1155
|
+
action: `Run /task recover ${taskId} if it remains pending.`,
|
|
1156
|
+
}), wireId);
|
|
1157
|
+
this.state.remember(wireId);
|
|
1158
|
+
try {
|
|
1159
|
+
await this.fleetOps.recoverTask(taskId);
|
|
1160
|
+
}
|
|
1161
|
+
catch (error) {
|
|
1162
|
+
await app.recordSettlementError({ actor, taskId,
|
|
1163
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1164
|
+
recoveryHint: `External settle worker failed to start. Retry /task recover ${taskId}.` });
|
|
1165
|
+
throw error;
|
|
1166
|
+
}
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
const { task, room, issues } = begin.result;
|
|
1170
|
+
const hints = issues.map(issue => issue.code === 'waiting_cowork' ? 'Cowork socket unreachable'
|
|
1171
|
+
: issue.code === 'waiting_owner_invite' ? 'Owner invite missing or invalid'
|
|
1172
|
+
: issue.code === 'owner_cid_mismatch' ? 'Owner CID mismatch'
|
|
1173
|
+
: issue.code === 'member_failed' ? `Member failed at step ${issue.stepIndex}`
|
|
1174
|
+
: issue.code === 'resume_failed' ? `Resume failed: ${issue.error}`
|
|
1175
|
+
: issue.code === 'provisioning_resumed' ? 'Provisioning resumed successfully'
|
|
1176
|
+
: issue.code);
|
|
1177
|
+
await this.send(sender.id, renderMarkdownResult({
|
|
1178
|
+
icon: '🛟', title: 'Task recovery',
|
|
1179
|
+
fields: [{ label: 'Task', value: task.task_id, kind: 'code' },
|
|
1180
|
+
{ label: 'Status', value: taskStatus(task.state), kind: 'markdown' },
|
|
1181
|
+
...(room ? [{ label: 'Room', value: room.room_id, kind: 'code' },
|
|
1182
|
+
{ label: 'Room status', value: roomStatus(room.state), kind: 'markdown' },
|
|
1183
|
+
{ label: 'Saga', value: room.saga.phase, kind: 'code' }] : [])],
|
|
1184
|
+
sections: hints.length ? [{ heading: 'Next steps', items: hints }]
|
|
1185
|
+
: [{ heading: 'Result', items: ['No automated recovery action is available.'] }],
|
|
1186
|
+
}), wireId);
|
|
1187
|
+
this.state.remember(wireId);
|
|
1188
|
+
}
|
|
1066
1189
|
async terminalTaskFromOwner(sender, taskId, kind, outcome, wireId) {
|
|
1067
|
-
const
|
|
1068
|
-
const {
|
|
1069
|
-
const
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
if (accepted.terminal_intent?.status === 'settled') {
|
|
1190
|
+
const app = new TaskRoomApplicationService(this.options.configPath);
|
|
1191
|
+
const actor = { kind: 'authenticated_owner', surface: 'messenger', cid: sender.id };
|
|
1192
|
+
const plan = kind === 'done'
|
|
1193
|
+
? await app.completeTask({ actor, taskId, outcome })
|
|
1194
|
+
: await app.cancelTask({ actor, taskId });
|
|
1195
|
+
if (!plan.settlementRequired) {
|
|
1074
1196
|
await this.send(sender.id, renderMarkdownResult({
|
|
1075
1197
|
icon: '📋', title: 'Task terminal action complete',
|
|
1076
1198
|
fields: [
|
|
1077
1199
|
{ label: 'ID', value: taskId, kind: 'code' },
|
|
1078
|
-
{ label: 'Status', value: taskStatus(
|
|
1200
|
+
{ label: 'Status', value: taskStatus(plan.task.state), kind: 'markdown' },
|
|
1079
1201
|
],
|
|
1080
1202
|
}), wireId);
|
|
1081
1203
|
this.state.remember(wireId);
|
|
@@ -1091,7 +1213,10 @@ export class OwnerChannel {
|
|
|
1091
1213
|
await this.fleetOps.settleTask(taskId);
|
|
1092
1214
|
}
|
|
1093
1215
|
catch (error) {
|
|
1094
|
-
await
|
|
1216
|
+
await app.recordSettlementError({
|
|
1217
|
+
actor, taskId, error: error instanceof Error ? error.message : String(error),
|
|
1218
|
+
recoveryHint: `External settle worker failed to start. Retry the identical task command or run task recover ${taskId}.`,
|
|
1219
|
+
});
|
|
1095
1220
|
throw error;
|
|
1096
1221
|
}
|
|
1097
1222
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { InterruptOutcome, SessionEvent, SessionSnapshot } from '../session/types.js';
|
|
2
|
-
import type { TaskOutcome, TaskTerminalIntent } from '../rooms-tasks/types.js';
|
|
2
|
+
import type { RoomOrchestrationRecord, TaskOutcome, TaskRecord, TaskTerminalIntent } from '../rooms-tasks/types.js';
|
|
3
3
|
import { type OwnerCommentsState } from './notices.js';
|
|
4
|
+
import type { CreateRoomRequest, CreateTaskRequest, TaskRoomApplicationService } from '../application/task-room-service.js';
|
|
5
|
+
import type { TaskState } from '../rooms-tasks/types.js';
|
|
4
6
|
/**
|
|
5
7
|
* Fleet-level effects a deterministic owner command may trigger. Production
|
|
6
8
|
* uses the detached CLI (`fleetCliOps`); tests inject fakes so no command can
|
|
@@ -15,6 +17,7 @@ export interface OwnerFleetOps {
|
|
|
15
17
|
closeRoom(roomId: string): Promise<void>;
|
|
16
18
|
/** Resume a task terminal intent outside the caller role's supervisor lifecycle. */
|
|
17
19
|
settleTask(taskId: string): Promise<void>;
|
|
20
|
+
recoverTask(taskId: string): Promise<void>;
|
|
18
21
|
}
|
|
19
22
|
/**
|
|
20
23
|
* The narrow capability surface a command executor sees. Everything here is
|
|
@@ -47,8 +50,30 @@ export interface OwnerCommandContext {
|
|
|
47
50
|
fleetList(): Promise<string>;
|
|
48
51
|
/** Persist acceptance, acknowledge it, then launch the external close worker. */
|
|
49
52
|
closeRoom(roomId: string): Promise<void>;
|
|
53
|
+
recoverRoom(roomId: string): Promise<void>;
|
|
50
54
|
/** Persist terminal intent, acknowledge it, then launch the external settle worker. */
|
|
51
55
|
terminalTask(taskId: string, kind: TaskTerminalIntent['kind'], outcome?: TaskOutcome): Promise<void>;
|
|
56
|
+
recoverTask(taskId: string): Promise<void>;
|
|
57
|
+
createTask(input: Omit<CreateTaskRequest, 'actor'>): Promise<TaskRecord>;
|
|
58
|
+
startTask(taskId: string): Promise<TaskRecord>;
|
|
59
|
+
listTasks(filter?: {
|
|
60
|
+
state?: TaskState | TaskState[];
|
|
61
|
+
}): TaskRecord[];
|
|
62
|
+
getTask(taskId: string): {
|
|
63
|
+
task: TaskRecord;
|
|
64
|
+
orchestration: RoomOrchestrationRecord | undefined;
|
|
65
|
+
};
|
|
66
|
+
blockTask(taskId: string, reason: string): TaskRecord;
|
|
67
|
+
unblockTask(taskId: string): TaskRecord;
|
|
68
|
+
reviewTask(taskId: string): TaskRecord;
|
|
69
|
+
deleteTask(taskId: string): boolean;
|
|
70
|
+
listRoomQueries(filter?: {
|
|
71
|
+
state?: 'active' | 'provisioning';
|
|
72
|
+
}): ReturnType<TaskRoomApplicationService['listRooms']>;
|
|
73
|
+
getRoomQuery(id: string): ReturnType<TaskRoomApplicationService['getRoomDetail']>;
|
|
74
|
+
listTemplateQueries(): ReturnType<TaskRoomApplicationService['listTemplates']>;
|
|
75
|
+
getTemplateQuery(name: string): ReturnType<TaskRoomApplicationService['getTemplate']>;
|
|
76
|
+
createRoom(input: Omit<CreateRoomRequest, 'actor'>): Promise<RoomOrchestrationRecord>;
|
|
52
77
|
recentEvents(limit: number): SessionEvent[];
|
|
53
78
|
readWorklogTail(maxChars: number): Promise<string | undefined>;
|
|
54
79
|
reply(text: string): Promise<void>;
|