@saadnvd1/agent-os 0.3.577 → 0.3.579
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/components/SessionList/SessionRowMenu.tsx +1 -1
- package/lib/bus/index.ts +80 -6
- package/lib/db/migrations.ts +19 -0
- package/lib/orchestrator/act.ts +41 -1
- package/lib/orchestrator/digest.ts +7 -1
- package/lib/orchestrator/tools.ts +1 -1
- package/lib/sidebar/queued.ts +7 -1
- package/lib/tasks/queue-held.ts +115 -0
- package/lib/tasks/queue.ts +31 -13
- package/lib/tasks/start.ts +13 -1
- package/package.json +1 -1
|
@@ -268,7 +268,7 @@ export function SessionRowMenu({
|
|
|
268
268
|
{summarizing ? "Summarizing..." : "Fresh start"}
|
|
269
269
|
</Item>
|
|
270
270
|
)}
|
|
271
|
-
{(session.role === "orchestrator" || session.has_brief) &&
|
|
271
|
+
{(session.role === "orchestrator" || !!session.has_brief) &&
|
|
272
272
|
session.view === "chat" &&
|
|
273
273
|
!session.archived_at &&
|
|
274
274
|
!blocked && (
|
package/lib/bus/index.ts
CHANGED
|
@@ -17,6 +17,12 @@ import { tmuxPane } from "./tmux-pane";
|
|
|
17
17
|
import { hostLink } from "../hosts/remote-api";
|
|
18
18
|
import { pauseHoldOf, pausedNote } from "../tasks/pause-hold";
|
|
19
19
|
import { relayToHeldTask } from "../tasks/held-remote";
|
|
20
|
+
import {
|
|
21
|
+
assertTakesMessages,
|
|
22
|
+
findQueuedTarget,
|
|
23
|
+
heldQueuedNote,
|
|
24
|
+
type QueuedTarget,
|
|
25
|
+
} from "../tasks/queue-held";
|
|
20
26
|
import { heldOrchestrator, heldTarget } from "./held-reply";
|
|
21
27
|
import { HUMAN, overRateLimit, wakeLine, type BusMessageView } from "./format";
|
|
22
28
|
|
|
@@ -115,13 +121,29 @@ export function resolveTarget(ref: string): {
|
|
|
115
121
|
session: Session;
|
|
116
122
|
note?: string;
|
|
117
123
|
} {
|
|
124
|
+
const found = tryResolveTarget(ref);
|
|
125
|
+
if (!found.ok) throw new Error(found.error);
|
|
126
|
+
return found;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function tryResolveTarget(
|
|
130
|
+
ref: string
|
|
131
|
+
):
|
|
132
|
+
| { ok: true; session: Session; note?: string }
|
|
133
|
+
| { ok: false; none: boolean; error: string } {
|
|
118
134
|
const sessions = liveSessions();
|
|
119
135
|
const r = resolveRef(ref, candidates(sessions));
|
|
120
136
|
if (!r.ok)
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
137
|
+
return {
|
|
138
|
+
ok: false,
|
|
139
|
+
none: r.reason === "none",
|
|
140
|
+
error: r.reason === "none" ? `${r.error}. Run: aos peers` : r.error,
|
|
141
|
+
};
|
|
142
|
+
return {
|
|
143
|
+
ok: true,
|
|
144
|
+
session: sessions.find((s) => s.id === r.id)!,
|
|
145
|
+
note: r.note,
|
|
146
|
+
};
|
|
125
147
|
}
|
|
126
148
|
|
|
127
149
|
export function resolveSession(ref: string): Session {
|
|
@@ -252,7 +274,13 @@ export async function sendMessage(opts: {
|
|
|
252
274
|
// A message pulled from a linked machine, by its key there: one already
|
|
253
275
|
// stored is not stored or delivered again.
|
|
254
276
|
relayKey?: string;
|
|
255
|
-
}): Promise<{
|
|
277
|
+
}): Promise<{
|
|
278
|
+
message: BusMessageView;
|
|
279
|
+
delivery: Delivery;
|
|
280
|
+
note?: string;
|
|
281
|
+
// The task it was held for, when that task is still in the start queue.
|
|
282
|
+
heldForQueued?: boolean;
|
|
283
|
+
}> {
|
|
256
284
|
const body = opts.body.trim();
|
|
257
285
|
if (!body) throw new Error("Message is empty");
|
|
258
286
|
const from = sessionById(opts.fromId);
|
|
@@ -260,7 +288,14 @@ export async function sendMessage(opts: {
|
|
|
260
288
|
// A held task's message to its orchestrator is kept for the holder to pull.
|
|
261
289
|
const held = heldTarget(from, opts.to);
|
|
262
290
|
if (from && held) return storeForHolder(from, held, body);
|
|
263
|
-
const
|
|
291
|
+
const target = tryResolveTarget(opts.to);
|
|
292
|
+
if (!target.ok) {
|
|
293
|
+
// A task still in the start queue has no session yet: hold it.
|
|
294
|
+
const queued = target.none ? findQueuedTarget(opts.to) : null;
|
|
295
|
+
if (!queued) throw new Error(target.error);
|
|
296
|
+
return storeForQueued(from, opts.fromLabel, queued, body, opts.relayKey);
|
|
297
|
+
}
|
|
298
|
+
const { session: to, note } = target;
|
|
264
299
|
if (from && from.id === to.id)
|
|
265
300
|
throw new Error("A session can't message itself");
|
|
266
301
|
if (from && overRateLimit(pairTimestamps(from.id, to.id))) {
|
|
@@ -355,6 +390,45 @@ function storeForHolder(
|
|
|
355
390
|
};
|
|
356
391
|
}
|
|
357
392
|
|
|
393
|
+
// A message for a task that hasn't started: kept with the queued task and
|
|
394
|
+
// delivered in its first prompt. A schedule's `line` is only a wake line;
|
|
395
|
+
// the body is what's held.
|
|
396
|
+
function storeForQueued(
|
|
397
|
+
from: Session | undefined,
|
|
398
|
+
fromLabel: string | undefined,
|
|
399
|
+
to: QueuedTarget,
|
|
400
|
+
body: string,
|
|
401
|
+
relayKey?: string
|
|
402
|
+
) {
|
|
403
|
+
assertTakesMessages(to);
|
|
404
|
+
if (from && overRateLimit(pairTimestamps(from.id, to.id)))
|
|
405
|
+
throw new Error(
|
|
406
|
+
`Too many messages between ${from.name} and ${to.name}; wait a few minutes`
|
|
407
|
+
);
|
|
408
|
+
const { lastInsertRowid } = db
|
|
409
|
+
.prepare(
|
|
410
|
+
`INSERT INTO bus_messages (from_id, from_name, to_id, to_name, body, relay_key, held_queued)
|
|
411
|
+
VALUES (?, ?, ?, ?, ?, ?, 1)`
|
|
412
|
+
)
|
|
413
|
+
.run(
|
|
414
|
+
from?.id ?? null,
|
|
415
|
+
from?.name ?? fromLabel ?? HUMAN,
|
|
416
|
+
to.id,
|
|
417
|
+
to.name,
|
|
418
|
+
body,
|
|
419
|
+
relayKey ?? null
|
|
420
|
+
);
|
|
421
|
+
const message = toView(
|
|
422
|
+
db.prepare(`${SELECT} WHERE m.id = ?`).get(Number(lastInsertRowid)) as Row
|
|
423
|
+
);
|
|
424
|
+
return {
|
|
425
|
+
message,
|
|
426
|
+
delivery: { state: "queued" as const },
|
|
427
|
+
note: heldQueuedNote(to.name),
|
|
428
|
+
heldForQueued: true,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
358
432
|
// Unread messages for a session, marked read as they're returned.
|
|
359
433
|
export function readInbox(sessionId: string): BusMessageView[] {
|
|
360
434
|
const rows = db
|
package/lib/db/migrations.ts
CHANGED
|
@@ -2242,6 +2242,25 @@ export const migrations: Migration[] = [
|
|
|
2242
2242
|
`);
|
|
2243
2243
|
},
|
|
2244
2244
|
},
|
|
2245
|
+
{
|
|
2246
|
+
key: "20261014T1600_queued_task_messages",
|
|
2247
|
+
up: (db) => {
|
|
2248
|
+
// A message for a task still in the start queue, held (addressed to
|
|
2249
|
+
// the queue id, which is the session id it starts as) until its
|
|
2250
|
+
// first prompt. Additive.
|
|
2251
|
+
const has = (
|
|
2252
|
+
db.prepare(`PRAGMA table_info(bus_messages)`).all() as {
|
|
2253
|
+
name: string;
|
|
2254
|
+
}[]
|
|
2255
|
+
).some((c) => c.name === "held_queued");
|
|
2256
|
+
if (!has)
|
|
2257
|
+
db.exec(`ALTER TABLE bus_messages ADD COLUMN held_queued INTEGER`);
|
|
2258
|
+
db.exec(
|
|
2259
|
+
`CREATE INDEX IF NOT EXISTS idx_bus_messages_held_queued
|
|
2260
|
+
ON bus_messages(to_id) WHERE held_queued = 1`
|
|
2261
|
+
);
|
|
2262
|
+
},
|
|
2263
|
+
},
|
|
2245
2264
|
];
|
|
2246
2265
|
|
|
2247
2266
|
// `upTo`: stop after this legacy id or timestamp key, inclusive (tests that
|
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
|
};
|
|
@@ -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/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.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Messages for a task still in the start queue. They're ordinary bus
|
|
3
|
+
* messages addressed to the queue id (the id the task starts as), flagged
|
|
4
|
+
* held_queued, and reach the task in its first prompt (tasks/start.ts).
|
|
5
|
+
* Nothing is deleted: a delivered or discarded message is marked, like the
|
|
6
|
+
* ones held for a paused task.
|
|
7
|
+
*
|
|
8
|
+
* Light on purpose: the message bus imports this, and must not pull in the
|
|
9
|
+
* queue's scheduler.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { db } from "../db";
|
|
13
|
+
import type { HeldMessage } from "./pause-policy";
|
|
14
|
+
|
|
15
|
+
export function queuedLabel(r: { name: string | null; prompt: string }) {
|
|
16
|
+
if (r.name) return r.name;
|
|
17
|
+
const line = r.prompt.split("\n")[0].trim();
|
|
18
|
+
return line.length > 60 ? `${line.slice(0, 57)}...` : line;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface QueuedTarget {
|
|
22
|
+
id: string;
|
|
23
|
+
name: string;
|
|
24
|
+
status: string;
|
|
25
|
+
error: string | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// A task waiting to start (or failed to) by id, id prefix of 4+ characters
|
|
29
|
+
// or name; in one workspace when given.
|
|
30
|
+
export function findQueuedTarget(
|
|
31
|
+
ref: string,
|
|
32
|
+
opts: { workspaceId?: string } = {}
|
|
33
|
+
): QueuedTarget | null {
|
|
34
|
+
const want = ref.trim();
|
|
35
|
+
if (!want) return null;
|
|
36
|
+
const rows = db
|
|
37
|
+
.prepare(
|
|
38
|
+
`SELECT q.id, q.name, q.prompt, q.status, q.error, p.name AS project
|
|
39
|
+
FROM task_queue q JOIN projects p ON p.id = q.project_id
|
|
40
|
+
WHERE q.status IN ('queued', 'starting', 'failed')
|
|
41
|
+
${opts.workspaceId ? "AND p.workspace_id = @ws" : ""}`
|
|
42
|
+
)
|
|
43
|
+
.all(opts.workspaceId ? { ws: opts.workspaceId } : {}) as {
|
|
44
|
+
id: string;
|
|
45
|
+
name: string | null;
|
|
46
|
+
prompt: string;
|
|
47
|
+
status: string;
|
|
48
|
+
error: string | null;
|
|
49
|
+
project: string;
|
|
50
|
+
}[];
|
|
51
|
+
const lower = want.toLowerCase();
|
|
52
|
+
const hits = rows.filter((r) => {
|
|
53
|
+
const name = queuedLabel(r).toLowerCase();
|
|
54
|
+
return (
|
|
55
|
+
r.id === want ||
|
|
56
|
+
(want.length >= 4 && r.id.startsWith(want)) ||
|
|
57
|
+
name === lower ||
|
|
58
|
+
`${r.project}/${name}`.toLowerCase() === lower
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
if (hits.length > 1)
|
|
62
|
+
throw new Error(
|
|
63
|
+
`"${ref}" matches ${hits.length} queued tasks: ${hits.map((h) => `${queuedLabel(h)} (${h.id.slice(0, 8)})`).join(", ")}`
|
|
64
|
+
);
|
|
65
|
+
const r = hits[0];
|
|
66
|
+
return r
|
|
67
|
+
? { id: r.id, name: queuedLabel(r), status: r.status, error: r.error }
|
|
68
|
+
: null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Refuses a task that failed to start: nothing will ever start it.
|
|
72
|
+
export function assertTakesMessages(q: QueuedTarget): void {
|
|
73
|
+
if (q.status === "failed")
|
|
74
|
+
throw new Error(
|
|
75
|
+
`${q.name} failed to start${q.error ? ` (${q.error})` : ""}; it takes no messages. Remove it and queue it again.`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const heldQueuedNote = (name: string) =>
|
|
80
|
+
`Queued for ${name}: it hasn't started; it gets this when it starts.`;
|
|
81
|
+
|
|
82
|
+
const UNDELIVERED = `held_queued = 1 AND delivered_at IS NULL AND read_at IS NULL`;
|
|
83
|
+
|
|
84
|
+
export function queuedHeldMessages(id: string): HeldMessage[] {
|
|
85
|
+
return (
|
|
86
|
+
db
|
|
87
|
+
.prepare(
|
|
88
|
+
`SELECT from_name, body FROM bus_messages
|
|
89
|
+
WHERE to_id = ? AND ${UNDELIVERED} ORDER BY id`
|
|
90
|
+
)
|
|
91
|
+
.all(id) as { from_name: string; body: string }[]
|
|
92
|
+
).map((r) => ({ from: r.from_name, text: r.body }));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// They're in the first prompt: delivered, and not for `aos inbox` again.
|
|
96
|
+
export function markQueuedHeldDelivered(id: string): void {
|
|
97
|
+
db.prepare(
|
|
98
|
+
`UPDATE bus_messages
|
|
99
|
+
SET delivered_at = datetime('now'), read_at = COALESCE(read_at, datetime('now'))
|
|
100
|
+
WHERE to_id = ? AND ${UNDELIVERED}`
|
|
101
|
+
).run(id);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// The task left the queue without starting.
|
|
105
|
+
export function discardQueuedHeld(id: string): void {
|
|
106
|
+
db.prepare(
|
|
107
|
+
`UPDATE bus_messages SET read_at = datetime('now')
|
|
108
|
+
WHERE to_id = ? AND ${UNDELIVERED}`
|
|
109
|
+
).run(id);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function queuedMessagesSection(held: HeldMessage[]): string {
|
|
113
|
+
if (!held.length) return "";
|
|
114
|
+
return `\n\n## Messages while queued\nThese arrived before you started, oldest first. They are part of your instructions:\n${held.map((m, i) => `${i + 1}. From ${m.from}: ${m.text}`).join("\n")}`;
|
|
115
|
+
}
|
package/lib/tasks/queue.ts
CHANGED
|
@@ -31,6 +31,13 @@ import {
|
|
|
31
31
|
import { queueEvent } from "../orchestrator/events";
|
|
32
32
|
import { readUsage, windowRefusal } from "../orchestrator/usage";
|
|
33
33
|
import { createTask } from "./index";
|
|
34
|
+
import {
|
|
35
|
+
discardQueuedHeld,
|
|
36
|
+
markQueuedHeldDelivered,
|
|
37
|
+
queuedHeldMessages,
|
|
38
|
+
queuedLabel,
|
|
39
|
+
queuedMessagesSection,
|
|
40
|
+
} from "./queue-held";
|
|
34
41
|
import { expandHome } from "./session";
|
|
35
42
|
import {
|
|
36
43
|
DEFAULT_PRIORITY,
|
|
@@ -148,6 +155,8 @@ export interface QueuedTaskView {
|
|
|
148
155
|
// Its place in the score order within its line, and how many are in it.
|
|
149
156
|
rank: number;
|
|
150
157
|
of: number;
|
|
158
|
+
// Messages held for it until it starts.
|
|
159
|
+
held: number;
|
|
151
160
|
}
|
|
152
161
|
|
|
153
162
|
export const START_ATTEMPTS = 3;
|
|
@@ -419,11 +428,7 @@ export async function startOrQueue<T>(
|
|
|
419
428
|
}
|
|
420
429
|
}
|
|
421
430
|
|
|
422
|
-
|
|
423
|
-
if (r.name) return r.name;
|
|
424
|
-
const line = r.prompt.split("\n")[0].trim();
|
|
425
|
-
return line.length > 60 ? `${line.slice(0, 57)}...` : line;
|
|
426
|
-
}
|
|
431
|
+
const label = queuedLabel;
|
|
427
432
|
|
|
428
433
|
function afterLabel(r: QueueRow): string | null {
|
|
429
434
|
if (r.after_task) {
|
|
@@ -438,7 +443,9 @@ function afterLabel(r: QueueRow): string | null {
|
|
|
438
443
|
export function listQueue(): QueuedTaskView[] {
|
|
439
444
|
const rows = db
|
|
440
445
|
.prepare(
|
|
441
|
-
`SELECT q.*, p.name AS project_name, p.workspace_id AS workspace_id
|
|
446
|
+
`SELECT q.*, p.name AS project_name, p.workspace_id AS workspace_id,
|
|
447
|
+
(SELECT COUNT(*) FROM bus_messages b WHERE b.to_id = q.id
|
|
448
|
+
AND b.held_queued = 1 AND b.delivered_at IS NULL AND b.read_at IS NULL) AS held_count
|
|
442
449
|
FROM task_queue q LEFT JOIN projects p ON p.id = q.project_id
|
|
443
450
|
WHERE q.status IN ('queued', 'starting', 'failed')
|
|
444
451
|
ORDER BY q.position`
|
|
@@ -446,6 +453,7 @@ export function listQueue(): QueuedTaskView[] {
|
|
|
446
453
|
.all() as (QueueRow & {
|
|
447
454
|
project_name: string | null;
|
|
448
455
|
workspace_id: string | null;
|
|
456
|
+
held_count: number;
|
|
449
457
|
})[];
|
|
450
458
|
const places = new Map<string, number>();
|
|
451
459
|
// The score order within each line, by priority and age alone: holds
|
|
@@ -489,6 +497,7 @@ export function listQueue(): QueuedTaskView[] {
|
|
|
489
497
|
wait: r.wait_code,
|
|
490
498
|
rank: rank?.rank ?? place,
|
|
491
499
|
of: rank?.of ?? place,
|
|
500
|
+
held: r.held_count,
|
|
492
501
|
};
|
|
493
502
|
});
|
|
494
503
|
}
|
|
@@ -635,10 +644,14 @@ async function startRow(
|
|
|
635
644
|
const placed = opts.placement ?? null;
|
|
636
645
|
if (!opts.force)
|
|
637
646
|
recordAutoStart(placed?.hostId ?? LOCAL, !!opts.heavy, Date.now());
|
|
647
|
+
// A linked machine's start never sees this machine's held messages: they
|
|
648
|
+
// go with the brief it is sent.
|
|
649
|
+
const remote = isRemoteHost(placed?.hostId ?? r.host_id ?? project?.host_id);
|
|
650
|
+
const remoteHeld = remote ? queuedHeldMessages(r.id) : [];
|
|
638
651
|
const task = {
|
|
639
652
|
id: r.id,
|
|
640
653
|
projectId: r.project_id,
|
|
641
|
-
prompt: r.prompt,
|
|
654
|
+
prompt: r.prompt + queuedMessagesSection(remoteHeld),
|
|
642
655
|
name: r.name ?? undefined,
|
|
643
656
|
model: r.model_overrides
|
|
644
657
|
? (JSON.parse(r.model_overrides) as ModelOverride)
|
|
@@ -668,6 +681,7 @@ async function startRow(
|
|
|
668
681
|
{ spendApproval: false }
|
|
669
682
|
)
|
|
670
683
|
: await createTask(task);
|
|
684
|
+
if (remoteHeld.length) markQueuedHeldDelivered(r.id);
|
|
671
685
|
patch(r.id, {
|
|
672
686
|
status: "started",
|
|
673
687
|
started_at: new Date().toISOString(),
|
|
@@ -1077,7 +1091,7 @@ export function findQueued(
|
|
|
1077
1091
|
return hits[0] ?? null;
|
|
1078
1092
|
}
|
|
1079
1093
|
|
|
1080
|
-
export
|
|
1094
|
+
export { queuedLabel };
|
|
1081
1095
|
|
|
1082
1096
|
// Changes the priority of a queued task or a running one (they share an
|
|
1083
1097
|
// id). Age is kept. Throws when no such task is waiting or running.
|
|
@@ -1105,11 +1119,15 @@ export function stampPriority(id: string, priority: Priority | undefined) {
|
|
|
1105
1119
|
}
|
|
1106
1120
|
|
|
1107
1121
|
export function removeQueued(id: string): void {
|
|
1108
|
-
const changed = db
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1122
|
+
const changed = db.transaction(() => {
|
|
1123
|
+
const n = db
|
|
1124
|
+
.prepare(
|
|
1125
|
+
`UPDATE task_queue SET status = 'removed' WHERE id = ? AND status IN ('queued', 'failed')`
|
|
1126
|
+
)
|
|
1127
|
+
.run(id).changes;
|
|
1128
|
+
if (n) discardQueuedHeld(id);
|
|
1129
|
+
return n;
|
|
1130
|
+
})();
|
|
1113
1131
|
if (!changed) throw new Error("That task isn't waiting in the queue");
|
|
1114
1132
|
tickQueueSoon();
|
|
1115
1133
|
}
|
package/lib/tasks/start.ts
CHANGED
|
@@ -28,6 +28,11 @@ import { planPrompt, preparePlanDir } from "../models/plan-step";
|
|
|
28
28
|
|
|
29
29
|
export { firstMessageId, launchPending };
|
|
30
30
|
import { expandHome } from "./session";
|
|
31
|
+
import {
|
|
32
|
+
markQueuedHeldDelivered,
|
|
33
|
+
queuedHeldMessages,
|
|
34
|
+
queuedMessagesSection,
|
|
35
|
+
} from "./queue-held";
|
|
31
36
|
import {
|
|
32
37
|
enterStage,
|
|
33
38
|
finishSetup,
|
|
@@ -138,7 +143,12 @@ export async function finishTaskStart(
|
|
|
138
143
|
ms: setup.ms,
|
|
139
144
|
error: "Not launched: the task was closed during setup",
|
|
140
145
|
});
|
|
141
|
-
|
|
146
|
+
// Messages sent while it waited in the queue follow the brief, in order.
|
|
147
|
+
const heldMessages = queuedHeldMessages(sessionId);
|
|
148
|
+
const task =
|
|
149
|
+
(session.task_prompt ?? "") +
|
|
150
|
+
queuedMessagesSection(heldMessages) +
|
|
151
|
+
setupNote(setup);
|
|
142
152
|
const planning = session.plan_phase === "planning";
|
|
143
153
|
if (planning) preparePlanDir(session.worktree_path);
|
|
144
154
|
const prompt = planning ? planPrompt(task, !!session.plan_only) : task;
|
|
@@ -169,6 +179,7 @@ export async function finishTaskStart(
|
|
|
169
179
|
);
|
|
170
180
|
return done({ status: "failed", ms: setup.ms, error: message });
|
|
171
181
|
}
|
|
182
|
+
if (heldMessages.length) markQueuedHeldDelivered(sessionId);
|
|
172
183
|
return done(setup);
|
|
173
184
|
}
|
|
174
185
|
|
|
@@ -237,6 +248,7 @@ export async function resumeTaskStarts(
|
|
|
237
248
|
: fs.existsSync(promptFileFor(row.id)) && (await alive(row.tmux_name));
|
|
238
249
|
if (launched) {
|
|
239
250
|
recordSetup(db, row.id, { status: "ok", ms: null, error: null });
|
|
251
|
+
markQueuedHeldDelivered(row.id);
|
|
240
252
|
if (queued)
|
|
241
253
|
inBackground(`first message of task ${row.id}`, () =>
|
|
242
254
|
sendQueuedNow(row.id, first)
|