@ours.network/fleet 0.19.0-nightly.9 โ 1.0.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 +66 -11
- package/dist/application/fleet-query-service.d.ts +1 -1
- package/dist/application/fleet-query-service.js +12 -0
- package/dist/application/role-creation-service.js +3 -1
- package/dist/application/types.d.ts +11 -0
- package/dist/briefing.d.ts +1 -1
- package/dist/briefing.js +103 -10
- package/dist/build-info.json +5 -5
- package/dist/cli.js +41 -11
- package/dist/config.d.ts +21 -7
- package/dist/config.js +32 -18
- package/dist/creation.d.ts +1 -1
- package/dist/docs.d.ts +2 -2
- package/dist/docs.js +117 -24
- package/dist/doctor.js +10 -10
- package/dist/duration.js +1 -1
- package/dist/fleet-proxy.d.ts +5 -0
- package/dist/harness/acp-agent.js +11 -6
- package/dist/harness/claude-code.js +201 -8
- package/dist/harness/codex.d.ts +4 -1
- package/dist/harness/codex.js +72 -13
- package/dist/harness/types.d.ts +57 -6
- package/dist/isolation/bubblewrap.d.ts +4 -4
- package/dist/isolation/bubblewrap.js +4 -4
- package/dist/isolation/policy.d.ts +1 -1
- package/dist/isolation/policy.js +2 -2
- package/dist/isolation/registry.d.ts +3 -3
- package/dist/isolation/registry.js +5 -5
- package/dist/isolation/resources.d.ts +1 -1
- package/dist/isolation/resources.js +1 -1
- package/dist/isolation/types.d.ts +3 -3
- package/dist/loops/manager.d.ts +30 -1
- package/dist/loops/manager.js +69 -6
- package/dist/loops/state.d.ts +18 -0
- package/dist/loops/state.js +4 -0
- package/dist/model-env.d.ts +71 -0
- package/dist/model-env.js +106 -0
- package/dist/monitor.d.ts +2 -2
- package/dist/monitor.js +4 -4
- package/dist/ops.d.ts +2 -2
- package/dist/ops.js +9 -9
- package/dist/owner-channel/channel.d.ts +17 -0
- package/dist/owner-channel/channel.js +104 -23
- package/dist/owner-channel/commands.d.ts +9 -0
- package/dist/owner-channel/commands.js +226 -99
- package/dist/owner-channel/notices.d.ts +7 -0
- package/dist/owner-channel/notices.js +9 -0
- package/dist/rooms-tasks/cli.js +731 -219
- package/dist/rooms-tasks/close.d.ts +35 -0
- package/dist/rooms-tasks/close.js +182 -0
- package/dist/rooms-tasks/cowork-adapter.d.ts +22 -0
- package/dist/rooms-tasks/cowork-adapter.js +116 -0
- package/dist/rooms-tasks/external-worker.d.ts +2 -0
- package/dist/rooms-tasks/external-worker.js +40 -0
- package/dist/rooms-tasks/index.d.ts +2 -0
- package/dist/rooms-tasks/index.js +2 -0
- package/dist/rooms-tasks/markdown.d.ts +49 -0
- package/dist/rooms-tasks/markdown.js +206 -0
- package/dist/rooms-tasks/member-startup.d.ts +51 -0
- package/dist/rooms-tasks/member-startup.js +151 -0
- package/dist/rooms-tasks/provision.d.ts +8 -0
- package/dist/rooms-tasks/provision.js +363 -97
- package/dist/rooms-tasks/room-state.d.ts +13 -1
- package/dist/rooms-tasks/room-state.js +149 -1
- package/dist/rooms-tasks/task-state.d.ts +13 -2
- package/dist/rooms-tasks/task-state.js +131 -3
- package/dist/rooms-tasks/terminal.d.ts +23 -0
- package/dist/rooms-tasks/terminal.js +56 -0
- package/dist/rooms-tasks/types.d.ts +108 -2
- package/dist/runner.d.ts +48 -0
- package/dist/runner.js +252 -100
- package/dist/session/acp.d.ts +106 -2
- package/dist/session/acp.js +217 -14
- package/dist/session/activity.d.ts +31 -0
- package/dist/session/activity.js +48 -0
- package/dist/session/control.d.ts +1 -1
- package/dist/session/conversation-normalizer.d.ts +7 -1
- package/dist/session/conversation-normalizer.js +154 -11
- package/dist/session/conversation-store.js +1 -1
- package/dist/session/conversation-types.d.ts +25 -6
- package/dist/session/types.d.ts +35 -0
- package/dist/spawn.d.ts +4 -2
- package/dist/spawn.js +39 -26
- package/dist/supervisor/launchd.d.ts +2 -2
- package/dist/supervisor/launchd.js +4 -4
- package/dist/supervisor/systemd.js +4 -4
- package/dist/supervisor/types.d.ts +1 -1
- package/dist/temp-lifecycle.d.ts +14 -2
- package/dist/temp-lifecycle.js +90 -0
- package/dist/watchdog/alerts.d.ts +2 -2
- package/dist/watchdog/alerts.js +2 -2
- package/dist/watchdog/briefing.d.ts +5 -5
- package/dist/watchdog/briefing.js +13 -6
- package/dist/watchdog/config.js +1 -1
- package/dist/watchdog/query.d.ts +2 -2
- package/dist/watchdog/query.js +2 -2
- package/dist/watchdog/report.js +2 -2
- package/dist/watchdog/run.d.ts +2 -2
- package/dist/watchdog/run.js +6 -6
- package/dist/watchdog/scheduler.d.ts +7 -7
- package/dist/watchdog/scheduler.js +10 -10
- package/dist/watchdog/service.d.ts +4 -4
- package/dist/watchdog/service.js +5 -6
- package/dist/watchdog/store.d.ts +4 -4
- package/dist/watchdog/store.js +4 -4
- package/dist/web/runtime.js +1 -1
- package/dist/web/server.js +1 -1
- package/dist/web/topology-promote.js +1 -1
- package/dist/web-app/assets/{TerminalView-BAVk1Bot.js โ TerminalView-C_G1ID2P.js} +1 -1
- package/dist/web-app/assets/{index-C3S-xFRU.js โ index-BCBK78hw.js} +5 -5
- package/dist/web-app/index.html +1 -1
- package/dist/worklog.d.ts +7 -1
- package/dist/worklog.js +191 -39
- package/package.json +1 -1
|
@@ -1,10 +1,92 @@
|
|
|
1
1
|
import { execFile, spawn } from 'node:child_process';
|
|
2
|
+
import { launchFleetWorker } from '../rooms-tasks/external-worker.js';
|
|
3
|
+
import { RoomStateError } from '../rooms-tasks/room-state.js';
|
|
4
|
+
import { TaskStateError } from '../rooms-tasks/task-state.js';
|
|
5
|
+
import { markdownCode, markdownProse, renderMarkdownFailure, renderMarkdownList, renderMarkdownResult, roomStatus, taskStatus, } from '../rooms-tasks/markdown.js';
|
|
2
6
|
import { OWNER_COMMENT_LABEL, ownerNotices } from './notices.js';
|
|
3
7
|
/** A malformed invocation; the dispatcher answers it with annotated help. */
|
|
4
8
|
class OwnerCommandUsageError extends Error {
|
|
5
9
|
}
|
|
6
10
|
const MODEL_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
7
11
|
const REPLY_MAX_CHARS = 3_500;
|
|
12
|
+
const taskListRecord = (task) => `${taskStatus(task.state)} ${markdownCode(task.task_id)} โ ${markdownProse(task.title)}`
|
|
13
|
+
+ (task.blocked ? ` โ ๐ง Blocked: ${markdownProse(task.blocked.reason)}` : '');
|
|
14
|
+
const roomListRecord = (room) => `${roomStatus(room.state)} ${markdownCode(room.room_id)} โ ${markdownProse(room.room_name)}`
|
|
15
|
+
+ (room.task_id ? ` โ Task ${markdownCode(room.task_id)}` : '');
|
|
16
|
+
const TASK_STATE_WORD = '(?:backlog|provisioning|active|review|done|cancelled|failed)';
|
|
17
|
+
const SAFE_ID_WORD = '[A-Za-z0-9_-]{1,128}';
|
|
18
|
+
function isKnownOwnerTaskState(message) {
|
|
19
|
+
return [
|
|
20
|
+
new RegExp(`^cannot transition from '${TASK_STATE_WORD}' to '${TASK_STATE_WORD}'$`, 'u'),
|
|
21
|
+
new RegExp(`^task ${SAFE_ID_WORD} has a pending '(?:done|cancelled)' terminal intent$`, 'u'),
|
|
22
|
+
new RegExp(`^cannot (?:block|cancel) a '${TASK_STATE_WORD}' task$`, 'u'),
|
|
23
|
+
/^task is not blocked$/u,
|
|
24
|
+
new RegExp(`^task ${SAFE_ID_WORD} already has a conflicting '(?:done|cancelled)' terminal intent$`, 'u'),
|
|
25
|
+
new RegExp(`^task ${SAFE_ID_WORD} is already in terminal state '${TASK_STATE_WORD}'$`, 'u'),
|
|
26
|
+
new RegExp(`^cannot delete a '${TASK_STATE_WORD}' task; only 'done' tasks can be deleted$`, 'u'),
|
|
27
|
+
].some(pattern => pattern.test(message));
|
|
28
|
+
}
|
|
29
|
+
function isKnownOwnerRoomState(message) {
|
|
30
|
+
return [
|
|
31
|
+
new RegExp(`^room ${SAFE_ID_WORD} is not closing$`, 'u'),
|
|
32
|
+
new RegExp(`^room ${SAFE_ID_WORD} has no recorded member .{1,128}$`, 'u'),
|
|
33
|
+
new RegExp(`^room ${SAFE_ID_WORD} close cannot move backward to [A-Za-z_]+$`, 'u'),
|
|
34
|
+
].some(pattern => pattern.test(message));
|
|
35
|
+
}
|
|
36
|
+
function ownerTaskFailure(error) {
|
|
37
|
+
if (error instanceof TaskStateError) {
|
|
38
|
+
const missing = /^task not found: ([A-Za-z0-9_-]{1,128})$/u.exec(error.message);
|
|
39
|
+
if (missing)
|
|
40
|
+
return {
|
|
41
|
+
kind: 'not_found', detail: `Task ${missing[1]} was not found.`,
|
|
42
|
+
action: 'Run /task list to find a valid task ID.',
|
|
43
|
+
};
|
|
44
|
+
if (isKnownOwnerTaskState(error.message))
|
|
45
|
+
return {
|
|
46
|
+
kind: 'state', detail: 'The current task state does not allow that action.',
|
|
47
|
+
action: 'Run /task show <id> to inspect the current task state.',
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
kind: 'unexpected',
|
|
52
|
+
action: 'Retry once; if it repeats, inspect the role logs.',
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function ownerRoomFailure(error) {
|
|
56
|
+
if (error instanceof RoomStateError) {
|
|
57
|
+
const missing = /^room not found: ([A-Za-z0-9_-]{1,128})$/u.exec(error.message);
|
|
58
|
+
if (missing)
|
|
59
|
+
return {
|
|
60
|
+
kind: 'not_found', detail: `Room ${missing[1]} was not found.`,
|
|
61
|
+
action: 'Run /room list to find a valid room ID.',
|
|
62
|
+
};
|
|
63
|
+
if (isKnownOwnerRoomState(error.message))
|
|
64
|
+
return {
|
|
65
|
+
kind: 'state', detail: 'The current room state does not allow that action.',
|
|
66
|
+
action: 'Run /room show <id> to inspect the current room state.',
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
kind: 'unexpected',
|
|
71
|
+
action: 'Retry once; if it repeats, inspect the role logs.',
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const taskAction = (title, task, fields = []) => renderMarkdownResult({
|
|
75
|
+
icon: '๐', title,
|
|
76
|
+
fields: [
|
|
77
|
+
{ label: 'ID', value: task.task_id, kind: 'code' },
|
|
78
|
+
{ label: 'Status', value: taskStatus(task.state), kind: 'markdown' },
|
|
79
|
+
...fields,
|
|
80
|
+
],
|
|
81
|
+
});
|
|
82
|
+
const roomAction = (title, room, fields = []) => renderMarkdownResult({
|
|
83
|
+
icon: '๐ ', title,
|
|
84
|
+
fields: [
|
|
85
|
+
{ label: 'ID', value: room.room_id, kind: 'code' },
|
|
86
|
+
{ label: 'Status', value: roomStatus(room.state), kind: 'markdown' },
|
|
87
|
+
...fields,
|
|
88
|
+
],
|
|
89
|
+
});
|
|
8
90
|
const strip = (value, max) => String(value).replace(/[\u0000-\u001f\u007f]/g, ' ').slice(0, max);
|
|
9
91
|
/** Like `strip`, but keeps newlines so multi-line listings stay readable. */
|
|
10
92
|
const stripMultiline = (value, max) => String(value).replace(/[\u0000-\u0009\u000b-\u001f\u007f]/g, ' ').slice(0, max);
|
|
@@ -149,18 +231,15 @@ export const ownerCommands = [
|
|
|
149
231
|
const { listTasks } = await import('../rooms-tasks/task-state.js');
|
|
150
232
|
const stateFilter = args?.trim() || undefined;
|
|
151
233
|
const tasks = listTasks(stateFilter && stateFilter !== 'all' ? { state: stateFilter } : undefined);
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
return `${t.task_id} ${t.state}${blocked} ${t.title}`;
|
|
157
|
-
});
|
|
158
|
-
await ctx.reply(tail(`๐ Tasks:\n${lines.join('\n')}`, REPLY_MAX_CHARS));
|
|
234
|
+
await ctx.reply(renderMarkdownList({
|
|
235
|
+
icon: '๐', title: 'Tasks', empty: 'No tasks found.',
|
|
236
|
+
records: tasks.map(taskListRecord),
|
|
237
|
+
}));
|
|
159
238
|
},
|
|
160
239
|
},
|
|
161
240
|
{
|
|
162
241
|
name: 'task',
|
|
163
|
-
usage: '/task <create|list|show|start|block|unblock|review|done|cancel|recover> ...',
|
|
242
|
+
usage: '/task <create|list|show|start|block|unblock|review|done|cancel|delete|recover> ...',
|
|
164
243
|
summary: 'task lifecycle subcommands',
|
|
165
244
|
execute: async (ctx, args) => {
|
|
166
245
|
if (!args)
|
|
@@ -173,16 +252,19 @@ export const ownerCommands = [
|
|
|
173
252
|
const showTask = async (id) => {
|
|
174
253
|
const { getTask } = await import('../rooms-tasks/task-state.js');
|
|
175
254
|
const t = getTask(id);
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
255
|
+
await ctx.reply(renderMarkdownResult({
|
|
256
|
+
icon: '๐', title: 'Task details',
|
|
257
|
+
fields: [
|
|
258
|
+
{ label: 'ID', value: t.task_id, kind: 'code' },
|
|
259
|
+
{ label: 'Title', value: t.title },
|
|
260
|
+
{ label: 'Status', value: taskStatus(t.state), kind: 'markdown' },
|
|
261
|
+
...(t.blocked ? [{ label: 'Blocked', value: t.blocked.reason }] : []),
|
|
262
|
+
...(t.template ? [{ label: 'Template', value: `${t.template.name}@${t.template.version}`, kind: 'code' }] : []),
|
|
263
|
+
...(t.room_id ? [{ label: 'Room', value: t.room_id, kind: 'code' }] : []),
|
|
264
|
+
{ label: 'Origin', value: t.origin.type },
|
|
265
|
+
{ label: 'Created', value: t.created_at, kind: 'code' },
|
|
266
|
+
],
|
|
267
|
+
}));
|
|
186
268
|
};
|
|
187
269
|
try {
|
|
188
270
|
switch (sub) {
|
|
@@ -205,12 +287,11 @@ export const ownerCommands = [
|
|
|
205
287
|
...(noRoom ? { no_room: true } : {}),
|
|
206
288
|
...(trailingLines ? { brief: trailingLines } : {}),
|
|
207
289
|
});
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
await ctx.reply(lines.join('\n'));
|
|
290
|
+
await ctx.reply(taskAction('Task created', t, [
|
|
291
|
+
{ label: 'Title', value: t.title },
|
|
292
|
+
...(template ? [{ label: 'Template', value: template, kind: 'code' }] : []),
|
|
293
|
+
...(noRoom ? [{ label: 'Room', value: 'Not requested' }] : []),
|
|
294
|
+
]));
|
|
214
295
|
break;
|
|
215
296
|
}
|
|
216
297
|
case 'list': {
|
|
@@ -226,20 +307,16 @@ export const ownerCommands = [
|
|
|
226
307
|
const tasks = listTasks(stateFilter);
|
|
227
308
|
if (filter === 'blocked') {
|
|
228
309
|
const blocked = tasks.filter(t => t.blocked);
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
}
|
|
233
|
-
const lines = blocked.map(t => `${t.task_id} ${t.state} ${t.title} [BLOCKED: ${t.blocked.reason}]`);
|
|
234
|
-
await ctx.reply(tail(`๐ Blocked tasks:\n${lines.join('\n')}`, REPLY_MAX_CHARS));
|
|
235
|
-
break;
|
|
236
|
-
}
|
|
237
|
-
if (!tasks.length) {
|
|
238
|
-
await ctx.reply('๐ No tasks.');
|
|
310
|
+
await ctx.reply(renderMarkdownList({
|
|
311
|
+
icon: '๐ง', title: 'Blocked tasks', empty: 'No blocked tasks found.',
|
|
312
|
+
records: blocked.map(taskListRecord),
|
|
313
|
+
}));
|
|
239
314
|
break;
|
|
240
315
|
}
|
|
241
|
-
|
|
242
|
-
|
|
316
|
+
await ctx.reply(renderMarkdownList({
|
|
317
|
+
icon: '๐', title: 'Tasks', empty: 'No tasks found.',
|
|
318
|
+
records: tasks.map(taskListRecord),
|
|
319
|
+
}));
|
|
243
320
|
break;
|
|
244
321
|
}
|
|
245
322
|
case 'show': {
|
|
@@ -253,7 +330,7 @@ export const ownerCommands = [
|
|
|
253
330
|
throw new OwnerCommandUsageError('usage: /task start <id>');
|
|
254
331
|
const { startTask } = await import('../rooms-tasks/task-state.js');
|
|
255
332
|
const t = startTask(rest[0]);
|
|
256
|
-
await ctx.reply(
|
|
333
|
+
await ctx.reply(taskAction('Task started', t));
|
|
257
334
|
break;
|
|
258
335
|
}
|
|
259
336
|
case 'block': {
|
|
@@ -262,7 +339,7 @@ export const ownerCommands = [
|
|
|
262
339
|
const { blockTask } = await import('../rooms-tasks/task-state.js');
|
|
263
340
|
const reason = rest.slice(1).join(' ');
|
|
264
341
|
const t = blockTask(rest[0], reason);
|
|
265
|
-
await ctx.reply(
|
|
342
|
+
await ctx.reply(taskAction('Task blocked', t, [{ label: 'Reason', value: reason }]));
|
|
266
343
|
break;
|
|
267
344
|
}
|
|
268
345
|
case 'unblock': {
|
|
@@ -270,7 +347,7 @@ export const ownerCommands = [
|
|
|
270
347
|
throw new OwnerCommandUsageError('usage: /task unblock <id>');
|
|
271
348
|
const { unblockTask } = await import('../rooms-tasks/task-state.js');
|
|
272
349
|
const t = unblockTask(rest[0]);
|
|
273
|
-
await ctx.reply(
|
|
350
|
+
await ctx.reply(taskAction('Task unblocked', t));
|
|
274
351
|
break;
|
|
275
352
|
}
|
|
276
353
|
case 'review': {
|
|
@@ -278,24 +355,31 @@ export const ownerCommands = [
|
|
|
278
355
|
throw new OwnerCommandUsageError('usage: /task review <id>');
|
|
279
356
|
const { reviewTask } = await import('../rooms-tasks/task-state.js');
|
|
280
357
|
const t = reviewTask(rest[0]);
|
|
281
|
-
await ctx.reply(
|
|
358
|
+
await ctx.reply(taskAction('Task ready for review', t));
|
|
282
359
|
break;
|
|
283
360
|
}
|
|
284
361
|
case 'done': {
|
|
285
362
|
if (!rest[0])
|
|
286
363
|
throw new OwnerCommandUsageError('usage: /task done <id> [summary]');
|
|
287
|
-
const { completeTask } = await import('../rooms-tasks/task-state.js');
|
|
288
364
|
const summary = rest.slice(1).join(' ') || undefined;
|
|
289
|
-
|
|
290
|
-
await ctx.reply(`โ
Task ${t.task_id} โ done`);
|
|
365
|
+
await ctx.terminalTask(rest[0], 'done', summary ? { summary } : undefined);
|
|
291
366
|
break;
|
|
292
367
|
}
|
|
293
368
|
case 'cancel': {
|
|
294
369
|
if (rest.length < 2 || rest[0] !== rest[1])
|
|
295
370
|
throw new OwnerCommandUsageError('destructive: /task cancel <id> <id> โ provide the task ID twice');
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
371
|
+
await ctx.terminalTask(rest[0], 'cancelled');
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
374
|
+
case 'delete': {
|
|
375
|
+
if (rest.length !== 2 || rest[0] !== rest[1])
|
|
376
|
+
throw new OwnerCommandUsageError('destructive: /task delete <id> <id> โ provide the task ID twice');
|
|
377
|
+
const { deleteTask } = await import('../rooms-tasks/task-state.js');
|
|
378
|
+
const deleted = deleteTask(rest[0]);
|
|
379
|
+
await ctx.reply(renderMarkdownResult({
|
|
380
|
+
icon: '๐๏ธ', title: deleted ? 'Task deleted' : 'Task already absent',
|
|
381
|
+
fields: [{ label: 'ID', value: rest[0], kind: 'code' }],
|
|
382
|
+
}));
|
|
299
383
|
break;
|
|
300
384
|
}
|
|
301
385
|
case 'recover': {
|
|
@@ -304,6 +388,10 @@ export const ownerCommands = [
|
|
|
304
388
|
const { getTask } = await import('../rooms-tasks/task-state.js');
|
|
305
389
|
const { getRoomRecord } = await import('../rooms-tasks/room-state.js');
|
|
306
390
|
const t = getTask(rest[0]);
|
|
391
|
+
if (t.terminal_intent?.status === 'pending') {
|
|
392
|
+
await ctx.terminalTask(t.task_id, t.terminal_intent.kind, t.terminal_intent.outcome);
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
307
395
|
const room = t.room_id ? getRoomRecord(t.room_id) : undefined;
|
|
308
396
|
const hints = [];
|
|
309
397
|
if (t.state === 'provisioning' && room) {
|
|
@@ -316,14 +404,21 @@ export const ownerCommands = [
|
|
|
316
404
|
if (room.provisioning_detail === 'member_failed')
|
|
317
405
|
hints.push(`Member failed at step ${room.saga.step_index}`);
|
|
318
406
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
407
|
+
await ctx.reply(renderMarkdownResult({
|
|
408
|
+
icon: '๐', title: 'Task recovery',
|
|
409
|
+
fields: [
|
|
410
|
+
{ label: 'Task', value: t.task_id, kind: 'code' },
|
|
411
|
+
{ label: 'Status', value: taskStatus(t.state), kind: 'markdown' },
|
|
412
|
+
...(room ? [
|
|
413
|
+
{ label: 'Room', value: room.room_id, kind: 'code' },
|
|
414
|
+
{ label: 'Room status', value: roomStatus(room.state), kind: 'markdown' },
|
|
415
|
+
{ label: 'Saga', value: room.saga.phase, kind: 'code' },
|
|
416
|
+
] : []),
|
|
417
|
+
],
|
|
418
|
+
sections: hints.length
|
|
419
|
+
? [{ heading: 'Next steps', items: hints }]
|
|
420
|
+
: [{ heading: 'Result', items: ['No automated recovery action is available.'] }],
|
|
421
|
+
}));
|
|
327
422
|
break;
|
|
328
423
|
}
|
|
329
424
|
default:
|
|
@@ -334,7 +429,13 @@ export const ownerCommands = [
|
|
|
334
429
|
catch (e) {
|
|
335
430
|
if (e instanceof OwnerCommandUsageError)
|
|
336
431
|
throw e;
|
|
337
|
-
|
|
432
|
+
const failure = ownerTaskFailure(e);
|
|
433
|
+
await ctx.reply(renderMarkdownFailure({
|
|
434
|
+
kind: failure.kind,
|
|
435
|
+
subject: `/task ${sub}`,
|
|
436
|
+
...(failure.detail ? { detail: failure.detail } : {}),
|
|
437
|
+
action: failure.action,
|
|
438
|
+
}));
|
|
338
439
|
}
|
|
339
440
|
},
|
|
340
441
|
},
|
|
@@ -342,16 +443,16 @@ export const ownerCommands = [
|
|
|
342
443
|
name: 'rooms', summary: 'list rooms',
|
|
343
444
|
execute: noArgs('/rooms', async (ctx) => {
|
|
344
445
|
const { listRoomRecords } = await import('../rooms-tasks/room-state.js');
|
|
345
|
-
const rooms = listRoomRecords();
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
446
|
+
const rooms = listRoomRecords().filter(room => room.state === 'active' || room.state === 'provisioning');
|
|
447
|
+
await ctx.reply(renderMarkdownList({
|
|
448
|
+
icon: '๐ ', title: 'Rooms', empty: 'No rooms found.',
|
|
449
|
+
records: rooms.map(roomListRecord),
|
|
450
|
+
}));
|
|
350
451
|
}),
|
|
351
452
|
},
|
|
352
453
|
{
|
|
353
454
|
name: 'room',
|
|
354
|
-
usage: '/room <create|list|show|close|recover> ...',
|
|
455
|
+
usage: '/room <create|list|show|delete|close|recover> ...',
|
|
355
456
|
summary: 'room lifecycle subcommands',
|
|
356
457
|
execute: async (ctx, args) => {
|
|
357
458
|
if (!args)
|
|
@@ -364,19 +465,24 @@ export const ownerCommands = [
|
|
|
364
465
|
const showRoom = async (id) => {
|
|
365
466
|
const { getRoomRecord } = await import('../rooms-tasks/room-state.js');
|
|
366
467
|
const r = getRoomRecord(id);
|
|
367
|
-
if (!r)
|
|
368
|
-
return ctx.reply(
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
468
|
+
if (!r || r.state === 'closing' || r.state === 'closed')
|
|
469
|
+
return ctx.reply(renderMarkdownFailure({
|
|
470
|
+
kind: 'not_found', subject: `/room show ${id}`,
|
|
471
|
+
detail: `Room ${id} was not found.`, action: 'Run /room list to find a valid room ID.',
|
|
472
|
+
}));
|
|
473
|
+
await ctx.reply(renderMarkdownResult({
|
|
474
|
+
icon: '๐ ', title: 'Room details',
|
|
475
|
+
fields: [
|
|
476
|
+
{ label: 'ID', value: r.room_id, kind: 'code' },
|
|
477
|
+
{ label: 'Name', value: r.room_name },
|
|
478
|
+
{ label: 'Status', value: roomStatus(r.state), kind: 'markdown' },
|
|
479
|
+
{ label: 'Saga', value: `${r.saga.phase} (step ${r.saga.step_index})` },
|
|
480
|
+
...(r.task_id ? [{ label: 'Task', value: r.task_id, kind: 'code' }] : []),
|
|
481
|
+
...(r.provisioning_detail ? [{ label: 'Detail', value: r.provisioning_detail }] : []),
|
|
482
|
+
...(r.saga.error ? [{ label: 'Last error', value: 'Provisioning failure recorded; inspect role logs.' }] : []),
|
|
483
|
+
{ label: 'Created', value: r.created_at, kind: 'code' },
|
|
484
|
+
],
|
|
485
|
+
}));
|
|
380
486
|
};
|
|
381
487
|
try {
|
|
382
488
|
switch (sub) {
|
|
@@ -402,24 +508,22 @@ export const ownerCommands = [
|
|
|
402
508
|
...(templateSnapshot ? { template_snapshot: templateSnapshot } : {}),
|
|
403
509
|
...(roomTrailingLines ? { goal: roomTrailingLines } : {}),
|
|
404
510
|
});
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
511
|
+
await ctx.reply(roomAction('Room created', r, [
|
|
512
|
+
{ label: 'Name', value: r.room_name },
|
|
513
|
+
...(templateName ? [{ label: 'Template', value: templateName, kind: 'code' }] : []),
|
|
514
|
+
]));
|
|
409
515
|
break;
|
|
410
516
|
}
|
|
411
517
|
case 'list': {
|
|
412
518
|
const { listRoomRecords } = await import('../rooms-tasks/room-state.js');
|
|
413
519
|
const filter = rest[0];
|
|
414
|
-
let rooms = listRoomRecords();
|
|
520
|
+
let rooms = listRoomRecords().filter(room => room.state === 'active' || room.state === 'provisioning');
|
|
415
521
|
if (filter === 'active')
|
|
416
522
|
rooms = rooms.filter(r => r.state === 'active');
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
}
|
|
421
|
-
const lines = rooms.map(r => `${r.room_id} ${r.state} ${r.room_name}${r.task_id ? ` (task: ${r.task_id})` : ''}`);
|
|
422
|
-
await ctx.reply(tail(`๐ Rooms:\n${lines.join('\n')}`, REPLY_MAX_CHARS));
|
|
523
|
+
await ctx.reply(renderMarkdownList({
|
|
524
|
+
icon: '๐ ', title: 'Rooms', empty: 'No rooms found.',
|
|
525
|
+
records: rooms.map(roomListRecord),
|
|
526
|
+
}));
|
|
423
527
|
break;
|
|
424
528
|
}
|
|
425
529
|
case 'show': {
|
|
@@ -428,12 +532,11 @@ export const ownerCommands = [
|
|
|
428
532
|
await showRoom(rest[0]);
|
|
429
533
|
break;
|
|
430
534
|
}
|
|
535
|
+
case 'delete':
|
|
431
536
|
case 'close': {
|
|
432
537
|
if (rest.length < 2 || rest[0] !== rest[1])
|
|
433
|
-
throw new OwnerCommandUsageError(
|
|
434
|
-
|
|
435
|
-
const r = closeRoom(rest[0]);
|
|
436
|
-
await ctx.reply(`๐ Room ${r.room_id} โ closed`);
|
|
538
|
+
throw new OwnerCommandUsageError(`destructive: /room ${sub} <id> <id> โ provide the room ID twice`);
|
|
539
|
+
await ctx.closeRoom(rest[0]);
|
|
437
540
|
break;
|
|
438
541
|
}
|
|
439
542
|
case 'recover': {
|
|
@@ -442,16 +545,29 @@ export const ownerCommands = [
|
|
|
442
545
|
const { getRoomRecord } = await import('../rooms-tasks/room-state.js');
|
|
443
546
|
const r = getRoomRecord(rest[0]);
|
|
444
547
|
if (!r) {
|
|
445
|
-
await ctx.reply(
|
|
548
|
+
await ctx.reply(renderMarkdownFailure({
|
|
549
|
+
kind: 'not_found', subject: `/room recover ${rest[0]}`,
|
|
550
|
+
detail: `Room ${rest[0]} was not found.`, action: 'Run /room list to find a valid room ID.',
|
|
551
|
+
}));
|
|
552
|
+
break;
|
|
553
|
+
}
|
|
554
|
+
if (r.state === 'closing' || r.state === 'closed') {
|
|
555
|
+
await ctx.closeRoom(r.room_id);
|
|
446
556
|
break;
|
|
447
557
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
558
|
+
await ctx.reply(renderMarkdownResult({
|
|
559
|
+
icon: '๐', title: 'Room recovery',
|
|
560
|
+
fields: [
|
|
561
|
+
{ label: 'Room', value: r.room_id, kind: 'code' },
|
|
562
|
+
{ label: 'Status', value: roomStatus(r.state), kind: 'markdown' },
|
|
563
|
+
{ label: 'Saga', value: r.saga.phase, kind: 'code' },
|
|
564
|
+
...(r.saga.error ? [{ label: 'Last error', value: 'Provisioning failure recorded; inspect role logs.' }] : []),
|
|
565
|
+
...(r.provisioning_detail ? [{ label: 'Detail', value: r.provisioning_detail }] : []),
|
|
566
|
+
],
|
|
567
|
+
sections: [{ heading: 'Next step', items: [r.saga.error
|
|
568
|
+
? 'Run ours-fleet room recover from the CLI for full recovery.'
|
|
569
|
+
: 'No recovery action is needed.'] }],
|
|
570
|
+
}));
|
|
455
571
|
break;
|
|
456
572
|
}
|
|
457
573
|
default:
|
|
@@ -462,7 +578,13 @@ export const ownerCommands = [
|
|
|
462
578
|
catch (e) {
|
|
463
579
|
if (e instanceof OwnerCommandUsageError)
|
|
464
580
|
throw e;
|
|
465
|
-
|
|
581
|
+
const failure = ownerRoomFailure(e);
|
|
582
|
+
await ctx.reply(renderMarkdownFailure({
|
|
583
|
+
kind: failure.kind,
|
|
584
|
+
subject: `/room ${sub}`,
|
|
585
|
+
...(failure.detail ? { detail: failure.detail } : {}),
|
|
586
|
+
action: failure.action,
|
|
587
|
+
}));
|
|
466
588
|
}
|
|
467
589
|
},
|
|
468
590
|
},
|
|
@@ -561,7 +683,10 @@ export async function dispatchOwnerCommand(text, ctx) {
|
|
|
561
683
|
}
|
|
562
684
|
catch (error) {
|
|
563
685
|
if (error instanceof OwnerCommandUsageError)
|
|
564
|
-
return ctx.reply(
|
|
686
|
+
return ctx.reply(renderMarkdownFailure({
|
|
687
|
+
kind: 'usage', subject: token, detail: error.message,
|
|
688
|
+
action: `Use ${command.usage ?? `/${command.name}`} or run /help.`,
|
|
689
|
+
}));
|
|
565
690
|
// The failure notice carries no internal detail; the channel logs it.
|
|
566
691
|
await ctx.reply(ownerNotices.commandFailed(command.usage ?? `/${command.name}`));
|
|
567
692
|
throw error;
|
|
@@ -583,5 +708,7 @@ export function fleetCliOps(role, configPath) {
|
|
|
583
708
|
list: () => new Promise((resolve, reject) => {
|
|
584
709
|
execFile(process.execPath, [process.argv[1], 'ls'], { timeout: 15_000, maxBuffer: 256 * 1024 }, (error, stdout) => error ? reject(error) : resolve(String(stdout).trim()));
|
|
585
710
|
}),
|
|
711
|
+
closeRoom: roomId => launchFleetWorker(['room', '_delete', roomId], `room-delete-${roomId}`, configPath),
|
|
712
|
+
settleTask: taskId => launchFleetWorker(['task', '_settle', taskId], `task-settle-${taskId}`, configPath),
|
|
586
713
|
};
|
|
587
714
|
}
|
|
@@ -22,6 +22,13 @@ export declare const ownerNotices: {
|
|
|
22
22
|
receivedStarted: () => string;
|
|
23
23
|
receivedQueued: (queuedBehind: number) => string;
|
|
24
24
|
receivedInterrupting: () => string;
|
|
25
|
+
/**
|
|
26
|
+
* The honest answer when the agent is mid-task and pre-empting it would have
|
|
27
|
+
* corrupted the conversation. Says "not started yet" rather than borrowing
|
|
28
|
+
* `receivedInterrupting`'s claim that something was cancelled for this
|
|
29
|
+
* request.
|
|
30
|
+
*/
|
|
31
|
+
receivedDeferred: () => string;
|
|
25
32
|
status: (role: string, snapshot: SessionSnapshot) => string;
|
|
26
33
|
interrupted: (role: string) => string;
|
|
27
34
|
/** The turn IS cancelled โ say how, without implying the owner must retry. */
|
|
@@ -26,6 +26,15 @@ export const ownerNotices = {
|
|
|
26
26
|
receivedInterrupting: () => "โน๏ธ Message received. The agent's previous task was interrupted to prioritize "
|
|
27
27
|
+ 'this request, and it is now working on a response. '
|
|
28
28
|
+ 'The response will arrive in this channel when ready.',
|
|
29
|
+
/**
|
|
30
|
+
* The honest answer when the agent is mid-task and pre-empting it would have
|
|
31
|
+
* corrupted the conversation. Says "not started yet" rather than borrowing
|
|
32
|
+
* `receivedInterrupting`'s claim that something was cancelled for this
|
|
33
|
+
* request.
|
|
34
|
+
*/
|
|
35
|
+
receivedDeferred: () => 'โน๏ธ Message received and held. The agent is in the middle of a task that '
|
|
36
|
+
+ 'cannot be interrupted safely; this request starts as soon as that work '
|
|
37
|
+
+ 'reaches a stopping point. The response will arrive in this channel when ready.',
|
|
29
38
|
status: (role, snapshot) => `๐ ${role} status: ${snapshot.readiness}; session is ${snapshot.alive ? 'online' : 'offline'}.`,
|
|
30
39
|
interrupted: (role) => `๐ Interrupt sent to ${role}'s active turn.`,
|
|
31
40
|
/** The turn IS cancelled โ say how, without implying the owner must retry. */
|