@saadnvd1/agent-os 0.3.584 → 0.3.585

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.
@@ -26,7 +26,9 @@ import {
26
26
  findQueued,
27
27
  queuedLabel,
28
28
  removeQueued,
29
+ slotState,
29
30
  startOrQueue,
31
+ startQueuedForOrchestrator,
30
32
  } from "../tasks/queue";
31
33
  import { getStack, previewStack, startStack } from "../stacks";
32
34
  import { treeOrder } from "../stacks/tree";
@@ -155,10 +157,14 @@ export async function startTask(
155
157
  planReason?: string;
156
158
  priority?: Priority;
157
159
  machine?: "mac" | "any";
160
+ now?: boolean;
161
+ nowReason?: string;
158
162
  } = {}
159
163
  ): Promise<string> {
160
164
  if (opts.planOnly && opts.plan === false)
161
165
  throw new Error("plan_only implies plan; pass plan true");
166
+ if (opts.now && !opts.nowReason)
167
+ throw new Error("now needs now_reason: say why it can't wait for a slot");
162
168
  const project = workspaceProject(workspaceId, projectRef);
163
169
  const model = parseOverride(opts.model) ?? undefined;
164
170
  const startedBy = orchestratorId(workspaceId);
@@ -168,6 +174,7 @@ export async function startTask(
168
174
  `${project.name} runs on another machine; the orchestrator starts tasks on this one only for now`
169
175
  );
170
176
  const view = opts.view ?? DEFAULT_START_VIEW;
177
+ const slots = slotState(project.id);
171
178
  // Held for a slot or its `after`, it starts by itself through the brakes;
172
179
  // you get an event when it does.
173
180
  const out = await startOrQueue(
@@ -186,6 +193,7 @@ export async function startTask(
186
193
  originWorkspaceId: workspaceId,
187
194
  priority: opts.priority,
188
195
  machine: opts.machine,
196
+ now: opts.now,
189
197
  },
190
198
  () =>
191
199
  braked(
@@ -213,6 +221,12 @@ export async function startTask(
213
221
  }
214
222
  const task = out.started;
215
223
  if (opts.priority) stampPriority(task.id, opts.priority);
224
+ const past = opts.now && slots.full && !opts.planOnly;
225
+ if (past)
226
+ addNote(
227
+ workspaceId,
228
+ `Started "${task.name}" past the running-task limit (${slots.running} of ${slots.limit} running): ${opts.nowReason}`
229
+ );
216
230
  const planOnlyNote = opts.planOnly
217
231
  ? " Plan only: it stops after the plan and waits; approve_plan continues it, drop rejects it."
218
232
  : "";
@@ -220,7 +234,39 @@ export async function startTask(
220
234
  opts.plan && !opts.planOnly && task.plan_phase !== "planning"
221
235
  ? ` Plan step skipped: ${view === "terminal" ? "a terminal task has none" : "planning and implementation use the same model"}.`
222
236
  : "";
223
- return `Started task "${task.name}" (id ${task.id.slice(0, 8)}, ${view}) in ${project.name} on ${task.branch_name} (from ${task.base_branch}). It ends in a PR; you'll get an event when it opens.${skipped}${planOnlyNote}`;
237
+ return `Started task "${task.name}" (id ${task.id.slice(0, 8)}, ${view}) in ${project.name} on ${task.branch_name} (from ${task.base_branch}). It ends in a PR; you'll get an event when it opens.${skipped}${planOnlyNote}${past ? " Started past the running-task limit." : ""}`;
238
+ }
239
+
240
+ // Starts a queued task now, past the running-task limit and its `after`.
241
+ // Pause, the brakes and a machine need still hold it.
242
+ export async function startNow(
243
+ workspaceId: string,
244
+ ref: string,
245
+ reason: string
246
+ ): Promise<string> {
247
+ const queued = findQueued(workspaceId, ref, { byName: true });
248
+ if (!queued) {
249
+ const running = workspaceTask(workspaceId, ref);
250
+ throw new Error(`${running.name} is already started, not queued`);
251
+ }
252
+ const name = queuedLabel(queued);
253
+ const out = await startQueuedForOrchestrator(workspaceId, queued.id);
254
+ if (out.outcome === "held")
255
+ return `Not started: ${out.text}. ${name} stays in the queue.`;
256
+ if (out.outcome === "failed") return `Could not start ${name}: ${out.text}.`;
257
+ const skipped = [
258
+ out.wasFull
259
+ ? `past the running-task limit (${out.running} of ${out.limit} running)`
260
+ : "",
261
+ out.skippedAfter ? "without waiting for its after" : "",
262
+ ]
263
+ .filter(Boolean)
264
+ .join(", ");
265
+ addNote(
266
+ workspaceId,
267
+ `Started queued ${name} now${skipped ? `, ${skipped}` : ""}: ${reason}`
268
+ );
269
+ return `Started ${name} (id ${queued.id.slice(0, 8)}) now${skipped ? `, ${skipped}` : ""}. You'll get an event when its PR opens.`;
224
270
  }
225
271
 
226
272
  // The orchestrator approves a plan-only task's plan; a message amends it.
@@ -119,7 +119,7 @@ Reading:
119
119
  Acting:
120
120
  - \`${TOOL_NAMES.send}\` (session, message, scope_change?): message a session; it arrives as its next prompt. scope_change records it on the task's brief for its review.
121
121
  - \`${TOOL_NAMES.message_orchestrator}\` (workspace, message): message another workspace's orchestrator, the only thing you reach outside this workspace. Use it to hand over a bug or ask for something in its projects; it decides what to do there.
122
- - \`${TOOL_NAMES.start_task}\` (project, prompt, base?, name?, view?, after?, priority?, machine?, model?, plan, plan_only?, plan_reason): a task in its own worktree that ends in a PR. priority (urgent, blocking, normal, background) sets where it stands in the queue; machine mac pins work that needs this Mac. A busy or loaded machine holds a queued start and says why; don't retry it. Write the prompt as a full brief; name it in 2-6 words, or it's named from the prompt. It runs as a chat; pass view terminal only when the job needs a TUI. Pass after (a task's id or name, or "any") to hold it until that task finishes, instead of remembering it in your notes. Over the workspace's running task limit it's queued too: the result says "Queued (position N)", it starts by itself, and you get an event when it does. Its models follow the role settings (it plans on the planning model, then implements on the implementation one); State plan and plan_reason for every task (see "Triage every job"). Pass model ("provider/model", or a map of role to one) only when Saad asks for one, or to put implementation on Opus when the work needs deep reasoning (say why in plan_reason or a note); another provider than anthropic only when Saad asks.
122
+ - \`${TOOL_NAMES.start_task}\` (project, prompt, base?, name?, view?, after?, priority?, machine?, now?, now_reason?, model?, plan, plan_only?, plan_reason): a task in its own worktree that ends in a PR. priority (urgent, blocking, normal, background) sets where it stands in the queue; machine mac pins work that needs this Mac. A busy or loaded machine holds a queued start and says why; don't retry it. Write the prompt as a full brief; name it in 2-6 words, or it's named from the prompt. It runs as a chat; pass view terminal only when the job needs a TUI. Pass after (a task's id or name, or "any") to hold it until that task finishes, instead of remembering it in your notes. Over the workspace's running task limit it's queued too: the result says "Queued (position N)", it starts by itself, and you get an event when it does. now true (with now_reason, not with after) starts it at once past the limit instead; it's logged for Saad, so use it only when it can't wait. Its models follow the role settings (it plans on the planning model, then implements on the implementation one); State plan and plan_reason for every task (see "Triage every job"). Pass model ("provider/model", or a map of role to one) only when Saad asks for one, or to put implementation on Opus when the work needs deep reasoning (say why in plan_reason or a note); another provider than anthropic only when Saad asks.
123
123
  - \`${TOOL_NAMES.plan_pieces}\` (task, merge?, drop?): when a plan split into pieces (the plan-done event names them), list them; merge or drop edits the ones still waiting. A piece starts by itself once what it waits on has merged (or deployed); you don't start it.
124
124
  - \`${TOOL_NAMES.approve_plan}\` (task, message?): continue a plan_only task whose plan is ready; it implements on the implementation model, once. message amends the plan and is recorded on the task's brief as a scope change. Reject a plan with drop.
125
125
  - \`${TOOL_NAMES.set_task_model}\` (task, model, message?): move a paused task to another model and carry on in the same conversation and worktree; it keeps that model from then on. Only when Saad asks.
@@ -135,6 +135,7 @@ Acting:
135
135
  - \`${TOOL_NAMES.move}\` (task, machine): carry a running task to a linked machine (heavy test or build work), or back with machine "here". It keeps its branch, conversation and plan, and everything you do with a task works the same there: its events come to you, read, send, stop, done and drop reach it, and the gates judge its PR at its head. The machine it lands on holds it for you and can't merge it. It runs in the background; an event says when it lands. Refused while paused, and for a task waiting on Saad.
136
136
  - \`${TOOL_NAMES.drop}\` (task, reason): close a task's PR and remove its worktree (on whichever machine holds it).
137
137
  - \`${TOOL_NAMES.set_priority}\` (task, priority): re-rank a queued task (urgent, blocking, normal, background), or tag a running one. Urgent is a bug Saad hit; background starts last.
138
+ - \`${TOOL_NAMES.start_now}\` (task, reason): start a queued task now, past the running-task limit and its after. Still refused while paused or braked, and a Mac-only task stays queued. Use it sparingly, when Saad asks or it unblocks others; the reason is logged for Saad.
138
139
  - \`${TOOL_NAMES.pause_task}\` (task, for, now?, reason): pause a running task for 1h, 2h, until tomorrow 9 AM or until resumed. Its agent ends at the end of its step (now interrupts the step); its worktree and conversation stay and its slot frees for the next queued task. It resumes by itself, ahead of new arrivals, with what was held (CI and review notes, messages) in one prompt. Use it to make room for urgent work or to wait out a loaded machine. Refused for a task that is merging, moving, setting up or waiting on Saad.
139
140
  - \`${TOOL_NAMES.resume_task}\` (task): resume a pause you set now. A pause Saad set is his to lift.
140
141
  - \`${TOOL_NAMES.stop}\` (session): stop a session's agent, keeping its work.
@@ -12,6 +12,7 @@ import {
12
12
  stackStatus,
13
13
  startSession,
14
14
  startTask,
15
+ startNow,
15
16
  setTaskPriority,
16
17
  pauseTask,
17
18
  resumeTask,
@@ -67,6 +68,7 @@ const ACTING = new Set<ToolName>([
67
68
  "approve_plan",
68
69
  "plan_pieces",
69
70
  "set_priority",
71
+ "start_now",
70
72
  "pause_task",
71
73
  "resume_task",
72
74
  "stop",
@@ -139,8 +141,14 @@ export async function runTool(
139
141
  planReason: a.plan_reason,
140
142
  priority: a.priority,
141
143
  machine: a.machine,
144
+ now: a.now,
145
+ nowReason: a.now_reason,
142
146
  });
143
147
  }
148
+ case "start_now": {
149
+ const a = p(tool, raw);
150
+ return startNow(w, a.task, a.reason);
151
+ }
144
152
  case "set_priority": {
145
153
  const a = p(tool, raw);
146
154
  return setTaskPriority(w, a.task, a.priority);
@@ -30,6 +30,7 @@ export const TOOL_NAMES = {
30
30
  drop: name("drop"),
31
31
  move: name("move"),
32
32
  set_priority: name("set_priority"),
33
+ start_now: name("start_now"),
33
34
  pause_task: name("pause_task"),
34
35
  resume_task: name("resume_task"),
35
36
  stop: name("stop"),
@@ -128,6 +128,19 @@ export const TOOL_SHAPES = {
128
128
  .describe(
129
129
  'Where it may run: "mac" for work that needs this Mac (Xcode, a simulator); "any" if no machine need applies. Unset: the project\'s setting, else decided from its files'
130
130
  ),
131
+ now: z
132
+ .boolean()
133
+ .optional()
134
+ .describe(
135
+ "Start it at once, past the workspace's running-task limit, instead of queueing for a slot. Needs now_reason; not with after"
136
+ ),
137
+ now_reason: z
138
+ .string()
139
+ .trim()
140
+ .min(1)
141
+ .max(300)
142
+ .optional()
143
+ .describe("With now: why it can't wait for a slot; logged for Saad"),
131
144
  model: modelOverride,
132
145
  plan: z
133
146
  .boolean()
@@ -250,6 +263,15 @@ export const TOOL_SHAPES = {
250
263
  task: ref("A queued or running task: name, project/name or id"),
251
264
  priority: z.enum(PRIORITIES),
252
265
  },
266
+ start_now: {
267
+ task: ref("A queued task: name, project/name or id"),
268
+ reason: z
269
+ .string()
270
+ .trim()
271
+ .min(1)
272
+ .max(300)
273
+ .describe("Why it can't wait for a slot; logged for Saad"),
274
+ },
253
275
  pause_task: {
254
276
  task,
255
277
  for: z
@@ -62,7 +62,7 @@ const DESCRIPTIONS: Record<ToolName, string> = {
62
62
  message_orchestrator:
63
63
  "Message another workspace's orchestrator, by workspace name; never any other session there. It arrives as its next prompt, marked as from you and fenced as untrusted.",
64
64
  start_task:
65
- 'Start a task: an agent in its own worktree that ends in a PR. It runs as a chat unless view is terminal (only for a job that needs a TUI). priority (urgent, blocking, normal, background) says where it stands in the queue; machine "mac" pins work that needs this Mac. A busy machine holds a start and the result says why. With after (a task\'s id or name, or "any"), or over the workspace\'s running task limit, it\'s queued and starts by itself; the result says Queued (position N). Its models follow the role settings (planning, implementation, review, scope); give model only when Saad asks for one. Always state plan (true or false) and plan_reason (plan_only true plans, then stops and waits for approve_plan instead of implementing): plan true for a big, unclear or cross-cutting job, false for a small, clear one whose steps you write into the prompt. Refused while a brake holds.',
65
+ 'Start a task: an agent in its own worktree that ends in a PR. It runs as a chat unless view is terminal (only for a job that needs a TUI). priority (urgent, blocking, normal, background) says where it stands in the queue; machine "mac" pins work that needs this Mac. A busy machine holds a start and the result says why. With after (a task\'s id or name, or "any"), or over the workspace\'s running task limit, it\'s queued and starts by itself; the result says Queued (position N). Its models follow the role settings (planning, implementation, review, scope); give model only when Saad asks for one. Always state plan (true or false) and plan_reason (plan_only true plans, then stops and waits for approve_plan instead of implementing): plan true for a big, unclear or cross-cutting job, false for a small, clear one whose steps you write into the prompt. now true (with now_reason) starts it at once past the running-task limit instead of queueing for a slot, logged; not with after. Refused while a brake holds.',
66
66
  set_task_model:
67
67
  'Resume a paused task on another model ("provider/model", or a map of role to one), keeping its conversation and worktree; it\'s told to carry on (or message). Refused mid-turn.',
68
68
  start_session:
@@ -83,6 +83,8 @@ const DESCRIPTIONS: Record<ToolName, string> = {
83
83
  move: 'Carry a running task to a linked machine, or back with "here": for heavy test or build work. It keeps its branch, conversation and plan; the gates, events, read, send, stop, done and drop work the same wherever it runs, and the machine it lands on can\'t merge it. Takes a few minutes and runs in the background; an event says when it lands. Refused while paused, for a task waiting on Saad (an escalation or open ask), and for card and stacked tasks.',
84
84
  set_priority:
85
85
  "Change the priority of a queued task (it re-ranks the line at once) or a running one (it only matters if it is paused and waits to resume). urgent starts first even on a busy machine; background starts last.",
86
+ start_now:
87
+ "Start a queued task now, past the workspace's running-task limit and its after, when it can't wait for a slot (Saad asked, or it unblocks others). Still refused while paused or braked, and a task that needs a Mac stays queued. Give a reason: it goes in the decision log. Says whether it started, is still held and why, or failed.",
86
88
  pause_task:
87
89
  "Pause a running task for a set time: its agent ends at the end of its current step (now true interrupts the step), its worktree and conversation stay, and it frees its slot so the next queued task can start. It resumes by itself at the time, ahead of new arrivals of the same priority, with whatever was held (CI and review notes, messages) in one continue prompt. Refused for a task that is merging, moving, setting up or waiting on Saad. Give a reason; it is shown with the pause.",
88
90
  resume_task:
@@ -312,6 +312,26 @@ export interface QueueRequest {
312
312
  priority?: Priority;
313
313
  // "mac" pins it to a Mac; "any" says it needs nothing in particular.
314
314
  machine?: "any" | "mac";
315
+ // Start at once, past the running-task limit, instead of queueing for a
316
+ // slot. Still needs the machine it needs, and an orchestrator's start
317
+ // still passes its brakes.
318
+ now?: boolean;
319
+ }
320
+
321
+ // How full the workspace's running-task limit is for this project.
322
+ export function slotState(projectId: string): {
323
+ running: number;
324
+ limit: number | null;
325
+ full: boolean;
326
+ } {
327
+ const workspaceId = workspaceOf(projectId);
328
+ const limit = limitOf(workspaceId);
329
+ if (!workspaceId || limit === null)
330
+ return { running: 0, limit: null, full: false };
331
+ const running =
332
+ runningIn({ workspaceId, projectId }, true).length +
333
+ startingIn(workspaceId);
334
+ return { running, limit, full: running >= limit };
315
335
  }
316
336
 
317
337
  const waitingIn = (workspaceId: string) =>
@@ -349,8 +369,11 @@ export function queueIfNeeded(req: QueueRequest): QueuedTaskView | null {
349
369
  }
350
370
  } else if (after)
351
371
  afterTask = resolveAfter(after, req.originWorkspaceId ?? workspaceId).id;
372
+ if (req.now && waits)
373
+ throw new Error("now and after don't go together: now starts it at once");
352
374
  const full =
353
375
  !req.planOnly &&
376
+ !req.now &&
354
377
  limit !== null &&
355
378
  !!workspaceId &&
356
379
  (runningIn({ workspaceId, projectId: project.id }, true).length +
@@ -364,9 +387,18 @@ export function queueIfNeeded(req: QueueRequest): QueuedTaskView | null {
364
387
  throw new Error(`Machine is "any" or "mac"`);
365
388
  // The orchestrator's own starts wait out a loaded machine; a person's
366
389
  // direct start goes ahead, as "start now" always has.
390
+ if (
391
+ req.now &&
392
+ !req.planOnly &&
393
+ needsUnmetHere(project.id, req.prompt, setting)
394
+ )
395
+ throw new Error(
396
+ `Can't start now: this task needs a Mac, and this machine isn't one. Queue it without now.`
397
+ );
367
398
  const loaded =
368
399
  !waits &&
369
400
  !full &&
401
+ !req.now &&
370
402
  !req.planOnly &&
371
403
  !!req.originWorkspaceId &&
372
404
  loadHeldStart(project.id, req.prompt, setting);
@@ -426,7 +458,8 @@ export async function startOrQueue<T>(
426
458
  const queued = queueIfNeeded(req);
427
459
  if (queued) return { queued };
428
460
  const workspaceId = workspaceOf(req.projectId);
429
- if (!workspaceId) return { started: await start() };
461
+ // A plan-only start never holds a slot, as its queue row doesn't.
462
+ if (!workspaceId || req.planOnly) return { started: await start() };
430
463
  reserved.set(workspaceId, (reserved.get(workspaceId) ?? 0) + 1);
431
464
  try {
432
465
  return { started: await start() };
@@ -539,6 +572,33 @@ function autoHold(workspaceId: string | null, planOnly = false): Hold | null {
539
572
  return window ? { code: "window", text: `Waiting: ${window}` } : null;
540
573
  }
541
574
 
575
+ const requestNeeds = (
576
+ projectId: string,
577
+ workingDirectory: string,
578
+ prompt: string,
579
+ setting: Need[] | null
580
+ ): Need[] =>
581
+ setting ??
582
+ needsOf({
583
+ projectId,
584
+ projectPath: expandHome(workingDirectory),
585
+ prompt,
586
+ }).needs;
587
+
588
+ // Whether the task needs something this machine lacks (a Mac).
589
+ function needsUnmetHere(
590
+ projectId: string,
591
+ prompt: string,
592
+ setting: Need[] | null
593
+ ): boolean {
594
+ const project = getProject(projectId);
595
+ if (!project || isRemoteHost(project.host_id)) return false;
596
+ return !meetsNeeds(
597
+ requestNeeds(projectId, project.working_directory, prompt, setting),
598
+ machineCapabilities()
599
+ );
600
+ }
601
+
542
602
  // Whether this machine's load rules out starting a task for this project now.
543
603
  function loadHeldStart(
544
604
  projectId: string,
@@ -547,13 +607,12 @@ function loadHeldStart(
547
607
  ): boolean {
548
608
  const project = getProject(projectId);
549
609
  if (!project || isRemoteHost(project.host_id)) return false;
550
- const needs =
551
- setting ??
552
- needsOf({
553
- projectId,
554
- projectPath: expandHome(project.working_directory),
555
- prompt,
556
- }).needs;
610
+ const needs = requestNeeds(
611
+ projectId,
612
+ project.working_directory,
613
+ prompt,
614
+ setting
615
+ );
557
616
  const hold = machineHold(
558
617
  {
559
618
  heavy: heavyNeeds(needs),
@@ -593,6 +652,8 @@ interface StartOpts {
593
652
  // Start on this linked machine instead of the project's.
594
653
  placement?: Placement | null;
595
654
  heavy?: boolean;
655
+ // Pass this workspace's brakes even for a row no orchestrator queued.
656
+ brakeAs?: string;
596
657
  }
597
658
 
598
659
  async function startRow(
@@ -683,9 +744,10 @@ async function startRow(
683
744
  try {
684
745
  // The orchestrator's queued starts pass its brakes when they start,
685
746
  // a person's "start now" included: it skips only the limit and `after`.
686
- const session = origin
747
+ const brakeWs = origin ?? opts.brakeAs;
748
+ const session = brakeWs
687
749
  ? await braked(
688
- origin,
750
+ brakeWs,
689
751
  "task",
690
752
  () => createTask(task),
691
753
  (s) => s.id,
@@ -1090,6 +1152,67 @@ export async function startQueuedNow(id: string): Promise<StartOutcome> {
1090
1152
  return startRow(r, { force: true });
1091
1153
  }
1092
1154
 
1155
+ export interface ForcedStart {
1156
+ outcome: "started" | "held" | "failed";
1157
+ // Why it is held, or what failed; empty when it started.
1158
+ text: string;
1159
+ // The running-task limit was full when it started.
1160
+ wasFull: boolean;
1161
+ running: number;
1162
+ limit: number | null;
1163
+ // It skipped an `after`.
1164
+ skippedAfter: boolean;
1165
+ }
1166
+
1167
+ // The orchestrator's start now: past the limit and `after`, never past
1168
+ // Pause, its brakes or a machine need. A held task stays queued.
1169
+ export async function startQueuedForOrchestrator(
1170
+ workspaceId: string,
1171
+ id: string
1172
+ ): Promise<ForcedStart> {
1173
+ const r = row(id);
1174
+ const project = r ? getProject(r.project_id) : null;
1175
+ if (!r || r.status !== "queued" || project?.workspace_id !== workspaceId)
1176
+ throw new Error("That task isn't waiting in the queue");
1177
+ const slots = slotState(r.project_id);
1178
+ const base = {
1179
+ wasFull: slots.full,
1180
+ running: slots.running,
1181
+ limit: slots.limit,
1182
+ skippedAfter: !!(r.after_task || r.after_any),
1183
+ };
1184
+ const held = (text: string): ForcedStart => ({
1185
+ ...base,
1186
+ outcome: "held",
1187
+ text,
1188
+ });
1189
+ if (isPaused(workspaceId)) return held("the orchestrator is paused by Saad");
1190
+ if (isRemoteHost(r.host_id ?? project.host_id))
1191
+ return held(
1192
+ "it runs on another machine, and the orchestrator starts tasks on this one only"
1193
+ );
1194
+ if (
1195
+ !meetsNeeds(
1196
+ parseNeeds(r.machine_needs) ??
1197
+ requestNeeds(project.id, project.working_directory, r.prompt, null),
1198
+ machineCapabilities()
1199
+ )
1200
+ )
1201
+ return held("it needs a Mac, and this machine isn't one");
1202
+ try {
1203
+ const outcome = await startRow(r, { force: true, brakeAs: workspaceId });
1204
+ if (outcome === "started") return { ...base, outcome, text: "" };
1205
+ return held("it could not be claimed just now; try again");
1206
+ } catch (error) {
1207
+ if (error instanceof BrakeRefused) return held(error.reason);
1208
+ return {
1209
+ ...base,
1210
+ outcome: "failed",
1211
+ text: row(id)?.error ?? String(error),
1212
+ };
1213
+ }
1214
+ }
1215
+
1093
1216
  // A task still waiting in the line (or failed to start) by id, an id prefix
1094
1217
  // of 4+ characters or, when asked, its name; in one workspace.
1095
1218
  export function findQueued(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saadnvd1/agent-os",
3
- "version": "0.3.584",
3
+ "version": "0.3.585",
4
4
  "description": "Self-hosted web UI for managing Claude Code sessions",
5
5
  "bin": {
6
6
  "agent-os": "scripts/agent-os"