@saadnvd1/agent-os 0.3.597 → 0.3.601
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/bus/spawn/route.ts +35 -16
- package/app/api/lumifyhub/cards/[cardId]/run/route.ts +3 -0
- package/app/api/tasks/route.ts +3 -0
- package/app/page.tsx +2 -1
- package/components/SessionList/LiveLine.tsx +33 -6
- package/components/SessionList/QueuedRow.tsx +45 -46
- package/components/SessionList/RowParts.tsx +1 -54
- package/components/SessionList/RowShell.tsx +13 -6
- package/components/SessionList/RowStatusPlacement.test.ts +114 -0
- package/components/SessionList/RowSubtitle.tsx +2 -3
- package/components/SessionList/SessionRow.tsx +14 -11
- package/lib/agents/spawn.ts +16 -4
- package/lib/chat/refresh.ts +2 -1
- package/lib/chat/runner.ts +29 -0
- package/lib/chat/store.ts +53 -0
- package/lib/db/migrations/20261014T2110_task_start_record.ts +21 -0
- package/lib/db/migrations/20261014T2200_end_orphaned_tasks.ts +23 -0
- package/lib/db/types.ts +2 -0
- package/lib/learning-cards/apply.ts +7 -1
- package/lib/models/plan-step.ts +25 -8
- package/lib/models/resolve.ts +1 -1
- package/lib/models/resume.ts +1 -1
- package/lib/models/roles.ts +0 -16
- package/lib/models/session-role.ts +19 -0
- package/lib/orchestrator/act.ts +22 -7
- package/lib/orchestrator/amendments.ts +10 -3
- package/lib/orchestrator/serve.ts +1 -0
- package/lib/orchestrator/testing.ts +8 -0
- package/lib/orchestrator/tool-schemas.ts +9 -0
- package/lib/orchestrator/tools.ts +1 -1
- package/lib/plans/claim.ts +131 -0
- package/lib/plans/release.ts +5 -2
- package/lib/recipes/run.ts +6 -0
- package/lib/schedules/start.ts +1 -0
- package/lib/sessions/fork.ts +10 -0
- package/lib/sessions/launch.ts +5 -1
- package/lib/sidebar/live-status.ts +33 -2
- package/lib/sidebar/row-badge.ts +26 -2
- package/lib/stacks/start.ts +1 -0
- package/lib/tasks/held-remote.ts +4 -2
- package/lib/tasks/held-replies.ts +5 -2
- package/lib/tasks/held.ts +2 -1
- package/lib/tasks/index.ts +85 -46
- package/lib/tasks/move-carry.ts +12 -1
- package/lib/tasks/move-guard.ts +2 -1
- package/lib/tasks/move.ts +7 -5
- package/lib/tasks/pause.ts +1 -1
- package/lib/tasks/queue.ts +54 -31
- package/lib/tasks/remote.ts +3 -1
- package/lib/tasks/starter.ts +124 -0
- package/lib/usage/calls.ts +2 -1
- package/lib/usage/turns.ts +2 -1
- package/package.json +1 -1
package/lib/tasks/queue.ts
CHANGED
|
@@ -85,6 +85,8 @@ import {
|
|
|
85
85
|
resumeHoldFor,
|
|
86
86
|
setPauseNote,
|
|
87
87
|
} from "./pause";
|
|
88
|
+
import { orchestratorOf, type StartVia } from "./starter";
|
|
89
|
+
import { claimPiece } from "../plans/claim";
|
|
88
90
|
|
|
89
91
|
export type QueueStatus =
|
|
90
92
|
| "queued"
|
|
@@ -107,6 +109,9 @@ export interface QueueRow {
|
|
|
107
109
|
plan_reason: string | null;
|
|
108
110
|
plan_only: number | null;
|
|
109
111
|
started_by: string | null;
|
|
112
|
+
parent_session_id: string | null;
|
|
113
|
+
started_by_device: string | null;
|
|
114
|
+
started_via: string | null;
|
|
110
115
|
view: "chat" | "terminal" | null;
|
|
111
116
|
agent_type: string | null;
|
|
112
117
|
base_branch: string | null;
|
|
@@ -306,6 +311,11 @@ export interface QueueRequest {
|
|
|
306
311
|
planReason?: string | null;
|
|
307
312
|
planOnly?: boolean;
|
|
308
313
|
startedBy?: string | null;
|
|
314
|
+
parentSessionId?: string | null;
|
|
315
|
+
startedByDevice?: string | null;
|
|
316
|
+
startedVia: StartVia;
|
|
317
|
+
// The waiting plan piece it builds, claimed for its queue id.
|
|
318
|
+
pieceId?: number;
|
|
309
319
|
view?: "chat" | "terminal";
|
|
310
320
|
agent?: AgentType;
|
|
311
321
|
baseBranch?: string;
|
|
@@ -412,36 +422,43 @@ export function queueIfNeeded(req: QueueRequest): QueuedTaskView | null {
|
|
|
412
422
|
if (!waits && !full && !loaded) return null;
|
|
413
423
|
if (!req.prompt.trim()) throw new Error("Describe the task");
|
|
414
424
|
const id = randomUUID();
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
425
|
+
// The row and the piece claim stand or fall together.
|
|
426
|
+
db.transaction(() => {
|
|
427
|
+
db.prepare(
|
|
428
|
+
`INSERT INTO task_queue (id, project_id, prompt, name, model, model_overrides, plan, plan_reason, plan_only, started_by, parent_session_id, started_by_device, started_via, view, agent_type, base_branch,
|
|
429
|
+
host_id, after_task, after_any, origin_workspace_id, priority, machine_needs, position)
|
|
430
|
+
VALUES (@id, @project_id, @prompt, @name, @model, @model_overrides, @plan, @plan_reason, @plan_only, @started_by, @parent_session_id, @started_by_device, @started_via, @view, @agent_type, @base_branch,
|
|
431
|
+
@host_id, @after_task, @after_any, @origin, @priority, @machine_needs,
|
|
432
|
+
(SELECT COALESCE(MAX(position), 0) + 1 FROM task_queue))`
|
|
433
|
+
).run({
|
|
434
|
+
id,
|
|
435
|
+
project_id: project.id,
|
|
436
|
+
prompt: req.prompt.trim(),
|
|
437
|
+
name: req.name?.trim() || null,
|
|
438
|
+
model: typeof req.model === "string" ? req.model || null : null,
|
|
439
|
+
model_overrides:
|
|
440
|
+
req.model && typeof req.model === "object"
|
|
441
|
+
? JSON.stringify(req.model)
|
|
442
|
+
: null,
|
|
443
|
+
plan: req.plan === undefined ? null : req.plan ? 1 : 0,
|
|
444
|
+
plan_reason: req.planReason?.trim() || null,
|
|
445
|
+
plan_only: req.planOnly ? 1 : null,
|
|
446
|
+
started_by: req.startedBy ?? null,
|
|
447
|
+
parent_session_id: req.parentSessionId ?? null,
|
|
448
|
+
started_by_device: req.startedByDevice ?? null,
|
|
449
|
+
started_via: req.startedVia,
|
|
450
|
+
view: req.view ?? null,
|
|
451
|
+
agent_type: req.agent ?? null,
|
|
452
|
+
base_branch: req.baseBranch || null,
|
|
453
|
+
host_id: req.hostId || null,
|
|
454
|
+
after_task: afterTask,
|
|
455
|
+
after_any: afterAny ? JSON.stringify(afterAny) : null,
|
|
456
|
+
origin: req.originWorkspaceId ?? null,
|
|
457
|
+
priority: req.priority ?? DEFAULT_PRIORITY,
|
|
458
|
+
machine_needs: setting ? JSON.stringify(setting) : null,
|
|
459
|
+
});
|
|
460
|
+
if (req.pieceId) claimPiece(req.pieceId, id);
|
|
461
|
+
})();
|
|
445
462
|
tickQueueSoon();
|
|
446
463
|
return queuedView(id)!;
|
|
447
464
|
}
|
|
@@ -729,6 +746,10 @@ async function startRow(
|
|
|
729
746
|
planReason: r.plan_reason,
|
|
730
747
|
planOnly: !!r.plan_only,
|
|
731
748
|
startedBy: r.started_by,
|
|
749
|
+
parentSessionId: r.parent_session_id,
|
|
750
|
+
startedByDevice: r.started_by_device,
|
|
751
|
+
startedVia: (r.started_via ??
|
|
752
|
+
(r.started_by ? "session" : "person")) as StartVia,
|
|
732
753
|
view: r.view ?? undefined,
|
|
733
754
|
agent: (r.agent_type as AgentType | null) ?? undefined,
|
|
734
755
|
baseBranch: r.base_branch ?? undefined,
|
|
@@ -1022,7 +1043,9 @@ export async function tickOnce(now = Date.now()): Promise<"stale" | void> {
|
|
|
1022
1043
|
)) {
|
|
1023
1044
|
const r = rows.find((x) => x.id === c.id)!;
|
|
1024
1045
|
const options = (peers.get(r.project_id) ?? []).filter(
|
|
1025
|
-
(p) =>
|
|
1046
|
+
(p) =>
|
|
1047
|
+
!taken.has(p.hostId) &&
|
|
1048
|
+
(!orchestratorOf(r.started_by) || p.holds !== false)
|
|
1026
1049
|
);
|
|
1027
1050
|
const spot = place(c, home, options, built.get(c.id)!.needs, now);
|
|
1028
1051
|
if (!spot) continue;
|
package/lib/tasks/remote.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { projectRef } from "./project-ref";
|
|
|
18
18
|
import { orchestratedStart } from "./held";
|
|
19
19
|
import type { TaskStatus } from "./state";
|
|
20
20
|
import type { TaskView } from "./index";
|
|
21
|
+
import { orchestratorOf } from "./starter";
|
|
21
22
|
|
|
22
23
|
// What this machine's task API does, for another machine deciding whether
|
|
23
24
|
// it can trust it with a pinned merge or a move; keyed-start: a session
|
|
@@ -140,7 +141,8 @@ export async function startRemoteTask(
|
|
|
140
141
|
}
|
|
141
142
|
): Promise<Session> {
|
|
142
143
|
const link = requireHostLink(hostId);
|
|
143
|
-
const { startedBy, ...rest } = opts;
|
|
144
|
+
const { startedBy: by, ...rest } = opts;
|
|
145
|
+
const startedBy = orchestratorOf(by);
|
|
144
146
|
if (startedBy)
|
|
145
147
|
await requireCapability(
|
|
146
148
|
link,
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// Who started a task and how. started_by is the session that started it (the
|
|
2
|
+
// calling task, chat or orchestrator; null for a person), parent_session_id
|
|
3
|
+
// the session it came from, started_via how it began.
|
|
4
|
+
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { db } from "../db";
|
|
7
|
+
|
|
8
|
+
export type StartVia =
|
|
9
|
+
| "session"
|
|
10
|
+
| "orchestrator"
|
|
11
|
+
| "person"
|
|
12
|
+
| "plan"
|
|
13
|
+
| "schedule"
|
|
14
|
+
| "recipe"
|
|
15
|
+
| "stack"
|
|
16
|
+
| "remote";
|
|
17
|
+
|
|
18
|
+
export interface Starter {
|
|
19
|
+
startedBy: string | null;
|
|
20
|
+
parentSessionId: string | null;
|
|
21
|
+
startedVia: StartVia;
|
|
22
|
+
startedByDevice?: string | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const PERSON: Starter = {
|
|
26
|
+
startedBy: null,
|
|
27
|
+
parentSessionId: null,
|
|
28
|
+
startedVia: "person",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const personStarter = (device?: string | null): Starter => ({
|
|
32
|
+
...PERSON,
|
|
33
|
+
startedByDevice: device ?? null,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
interface Caller {
|
|
37
|
+
id: string;
|
|
38
|
+
role: string | null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// The session making the call: `from` when it names a real session, else the
|
|
42
|
+
// one whose worktree holds `cwd`. A project folder is shared by many
|
|
43
|
+
// sessions, so only a worktree identifies one, and only a task's: a cwd
|
|
44
|
+
// outside every known task worktree names nobody. Remaining trust: any local
|
|
45
|
+
// process can still name a task by its id or its worktree path, exactly as
|
|
46
|
+
// it could name one in `from`; this records who asked, it grants nothing
|
|
47
|
+
// (a piece it claims is that task's own plan's).
|
|
48
|
+
export function callerSession(input: {
|
|
49
|
+
from?: unknown;
|
|
50
|
+
cwd?: unknown;
|
|
51
|
+
}): Caller | null {
|
|
52
|
+
if (typeof input.from === "string" && input.from) {
|
|
53
|
+
const row = db
|
|
54
|
+
.prepare(`SELECT id, role FROM sessions WHERE id = ?`)
|
|
55
|
+
.get(input.from) as Caller | undefined;
|
|
56
|
+
if (row) return row;
|
|
57
|
+
}
|
|
58
|
+
if (typeof input.cwd !== "string" || !path.isAbsolute(input.cwd)) return null;
|
|
59
|
+
const cwd = path.resolve(input.cwd);
|
|
60
|
+
const rows = db
|
|
61
|
+
.prepare(
|
|
62
|
+
`SELECT id, role, worktree_path FROM sessions
|
|
63
|
+
WHERE worktree_path IS NOT NULL AND archived_at IS NULL
|
|
64
|
+
AND task_status IS NOT NULL
|
|
65
|
+
AND (host_id IS NULL OR host_id = 'local')`
|
|
66
|
+
)
|
|
67
|
+
.all() as (Caller & { worktree_path: string })[];
|
|
68
|
+
let best: (Caller & { worktree_path: string }) | null = null;
|
|
69
|
+
for (const r of rows) {
|
|
70
|
+
const wt = path.resolve(r.worktree_path);
|
|
71
|
+
if (
|
|
72
|
+
(cwd === wt || cwd.startsWith(wt + path.sep)) &&
|
|
73
|
+
(!best || wt.length > best.worktree_path.length)
|
|
74
|
+
)
|
|
75
|
+
best = r;
|
|
76
|
+
}
|
|
77
|
+
return best;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function starterOf(
|
|
81
|
+
caller: Caller | null,
|
|
82
|
+
device?: string | null
|
|
83
|
+
): Starter {
|
|
84
|
+
if (!caller) return personStarter(device);
|
|
85
|
+
return {
|
|
86
|
+
startedBy: caller.id,
|
|
87
|
+
parentSessionId: caller.id,
|
|
88
|
+
startedVia: caller.role === "orchestrator" ? "orchestrator" : "session",
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// The orchestrator that owns a session's work: follows started_by up to the
|
|
93
|
+
// first orchestrator, null when the chain ends at a person. It gives up after
|
|
94
|
+
// 6 hops (a deeper chain, or a cycle, counts as no orchestrator), so a bad
|
|
95
|
+
// row can never loop it.
|
|
96
|
+
export function orchestratorOf(startedBy: string | null | undefined) {
|
|
97
|
+
const seen = new Set<string>();
|
|
98
|
+
let id = startedBy ?? null;
|
|
99
|
+
for (let hops = 0; id && hops < 6 && !seen.has(id); hops++) {
|
|
100
|
+
seen.add(id);
|
|
101
|
+
const row = db
|
|
102
|
+
.prepare(`SELECT role, started_by FROM sessions WHERE id = ?`)
|
|
103
|
+
.get(id) as
|
|
104
|
+
| { role: string | null; started_by: string | null }
|
|
105
|
+
| undefined;
|
|
106
|
+
if (!row) return null;
|
|
107
|
+
if (row.role === "orchestrator") return id;
|
|
108
|
+
id = row.started_by;
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function recordStart(id: string, s: Starter): void {
|
|
114
|
+
db.prepare(
|
|
115
|
+
`UPDATE sessions SET started_by = ?, parent_session_id = (SELECT id FROM sessions WHERE id = ?), started_via = ?,
|
|
116
|
+
started_by_device = COALESCE(?, started_by_device) WHERE id = ?`
|
|
117
|
+
).run(
|
|
118
|
+
s.startedBy,
|
|
119
|
+
s.parentSessionId,
|
|
120
|
+
s.startedVia,
|
|
121
|
+
s.startedByDevice ?? null,
|
|
122
|
+
id
|
|
123
|
+
);
|
|
124
|
+
}
|
package/lib/usage/calls.ts
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { db } from "../db";
|
|
9
|
-
import { ANTHROPIC
|
|
9
|
+
import { ANTHROPIC } from "../models/roles";
|
|
10
|
+
import { sessionRole } from "../models/session-role";
|
|
10
11
|
import { ctxOf, weighted, type TokenRow } from "./weights";
|
|
11
12
|
import type { CallKind, CallRow, CallSource, CallUsage } from "./types";
|
|
12
13
|
|
package/lib/usage/turns.ts
CHANGED
|
@@ -8,7 +8,8 @@ import { db, type Session } from "../db";
|
|
|
8
8
|
import type { UsageTotals } from "../chat/events";
|
|
9
9
|
import { totalTokens, turnDelta } from "../chat/context";
|
|
10
10
|
import { readUsage } from "../orchestrator/usage";
|
|
11
|
-
import { ANTHROPIC
|
|
11
|
+
import { ANTHROPIC } from "../models/roles";
|
|
12
|
+
import { sessionRole } from "../models/session-role";
|
|
12
13
|
|
|
13
14
|
// Where a worker's totals start: a resumed conversation continues from the
|
|
14
15
|
// totals its transcript saved, which the last worker stored; a new one
|