@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.
@@ -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
- throw new Error(
122
- r.reason === "none" ? `${r.error}. Run: aos peers` : r.error
123
- );
124
- return { session: sessions.find((s) => s.id === r.id)!, note: r.note };
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<{ message: BusMessageView; delivery: Delivery; note?: string }> {
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 { session: to, note } = resolveTarget(opts.to);
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
@@ -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
@@ -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
- const to = findWorkspaceSession(workspaceId, ref);
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:
@@ -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
- return { lead: `#${item.rank} of ${item.of}${priority}`, after: null };
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
+ }
@@ -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
- function label(r: Pick<QueueRow, "name" | "prompt">): string {
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 const queuedLabel = label;
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
- .prepare(
1110
- `UPDATE task_queue SET status = 'removed' WHERE id = ? AND status IN ('queued', 'failed')`
1111
- )
1112
- .run(id).changes;
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
  }
@@ -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
- const task = (session.task_prompt ?? "") + setupNote(setup);
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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saadnvd1/agent-os",
3
- "version": "0.3.577",
3
+ "version": "0.3.579",
4
4
  "description": "Self-hosted web UI for managing Claude Code sessions",
5
5
  "bin": {
6
6
  "agent-os": "scripts/agent-os"