@saadnvd1/agent-os 0.3.631 → 0.3.639
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/sessions/[id]/pr/route.ts +3 -3
- package/app/api/sessions/[id]/setup/route.ts +3 -3
- package/app/api/tasks/[id]/approve-plan/route.ts +5 -1
- package/app/api/tasks/[id]/arm/route.ts +6 -5
- package/app/api/tasks/[id]/chat-facts/route.ts +2 -2
- package/app/api/tasks/[id]/chat-tail/route.ts +2 -2
- package/app/api/tasks/[id]/checks/route.ts +1 -1
- package/app/api/tasks/[id]/diff/route.ts +1 -1
- package/app/api/tasks/[id]/done/route.ts +2 -2
- package/app/api/tasks/[id]/drop/route.ts +6 -2
- package/app/api/tasks/[id]/export/route.ts +3 -4
- package/app/api/tasks/[id]/merge/route.ts +3 -3
- package/app/api/tasks/[id]/message/route.ts +2 -2
- package/app/api/tasks/[id]/model/route.ts +5 -1
- package/app/api/tasks/[id]/move/route.ts +5 -1
- package/app/api/tasks/[id]/moved/route.ts +6 -8
- package/app/api/tasks/[id]/plan/route.ts +35 -0
- package/app/api/tasks/[id]/plan-edits/route.ts +6 -5
- package/app/api/tasks/[id]/priority/route.ts +4 -1
- package/app/api/tasks/[id]/resume/route.ts +3 -4
- package/app/api/tasks/[id]/stop/route.ts +2 -2
- package/app/api/tasks/route.ts +5 -2
- package/app/globals.css +28 -0
- package/components/Chat/ChatPanel.tsx +2 -11
- package/components/Chat/EventTurns.tsx +1 -7
- package/components/Chat/Markdown.tsx +1 -0
- package/components/Chat/Messages.tsx +1 -1
- package/lib/approvals/enable.ts +9 -2
- package/lib/away/report.ts +4 -4
- package/lib/away/run.ts +2 -2
- package/lib/bookmarks/store.ts +3 -3
- package/lib/bus/held-reply.ts +2 -2
- package/lib/bus/index.ts +3 -3
- package/lib/chat/compact-context.ts +1 -1
- package/lib/chat/events.ts +4 -0
- package/lib/chat/group.ts +3 -1
- package/lib/chat/hold.ts +3 -3
- package/lib/chat/reading-order.ts +50 -0
- package/lib/chat/refresh.ts +1 -1
- package/lib/chat/registry.ts +3 -3
- package/lib/chat/runner.ts +1 -1
- package/lib/chat/turns.ts +22 -3
- package/lib/chat/worker/host.ts +3 -0
- package/lib/chat/worker/main.ts +1 -1
- package/lib/db/index.ts +5 -1
- package/lib/db/lumifyhub.ts +8 -9
- package/lib/db/machine-scope.ts +12 -0
- package/lib/db/migrations/20261015T1600_plan_waits_for_caller.ts +19 -0
- package/lib/db/migrations/20261015T1800_split_sessions_tables.ts +172 -0
- package/lib/db/migrations/README.md +7 -0
- package/lib/db/queries.ts +16 -31
- package/lib/db/session-parts/columns.ts +186 -0
- package/lib/db/session-parts/fill.ts +33 -0
- package/lib/db/session-parts/index.ts +9 -0
- package/lib/db/session-parts/pauses.ts +11 -0
- package/lib/db/session-parts/placement.ts +11 -0
- package/lib/db/session-parts/plans.ts +31 -0
- package/lib/db/session-parts/prs.ts +31 -0
- package/lib/db/session-parts/tasks.ts +27 -0
- package/lib/db/session-parts/view.ts +43 -0
- package/lib/db/session-parts/write.ts +69 -0
- package/lib/db/types.ts +6 -1
- package/lib/done/archive.ts +2 -2
- package/lib/done/auto-archive.ts +1 -1
- package/lib/done/bulk.ts +1 -1
- package/lib/done/index.ts +8 -14
- package/lib/env-setup.ts +17 -4
- package/lib/health/evidence.ts +1 -1
- package/lib/health/facts.ts +1 -1
- package/lib/health/pieces.ts +1 -1
- package/lib/hosts/peer-actions.ts +2 -2
- package/lib/hosts/peer-mirror.ts +2 -2
- package/lib/hosts/peer-sync.ts +69 -32
- package/lib/learning-cards/input.ts +1 -1
- package/lib/learning-cards/store.ts +1 -1
- package/lib/models/plan-step.ts +83 -46
- package/lib/models/resume.ts +5 -7
- package/lib/notify/index.ts +1 -1
- package/lib/notify/push/events.ts +1 -1
- package/lib/notify/push/index.ts +1 -1
- package/lib/orchestrator/act.ts +1 -1
- package/lib/orchestrator/armed.ts +17 -15
- package/lib/orchestrator/ask-approvals.ts +1 -1
- package/lib/orchestrator/ask-heads.ts +1 -1
- package/lib/orchestrator/ask-settle.ts +1 -1
- package/lib/orchestrator/brakes.ts +1 -1
- package/lib/orchestrator/digest.ts +2 -2
- package/lib/orchestrator/external-pr.ts +2 -2
- package/lib/orchestrator/facts.ts +4 -28
- package/lib/orchestrator/held-asks.ts +3 -3
- package/lib/orchestrator/home.ts +2 -2
- package/lib/orchestrator/overview.ts +1 -1
- package/lib/orchestrator/pr-wake.ts +3 -5
- package/lib/orchestrator/questions.ts +1 -1
- package/lib/orchestrator/quick-store.ts +1 -1
- package/lib/orchestrator/quick.ts +10 -7
- package/lib/orchestrator/read.ts +1 -1
- package/lib/orchestrator/rebase.ts +8 -11
- package/lib/orchestrator/reconcile.ts +2 -2
- package/lib/orchestrator/signoff.ts +3 -1
- package/lib/orchestrator/targets.ts +1 -1
- package/lib/orchestrator/task-state.ts +4 -2
- package/lib/plans/features.ts +1 -1
- package/lib/plans/release.ts +1 -1
- package/lib/ports.ts +1 -1
- package/lib/resurrect/find.ts +1 -1
- package/lib/resurrect/index.ts +24 -18
- package/lib/schedules/store.ts +1 -1
- package/lib/session-audit/sources.ts +3 -3
- package/lib/session-logs/index.ts +1 -1
- package/lib/session-titles.ts +1 -1
- package/lib/sessions/setup-progress.ts +1 -1
- package/lib/sessions/stop.ts +1 -1
- package/lib/sessions/worktree-setup.ts +2 -1
- package/lib/simulator/index.ts +1 -1
- package/lib/simulator/task-devices.ts +1 -1
- package/lib/stacks/board.ts +1 -1
- package/lib/stacks/land.ts +1 -1
- package/lib/stacks/restack.ts +1 -1
- package/lib/stacks/start.ts +2 -2
- package/lib/stacks/tick.ts +1 -1
- package/lib/stacks/view.ts +1 -1
- package/lib/status/incremental.ts +1 -1
- package/lib/tasks/contract/actions.ts +35 -0
- package/lib/tasks/contract/authorize.ts +82 -0
- package/lib/tasks/contract/client.ts +296 -0
- package/lib/tasks/{held-remote.ts → contract/held-calls.ts} +35 -66
- package/lib/tasks/contract/index.ts +28 -0
- package/lib/tasks/contract/plan-path.ts +7 -0
- package/lib/tasks/contract/plan.ts +274 -0
- package/lib/tasks/contract/routes.ts +26 -0
- package/lib/tasks/contract/start.ts +122 -0
- package/lib/tasks/contract/sync.ts +126 -0
- package/lib/tasks/contract/types.ts +41 -0
- package/lib/tasks/{remote-views.ts → contract/views.ts} +11 -18
- package/lib/tasks/finish.ts +10 -9
- package/lib/tasks/held-replies.ts +7 -13
- package/lib/tasks/held.ts +36 -17
- package/lib/tasks/hold-checks.ts +134 -0
- package/lib/tasks/holds.ts +281 -0
- package/lib/tasks/import.ts +26 -14
- package/lib/tasks/index.ts +56 -22
- package/lib/tasks/launch-gate.ts +1 -1
- package/lib/tasks/move-bundle.ts +17 -2
- package/lib/tasks/move-carry.ts +15 -7
- package/lib/tasks/move-flow.ts +29 -36
- package/lib/tasks/move-guard.ts +10 -2
- package/lib/tasks/move-recover.ts +6 -25
- package/lib/tasks/move.ts +28 -27
- package/lib/tasks/moved-away.ts +1 -1
- package/lib/tasks/pause-hold.ts +13 -5
- package/lib/tasks/pause.ts +21 -54
- package/lib/tasks/placement.ts +16 -16
- package/lib/tasks/plan-view.ts +11 -3
- package/lib/tasks/queue-candidates.ts +151 -0
- package/lib/tasks/queue-placement.ts +155 -0
- package/lib/tasks/queue-rows.ts +236 -0
- package/lib/tasks/queue-start.ts +213 -0
- package/lib/tasks/queue-tick.ts +148 -0
- package/lib/tasks/queue-types.ts +141 -0
- package/lib/tasks/queue-view.ts +112 -0
- package/lib/tasks/queue.ts +67 -1102
- package/lib/tasks/schedule.ts +44 -147
- package/lib/tasks/session.ts +7 -4
- package/lib/tasks/start.ts +4 -4
- package/lib/tasks/starter.ts +8 -11
- package/lib/usage/calls.ts +1 -1
- package/lib/usage/compare.ts +1 -1
- package/lib/usage/quality.ts +1 -1
- package/lib/usage/turns.ts +1 -1
- package/lib/voice/vocab.ts +1 -1
- package/package.json +1 -1
- package/scripts/backfill-session-logs.ts +1 -1
- package/scripts/check +55 -7
- package/lib/tasks/auto-hold.ts +0 -17
- package/lib/tasks/held-routes.ts +0 -33
- package/lib/tasks/remote.ts +0 -238
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one loop that brings this machine's mirrors in line with what their
|
|
3
|
+
* machines say. Each tick reads every machine's task list once and applies it
|
|
4
|
+
* to each mirror: finished and moved, a plan written, a plan note to tell the
|
|
5
|
+
* orchestrator about, an approved plan coming home. Nothing else polls a
|
|
6
|
+
* machine's tasks to write a mirror.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { db, type Session } from "../../db";
|
|
10
|
+
import { isRemoteHost } from "../../hosts";
|
|
11
|
+
import { hostLink, cleanRemoteText } from "../../hosts/remote-api";
|
|
12
|
+
import { sessionWorkspace } from "../../models/resolve";
|
|
13
|
+
import { queueEvent } from "../../orchestrator/events";
|
|
14
|
+
import { orchestratorOf } from "../starter";
|
|
15
|
+
import type { TaskStatus } from "../state";
|
|
16
|
+
import type { TaskView } from "../index";
|
|
17
|
+
import * as client from "./client";
|
|
18
|
+
import {
|
|
19
|
+
finishArrival,
|
|
20
|
+
fire,
|
|
21
|
+
handingOff,
|
|
22
|
+
handoffHome,
|
|
23
|
+
planWritten,
|
|
24
|
+
} from "./plan";
|
|
25
|
+
import { setMirrorStatus } from "./start";
|
|
26
|
+
import { setPlan } from "../../db/session-parts";
|
|
27
|
+
|
|
28
|
+
const FINISHED: Record<string, TaskStatus> = {
|
|
29
|
+
merged: "merged",
|
|
30
|
+
dropped: "dropped",
|
|
31
|
+
done: "done",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const getSession = (id: string) =>
|
|
35
|
+
db.prepare(`SELECT * FROM session_rows WHERE id = ?`).get(id) as
|
|
36
|
+
| Session
|
|
37
|
+
| undefined;
|
|
38
|
+
|
|
39
|
+
// The plan-written line a held task stored on its machine (plan-step.ts),
|
|
40
|
+
// once per distinct plan: the mirror keeps the last one it told about.
|
|
41
|
+
function announcePlanNote(s: Session, view: TaskView): void {
|
|
42
|
+
const workspaceId = sessionWorkspace(s);
|
|
43
|
+
if (!workspaceId || !orchestratorOf(s.started_by)) return;
|
|
44
|
+
const note =
|
|
45
|
+
typeof view.planNote === "string"
|
|
46
|
+
? cleanRemoteText(view.planNote).slice(0, 2000)
|
|
47
|
+
: "";
|
|
48
|
+
if (!note || note === s.plan_note) return;
|
|
49
|
+
setPlan(db, s.id, { plan_note: note });
|
|
50
|
+
queueEvent(workspaceId, `plan-done:${s.id}`, s.id, note);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function applyView(
|
|
54
|
+
s: Session,
|
|
55
|
+
view: TaskView | undefined,
|
|
56
|
+
gone: Set<string>
|
|
57
|
+
): Promise<void> {
|
|
58
|
+
if (!view) {
|
|
59
|
+
if (gone.has(s.id)) setMirrorStatus(s.id, "moved");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (FINISHED[view.state]) {
|
|
63
|
+
setMirrorStatus(s.id, FINISHED[view.state]);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (!s.plan_waits_for_caller) return announcePlanNote(s, view);
|
|
67
|
+
if (s.plan_phase === "planning") await planWritten(s, view);
|
|
68
|
+
else if (s.plan_phase === "handoff") fire(s.id, () => handoffHome(s.id));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Every task this machine mirrors from a linked machine, that machine's word applied. */
|
|
72
|
+
export async function syncMirrors(): Promise<void> {
|
|
73
|
+
const rows = (
|
|
74
|
+
db
|
|
75
|
+
.prepare(
|
|
76
|
+
`SELECT * FROM session_rows
|
|
77
|
+
WHERE task_prompt IS NOT NULL AND task_status = 'running'
|
|
78
|
+
AND archived_at IS NULL AND peer_mirror IS NOT 1
|
|
79
|
+
AND host_id IS NOT NULL`
|
|
80
|
+
)
|
|
81
|
+
.all() as Session[]
|
|
82
|
+
).filter((s) => isRemoteHost(s.host_id));
|
|
83
|
+
const byHost = new Map<string, Session[]>();
|
|
84
|
+
for (const s of rows)
|
|
85
|
+
byHost.set(s.host_id, [...(byHost.get(s.host_id) ?? []), s]);
|
|
86
|
+
for (const [hostId, mirrors] of byHost) {
|
|
87
|
+
const link = hostLink(hostId);
|
|
88
|
+
if (!link) continue;
|
|
89
|
+
let listing: Awaited<ReturnType<typeof client.list>>;
|
|
90
|
+
try {
|
|
91
|
+
listing = await client.list(link);
|
|
92
|
+
} catch {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const live = new Map(
|
|
96
|
+
(Array.isArray(listing.tasks) ? listing.tasks : []).map((t) => [t?.id, t])
|
|
97
|
+
);
|
|
98
|
+
const gone = new Set((listing.moved ?? []).map((m) => m.id));
|
|
99
|
+
for (const s of mirrors) {
|
|
100
|
+
try {
|
|
101
|
+
await applyView(s, live.get(s.id), gone);
|
|
102
|
+
} catch (e) {
|
|
103
|
+
console.error(`Mirror of ${s.id} on ${hostId} failed:`, e);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// A task that arrived but never began implementing (a restart between).
|
|
108
|
+
const arrived = db
|
|
109
|
+
.prepare(
|
|
110
|
+
`SELECT id, plan_handoff_message FROM session_rows
|
|
111
|
+
WHERE plan_phase = 'awaiting' AND plan_handoff_message IS NOT NULL
|
|
112
|
+
AND task_status = 'running' AND archived_at IS NULL AND peer_mirror IS NOT 1`
|
|
113
|
+
)
|
|
114
|
+
.all() as { id: string; plan_handoff_message: string }[];
|
|
115
|
+
for (const a of arrived) {
|
|
116
|
+
if (isRemoteHost(getSession(a.id)?.host_id)) continue;
|
|
117
|
+
fire(a.id, async () => {
|
|
118
|
+
handingOff.add(a.id);
|
|
119
|
+
try {
|
|
120
|
+
await finishArrival(a.id, a.plan_handoff_message);
|
|
121
|
+
} finally {
|
|
122
|
+
handingOff.delete(a.id);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { TaskView } from "../index";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* What this machine's task API does, for another machine deciding whether
|
|
5
|
+
* it can trust it with a pinned merge or a move; keyed-start: a session
|
|
6
|
+
* start takes a key, so it's safe to retry (lib/sessions/launch.ts).
|
|
7
|
+
*/
|
|
8
|
+
export const TASK_CAPABILITIES = [
|
|
9
|
+
"pinned-merge",
|
|
10
|
+
"move",
|
|
11
|
+
"chat-turn",
|
|
12
|
+
"keyed-start",
|
|
13
|
+
"chat-move",
|
|
14
|
+
// Holds an orchestrator's task for the machine that sent it, and answers
|
|
15
|
+
// its messages, reads, stops and done (contract/authorize.ts).
|
|
16
|
+
"orchestrated-move",
|
|
17
|
+
// A held task's chat facts and its messages for its orchestrator.
|
|
18
|
+
"held-chat-facts",
|
|
19
|
+
"held-replies",
|
|
20
|
+
// A keyed start that carries an orchestrator's description holds the task
|
|
21
|
+
// for the device that sent it, as a move does (lib/tasks/held.ts).
|
|
22
|
+
"held-start",
|
|
23
|
+
// A plan placed here by another machine stops at the plan for that machine
|
|
24
|
+
// to decide what follows (contract/plan.ts).
|
|
25
|
+
"remote-plan",
|
|
26
|
+
// Every task route a peer calls checks the holder or starter the same way.
|
|
27
|
+
// compat: the shims marked "task-contract-1" in contract/ go once every
|
|
28
|
+
// linked machine reports this.
|
|
29
|
+
"task-contract-1",
|
|
30
|
+
] as const;
|
|
31
|
+
|
|
32
|
+
export interface HostTasks {
|
|
33
|
+
tasks: TaskView[];
|
|
34
|
+
moved?: { id: string; movedTo: string | null }[];
|
|
35
|
+
capabilities?: string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ChatFacts {
|
|
39
|
+
blocked: string | null;
|
|
40
|
+
turn: string | null;
|
|
41
|
+
}
|
|
@@ -1,21 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* How tasks on other machines look here: each mirror's view as its machine
|
|
3
|
-
* reports it.
|
|
4
|
-
* finished
|
|
3
|
+
* reports it. Reading only: the sync loop (sync.ts) is what records that a
|
|
4
|
+
* mirror finished or moved, never because it went missing once.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type { Session } from "
|
|
8
|
-
import { requireHostLink, type HostLink } from "
|
|
9
|
-
import { deriveTaskState
|
|
10
|
-
import {
|
|
11
|
-
import
|
|
12
|
-
import
|
|
13
|
-
|
|
14
|
-
const FINISHED: Record<string, TaskStatus> = {
|
|
15
|
-
merged: "merged",
|
|
16
|
-
dropped: "dropped",
|
|
17
|
-
done: "done",
|
|
18
|
-
};
|
|
7
|
+
import type { Session } from "../../db";
|
|
8
|
+
import { requireHostLink, type HostLink } from "../../hosts/remote-api";
|
|
9
|
+
import { deriveTaskState } from "../state";
|
|
10
|
+
import type { TaskView } from "../index";
|
|
11
|
+
import { viewOf } from "../move-targets";
|
|
12
|
+
import * as client from "./client";
|
|
19
13
|
|
|
20
14
|
function offlineView(
|
|
21
15
|
session: Session,
|
|
@@ -57,13 +51,13 @@ async function viewsOn(hostId: string, rows: Session[]): Promise<TaskView[]> {
|
|
|
57
51
|
return rows.map((s) => offlineView(s, "", (err as Error).message));
|
|
58
52
|
}
|
|
59
53
|
try {
|
|
60
|
-
const { tasks, moved = [] } = await
|
|
54
|
+
const { tasks, moved = [] } = await client.list(link);
|
|
61
55
|
const live = new Map(tasks.map((t) => [t.id, t]));
|
|
62
56
|
const gone = new Set(moved.map((m) => m.id));
|
|
63
57
|
return rows.flatMap((s): TaskView[] => {
|
|
64
58
|
const t = live.get(s.id);
|
|
65
59
|
if (!t) {
|
|
66
|
-
if (gone.has(s.id)) return
|
|
60
|
+
if (gone.has(s.id)) return [];
|
|
67
61
|
return [
|
|
68
62
|
offlineView(
|
|
69
63
|
s,
|
|
@@ -72,7 +66,6 @@ async function viewsOn(hostId: string, rows: Session[]): Promise<TaskView[]> {
|
|
|
72
66
|
),
|
|
73
67
|
];
|
|
74
68
|
}
|
|
75
|
-
if (FINISHED[t.state]) setMirrorStatus(s.id, FINISHED[t.state]);
|
|
76
69
|
return [
|
|
77
70
|
{
|
|
78
71
|
...t,
|
|
@@ -92,7 +85,7 @@ async function viewsOn(hostId: string, rows: Session[]): Promise<TaskView[]> {
|
|
|
92
85
|
}
|
|
93
86
|
}
|
|
94
87
|
|
|
95
|
-
export async function
|
|
88
|
+
export async function mirrorViews(mirrors: Session[]): Promise<TaskView[]> {
|
|
96
89
|
const byHost = new Map<string, Session[]>();
|
|
97
90
|
for (const s of mirrors)
|
|
98
91
|
byHost.set(s.host_id, [...(byHost.get(s.host_id) ?? []), s]);
|
package/lib/tasks/finish.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { db, type Session } from "../db";
|
|
2
2
|
import { endPauseOnFinish } from "./pause-hold";
|
|
3
|
-
import {
|
|
3
|
+
import { dropThere, mergeThere } from "./contract/actions";
|
|
4
|
+
import { isMirror } from "./contract/start";
|
|
4
5
|
import { deleteWorktree } from "../worktrees";
|
|
5
6
|
import { settleWorktree, type MergedAs } from "../done/worktree";
|
|
6
7
|
import {
|
|
@@ -37,6 +38,7 @@ import { captureMergedTask } from "../session-logs";
|
|
|
37
38
|
import { sessionEnded } from "../experiments/hooks";
|
|
38
39
|
import { snapshotWorktree } from "../resurrect/snapshot";
|
|
39
40
|
import { keepSessionFiles } from "../session-artifacts/collect";
|
|
41
|
+
import { setPr, setTask } from "../db/session-parts";
|
|
40
42
|
|
|
41
43
|
// Never deleted, whatever a setting says.
|
|
42
44
|
export const PROTECTED_BRANCHES = new Set(["main", "master", "HEAD"]);
|
|
@@ -141,7 +143,7 @@ export async function signOffTask(
|
|
|
141
143
|
// Its own machine checks the gates and merges.
|
|
142
144
|
// That machine's settings chose the method; null if it didn't say.
|
|
143
145
|
if (isMirror(session))
|
|
144
|
-
return
|
|
146
|
+
return mergeThere(session, opts.head, session.base_branch);
|
|
145
147
|
const repo = projectPathFor(session);
|
|
146
148
|
if (!repo) throw new Error("Task has no project");
|
|
147
149
|
const refusal = signOffRefusal(id);
|
|
@@ -178,9 +180,10 @@ export async function signOffTask(
|
|
|
178
180
|
method: policy.method,
|
|
179
181
|
head: opts.head,
|
|
180
182
|
});
|
|
181
|
-
db.
|
|
182
|
-
|
|
183
|
-
|
|
183
|
+
db.transaction(() => {
|
|
184
|
+
setTask(db, id, { task_status: "merged" });
|
|
185
|
+
setPr(db, id, { pr_status: "merged" });
|
|
186
|
+
})();
|
|
184
187
|
endPauseOnFinish(id);
|
|
185
188
|
db.prepare(
|
|
186
189
|
`UPDATE stack_items SET status = 'merged', error = NULL WHERE session_id = ?`
|
|
@@ -225,7 +228,7 @@ export async function dropTask(id: string): Promise<void> {
|
|
|
225
228
|
if (session.task_status !== "running")
|
|
226
229
|
throw new Error(`Task is already ${session.task_status}`);
|
|
227
230
|
if (isMirror(session)) {
|
|
228
|
-
await
|
|
231
|
+
await dropThere(session);
|
|
229
232
|
return;
|
|
230
233
|
}
|
|
231
234
|
const repo = projectPathFor(session);
|
|
@@ -247,9 +250,7 @@ export async function dropTask(id: string): Promise<void> {
|
|
|
247
250
|
if (pr?.state === "OPEN") {
|
|
248
251
|
await run("gh", ["pr", "close", String(pr.number)], repo).catch(() => {});
|
|
249
252
|
}
|
|
250
|
-
db
|
|
251
|
-
id
|
|
252
|
-
);
|
|
253
|
+
setTask(db, id, { task_status: "dropped" });
|
|
253
254
|
endPauseOnFinish(id);
|
|
254
255
|
db.prepare(
|
|
255
256
|
`UPDATE stack_items SET status = 'dropped' WHERE session_id = ?`
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
import { db, type Session } from "../db";
|
|
10
10
|
import { sendMessage } from "../bus";
|
|
11
|
-
import {
|
|
11
|
+
import type { HostLink } from "../hosts/remote-api";
|
|
12
|
+
import * as client from "./contract/client";
|
|
12
13
|
|
|
13
14
|
const PULL_EVERY_MS = 5000;
|
|
14
15
|
const MISSING_FOR_MS = 10 * 60 * 1000;
|
|
@@ -27,13 +28,13 @@ interface Pulled {
|
|
|
27
28
|
body: string;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
// Held means an orchestrator started it, as isHeldRemote reads it.
|
|
31
|
+
// Held means an orchestrator started it, as isHeldRemote reads it (contract/held-calls.ts).
|
|
31
32
|
const HELD_BY = `started_by IN (SELECT id FROM sessions WHERE role = 'orchestrator')`;
|
|
32
33
|
|
|
33
34
|
const heldMirror = (hostId: string, id: string) =>
|
|
34
35
|
db
|
|
35
36
|
.prepare(
|
|
36
|
-
`SELECT * FROM
|
|
37
|
+
`SELECT * FROM session_rows WHERE id = ? AND host_id = ?
|
|
37
38
|
AND ${HELD_BY} AND task_status IN ('running', 'moving')`
|
|
38
39
|
)
|
|
39
40
|
.get(id, hostId) as Session | undefined;
|
|
@@ -41,7 +42,7 @@ const heldMirror = (hostId: string, id: string) =>
|
|
|
41
42
|
export function hasHeldMirrors(hostId: string): boolean {
|
|
42
43
|
return !!db
|
|
43
44
|
.prepare(
|
|
44
|
-
`SELECT 1 FROM
|
|
45
|
+
`SELECT 1 FROM session_rows WHERE host_id = ? AND ${HELD_BY}
|
|
45
46
|
AND task_prompt IS NOT NULL AND task_status IN ('running', 'moving')`
|
|
46
47
|
)
|
|
47
48
|
.get(hostId);
|
|
@@ -58,11 +59,7 @@ export async function pullHeldReplies(link: HostLink): Promise<number> {
|
|
|
58
59
|
try {
|
|
59
60
|
let messages: Pulled[];
|
|
60
61
|
try {
|
|
61
|
-
const res = await
|
|
62
|
-
link,
|
|
63
|
-
"/api/tasks/held-replies",
|
|
64
|
-
{ timeout: 8000 }
|
|
65
|
-
);
|
|
62
|
+
const res = await client.replies(link);
|
|
66
63
|
messages = (Array.isArray(res.messages) ? res.messages : []).filter(
|
|
67
64
|
(m): m is Pulled =>
|
|
68
65
|
!!m &&
|
|
@@ -98,10 +95,7 @@ export async function pullHeldReplies(link: HostLink): Promise<number> {
|
|
|
98
95
|
}
|
|
99
96
|
}
|
|
100
97
|
if (handled.length)
|
|
101
|
-
await
|
|
102
|
-
body: { ids: handled },
|
|
103
|
-
timeout: 8000,
|
|
104
|
-
}).catch(() => undefined);
|
|
98
|
+
await client.ackReplies(link, handled).catch(() => undefined);
|
|
105
99
|
return delivered;
|
|
106
100
|
} finally {
|
|
107
101
|
busy.delete(hostId);
|
package/lib/tasks/held.ts
CHANGED
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
import { orchestratorOf } from "./starter";
|
|
9
9
|
import os from "os";
|
|
10
10
|
import { db, type Session } from "../db";
|
|
11
|
+
import { isRemoteHost } from "../hosts";
|
|
11
12
|
import type { OrchestratedBundle } from "./move-bundle";
|
|
13
|
+
import { setPlacement, setTask } from "../db/session-parts";
|
|
12
14
|
|
|
13
15
|
export function holderRefusal(
|
|
14
16
|
session: Pick<Session, "held_by_device" | "held_from">,
|
|
@@ -25,7 +27,7 @@ export function heldRefusal(
|
|
|
25
27
|
device: string | null | undefined
|
|
26
28
|
): string | null {
|
|
27
29
|
const session = db
|
|
28
|
-
.prepare(`SELECT held_by_device, held_from FROM
|
|
30
|
+
.prepare(`SELECT held_by_device, held_from FROM session_rows WHERE id = ?`)
|
|
29
31
|
.get(id) as Pick<Session, "held_by_device" | "held_from"> | undefined;
|
|
30
32
|
return session ? holderRefusal(session, device) : null;
|
|
31
33
|
}
|
|
@@ -40,17 +42,15 @@ export function holdFor(
|
|
|
40
42
|
since?: string | null;
|
|
41
43
|
}
|
|
42
44
|
): void {
|
|
43
|
-
db.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
held.
|
|
51
|
-
|
|
52
|
-
id
|
|
53
|
-
);
|
|
45
|
+
db.transaction(() => {
|
|
46
|
+
setPlacement(db, id, {
|
|
47
|
+
held_by_device: holder,
|
|
48
|
+
held_from: held.heldFrom,
|
|
49
|
+
held_for: held.orchestrator?.id ?? null,
|
|
50
|
+
held_for_name: held.orchestrator?.name ?? null,
|
|
51
|
+
});
|
|
52
|
+
setTask(db, id, { task_since: held.since ?? undefined });
|
|
53
|
+
})();
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
/**
|
|
@@ -80,10 +80,7 @@ const UNRECORDED_STARTER = "unrecorded";
|
|
|
80
80
|
|
|
81
81
|
/** Records the paired device that started a task here, the only one that may later hold it. */
|
|
82
82
|
export function recordStarter(id: string, device: string): void {
|
|
83
|
-
db
|
|
84
|
-
device,
|
|
85
|
-
id
|
|
86
|
-
);
|
|
83
|
+
setPlacement(db, id, { started_by_device: device });
|
|
87
84
|
}
|
|
88
85
|
|
|
89
86
|
type Holdable = Pick<
|
|
@@ -123,7 +120,7 @@ export function holdExisting(
|
|
|
123
120
|
const s = db
|
|
124
121
|
.prepare(
|
|
125
122
|
`SELECT held_by_device, started_by, started_by_device, task_status, host_id, task_prompt
|
|
126
|
-
FROM
|
|
123
|
+
FROM session_rows WHERE id = ?`
|
|
127
124
|
)
|
|
128
125
|
.get(id) as Holdable | undefined;
|
|
129
126
|
if (!s?.task_prompt || s.task_status !== "running") return false;
|
|
@@ -133,3 +130,25 @@ export function holdExisting(
|
|
|
133
130
|
holdFor(id, device, held);
|
|
134
131
|
return true;
|
|
135
132
|
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* A plan another machine placed here is read and approved by that machine
|
|
136
|
+
* only: it decides where the task is built. Null for any other task.
|
|
137
|
+
*/
|
|
138
|
+
export function planWaitRefusal(
|
|
139
|
+
id: string,
|
|
140
|
+
device: string | null | undefined
|
|
141
|
+
): string | null {
|
|
142
|
+
const s = db
|
|
143
|
+
.prepare(
|
|
144
|
+
`SELECT plan_waits_for_caller, started_by_device, host_id FROM session_rows WHERE id = ?`
|
|
145
|
+
)
|
|
146
|
+
.get(id) as
|
|
147
|
+
| Pick<Session, "plan_waits_for_caller" | "started_by_device" | "host_id">
|
|
148
|
+
| undefined;
|
|
149
|
+
if (!s?.plan_waits_for_caller) return null;
|
|
150
|
+
// The placing machine's own mirror of the plan: its people decide it.
|
|
151
|
+
if (isRemoteHost(s.host_id)) return null;
|
|
152
|
+
if (device && device === s.started_by_device) return null;
|
|
153
|
+
return "The machine that placed this plan decides it; approve it from there";
|
|
154
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The holds that read the database or this machine, each returning `Hold`s
|
|
3
|
+
* (holds.ts) so every caller that asks "may this start now" gets the same
|
|
4
|
+
* answer and the same words: the orchestrator's Pause and the usage window,
|
|
5
|
+
* `after`, and this machine's load for a start here.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Session } from "../db";
|
|
9
|
+
import { getProject } from "../projects";
|
|
10
|
+
import { isRemoteHost } from "../hosts";
|
|
11
|
+
import { isPaused } from "../orchestrator/pause";
|
|
12
|
+
import { brakesEnabled } from "../orchestrator/brakes";
|
|
13
|
+
import { readUsage, windowRefusal } from "../orchestrator/usage";
|
|
14
|
+
import { localView, machineCapabilities } from "../load/machines";
|
|
15
|
+
import { DEFAULT_PRIORITY, type Priority } from "./schedule-types";
|
|
16
|
+
import { machineHolds, holdReason, type Hold } from "./holds";
|
|
17
|
+
import { heavyNeeds, meetsNeeds, needsOf, type Need } from "./placement";
|
|
18
|
+
import { expandHome } from "./session";
|
|
19
|
+
import { afterLabel, hasFinished } from "./queue-rows";
|
|
20
|
+
import type { QueueRow } from "./queue-types";
|
|
21
|
+
import type { PauseBy } from "./schedule-types";
|
|
22
|
+
|
|
23
|
+
// Why an automatic start has to wait: the orchestrator's Pause, then the
|
|
24
|
+
// usage window. A start now by a person isn't held, and a plan-only start
|
|
25
|
+
// is held by Pause alone.
|
|
26
|
+
export function autoHolds(
|
|
27
|
+
workspaceId: string | null,
|
|
28
|
+
planOnly = false
|
|
29
|
+
): Hold[] {
|
|
30
|
+
const out: Hold[] = [];
|
|
31
|
+
if (workspaceId && isPaused(workspaceId))
|
|
32
|
+
out.push({
|
|
33
|
+
code: "orch_paused",
|
|
34
|
+
text: "Waiting: the orchestrator is paused",
|
|
35
|
+
});
|
|
36
|
+
if (planOnly) return out;
|
|
37
|
+
const window = brakesEnabled() ? windowRefusal(readUsage()) : null;
|
|
38
|
+
if (window) out.push({ code: "window", text: `Waiting: ${window}` });
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const autoHold = (
|
|
43
|
+
workspaceId: string | null,
|
|
44
|
+
planOnly = false
|
|
45
|
+
): Hold | null => autoHolds(workspaceId, planOnly)[0] ?? null;
|
|
46
|
+
|
|
47
|
+
// `after X`: the task it waits on hasn't finished.
|
|
48
|
+
export function afterHold(r: QueueRow): Hold | null {
|
|
49
|
+
if (!r.after_task || hasFinished(r.after_task)) return null;
|
|
50
|
+
return {
|
|
51
|
+
code: "after",
|
|
52
|
+
text: `After ${afterLabel({ ...r, after_any: null }) ?? "its task"}`,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// What this machine would say to a start of a task with these needs here
|
|
57
|
+
// and now: empty means it may start.
|
|
58
|
+
export function localHolds(
|
|
59
|
+
ask: { needs: Need[]; priority?: Priority },
|
|
60
|
+
now = Date.now()
|
|
61
|
+
): Hold[] {
|
|
62
|
+
return machineHolds(
|
|
63
|
+
{
|
|
64
|
+
heavy: heavyNeeds(ask.needs),
|
|
65
|
+
needsUnmet: !meetsNeeds(ask.needs, machineCapabilities()),
|
|
66
|
+
remote: false,
|
|
67
|
+
priority: ask.priority ?? DEFAULT_PRIORITY,
|
|
68
|
+
},
|
|
69
|
+
localView(now),
|
|
70
|
+
now
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const requestNeeds = (
|
|
75
|
+
projectId: string,
|
|
76
|
+
workingDirectory: string,
|
|
77
|
+
prompt: string,
|
|
78
|
+
setting: Need[] | null
|
|
79
|
+
): Need[] =>
|
|
80
|
+
setting ??
|
|
81
|
+
needsOf({
|
|
82
|
+
projectId,
|
|
83
|
+
projectPath: expandHome(workingDirectory),
|
|
84
|
+
prompt,
|
|
85
|
+
}).needs;
|
|
86
|
+
|
|
87
|
+
// Whether the task needs something this machine lacks (a Mac).
|
|
88
|
+
export function needsUnmetHere(
|
|
89
|
+
projectId: string,
|
|
90
|
+
prompt: string,
|
|
91
|
+
setting: Need[] | null
|
|
92
|
+
): boolean {
|
|
93
|
+
const project = getProject(projectId);
|
|
94
|
+
if (!project || isRemoteHost(project.host_id)) return false;
|
|
95
|
+
return !meetsNeeds(
|
|
96
|
+
requestNeeds(projectId, project.working_directory, prompt, setting),
|
|
97
|
+
machineCapabilities()
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Whether this machine's load rules out starting a task for this project now.
|
|
102
|
+
export function loadHeldStart(
|
|
103
|
+
projectId: string,
|
|
104
|
+
prompt: string,
|
|
105
|
+
setting: Need[] | null
|
|
106
|
+
): boolean {
|
|
107
|
+
const project = getProject(projectId);
|
|
108
|
+
if (!project || isRemoteHost(project.host_id)) return false;
|
|
109
|
+
const head = localHolds({
|
|
110
|
+
needs: requestNeeds(projectId, project.working_directory, prompt, setting),
|
|
111
|
+
})[0];
|
|
112
|
+
return head?.code === "load_red" || head?.code === "heavy";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Why this machine's load rules out starting an agent now, or null. Only a
|
|
116
|
+
// red (or still settling) machine holds a resume; amber doesn't.
|
|
117
|
+
export function loadHoldFor(s: Session): string | null {
|
|
118
|
+
const project = s.project_id ? getProject(s.project_id) : null;
|
|
119
|
+
if (!project || isRemoteHost(s.host_id ?? project.host_id)) return null;
|
|
120
|
+
const hold = localHolds({ needs: [] })[0];
|
|
121
|
+
return hold?.code === "load_red" ? holdReason(hold) : null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Why a resume this actor asked for must wait: this machine's load, and for
|
|
125
|
+
// the orchestrator also what holds its own starts (it is paused, or the
|
|
126
|
+
// usage window). A person's own resume skips the second.
|
|
127
|
+
export function resumeHoldFor(s: Session, actor: PauseBy): string | null {
|
|
128
|
+
const load = loadHoldFor(s);
|
|
129
|
+
if (load) return load;
|
|
130
|
+
if (actor !== "orchestrator") return null;
|
|
131
|
+
const ws = s.project_id ? getProject(s.project_id)?.workspace_id : null;
|
|
132
|
+
const hold = autoHold(ws ?? null);
|
|
133
|
+
return hold ? holdReason(hold) : null;
|
|
134
|
+
}
|