@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.
- package/lib/orchestrator/act.ts +47 -1
- package/lib/orchestrator/brief.ts +2 -1
- package/lib/orchestrator/serve.ts +8 -0
- package/lib/orchestrator/tool-names.ts +1 -0
- package/lib/orchestrator/tool-schemas.ts +22 -0
- package/lib/orchestrator/tools.ts +3 -1
- package/lib/tasks/queue.ts +133 -10
- package/package.json +1 -1
package/lib/orchestrator/act.ts
CHANGED
|
@@ -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);
|
|
@@ -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:
|
package/lib/tasks/queue.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
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
|
|
747
|
+
const brakeWs = origin ?? opts.brakeAs;
|
|
748
|
+
const session = brakeWs
|
|
687
749
|
? await braked(
|
|
688
|
-
|
|
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(
|