@ours.network/fleet 0.19.0-nightly.13 โ 0.19.0-nightly.15
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 +11 -4
- package/dist/build-info.json +4 -4
- package/dist/cli.js +41 -11
- package/dist/config.d.ts +10 -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 +96 -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 +199 -8
- package/dist/harness/codex.d.ts +4 -1
- package/dist/harness/codex.js +70 -13
- package/dist/harness/types.d.ts +55 -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 +10 -0
- package/dist/owner-channel/channel.js +65 -26
- package/dist/owner-channel/commands.js +198 -87
- package/dist/owner-channel/notices.d.ts +7 -0
- package/dist/owner-channel/notices.js +9 -0
- package/dist/rooms-tasks/cli.js +453 -192
- package/dist/rooms-tasks/markdown.d.ts +49 -0
- package/dist/rooms-tasks/markdown.js +206 -0
- package/dist/rooms-tasks/provision.js +1 -1
- 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 +1 -1
- package/dist/spawn.js +38 -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/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
|
@@ -6,6 +6,7 @@ import { DEFAULT_OWNER_ATTACHMENT_MIME, canonicalCid, } from '../config.js';
|
|
|
6
6
|
import { replaceFileAtomically } from '../atomic-file.js';
|
|
7
7
|
import { ACP_CANCEL_DEADLINE_EXCEEDED, SessionControlError, interruptOutcome, } from '../session/types.js';
|
|
8
8
|
import { VERSION } from '../version.js';
|
|
9
|
+
import { renderMarkdownFailure, renderMarkdownResult, taskStatus, } from '../rooms-tasks/markdown.js';
|
|
9
10
|
import { dispatchOwnerCommand, fleetCliOps, isOwnerCommandText, } from './commands.js';
|
|
10
11
|
import { OURS_BOUND_ELSEWHERE, OursSdkClient, oursErrorCode, } from './ours-client.js';
|
|
11
12
|
import { ownerNotices, } from './notices.js';
|
|
@@ -195,10 +196,13 @@ export class OwnerChannel {
|
|
|
195
196
|
? ' with interruption'
|
|
196
197
|
: event.monitor.interrupt === 'after_tool' ? ' with after-tool steering' : '';
|
|
197
198
|
const monitor = `${event.monitor.mode} monitor${monitorPolicy}`;
|
|
199
|
+
const permission = event.permissionMode
|
|
200
|
+
? `; permission ${event.permissionMode.fleetMode}, native ${event.permissionMode.nativeMode}`
|
|
201
|
+
: '';
|
|
198
202
|
const inherited = event.inherited.length
|
|
199
203
|
? ` Supervisor inherited omitted defaults: ${event.inherited.join(', ')}.` : '';
|
|
200
204
|
await this.sendProactiveMessage(`๐งโ๐ป ${event.caller} spawned ${event.lifetime} agent ${event.role} `
|
|
201
|
-
+ `(${event.harness}/${event.session}${model}; ${monitor}).${inherited}`, `fleet-spawn\0${event.creationActionId}`, 0);
|
|
205
|
+
+ `(${event.harness}/${event.session}${model}; ${monitor}${permission}).${inherited}`, `fleet-spawn\0${event.creationActionId}`, 0);
|
|
202
206
|
});
|
|
203
207
|
this.managementTail = run.then(() => undefined, () => undefined);
|
|
204
208
|
return run;
|
|
@@ -789,17 +793,13 @@ export class OwnerChannel {
|
|
|
789
793
|
outbox = this.outboxDir(originWireId);
|
|
790
794
|
await mkdir(outbox, { recursive: true, mode: 0o700 });
|
|
791
795
|
const activityCursor = this.latestEventSeq(this.options.session.eventsSince(0));
|
|
792
|
-
const queued = await this.options.session.queuePrompt(this.ownerAttachmentPrompt(sender, originWireId, requestId,
|
|
796
|
+
const queued = await this.options.session.queuePrompt(this.ownerAttachmentPrompt(sender, originWireId, requestId, admitted, group.caption), {
|
|
793
797
|
interrupt: this.options.config.interrupt,
|
|
794
798
|
...(this.options.config.interrupt ? { interruptSource: 'owner' } : {}),
|
|
795
799
|
origin: { kind: 'owner', requestId,
|
|
796
800
|
...(group.caption ? { displayText: String(group.caption.text ?? '') } : {}) },
|
|
797
801
|
});
|
|
798
|
-
const accepted = this.
|
|
799
|
-
? ownerNotices.receivedInterrupting()
|
|
800
|
-
: queued.queuedBehind > 0
|
|
801
|
-
? ownerNotices.receivedQueued(queued.queuedBehind)
|
|
802
|
-
: ownerNotices.receivedStarted();
|
|
802
|
+
const accepted = this.acceptanceNotice(queued);
|
|
803
803
|
handledWireIds.forEach(wire => this.inFlight.add(wire));
|
|
804
804
|
const receipt = this.send(sender.id, accepted, originWireId).then(() => undefined).catch(error => {
|
|
805
805
|
this.logError(`attachment request ${requestId.slice(0, 12)} acceptance notice failed`, error);
|
|
@@ -918,7 +918,7 @@ export class OwnerChannel {
|
|
|
918
918
|
let queued;
|
|
919
919
|
const activityCursor = this.latestEventSeq(this.options.session.eventsSince(0));
|
|
920
920
|
try {
|
|
921
|
-
queued = await this.options.session.queuePrompt(this.ownerPrompt(sender, text, wireId
|
|
921
|
+
queued = await this.options.session.queuePrompt(this.ownerPrompt(sender, text, wireId), {
|
|
922
922
|
interrupt: this.options.config.interrupt,
|
|
923
923
|
...(this.options.config.interrupt ? { interruptSource: 'owner' } : {}),
|
|
924
924
|
origin: { kind: 'owner', requestId, displayText: text },
|
|
@@ -940,14 +940,7 @@ export class OwnerChannel {
|
|
|
940
940
|
this.state.remember(wireId);
|
|
941
941
|
return true;
|
|
942
942
|
}
|
|
943
|
-
|
|
944
|
-
// accepted into the ACP queue. Never claim this request is running while
|
|
945
|
-
// the session itself says earlier work remains ahead of it.
|
|
946
|
-
const accepted = queued.queuedBehind > 0
|
|
947
|
-
? ownerNotices.receivedQueued(queued.queuedBehind)
|
|
948
|
-
: this.options.config.interrupt
|
|
949
|
-
? ownerNotices.receivedInterrupting()
|
|
950
|
-
: ownerNotices.receivedStarted();
|
|
943
|
+
const accepted = this.acceptanceNotice(queued);
|
|
951
944
|
this.inFlight.add(wireId);
|
|
952
945
|
const receipt = this.send(sender.id, accepted, wireId).then(() => undefined).catch(error => {
|
|
953
946
|
this.logError(`request ${requestId.slice(0, 12)} acceptance notice failed`, error);
|
|
@@ -1055,7 +1048,11 @@ export class OwnerChannel {
|
|
|
1055
1048
|
async closeRoomFromOwner(sender, roomId, wireId) {
|
|
1056
1049
|
const { acceptManagedRoomClose, recordManagedRoomCloseError } = await import('../rooms-tasks/close.js');
|
|
1057
1050
|
await acceptManagedRoomClose(roomId);
|
|
1058
|
-
await this.send(sender.id,
|
|
1051
|
+
await this.send(sender.id, renderMarkdownFailure({
|
|
1052
|
+
kind: 'pending', subject: `/room delete ${roomId} ${roomId}`,
|
|
1053
|
+
detail: 'The deletion request was accepted and is still being settled.',
|
|
1054
|
+
action: `Run /room recover ${roomId} if deletion remains pending.`,
|
|
1055
|
+
}), wireId);
|
|
1059
1056
|
this.state.remember(wireId);
|
|
1060
1057
|
try {
|
|
1061
1058
|
await this.fleetOps.closeRoom(roomId);
|
|
@@ -1074,11 +1071,21 @@ export class OwnerChannel {
|
|
|
1074
1071
|
taskId, kind, roomId: task.room_id, outcome,
|
|
1075
1072
|
});
|
|
1076
1073
|
if (accepted.terminal_intent?.status === 'settled') {
|
|
1077
|
-
await this.send(sender.id,
|
|
1074
|
+
await this.send(sender.id, renderMarkdownResult({
|
|
1075
|
+
icon: '๐', title: 'Task terminal action complete',
|
|
1076
|
+
fields: [
|
|
1077
|
+
{ label: 'ID', value: taskId, kind: 'code' },
|
|
1078
|
+
{ label: 'Status', value: taskStatus(accepted.state), kind: 'markdown' },
|
|
1079
|
+
],
|
|
1080
|
+
}), wireId);
|
|
1078
1081
|
this.state.remember(wireId);
|
|
1079
1082
|
return;
|
|
1080
1083
|
}
|
|
1081
|
-
await this.send(sender.id,
|
|
1084
|
+
await this.send(sender.id, renderMarkdownFailure({
|
|
1085
|
+
kind: 'pending', subject: `/task ${kind === 'done' ? 'done' : 'cancel'} ${taskId}`,
|
|
1086
|
+
detail: 'The terminal request was accepted and is still being settled.',
|
|
1087
|
+
action: `Run /task recover ${taskId} if it remains pending.`,
|
|
1088
|
+
}), wireId);
|
|
1082
1089
|
this.state.remember(wireId);
|
|
1083
1090
|
try {
|
|
1084
1091
|
await this.fleetOps.settleTask(taskId);
|
|
@@ -1374,6 +1381,32 @@ export class OwnerChannel {
|
|
|
1374
1381
|
authorizationIntegrity() {
|
|
1375
1382
|
return this.options.config.agent ? { ok: true } : this.authorizations.integrity();
|
|
1376
1383
|
}
|
|
1384
|
+
/**
|
|
1385
|
+
* Report what the session actually did with the prompt, not what the config
|
|
1386
|
+
* asked for. `interrupt: true` used to be reported as "your request
|
|
1387
|
+
* interrupted the previous task" unconditionally; the session now answers
|
|
1388
|
+
* whether anything was cancelled, whether the request is queued behind
|
|
1389
|
+
* earlier prompts, or whether it is held until the current task reaches a
|
|
1390
|
+
* safe stopping point. Backends that report no delivery state keep the old
|
|
1391
|
+
* queuedBehind-based wording.
|
|
1392
|
+
*/
|
|
1393
|
+
acceptanceNotice(queued) {
|
|
1394
|
+
switch (queued.delivery) {
|
|
1395
|
+
case 'interrupted': return ownerNotices.receivedInterrupting();
|
|
1396
|
+
case 'deferred': return ownerNotices.receivedDeferred();
|
|
1397
|
+
case 'queued': return ownerNotices.receivedQueued(Math.max(1, queued.queuedBehind));
|
|
1398
|
+
case 'started': return ownerNotices.receivedStarted();
|
|
1399
|
+
default:
|
|
1400
|
+
// Interrupting the live turn does not remove prompts which were already
|
|
1401
|
+
// accepted into the ACP queue. Never claim this request is running while
|
|
1402
|
+
// the session itself says earlier work remains ahead of it.
|
|
1403
|
+
return queued.queuedBehind > 0
|
|
1404
|
+
? ownerNotices.receivedQueued(queued.queuedBehind)
|
|
1405
|
+
: this.options.config.interrupt
|
|
1406
|
+
? ownerNotices.receivedInterrupting()
|
|
1407
|
+
: ownerNotices.receivedStarted();
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1377
1410
|
async complete(active, outbox, queued, activityCursor) {
|
|
1378
1411
|
const progressMs = this.options.config.progress_interval_ms;
|
|
1379
1412
|
let lastSeq = activityCursor;
|
|
@@ -1574,7 +1607,7 @@ export class OwnerChannel {
|
|
|
1574
1607
|
throw new Error('commentary appears to contain secret, reasoning, or raw tool content');
|
|
1575
1608
|
return message;
|
|
1576
1609
|
}
|
|
1577
|
-
ownerAttachmentPrompt(sender, wireId, requestId,
|
|
1610
|
+
ownerAttachmentPrompt(sender, wireId, requestId, files, caption) {
|
|
1578
1611
|
const lines = [
|
|
1579
1612
|
'[fleet-owner]',
|
|
1580
1613
|
`Authenticated owner ${safeField(sender.name, 160)} (${sender.id}) sent owner-channel attachment request ${wireId}.`,
|
|
@@ -1598,10 +1631,13 @@ export class OwnerChannel {
|
|
|
1598
1631
|
lines.push(`- voice transcript status: ${transcription?.status ?? 'unavailable'}`, `- voice transcript fallback: audio path above; category ${transcription?.errorCategory ?? 'not_provided'}`);
|
|
1599
1632
|
}
|
|
1600
1633
|
}
|
|
1601
|
-
lines.push('Answer in your final assistant response; fleet routes it only to the authenticated sender and correlates it to the originating file wire.',
|
|
1634
|
+
lines.push('Answer in your final assistant response; fleet routes it only to the authenticated sender and correlates it to the originating file wire.',
|
|
1635
|
+
// send_file is the only delivery route an agent is given: a tool call either
|
|
1636
|
+
// delivers or reports an error, where a file written to disk does neither.
|
|
1637
|
+
'To send the owner a file โ now or later in this turn โ call ours `send_file`:', `contact: ${this.options.config.identity}`, 'and the path of the finished file. Fleet routes it to the authenticated owner.', 'A file written anywhere else is not delivered and nothing will report that it was not.', 'Use descriptive unique filenames. Send nothing the owner did not request or should not receive.');
|
|
1602
1638
|
return lines.join('\n');
|
|
1603
1639
|
}
|
|
1604
|
-
ownerPrompt(sender, text, wireId
|
|
1640
|
+
ownerPrompt(sender, text, wireId) {
|
|
1605
1641
|
return [
|
|
1606
1642
|
'[fleet-owner]',
|
|
1607
1643
|
`Authenticated owner ${sender.name} (${sender.id}) sent owner-channel message ${wireId}.`,
|
|
@@ -1615,10 +1651,13 @@ export class OwnerChannel {
|
|
|
1615
1651
|
] : [
|
|
1616
1652
|
'Managed-agent outbound relay is not configured; do not send intermediate or proactive owner-channel messages.',
|
|
1617
1653
|
]),
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
'
|
|
1621
|
-
|
|
1654
|
+
// send_file is the only delivery route an agent is given: a tool call either
|
|
1655
|
+
// delivers or reports an error, where a file written to disk does neither.
|
|
1656
|
+
'To send the owner a file โ now or later in this turn โ call ours `send_file`:',
|
|
1657
|
+
`contact: ${this.options.config.identity}`,
|
|
1658
|
+
'and the path of the finished file. Fleet routes it to the authenticated owner.',
|
|
1659
|
+
'A file written anywhere else is not delivered and nothing will report that it was not.',
|
|
1660
|
+
'Use descriptive unique filenames. Send nothing the owner did not request or should not receive.',
|
|
1622
1661
|
'',
|
|
1623
1662
|
text || '(empty message)',
|
|
1624
1663
|
].join('\n');
|
|
@@ -1,11 +1,92 @@
|
|
|
1
1
|
import { execFile, spawn } from 'node:child_process';
|
|
2
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';
|
|
3
6
|
import { OWNER_COMMENT_LABEL, ownerNotices } from './notices.js';
|
|
4
7
|
/** A malformed invocation; the dispatcher answers it with annotated help. */
|
|
5
8
|
class OwnerCommandUsageError extends Error {
|
|
6
9
|
}
|
|
7
10
|
const MODEL_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
8
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
|
+
});
|
|
9
90
|
const strip = (value, max) => String(value).replace(/[\u0000-\u001f\u007f]/g, ' ').slice(0, max);
|
|
10
91
|
/** Like `strip`, but keeps newlines so multi-line listings stay readable. */
|
|
11
92
|
const stripMultiline = (value, max) => String(value).replace(/[\u0000-\u0009\u000b-\u001f\u007f]/g, ' ').slice(0, max);
|
|
@@ -150,13 +231,10 @@ export const ownerCommands = [
|
|
|
150
231
|
const { listTasks } = await import('../rooms-tasks/task-state.js');
|
|
151
232
|
const stateFilter = args?.trim() || undefined;
|
|
152
233
|
const tasks = listTasks(stateFilter && stateFilter !== 'all' ? { state: stateFilter } : undefined);
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
return `${t.task_id} ${t.state}${blocked} ${t.title}`;
|
|
158
|
-
});
|
|
159
|
-
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
|
+
}));
|
|
160
238
|
},
|
|
161
239
|
},
|
|
162
240
|
{
|
|
@@ -174,16 +252,19 @@ export const ownerCommands = [
|
|
|
174
252
|
const showTask = async (id) => {
|
|
175
253
|
const { getTask } = await import('../rooms-tasks/task-state.js');
|
|
176
254
|
const t = getTask(id);
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
+
}));
|
|
187
268
|
};
|
|
188
269
|
try {
|
|
189
270
|
switch (sub) {
|
|
@@ -206,12 +287,11 @@ export const ownerCommands = [
|
|
|
206
287
|
...(noRoom ? { no_room: true } : {}),
|
|
207
288
|
...(trailingLines ? { brief: trailingLines } : {}),
|
|
208
289
|
});
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
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
|
+
]));
|
|
215
295
|
break;
|
|
216
296
|
}
|
|
217
297
|
case 'list': {
|
|
@@ -227,20 +307,16 @@ export const ownerCommands = [
|
|
|
227
307
|
const tasks = listTasks(stateFilter);
|
|
228
308
|
if (filter === 'blocked') {
|
|
229
309
|
const blocked = tasks.filter(t => t.blocked);
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
}
|
|
234
|
-
const lines = blocked.map(t => `${t.task_id} ${t.state} ${t.title} [BLOCKED: ${t.blocked.reason}]`);
|
|
235
|
-
await ctx.reply(tail(`๐ Blocked tasks:\n${lines.join('\n')}`, REPLY_MAX_CHARS));
|
|
236
|
-
break;
|
|
237
|
-
}
|
|
238
|
-
if (!tasks.length) {
|
|
239
|
-
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
|
+
}));
|
|
240
314
|
break;
|
|
241
315
|
}
|
|
242
|
-
|
|
243
|
-
|
|
316
|
+
await ctx.reply(renderMarkdownList({
|
|
317
|
+
icon: '๐', title: 'Tasks', empty: 'No tasks found.',
|
|
318
|
+
records: tasks.map(taskListRecord),
|
|
319
|
+
}));
|
|
244
320
|
break;
|
|
245
321
|
}
|
|
246
322
|
case 'show': {
|
|
@@ -254,7 +330,7 @@ export const ownerCommands = [
|
|
|
254
330
|
throw new OwnerCommandUsageError('usage: /task start <id>');
|
|
255
331
|
const { startTask } = await import('../rooms-tasks/task-state.js');
|
|
256
332
|
const t = startTask(rest[0]);
|
|
257
|
-
await ctx.reply(
|
|
333
|
+
await ctx.reply(taskAction('Task started', t));
|
|
258
334
|
break;
|
|
259
335
|
}
|
|
260
336
|
case 'block': {
|
|
@@ -263,7 +339,7 @@ export const ownerCommands = [
|
|
|
263
339
|
const { blockTask } = await import('../rooms-tasks/task-state.js');
|
|
264
340
|
const reason = rest.slice(1).join(' ');
|
|
265
341
|
const t = blockTask(rest[0], reason);
|
|
266
|
-
await ctx.reply(
|
|
342
|
+
await ctx.reply(taskAction('Task blocked', t, [{ label: 'Reason', value: reason }]));
|
|
267
343
|
break;
|
|
268
344
|
}
|
|
269
345
|
case 'unblock': {
|
|
@@ -271,7 +347,7 @@ export const ownerCommands = [
|
|
|
271
347
|
throw new OwnerCommandUsageError('usage: /task unblock <id>');
|
|
272
348
|
const { unblockTask } = await import('../rooms-tasks/task-state.js');
|
|
273
349
|
const t = unblockTask(rest[0]);
|
|
274
|
-
await ctx.reply(
|
|
350
|
+
await ctx.reply(taskAction('Task unblocked', t));
|
|
275
351
|
break;
|
|
276
352
|
}
|
|
277
353
|
case 'review': {
|
|
@@ -279,7 +355,7 @@ export const ownerCommands = [
|
|
|
279
355
|
throw new OwnerCommandUsageError('usage: /task review <id>');
|
|
280
356
|
const { reviewTask } = await import('../rooms-tasks/task-state.js');
|
|
281
357
|
const t = reviewTask(rest[0]);
|
|
282
|
-
await ctx.reply(
|
|
358
|
+
await ctx.reply(taskAction('Task ready for review', t));
|
|
283
359
|
break;
|
|
284
360
|
}
|
|
285
361
|
case 'done': {
|
|
@@ -300,9 +376,10 @@ export const ownerCommands = [
|
|
|
300
376
|
throw new OwnerCommandUsageError('destructive: /task delete <id> <id> โ provide the task ID twice');
|
|
301
377
|
const { deleteTask } = await import('../rooms-tasks/task-state.js');
|
|
302
378
|
const deleted = deleteTask(rest[0]);
|
|
303
|
-
await ctx.reply(
|
|
304
|
-
?
|
|
305
|
-
:
|
|
379
|
+
await ctx.reply(renderMarkdownResult({
|
|
380
|
+
icon: '๐๏ธ', title: deleted ? 'Task deleted' : 'Task already absent',
|
|
381
|
+
fields: [{ label: 'ID', value: rest[0], kind: 'code' }],
|
|
382
|
+
}));
|
|
306
383
|
break;
|
|
307
384
|
}
|
|
308
385
|
case 'recover': {
|
|
@@ -327,14 +404,21 @@ export const ownerCommands = [
|
|
|
327
404
|
if (room.provisioning_detail === 'member_failed')
|
|
328
405
|
hints.push(`Member failed at step ${room.saga.step_index}`);
|
|
329
406
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
+
}));
|
|
338
422
|
break;
|
|
339
423
|
}
|
|
340
424
|
default:
|
|
@@ -345,7 +429,13 @@ export const ownerCommands = [
|
|
|
345
429
|
catch (e) {
|
|
346
430
|
if (e instanceof OwnerCommandUsageError)
|
|
347
431
|
throw e;
|
|
348
|
-
|
|
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
|
+
}));
|
|
349
439
|
}
|
|
350
440
|
},
|
|
351
441
|
},
|
|
@@ -354,10 +444,10 @@ export const ownerCommands = [
|
|
|
354
444
|
execute: noArgs('/rooms', async (ctx) => {
|
|
355
445
|
const { listRoomRecords } = await import('../rooms-tasks/room-state.js');
|
|
356
446
|
const rooms = listRoomRecords().filter(room => room.state === 'active' || room.state === 'provisioning');
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
447
|
+
await ctx.reply(renderMarkdownList({
|
|
448
|
+
icon: '๐ ', title: 'Rooms', empty: 'No rooms found.',
|
|
449
|
+
records: rooms.map(roomListRecord),
|
|
450
|
+
}));
|
|
361
451
|
}),
|
|
362
452
|
},
|
|
363
453
|
{
|
|
@@ -376,18 +466,23 @@ export const ownerCommands = [
|
|
|
376
466
|
const { getRoomRecord } = await import('../rooms-tasks/room-state.js');
|
|
377
467
|
const r = getRoomRecord(id);
|
|
378
468
|
if (!r || r.state === 'closing' || r.state === 'closed')
|
|
379
|
-
return ctx.reply(
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
+
}));
|
|
391
486
|
};
|
|
392
487
|
try {
|
|
393
488
|
switch (sub) {
|
|
@@ -413,10 +508,10 @@ export const ownerCommands = [
|
|
|
413
508
|
...(templateSnapshot ? { template_snapshot: templateSnapshot } : {}),
|
|
414
509
|
...(roomTrailingLines ? { goal: roomTrailingLines } : {}),
|
|
415
510
|
});
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
511
|
+
await ctx.reply(roomAction('Room created', r, [
|
|
512
|
+
{ label: 'Name', value: r.room_name },
|
|
513
|
+
...(templateName ? [{ label: 'Template', value: templateName, kind: 'code' }] : []),
|
|
514
|
+
]));
|
|
420
515
|
break;
|
|
421
516
|
}
|
|
422
517
|
case 'list': {
|
|
@@ -425,12 +520,10 @@ export const ownerCommands = [
|
|
|
425
520
|
let rooms = listRoomRecords().filter(room => room.state === 'active' || room.state === 'provisioning');
|
|
426
521
|
if (filter === 'active')
|
|
427
522
|
rooms = rooms.filter(r => r.state === 'active');
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
}
|
|
432
|
-
const lines = rooms.map(r => `${r.room_id} ${r.state} ${r.room_name}${r.task_id ? ` (task: ${r.task_id})` : ''}`);
|
|
433
|
-
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
|
+
}));
|
|
434
527
|
break;
|
|
435
528
|
}
|
|
436
529
|
case 'show': {
|
|
@@ -452,20 +545,29 @@ export const ownerCommands = [
|
|
|
452
545
|
const { getRoomRecord } = await import('../rooms-tasks/room-state.js');
|
|
453
546
|
const r = getRoomRecord(rest[0]);
|
|
454
547
|
if (!r) {
|
|
455
|
-
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
|
+
}));
|
|
456
552
|
break;
|
|
457
553
|
}
|
|
458
554
|
if (r.state === 'closing' || r.state === 'closed') {
|
|
459
555
|
await ctx.closeRoom(r.room_id);
|
|
460
556
|
break;
|
|
461
557
|
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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
|
+
}));
|
|
469
571
|
break;
|
|
470
572
|
}
|
|
471
573
|
default:
|
|
@@ -476,7 +578,13 @@ export const ownerCommands = [
|
|
|
476
578
|
catch (e) {
|
|
477
579
|
if (e instanceof OwnerCommandUsageError)
|
|
478
580
|
throw e;
|
|
479
|
-
|
|
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
|
+
}));
|
|
480
588
|
}
|
|
481
589
|
},
|
|
482
590
|
},
|
|
@@ -575,7 +683,10 @@ export async function dispatchOwnerCommand(text, ctx) {
|
|
|
575
683
|
}
|
|
576
684
|
catch (error) {
|
|
577
685
|
if (error instanceof OwnerCommandUsageError)
|
|
578
|
-
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
|
+
}));
|
|
579
690
|
// The failure notice carries no internal detail; the channel logs it.
|
|
580
691
|
await ctx.reply(ownerNotices.commandFailed(command.usage ?? `/${command.name}`));
|
|
581
692
|
throw error;
|
|
@@ -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. */
|