@saadnvd1/agent-os 0.3.578 → 0.3.580
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/app/api/tasks/[id]/hold/route.ts +28 -0
- package/app/api/tasks/route.ts +27 -2
- package/lib/bus/index.ts +82 -9
- package/lib/db/migrations.ts +55 -0
- package/lib/db/types.ts +2 -0
- package/lib/done/index.ts +2 -2
- package/lib/orchestrator/act.ts +41 -1
- package/lib/orchestrator/digest.ts +7 -1
- package/lib/orchestrator/read.ts +2 -2
- package/lib/orchestrator/task-state.ts +2 -2
- package/lib/orchestrator/tools.ts +1 -1
- package/lib/sessions/stop.ts +3 -3
- package/lib/sidebar/queued.ts +7 -1
- package/lib/tasks/held-remote.ts +140 -29
- package/lib/tasks/held.ts +106 -0
- package/lib/tasks/import.ts +6 -13
- package/lib/tasks/index.ts +21 -2
- package/lib/tasks/move-bundle.ts +3 -1
- package/lib/tasks/move-flow.ts +1 -20
- package/lib/tasks/placement.ts +2 -0
- package/lib/tasks/queue-held.ts +115 -0
- package/lib/tasks/queue.ts +49 -15
- package/lib/tasks/remote.ts +51 -3
- package/lib/tasks/start.ts +13 -1
- package/package.json +1 -1
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { callingDevice } from "@/lib/security/device-me";
|
|
3
|
+
import { holdExisting } from "@/lib/tasks/held";
|
|
4
|
+
import { validateOrchestrated } from "@/lib/tasks/move-bundle";
|
|
5
|
+
|
|
6
|
+
// POST /api/tasks/:id/hold - the paired device whose orchestrator started a
|
|
7
|
+
// task here asks to hold it, as a move would have: for a queue-placed task
|
|
8
|
+
// started before holding existed. It never starts a task.
|
|
9
|
+
export async function POST(
|
|
10
|
+
request: NextRequest,
|
|
11
|
+
ctx: { params: Promise<{ id: string }> }
|
|
12
|
+
) {
|
|
13
|
+
const id = (await ctx.params).id;
|
|
14
|
+
const device = callingDevice(request);
|
|
15
|
+
if (!device)
|
|
16
|
+
return NextResponse.json(
|
|
17
|
+
{ error: "An orchestrator's task comes only from a paired machine" },
|
|
18
|
+
{ status: 403 }
|
|
19
|
+
);
|
|
20
|
+
try {
|
|
21
|
+
const { orchestrated } = await request.json();
|
|
22
|
+
const held = holdExisting(id, device, validateOrchestrated(orchestrated));
|
|
23
|
+
return NextResponse.json({ held }, { status: held ? 200 : 404 });
|
|
24
|
+
} catch (error) {
|
|
25
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
26
|
+
return NextResponse.json({ error: message }, { status: 400 });
|
|
27
|
+
}
|
|
28
|
+
}
|
package/app/api/tasks/route.ts
CHANGED
|
@@ -5,7 +5,12 @@ import { stampPriority, startOrQueue } from "@/lib/tasks/queue";
|
|
|
5
5
|
import { isPriority } from "@/lib/tasks/schedule-types";
|
|
6
6
|
import { TASK_CAPABILITIES } from "@/lib/tasks/remote";
|
|
7
7
|
import { ensureProject, type ProjectRef } from "@/lib/tasks/project-ref";
|
|
8
|
-
import {
|
|
8
|
+
import { callingDevice } from "@/lib/security/device-me";
|
|
9
|
+
import {
|
|
10
|
+
isSessionId,
|
|
11
|
+
statusFor,
|
|
12
|
+
validateOrchestrated,
|
|
13
|
+
} from "@/lib/tasks/move-bundle";
|
|
9
14
|
import { startView } from "@/lib/sessions/launch";
|
|
10
15
|
import { isValidAgentType } from "@/lib/providers";
|
|
11
16
|
import { isAgentModelName } from "@/lib/providers/registry";
|
|
@@ -43,6 +48,7 @@ export async function POST(request: NextRequest) {
|
|
|
43
48
|
plan_only,
|
|
44
49
|
priority,
|
|
45
50
|
machine,
|
|
51
|
+
orchestrated,
|
|
46
52
|
} = await request.json();
|
|
47
53
|
if (id !== undefined && !isSessionId(id)) throw new Error("Bad task id");
|
|
48
54
|
const pid =
|
|
@@ -81,7 +87,26 @@ export async function POST(request: NextRequest) {
|
|
|
81
87
|
view: startView(view),
|
|
82
88
|
};
|
|
83
89
|
if (id !== undefined) {
|
|
84
|
-
|
|
90
|
+
// An orchestrator's task is held for the paired device that sent it,
|
|
91
|
+
// as a moved one is (lib/tasks/held.ts).
|
|
92
|
+
const device = callingDevice(request);
|
|
93
|
+
let held;
|
|
94
|
+
if (orchestrated !== undefined) {
|
|
95
|
+
if (!device)
|
|
96
|
+
return NextResponse.json(
|
|
97
|
+
{
|
|
98
|
+
error: "An orchestrator's task comes only from a paired machine",
|
|
99
|
+
},
|
|
100
|
+
{ status: 403 }
|
|
101
|
+
);
|
|
102
|
+
held = { device, orchestrated: validateOrchestrated(orchestrated) };
|
|
103
|
+
}
|
|
104
|
+
const session = await createTask({
|
|
105
|
+
id,
|
|
106
|
+
...opts,
|
|
107
|
+
held,
|
|
108
|
+
startedByDevice: device ?? undefined,
|
|
109
|
+
});
|
|
85
110
|
return NextResponse.json({ session }, { status: 201 });
|
|
86
111
|
}
|
|
87
112
|
const out = await startOrQueue(
|
package/lib/bus/index.ts
CHANGED
|
@@ -14,9 +14,14 @@ import { previousNames } from "../session-names";
|
|
|
14
14
|
import { deliverToPane, type Delivery } from "./delivery";
|
|
15
15
|
import { resolveRef, wasNames, type Candidate } from "./resolve";
|
|
16
16
|
import { tmuxPane } from "./tmux-pane";
|
|
17
|
-
import { hostLink } from "../hosts/remote-api";
|
|
18
17
|
import { pauseHoldOf, pausedNote } from "../tasks/pause-hold";
|
|
19
|
-
import { relayToHeldTask } from "../tasks/held-remote";
|
|
18
|
+
import { isHeldRemote, relayToHeldTask } from "../tasks/held-remote";
|
|
19
|
+
import {
|
|
20
|
+
assertTakesMessages,
|
|
21
|
+
findQueuedTarget,
|
|
22
|
+
heldQueuedNote,
|
|
23
|
+
type QueuedTarget,
|
|
24
|
+
} from "../tasks/queue-held";
|
|
20
25
|
import { heldOrchestrator, heldTarget } from "./held-reply";
|
|
21
26
|
import { HUMAN, overRateLimit, wakeLine, type BusMessageView } from "./format";
|
|
22
27
|
|
|
@@ -115,13 +120,29 @@ export function resolveTarget(ref: string): {
|
|
|
115
120
|
session: Session;
|
|
116
121
|
note?: string;
|
|
117
122
|
} {
|
|
123
|
+
const found = tryResolveTarget(ref);
|
|
124
|
+
if (!found.ok) throw new Error(found.error);
|
|
125
|
+
return found;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function tryResolveTarget(
|
|
129
|
+
ref: string
|
|
130
|
+
):
|
|
131
|
+
| { ok: true; session: Session; note?: string }
|
|
132
|
+
| { ok: false; none: boolean; error: string } {
|
|
118
133
|
const sessions = liveSessions();
|
|
119
134
|
const r = resolveRef(ref, candidates(sessions));
|
|
120
135
|
if (!r.ok)
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
136
|
+
return {
|
|
137
|
+
ok: false,
|
|
138
|
+
none: r.reason === "none",
|
|
139
|
+
error: r.reason === "none" ? `${r.error}. Run: aos peers` : r.error,
|
|
140
|
+
};
|
|
141
|
+
return {
|
|
142
|
+
ok: true,
|
|
143
|
+
session: sessions.find((s) => s.id === r.id)!,
|
|
144
|
+
note: r.note,
|
|
145
|
+
};
|
|
125
146
|
}
|
|
126
147
|
|
|
127
148
|
export function resolveSession(ref: string): Session {
|
|
@@ -188,7 +209,7 @@ export async function deliver(
|
|
|
188
209
|
): Promise<Delivery> {
|
|
189
210
|
// A task this machine's orchestrator moved to a linked machine: that
|
|
190
211
|
// machine's AgentOS delivers it, labelled as the orchestrator's (held.ts).
|
|
191
|
-
if (
|
|
212
|
+
if (isHeldRemote(to))
|
|
192
213
|
return relayToHeldTask(to, line, from.name, from.messageId);
|
|
193
214
|
// A linked machine's own session: an agent's message would reach it as
|
|
194
215
|
// the user's (see runner.ts), so none is relayed yet.
|
|
@@ -252,7 +273,13 @@ export async function sendMessage(opts: {
|
|
|
252
273
|
// A message pulled from a linked machine, by its key there: one already
|
|
253
274
|
// stored is not stored or delivered again.
|
|
254
275
|
relayKey?: string;
|
|
255
|
-
}): Promise<{
|
|
276
|
+
}): Promise<{
|
|
277
|
+
message: BusMessageView;
|
|
278
|
+
delivery: Delivery;
|
|
279
|
+
note?: string;
|
|
280
|
+
// The task it was held for, when that task is still in the start queue.
|
|
281
|
+
heldForQueued?: boolean;
|
|
282
|
+
}> {
|
|
256
283
|
const body = opts.body.trim();
|
|
257
284
|
if (!body) throw new Error("Message is empty");
|
|
258
285
|
const from = sessionById(opts.fromId);
|
|
@@ -260,7 +287,14 @@ export async function sendMessage(opts: {
|
|
|
260
287
|
// A held task's message to its orchestrator is kept for the holder to pull.
|
|
261
288
|
const held = heldTarget(from, opts.to);
|
|
262
289
|
if (from && held) return storeForHolder(from, held, body);
|
|
263
|
-
const
|
|
290
|
+
const target = tryResolveTarget(opts.to);
|
|
291
|
+
if (!target.ok) {
|
|
292
|
+
// A task still in the start queue has no session yet: hold it.
|
|
293
|
+
const queued = target.none ? findQueuedTarget(opts.to) : null;
|
|
294
|
+
if (!queued) throw new Error(target.error);
|
|
295
|
+
return storeForQueued(from, opts.fromLabel, queued, body, opts.relayKey);
|
|
296
|
+
}
|
|
297
|
+
const { session: to, note } = target;
|
|
264
298
|
if (from && from.id === to.id)
|
|
265
299
|
throw new Error("A session can't message itself");
|
|
266
300
|
if (from && overRateLimit(pairTimestamps(from.id, to.id))) {
|
|
@@ -355,6 +389,45 @@ function storeForHolder(
|
|
|
355
389
|
};
|
|
356
390
|
}
|
|
357
391
|
|
|
392
|
+
// A message for a task that hasn't started: kept with the queued task and
|
|
393
|
+
// delivered in its first prompt. A schedule's `line` is only a wake line;
|
|
394
|
+
// the body is what's held.
|
|
395
|
+
function storeForQueued(
|
|
396
|
+
from: Session | undefined,
|
|
397
|
+
fromLabel: string | undefined,
|
|
398
|
+
to: QueuedTarget,
|
|
399
|
+
body: string,
|
|
400
|
+
relayKey?: string
|
|
401
|
+
) {
|
|
402
|
+
assertTakesMessages(to);
|
|
403
|
+
if (from && overRateLimit(pairTimestamps(from.id, to.id)))
|
|
404
|
+
throw new Error(
|
|
405
|
+
`Too many messages between ${from.name} and ${to.name}; wait a few minutes`
|
|
406
|
+
);
|
|
407
|
+
const { lastInsertRowid } = db
|
|
408
|
+
.prepare(
|
|
409
|
+
`INSERT INTO bus_messages (from_id, from_name, to_id, to_name, body, relay_key, held_queued)
|
|
410
|
+
VALUES (?, ?, ?, ?, ?, ?, 1)`
|
|
411
|
+
)
|
|
412
|
+
.run(
|
|
413
|
+
from?.id ?? null,
|
|
414
|
+
from?.name ?? fromLabel ?? HUMAN,
|
|
415
|
+
to.id,
|
|
416
|
+
to.name,
|
|
417
|
+
body,
|
|
418
|
+
relayKey ?? null
|
|
419
|
+
);
|
|
420
|
+
const message = toView(
|
|
421
|
+
db.prepare(`${SELECT} WHERE m.id = ?`).get(Number(lastInsertRowid)) as Row
|
|
422
|
+
);
|
|
423
|
+
return {
|
|
424
|
+
message,
|
|
425
|
+
delivery: { state: "queued" as const },
|
|
426
|
+
note: heldQueuedNote(to.name),
|
|
427
|
+
heldForQueued: true,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
358
431
|
// Unread messages for a session, marked read as they're returned.
|
|
359
432
|
export function readInbox(sessionId: string): BusMessageView[] {
|
|
360
433
|
const rows = db
|
package/lib/db/migrations.ts
CHANGED
|
@@ -2242,6 +2242,61 @@ export const migrations: Migration[] = [
|
|
|
2242
2242
|
`);
|
|
2243
2243
|
},
|
|
2244
2244
|
},
|
|
2245
|
+
{
|
|
2246
|
+
key: "20261014T1235_placed_mirror_started_by",
|
|
2247
|
+
up: (db) => {
|
|
2248
|
+
// A task the queue started on a linked machine kept no record of the
|
|
2249
|
+
// orchestrator that queued it, so it couldn't be read, messaged or
|
|
2250
|
+
// stopped from there. Fills the mirror from the queue row; additive,
|
|
2251
|
+
// only where the mirror has none.
|
|
2252
|
+
db.exec(`
|
|
2253
|
+
UPDATE sessions SET
|
|
2254
|
+
started_by = (SELECT q.started_by FROM task_queue q WHERE q.id = sessions.id),
|
|
2255
|
+
task_since = COALESCE(task_since, created_at)
|
|
2256
|
+
WHERE started_by IS NULL AND task_prompt IS NOT NULL
|
|
2257
|
+
AND host_id IS NOT NULL AND host_id != 'local'
|
|
2258
|
+
AND (SELECT q.started_by FROM task_queue q WHERE q.id = sessions.id) IS NOT NULL
|
|
2259
|
+
`);
|
|
2260
|
+
},
|
|
2261
|
+
},
|
|
2262
|
+
{
|
|
2263
|
+
key: "20261014T1600_queued_task_messages",
|
|
2264
|
+
up: (db) => {
|
|
2265
|
+
// A message for a task still in the start queue, held (addressed to
|
|
2266
|
+
// the queue id, which is the session id it starts as) until its
|
|
2267
|
+
// first prompt. Additive.
|
|
2268
|
+
const has = (
|
|
2269
|
+
db.prepare(`PRAGMA table_info(bus_messages)`).all() as {
|
|
2270
|
+
name: string;
|
|
2271
|
+
}[]
|
|
2272
|
+
).some((c) => c.name === "held_queued");
|
|
2273
|
+
if (!has)
|
|
2274
|
+
db.exec(`ALTER TABLE bus_messages ADD COLUMN held_queued INTEGER`);
|
|
2275
|
+
db.exec(
|
|
2276
|
+
`CREATE INDEX IF NOT EXISTS idx_bus_messages_held_queued
|
|
2277
|
+
ON bus_messages(to_id) WHERE held_queued = 1`
|
|
2278
|
+
);
|
|
2279
|
+
},
|
|
2280
|
+
},
|
|
2281
|
+
{
|
|
2282
|
+
key: "20261014T1800_task_starter_device",
|
|
2283
|
+
up: (db) => {
|
|
2284
|
+
// The paired device that started a task over the API: the only one
|
|
2285
|
+
// that may later hold it. Tasks already running have no record, so
|
|
2286
|
+
// they're marked unrecorded. Additive.
|
|
2287
|
+
const has = (
|
|
2288
|
+
db.prepare(`PRAGMA table_info(sessions)`).all() as { name: string }[]
|
|
2289
|
+
).some((c) => c.name === "started_by_device");
|
|
2290
|
+
if (!has)
|
|
2291
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN started_by_device TEXT`);
|
|
2292
|
+
db.exec(`
|
|
2293
|
+
UPDATE sessions SET started_by_device = 'unrecorded'
|
|
2294
|
+
WHERE started_by_device IS NULL AND started_by IS NULL
|
|
2295
|
+
AND held_by_device IS NULL AND task_status = 'running'
|
|
2296
|
+
AND task_prompt IS NOT NULL AND (host_id IS NULL OR host_id = 'local')
|
|
2297
|
+
`);
|
|
2298
|
+
},
|
|
2299
|
+
},
|
|
2245
2300
|
];
|
|
2246
2301
|
|
|
2247
2302
|
// `upTo`: stop after this legacy id or timestamp key, inclusive (tests that
|
package/lib/db/types.ts
CHANGED
|
@@ -146,6 +146,8 @@ export interface Session {
|
|
|
146
146
|
// The device and machine of the orchestrator that moved this task here.
|
|
147
147
|
held_by_device?: string | null;
|
|
148
148
|
held_from?: string | null;
|
|
149
|
+
// The paired device that started this task here over the API.
|
|
150
|
+
started_by_device?: string | null;
|
|
149
151
|
held_for?: string | null;
|
|
150
152
|
held_for_name?: string | null;
|
|
151
153
|
// A held task's "plan written" line, for its orchestrator to read.
|
package/lib/done/index.ts
CHANGED
|
@@ -31,7 +31,7 @@ import { releasePorts } from "../ports";
|
|
|
31
31
|
import { dropSessionDatabase } from "../project-config/database";
|
|
32
32
|
import { doneOnPeer, peerSessionLink } from "../hosts/peer-actions";
|
|
33
33
|
import { hostLink } from "../hosts/remote-api";
|
|
34
|
-
import { doneHeldTask } from "../tasks/held-remote";
|
|
34
|
+
import { doneHeldTask, isHeldRemote } from "../tasks/held-remote";
|
|
35
35
|
import { captureMergedTask } from "../session-logs";
|
|
36
36
|
import { sessionEnded } from "../experiments/hooks";
|
|
37
37
|
import { pushMerged } from "../notify/push/events";
|
|
@@ -175,7 +175,7 @@ export async function doneSession(
|
|
|
175
175
|
await stopAgent(s);
|
|
176
176
|
// The sign-off's cleanup has already settled it; this reads the result.
|
|
177
177
|
worktree = await settleWorktree(s, { prHead: m.sha });
|
|
178
|
-
} else if (
|
|
178
|
+
} else if (isHeldRemote(s)) {
|
|
179
179
|
// Moved to a linked machine by its orchestrator: that machine stops the
|
|
180
180
|
// agent and removes the worktree, which is where they are.
|
|
181
181
|
await doneHeldTask(s);
|
package/lib/orchestrator/act.ts
CHANGED
|
@@ -40,6 +40,12 @@ import { braked } from "./brakes";
|
|
|
40
40
|
import { getOrchestrator } from "./home";
|
|
41
41
|
import { addNote } from "./notes";
|
|
42
42
|
import { findWorkspaceSession } from "./read";
|
|
43
|
+
import {
|
|
44
|
+
assertTakesMessages,
|
|
45
|
+
findQueuedTarget,
|
|
46
|
+
heldQueuedNote,
|
|
47
|
+
type QueuedTarget,
|
|
48
|
+
} from "../tasks/queue-held";
|
|
43
49
|
import { workspaceProject, workspaceStack, workspaceTask } from "./targets";
|
|
44
50
|
import { setPriority, stampPriority } from "../tasks/queue";
|
|
45
51
|
import {
|
|
@@ -68,7 +74,15 @@ export async function send(
|
|
|
68
74
|
message: string,
|
|
69
75
|
opts: { scopeChange?: boolean } = {}
|
|
70
76
|
): Promise<string> {
|
|
71
|
-
|
|
77
|
+
let to;
|
|
78
|
+
try {
|
|
79
|
+
to = findWorkspaceSession(workspaceId, ref);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
// A task still in the queue has no session: it holds the message.
|
|
82
|
+
const queued = findQueuedTarget(ref, { workspaceId });
|
|
83
|
+
if (!queued) throw error;
|
|
84
|
+
return sendToQueued(workspaceId, queued, message, opts);
|
|
85
|
+
}
|
|
72
86
|
let amended = "";
|
|
73
87
|
if (opts.scopeChange) {
|
|
74
88
|
if (to.task_status !== "running")
|
|
@@ -96,6 +110,32 @@ export async function send(
|
|
|
96
110
|
: `Delivered to ${to.name}.${amended}`;
|
|
97
111
|
}
|
|
98
112
|
|
|
113
|
+
// A queued task's id is the session id it starts as, so the amendment
|
|
114
|
+
// recorded under it is already the running task's.
|
|
115
|
+
async function sendToQueued(
|
|
116
|
+
workspaceId: string,
|
|
117
|
+
queued: QueuedTarget,
|
|
118
|
+
message: string,
|
|
119
|
+
opts: { scopeChange?: boolean }
|
|
120
|
+
): Promise<string> {
|
|
121
|
+
assertTakesMessages(queued);
|
|
122
|
+
let amended = "";
|
|
123
|
+
if (opts.scopeChange) {
|
|
124
|
+
amendBrief(workspaceId, queued.id, message);
|
|
125
|
+
addNote(workspaceId, `Scope change for ${queued.name}: ${message}`);
|
|
126
|
+
amended = ` Recorded as scope change ${amendmentsOf(queued.id).length} on its brief: reviews judge against it once it has a PR.`;
|
|
127
|
+
}
|
|
128
|
+
const sent = await sendMessage({
|
|
129
|
+
fromId: orchestratorId(workspaceId),
|
|
130
|
+
to: queued.id,
|
|
131
|
+
body: message,
|
|
132
|
+
});
|
|
133
|
+
// It may have started in between: then it was delivered as to any task.
|
|
134
|
+
return sent.heldForQueued
|
|
135
|
+
? `${heldQueuedNote(queued.name)}${amended}`
|
|
136
|
+
: `Delivered to ${queued.name}.${amended}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
99
139
|
// What the orchestrator starts opens as a chat, as a new session in the UI
|
|
100
140
|
// does (one default, in launch.ts); terminal is for a job that needs a TUI.
|
|
101
141
|
export { DEFAULT_START_VIEW, type StartView };
|
|
@@ -218,6 +218,8 @@ function queueSection(workspaceId: string): Section {
|
|
|
218
218
|
const rows = db
|
|
219
219
|
.prepare(
|
|
220
220
|
`SELECT q.id, q.name, q.prompt, q.status, q.after_task, q.after_any, q.priority, q.note,
|
|
221
|
+
(SELECT COUNT(*) FROM bus_messages b WHERE b.to_id = q.id
|
|
222
|
+
AND b.held_queued = 1 AND b.delivered_at IS NULL AND b.read_at IS NULL) AS held,
|
|
221
223
|
p.name AS project, a.name AS after_name
|
|
222
224
|
FROM task_queue q JOIN projects p ON p.id = q.project_id
|
|
223
225
|
LEFT JOIN sessions a ON a.id = q.after_task
|
|
@@ -235,6 +237,7 @@ function queueSection(workspaceId: string): Section {
|
|
|
235
237
|
after_name: string | null;
|
|
236
238
|
priority: string;
|
|
237
239
|
note: string | null;
|
|
240
|
+
held: number;
|
|
238
241
|
}[];
|
|
239
242
|
return {
|
|
240
243
|
title: "Queued tasks, in line order",
|
|
@@ -246,8 +249,11 @@ function queueSection(workspaceId: string): Section {
|
|
|
246
249
|
: "";
|
|
247
250
|
const status = r.status === "queued" ? "" : ` | ${r.status}`;
|
|
248
251
|
const priority = r.priority === "normal" ? "" : ` | ${r.priority}`;
|
|
252
|
+
const held = r.held
|
|
253
|
+
? ` | ${r.held} held message${r.held === 1 ? "" : "s"}`
|
|
254
|
+
: "";
|
|
249
255
|
const why = r.note ? ` | ${clip(r.note, 100)}` : "";
|
|
250
|
-
return `- ${i + 1}. ${clip(r.name ?? r.prompt, 80)} (${r.project}, id ${shortId(r.id)})${after}${priority}${status}${why}`;
|
|
256
|
+
return `- ${i + 1}. ${clip(r.name ?? r.prompt, 80)} (${r.project}, id ${shortId(r.id)})${after}${priority}${status}${held}${why}`;
|
|
251
257
|
}),
|
|
252
258
|
more: "the sessions tool",
|
|
253
259
|
};
|
package/lib/orchestrator/read.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { hostLink } from "../hosts/remote-api";
|
|
|
10
10
|
import { peerPane } from "../hosts/peer-sessions";
|
|
11
11
|
import { workspaceSessions } from "./facts";
|
|
12
12
|
import { untrusted } from "./untrusted";
|
|
13
|
-
import { heldChatTail } from "../tasks/held-remote";
|
|
13
|
+
import { heldChatTail, isHeldRemote } from "../tasks/held-remote";
|
|
14
14
|
import {
|
|
15
15
|
findExternalSession,
|
|
16
16
|
knownExternalSession,
|
|
@@ -130,7 +130,7 @@ export async function readSession(
|
|
|
130
130
|
const link = hostLink(s.host_id);
|
|
131
131
|
if (s.view === "chat" && link) {
|
|
132
132
|
// Its orchestrator's task is read through the machine that holds it.
|
|
133
|
-
const text = s
|
|
133
|
+
const text = isHeldRemote(s) ? await heldChatTail(s, n) : null;
|
|
134
134
|
return text
|
|
135
135
|
? `${s.name} (chat on ${link.hostName}), last ${n} lines:\n${untrusted(s.name, text)}`
|
|
136
136
|
: `${s.name} (chat on ${link.hostName}): its messages are on that machine; open it to read them.`;
|
|
@@ -9,7 +9,7 @@ import { blockedReason } from "../tasks/state";
|
|
|
9
9
|
import { checkWaitingPatterns, statusDetector } from "../status-detector";
|
|
10
10
|
import { getCheck, putCheck } from "./checks";
|
|
11
11
|
import { cleanRemoteText, hostLink, type HostLink } from "../hosts/remote-api";
|
|
12
|
-
import { heldChatFacts } from "../tasks/held-remote";
|
|
12
|
+
import { heldChatFacts, isHeldRemote } from "../tasks/held-remote";
|
|
13
13
|
import { hostTasks } from "../tasks/remote";
|
|
14
14
|
import { peerPane } from "../hosts/peer-sessions";
|
|
15
15
|
import { untrusted } from "./untrusted";
|
|
@@ -88,7 +88,7 @@ async function peerChatWaiting(
|
|
|
88
88
|
};
|
|
89
89
|
// A task its orchestrator moved is asked about directly: the whole task
|
|
90
90
|
// list is slow, and a miss there reads as unread.
|
|
91
|
-
const facts = task
|
|
91
|
+
const facts = isHeldRemote(task) ? await heldChatFacts(task) : null;
|
|
92
92
|
if (facts) {
|
|
93
93
|
const { turn } = facts;
|
|
94
94
|
return {
|
|
@@ -56,7 +56,7 @@ const DESCRIPTIONS: Record<ToolName, string> = {
|
|
|
56
56
|
read: "The end of one session's terminal or chat, or an external session's screen, as plain text (capped at about 4k tokens).",
|
|
57
57
|
cards: "Cards on this workspace's linked LumifyHub boards, by list.",
|
|
58
58
|
logs: "Session logs AgentOS wrote when tasks merged: the last N headers (task, PR, cost, duration) of the workspace or one project, or one log in full (brief, plan, timeline, usage, outcome). Read straight from the database; nothing is summarised.",
|
|
59
|
-
send: "Message a session in this workspace over the bus. It arrives as its next prompt. When Saad changes a running task's scope, tell it with scope_change set, so its review judges the PR against the brief as amended.",
|
|
59
|
+
send: "Message a session in this workspace over the bus. It arrives as its next prompt. When Saad changes a running task's scope, tell it with scope_change set, so its review judges the PR against the brief as amended. A task still in the queue (by id or name) takes messages too: they are held and reach it with its first prompt; scope_change on one records the change on its brief at once.",
|
|
60
60
|
orchestrators:
|
|
61
61
|
"Every workspace, and whether it has an orchestrator you can message.",
|
|
62
62
|
message_orchestrator:
|
package/lib/sessions/stop.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { endTmuxSession } from "../tmux/end";
|
|
|
2
2
|
import { hostLink } from "../hosts/remote-api";
|
|
3
3
|
import { stopChatTurn } from "../chat/stop";
|
|
4
4
|
import type { Session } from "../db";
|
|
5
|
-
import { stopHeldTask } from "../tasks/held-remote";
|
|
5
|
+
import { isHeldRemote, stopHeldTask } from "../tasks/held-remote";
|
|
6
6
|
import { releaseOnEnd } from "../simulator";
|
|
7
7
|
import { forgetPlanWait } from "../models/plan-step";
|
|
8
8
|
|
|
@@ -21,12 +21,12 @@ export async function stopSession(
|
|
|
21
21
|
| "tmux_name"
|
|
22
22
|
| "started_by"
|
|
23
23
|
| "task_prompt"
|
|
24
|
+
| "task_since"
|
|
24
25
|
>
|
|
25
26
|
): Promise<string> {
|
|
26
27
|
// A task its orchestrator moved to a linked machine stops through that
|
|
27
28
|
// machine's AgentOS, chat or terminal.
|
|
28
|
-
if (s
|
|
29
|
-
return stopHeldTask(s);
|
|
29
|
+
if (isHeldRemote(s)) return stopHeldTask(s);
|
|
30
30
|
if (s.view === "chat") {
|
|
31
31
|
if (hostLink(s.host_id))
|
|
32
32
|
throw new Error(
|
package/lib/sidebar/queued.ts
CHANGED
|
@@ -78,7 +78,13 @@ export function queuedSubtitle(item: QueuedTaskView): {
|
|
|
78
78
|
if (item.after) return { lead: "After", after: item.after };
|
|
79
79
|
const priority =
|
|
80
80
|
item.priority === "normal" ? "" : ` · ${PRIORITY_LABEL[item.priority]}`;
|
|
81
|
-
|
|
81
|
+
const held = item.held
|
|
82
|
+
? ` · ${item.held} message${item.held === 1 ? "" : "s"} held`
|
|
83
|
+
: "";
|
|
84
|
+
return {
|
|
85
|
+
lead: `#${item.rank} of ${item.of}${priority}${held}`,
|
|
86
|
+
after: null,
|
|
87
|
+
};
|
|
82
88
|
}
|
|
83
89
|
|
|
84
90
|
// The priority choices for a waiting task's menu, the current one marked.
|