@saadnvd1/agent-os 0.3.578 → 0.3.580
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/api/tasks/[id]/hold/route.ts +28 -0
- package/app/api/tasks/route.ts +27 -2
- package/lib/bus/index.ts +82 -9
- package/lib/db/migrations.ts +55 -0
- package/lib/db/types.ts +2 -0
- package/lib/done/index.ts +2 -2
- package/lib/orchestrator/act.ts +41 -1
- package/lib/orchestrator/digest.ts +7 -1
- package/lib/orchestrator/read.ts +2 -2
- package/lib/orchestrator/task-state.ts +2 -2
- package/lib/orchestrator/tools.ts +1 -1
- package/lib/sessions/stop.ts +3 -3
- package/lib/sidebar/queued.ts +7 -1
- package/lib/tasks/held-remote.ts +140 -29
- package/lib/tasks/held.ts +106 -0
- package/lib/tasks/import.ts +6 -13
- package/lib/tasks/index.ts +21 -2
- package/lib/tasks/move-bundle.ts +3 -1
- package/lib/tasks/move-flow.ts +1 -20
- package/lib/tasks/placement.ts +2 -0
- package/lib/tasks/queue-held.ts +115 -0
- package/lib/tasks/queue.ts +49 -15
- package/lib/tasks/remote.ts +51 -3
- package/lib/tasks/start.ts +13 -1
- package/package.json +1 -1
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,
|
|
@@ -67,7 +74,8 @@ import {
|
|
|
67
74
|
viewOf,
|
|
68
75
|
} from "../load/machines";
|
|
69
76
|
import { peerReading, peerSource, refreshPeers } from "../hosts/peer-load";
|
|
70
|
-
import { linkedHostIds } from "../hosts/remote-api";
|
|
77
|
+
import { hostLink, linkedHostIds } from "../hosts/remote-api";
|
|
78
|
+
import { hostTasks } from "./remote";
|
|
71
79
|
import { projectMachines } from "../hosts/project-machines";
|
|
72
80
|
import { claimResume, dueResumes } from "./pause";
|
|
73
81
|
|
|
@@ -148,6 +156,8 @@ export interface QueuedTaskView {
|
|
|
148
156
|
// Its place in the score order within its line, and how many are in it.
|
|
149
157
|
rank: number;
|
|
150
158
|
of: number;
|
|
159
|
+
// Messages held for it until it starts.
|
|
160
|
+
held: number;
|
|
151
161
|
}
|
|
152
162
|
|
|
153
163
|
export const START_ATTEMPTS = 3;
|
|
@@ -419,11 +429,7 @@ export async function startOrQueue<T>(
|
|
|
419
429
|
}
|
|
420
430
|
}
|
|
421
431
|
|
|
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
|
-
}
|
|
432
|
+
const label = queuedLabel;
|
|
427
433
|
|
|
428
434
|
function afterLabel(r: QueueRow): string | null {
|
|
429
435
|
if (r.after_task) {
|
|
@@ -438,7 +444,9 @@ function afterLabel(r: QueueRow): string | null {
|
|
|
438
444
|
export function listQueue(): QueuedTaskView[] {
|
|
439
445
|
const rows = db
|
|
440
446
|
.prepare(
|
|
441
|
-
`SELECT q.*, p.name AS project_name, p.workspace_id AS workspace_id
|
|
447
|
+
`SELECT q.*, p.name AS project_name, p.workspace_id AS workspace_id,
|
|
448
|
+
(SELECT COUNT(*) FROM bus_messages b WHERE b.to_id = q.id
|
|
449
|
+
AND b.held_queued = 1 AND b.delivered_at IS NULL AND b.read_at IS NULL) AS held_count
|
|
442
450
|
FROM task_queue q LEFT JOIN projects p ON p.id = q.project_id
|
|
443
451
|
WHERE q.status IN ('queued', 'starting', 'failed')
|
|
444
452
|
ORDER BY q.position`
|
|
@@ -446,6 +454,7 @@ export function listQueue(): QueuedTaskView[] {
|
|
|
446
454
|
.all() as (QueueRow & {
|
|
447
455
|
project_name: string | null;
|
|
448
456
|
workspace_id: string | null;
|
|
457
|
+
held_count: number;
|
|
449
458
|
})[];
|
|
450
459
|
const places = new Map<string, number>();
|
|
451
460
|
// The score order within each line, by priority and age alone: holds
|
|
@@ -489,6 +498,7 @@ export function listQueue(): QueuedTaskView[] {
|
|
|
489
498
|
wait: r.wait_code,
|
|
490
499
|
rank: rank?.rank ?? place,
|
|
491
500
|
of: rank?.of ?? place,
|
|
501
|
+
held: r.held_count,
|
|
492
502
|
};
|
|
493
503
|
});
|
|
494
504
|
}
|
|
@@ -635,10 +645,14 @@ async function startRow(
|
|
|
635
645
|
const placed = opts.placement ?? null;
|
|
636
646
|
if (!opts.force)
|
|
637
647
|
recordAutoStart(placed?.hostId ?? LOCAL, !!opts.heavy, Date.now());
|
|
648
|
+
// A linked machine's start never sees this machine's held messages: they
|
|
649
|
+
// go with the brief it is sent.
|
|
650
|
+
const remote = isRemoteHost(placed?.hostId ?? r.host_id ?? project?.host_id);
|
|
651
|
+
const remoteHeld = remote ? queuedHeldMessages(r.id) : [];
|
|
638
652
|
const task = {
|
|
639
653
|
id: r.id,
|
|
640
654
|
projectId: r.project_id,
|
|
641
|
-
prompt: r.prompt,
|
|
655
|
+
prompt: r.prompt + queuedMessagesSection(remoteHeld),
|
|
642
656
|
name: r.name ?? undefined,
|
|
643
657
|
model: r.model_overrides
|
|
644
658
|
? (JSON.parse(r.model_overrides) as ModelOverride)
|
|
@@ -668,6 +682,7 @@ async function startRow(
|
|
|
668
682
|
{ spendApproval: false }
|
|
669
683
|
)
|
|
670
684
|
: await createTask(task);
|
|
685
|
+
if (remoteHeld.length) markQueuedHeldDelivered(r.id);
|
|
671
686
|
patch(r.id, {
|
|
672
687
|
status: "started",
|
|
673
688
|
started_at: new Date().toISOString(),
|
|
@@ -815,6 +830,20 @@ async function peerOptions(
|
|
|
815
830
|
const hostIds = [...linkedHostIds()];
|
|
816
831
|
if (!hostIds.length || !rows.length) return out;
|
|
817
832
|
await refreshPeers(hostIds, now);
|
|
833
|
+
// Whether each machine can hold an orchestrator's task (an older AgentOS
|
|
834
|
+
// can't, and would fail its start); unreadable counts as no.
|
|
835
|
+
const holds = new Map<string, boolean>();
|
|
836
|
+
await Promise.all(
|
|
837
|
+
hostIds.map(async (hostId) => {
|
|
838
|
+
const link = hostLink(hostId);
|
|
839
|
+
const caps = link
|
|
840
|
+
? await hostTasks(link)
|
|
841
|
+
.then((t) => t.capabilities ?? [])
|
|
842
|
+
.catch(() => [])
|
|
843
|
+
: [];
|
|
844
|
+
holds.set(hostId, caps.includes("held-start"));
|
|
845
|
+
})
|
|
846
|
+
);
|
|
818
847
|
const perProject = new Map<string, Record<string, string | null>>();
|
|
819
848
|
for (const r of rows) {
|
|
820
849
|
if (out.has(r.project_id)) continue;
|
|
@@ -836,6 +865,7 @@ async function peerOptions(
|
|
|
836
865
|
name: src.label,
|
|
837
866
|
view: viewOf(src, now),
|
|
838
867
|
capabilities: src.capabilities,
|
|
868
|
+
holds: holds.get(hostId) ?? false,
|
|
839
869
|
hasProject: can[hostId] === null,
|
|
840
870
|
loadPerCore: reading ? reading.load1 / reading.cores : 99,
|
|
841
871
|
};
|
|
@@ -911,7 +941,7 @@ export async function tickOnce(now = Date.now()): Promise<"stale" | void> {
|
|
|
911
941
|
)) {
|
|
912
942
|
const r = rows.find((x) => x.id === c.id)!;
|
|
913
943
|
const options = (peers.get(r.project_id) ?? []).filter(
|
|
914
|
-
(p) => !taken.has(p.hostId)
|
|
944
|
+
(p) => !taken.has(p.hostId) && (!r.started_by || p.holds !== false)
|
|
915
945
|
);
|
|
916
946
|
const spot = place(c, home, options, built.get(c.id)!.needs, now);
|
|
917
947
|
if (!spot) continue;
|
|
@@ -1077,7 +1107,7 @@ export function findQueued(
|
|
|
1077
1107
|
return hits[0] ?? null;
|
|
1078
1108
|
}
|
|
1079
1109
|
|
|
1080
|
-
export
|
|
1110
|
+
export { queuedLabel };
|
|
1081
1111
|
|
|
1082
1112
|
// Changes the priority of a queued task or a running one (they share an
|
|
1083
1113
|
// id). Age is kept. Throws when no such task is waiting or running.
|
|
@@ -1105,11 +1135,15 @@ export function stampPriority(id: string, priority: Priority | undefined) {
|
|
|
1105
1135
|
}
|
|
1106
1136
|
|
|
1107
1137
|
export function removeQueued(id: string): void {
|
|
1108
|
-
const changed = db
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1138
|
+
const changed = db.transaction(() => {
|
|
1139
|
+
const n = db
|
|
1140
|
+
.prepare(
|
|
1141
|
+
`UPDATE task_queue SET status = 'removed' WHERE id = ? AND status IN ('queued', 'failed')`
|
|
1142
|
+
)
|
|
1143
|
+
.run(id).changes;
|
|
1144
|
+
if (n) discardQueuedHeld(id);
|
|
1145
|
+
return n;
|
|
1146
|
+
})();
|
|
1113
1147
|
if (!changed) throw new Error("That task isn't waiting in the queue");
|
|
1114
1148
|
tickQueueSoon();
|
|
1115
1149
|
}
|
package/lib/tasks/remote.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from "../hosts/remote-api";
|
|
16
16
|
import { isRemoteHost } from "../hosts";
|
|
17
17
|
import { projectRef } from "./project-ref";
|
|
18
|
+
import { orchestratedStart } from "./held";
|
|
18
19
|
import type { TaskStatus } from "./state";
|
|
19
20
|
import type { TaskView } from "./index";
|
|
20
21
|
|
|
@@ -33,6 +34,9 @@ export const TASK_CAPABILITIES = [
|
|
|
33
34
|
// A held task's chat facts and its messages for its orchestrator.
|
|
34
35
|
"held-chat-facts",
|
|
35
36
|
"held-replies",
|
|
37
|
+
// A keyed start that carries an orchestrator's description holds the task
|
|
38
|
+
// for the device that sent it, as a move does (lib/tasks/held.ts).
|
|
39
|
+
"held-start",
|
|
36
40
|
] as const;
|
|
37
41
|
|
|
38
42
|
export interface HostTasks {
|
|
@@ -48,7 +52,8 @@ export function isMirror(session: Session): boolean {
|
|
|
48
52
|
export function mirrorTask(
|
|
49
53
|
hostId: string,
|
|
50
54
|
projectId: string | null,
|
|
51
|
-
remote: Session
|
|
55
|
+
remote: Session,
|
|
56
|
+
startedBy?: string | null
|
|
52
57
|
): void {
|
|
53
58
|
db.prepare(
|
|
54
59
|
`INSERT INTO sessions (id, name, tmux_name, working_directory, model, group_path,
|
|
@@ -71,6 +76,35 @@ export function mirrorTask(
|
|
|
71
76
|
// As that machine runs it: one that predates chat tasks says terminal.
|
|
72
77
|
remote.view === "chat" ? "chat" : "terminal"
|
|
73
78
|
);
|
|
79
|
+
// What carryTaskState gives a moved task's mirror: the orchestrator that
|
|
80
|
+
// holds it, and when it began (so its PR is judged from then).
|
|
81
|
+
if (startedBy)
|
|
82
|
+
db.prepare(
|
|
83
|
+
`UPDATE sessions SET started_by = COALESCE(started_by, ?),
|
|
84
|
+
task_since = COALESCE(task_since, created_at) WHERE id = ?`
|
|
85
|
+
).run(startedBy, remote.id);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* A machine on an older AgentOS would take a chat task as a terminal one,
|
|
90
|
+
* or an orchestrator's task as a plain one that anyone there could merge, or
|
|
91
|
+
* refuse to hand one over.
|
|
92
|
+
*/
|
|
93
|
+
export async function requireCapability(
|
|
94
|
+
link: HostLink,
|
|
95
|
+
capability: string,
|
|
96
|
+
what: string,
|
|
97
|
+
verb = "move"
|
|
98
|
+
): Promise<void> {
|
|
99
|
+
const { capabilities = [] } = await hostTasks(link, true).catch((err) => {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`Can't ask ${link.hostName} whether it takes ${what}: ${(err as Error).message}`
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
if (!capabilities.includes(capability))
|
|
105
|
+
throw new Error(
|
|
106
|
+
`${link.hostName}'s AgentOS can't ${verb} ${what} yet; update it first`
|
|
107
|
+
);
|
|
74
108
|
}
|
|
75
109
|
|
|
76
110
|
export const unknownOutcome = (err: unknown) =>
|
|
@@ -101,16 +135,30 @@ export async function startRemoteTask(
|
|
|
101
135
|
model?: string;
|
|
102
136
|
baseBranch?: string;
|
|
103
137
|
view?: "chat" | "terminal";
|
|
138
|
+
// The orchestrator that started it: that machine holds the task for it.
|
|
139
|
+
startedBy?: string | null;
|
|
104
140
|
}
|
|
105
141
|
): Promise<Session> {
|
|
106
142
|
const link = requireHostLink(hostId);
|
|
143
|
+
const { startedBy, ...rest } = opts;
|
|
144
|
+
if (startedBy)
|
|
145
|
+
await requireCapability(
|
|
146
|
+
link,
|
|
147
|
+
"held-start",
|
|
148
|
+
"an orchestrator's task",
|
|
149
|
+
"hold"
|
|
150
|
+
);
|
|
107
151
|
const { session } = await postIdempotent<{ session: Session }>(
|
|
108
152
|
link,
|
|
109
153
|
"/api/tasks",
|
|
110
|
-
{
|
|
154
|
+
{
|
|
155
|
+
project: await projectRef(project),
|
|
156
|
+
...rest,
|
|
157
|
+
orchestrated: startedBy ? orchestratedStart(startedBy) : undefined,
|
|
158
|
+
},
|
|
111
159
|
180000
|
|
112
160
|
);
|
|
113
|
-
mirrorTask(hostId, project.id, session);
|
|
161
|
+
mirrorTask(hostId, project.id, session, startedBy);
|
|
114
162
|
forgetHostTasks(hostId);
|
|
115
163
|
return db
|
|
116
164
|
.prepare(`SELECT * FROM sessions WHERE id = ?`)
|
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)
|