@saadnvd1/agent-os 0.3.637 → 0.3.644
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/branches/route.ts +25 -0
- package/app/api/bus/resurrect/route.test.ts +3 -6
- 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 +3 -4
- 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 -5
- 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 +4 -6
- 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 +1 -1
- package/app/api/worktree-sweep/route.test.ts +40 -0
- package/app/api/worktree-sweep/route.ts +31 -0
- package/app/api/worktree-sweep/run/route.ts +26 -0
- package/components/Health/HealthPanel.tsx +4 -1
- package/components/Health/SweepRuns.tsx +125 -0
- package/components/Merge/GlobalMergeSection.tsx +2 -0
- package/components/Merge/ProjectMergeSection.tsx +2 -0
- package/components/Merge/WorktreeSweepSection.tsx +125 -0
- package/components/Settings/NoApprovalsSection.tsx +1 -1
- package/components/Settings/SettingsDialog.tsx +1 -1
- package/data/worktree-sweep/index.ts +56 -0
- package/lib/approvals/enable.ts +1 -1
- package/lib/away/report.ts +5 -5
- 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/hold.ts +3 -3
- package/lib/chat/refresh.ts +1 -1
- package/lib/chat/registry.ts +3 -3
- package/lib/chat/runner.ts +1 -1
- package/lib/chat/worker/main.ts +1 -1
- package/lib/db/backup.ts +49 -0
- 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/20261015T1733_gate_loop_guard.ts +40 -0
- package/lib/db/migrations/20261015T1800_split_sessions_tables.ts +172 -0
- package/lib/db/migrations/20261015T1830_drop_moved_session_columns.ts +173 -0
- package/lib/db/migrations/20261015T1900_worktree_sweep.ts +30 -0
- package/lib/db/migrations/README.md +11 -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 +10 -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/rows.ts +69 -0
- package/lib/db/session-parts/tasks.ts +27 -0
- package/lib/db/session-parts/view.ts +50 -0
- package/lib/db/session-parts/write.ts +61 -0
- 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/done/remote.ts +9 -20
- 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 +10 -24
- 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.test-helpers.ts +228 -0
- package/lib/orchestrator/armed.ts +18 -16
- 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/ask-verdict.ts +111 -0
- package/lib/orchestrator/asks.ts +26 -0
- package/lib/orchestrator/brakes.ts +1 -1
- package/lib/orchestrator/brief.ts +2 -1
- package/lib/orchestrator/digest.ts +2 -2
- package/lib/orchestrator/escalate.ts +25 -12
- package/lib/orchestrator/external-gates.ts +1 -0
- package/lib/orchestrator/external-pr.ts +2 -2
- package/lib/orchestrator/facts.ts +4 -28
- package/lib/orchestrator/gates.ts +94 -25
- package/lib/orchestrator/held-asks.ts +8 -11
- package/lib/orchestrator/held-review.ts +8 -4
- package/lib/orchestrator/home.ts +2 -2
- package/lib/orchestrator/loop-guard.ts +145 -0
- package/lib/orchestrator/merge-approvals.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 +11 -8
- package/lib/orchestrator/read.ts +1 -1
- package/lib/orchestrator/rebase.ts +8 -11
- package/lib/orchestrator/reconcile.ts +2 -2
- package/lib/orchestrator/review-prompt.ts +2 -0
- package/lib/orchestrator/review.ts +14 -4
- package/lib/orchestrator/signoff.test-helpers.ts +322 -0
- package/lib/orchestrator/signoff.ts +49 -11
- package/lib/orchestrator/targets.ts +1 -1
- package/lib/orchestrator/task-state.ts +4 -2
- package/lib/orchestrator/testing.ts +11 -14
- package/lib/orchestrator/watcher.ts +4 -0
- package/lib/plans/features.ts +1 -1
- package/lib/plans/release.ts +1 -1
- package/lib/ports.ts +1 -1
- package/lib/recipes/test-fixtures.ts +2 -3
- package/lib/resurrect/find.ts +1 -1
- package/lib/resurrect/index.ts +44 -20
- package/lib/resurrect/swept.ts +61 -0
- 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/testing.ts +17 -16
- 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/{remote-plan-path.ts → contract/plan-path.ts} +2 -2
- package/lib/tasks/{remote-plan.ts → contract/plan.ts} +76 -110
- 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 +30 -23
- package/lib/tasks/held-replies.ts +7 -13
- package/lib/tasks/held.ts +14 -18
- package/lib/tasks/import.ts +19 -15
- package/lib/tasks/index.ts +34 -22
- package/lib/tasks/launch-gate.ts +1 -1
- package/lib/tasks/move-carry.ts +15 -7
- package/lib/tasks/move-flow.ts +29 -36
- package/lib/tasks/move-guard.ts +1 -1
- package/lib/tasks/move-recover.ts +6 -25
- package/lib/tasks/move-testing.ts +16 -7
- package/lib/tasks/move.ts +23 -23
- package/lib/tasks/moved-away.ts +1 -1
- package/lib/tasks/pause-hold.ts +13 -5
- package/lib/tasks/pause.ts +19 -23
- package/lib/tasks/plan-view.ts +1 -1
- package/lib/tasks/pr-activity.ts +11 -2
- package/lib/tasks/prune-branches.ts +193 -0
- package/lib/tasks/prune-scope.ts +22 -0
- package/lib/tasks/queue-placement.ts +1 -1
- package/lib/tasks/queue-rows.ts +4 -4
- package/lib/tasks/queue-start.ts +5 -3
- package/lib/tasks/queue-tick.ts +2 -2
- package/lib/tasks/queue.ts +14 -6
- package/lib/tasks/remote-branch.ts +170 -0
- package/lib/tasks/session.ts +7 -4
- package/lib/tasks/start.ts +4 -4
- package/lib/tasks/starter.ts +8 -11
- package/lib/tasks/testing.ts +13 -7
- 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/lib/worktree-sweep/commits.ts +65 -0
- package/lib/worktree-sweep/findOrphans.ts +139 -0
- package/lib/worktree-sweep/findSettled.ts +112 -0
- package/lib/worktree-sweep/index.ts +10 -0
- package/lib/worktree-sweep/judge.ts +31 -0
- package/lib/worktree-sweep/panes.ts +18 -0
- package/lib/worktree-sweep/runSweep.ts +243 -0
- package/lib/worktree-sweep/settings.ts +83 -0
- package/lib/worktree-sweep/start.ts +64 -0
- package/lib/worktree-sweep/state.ts +36 -0
- package/lib/worktree-sweep/store.ts +96 -0
- package/lib/worktree-sweep/types.ts +53 -0
- package/package.json +1 -1
- package/scripts/backfill-session-logs.ts +1 -1
- package/scripts/screenshots/seed.ts +19 -18
- package/server.ts +3 -0
- package/lib/tasks/held-routes.ts +0 -33
- package/lib/tasks/remote.ts +0 -255
|
@@ -9,40 +9,44 @@
|
|
|
9
9
|
* Moving the task reuses the ordinary move (move-flow.ts).
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import fs from "fs";
|
|
12
|
+
import fs from "fs/promises";
|
|
13
13
|
import path from "path";
|
|
14
|
-
import { db, type Session } from "
|
|
15
|
-
import { isRemoteHost } from "
|
|
16
|
-
import { peerSource } from "
|
|
17
|
-
import {
|
|
18
|
-
import { LOCAL, machineCapabilities } from "
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
34
|
-
import {
|
|
14
|
+
import { db, type Session } from "../../db";
|
|
15
|
+
import { isRemoteHost } from "../../hosts";
|
|
16
|
+
import { peerSource } from "../../hosts/peer-load";
|
|
17
|
+
import { hostLink, requireHostLink } from "../../hosts/remote-api";
|
|
18
|
+
import { LOCAL, machineCapabilities } from "../../load/machines";
|
|
19
|
+
import {
|
|
20
|
+
noApprovalsOn,
|
|
21
|
+
recordAutoApproval,
|
|
22
|
+
} from "../../approvals/no-approvals";
|
|
23
|
+
import { implementArrived, planNoteFor } from "../../models/plan-step";
|
|
24
|
+
import { sessionWorkspace } from "../../models/resolve";
|
|
25
|
+
import { queueEvent } from "../../orchestrator/events";
|
|
26
|
+
import { brakesOn } from "../../orchestrator/brakes";
|
|
27
|
+
import { isPaused } from "../../orchestrator/pause";
|
|
28
|
+
import { getProject } from "../../projects";
|
|
29
|
+
import { planFor, splitOf } from "../../plans/features";
|
|
30
|
+
import { moveTask } from "../move-flow";
|
|
31
|
+
import * as client from "./client";
|
|
32
|
+
import { meetsNeeds, needsOf } from "../placement";
|
|
33
|
+
import { remotePlanPath } from "./plan-path";
|
|
34
|
+
import { DEFAULT_PRIORITY, isPriority } from "../schedule-types";
|
|
35
|
+
import { localHolds } from "../hold-checks";
|
|
36
|
+
import { expandHome } from "../session";
|
|
37
|
+
import { orchestratorOf } from "../starter";
|
|
38
|
+
import { setPlan } from "../../db/session-parts";
|
|
35
39
|
|
|
36
40
|
export { remotePlanPath };
|
|
37
41
|
|
|
38
42
|
const getSession = (id: string) =>
|
|
39
|
-
db.prepare(`SELECT * FROM
|
|
43
|
+
db.prepare(`SELECT * FROM session_rows WHERE id = ?`).get(id) as
|
|
40
44
|
| Session
|
|
41
45
|
| undefined;
|
|
42
46
|
|
|
43
|
-
function readCopy(id: string): string | null {
|
|
47
|
+
async function readCopy(id: string): Promise<string | null> {
|
|
44
48
|
try {
|
|
45
|
-
return fs.
|
|
49
|
+
return await fs.readFile(remotePlanPath(id), "utf8");
|
|
46
50
|
} catch {
|
|
47
51
|
return null;
|
|
48
52
|
}
|
|
@@ -81,29 +85,27 @@ export function implementTarget(
|
|
|
81
85
|
return meetsNeeds(needs, machineCapabilities()) ? "home" : "there";
|
|
82
86
|
}
|
|
83
87
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
88
|
+
/**
|
|
89
|
+
* A mirror that is planning, its machine's listing of it in hand: if its plan
|
|
90
|
+
* is written there, copy it, say so, and carry on for a plan-first task.
|
|
91
|
+
*/
|
|
92
|
+
export async function planWritten(
|
|
93
|
+
s: Session,
|
|
94
|
+
view: { planReady?: boolean } | undefined
|
|
95
|
+
): Promise<void> {
|
|
87
96
|
const link = hostLink(s.host_id);
|
|
88
|
-
if (!link) return;
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
`/api/tasks/${encodeURIComponent(s.id)}/plan`,
|
|
96
|
-
{ timeout: 30000 }
|
|
97
|
+
if (!link || !view?.planReady) return;
|
|
98
|
+
const text = await client.plan(link, s.id);
|
|
99
|
+
const claimed = setPlan(
|
|
100
|
+
db,
|
|
101
|
+
s.id,
|
|
102
|
+
{ plan_phase: "awaiting" },
|
|
103
|
+
{ plan_phase: "planning" }
|
|
97
104
|
);
|
|
98
|
-
const claimed = db
|
|
99
|
-
.prepare(
|
|
100
|
-
`UPDATE sessions SET plan_phase = 'awaiting' WHERE id = ? AND plan_phase = 'planning'`
|
|
101
|
-
)
|
|
102
|
-
.run(s.id).changes;
|
|
103
105
|
if (!claimed) return;
|
|
104
106
|
if (text !== null) {
|
|
105
|
-
fs.
|
|
106
|
-
fs.
|
|
107
|
+
await fs.mkdir(path.dirname(remotePlanPath(s.id)), { recursive: true });
|
|
108
|
+
await fs.writeFile(remotePlanPath(s.id), text);
|
|
107
109
|
}
|
|
108
110
|
const fresh = getSession(s.id)!;
|
|
109
111
|
const split = checkedSplit(fresh, text);
|
|
@@ -121,7 +123,7 @@ async function planWritten(s: Session): Promise<void> {
|
|
|
121
123
|
? "moving here to implement once this machine is free"
|
|
122
124
|
: "waiting for approval, not implementing (approve_plan to go ahead, drop to reject)";
|
|
123
125
|
const note = planNoteFor(fresh, remotePlanPath(s.id), next);
|
|
124
|
-
db.
|
|
126
|
+
setPlan(db, s.id, { plan_note: note });
|
|
125
127
|
const workspaceId = sessionWorkspace(fresh);
|
|
126
128
|
if (orchestratorOf(fresh.started_by) && workspaceId) {
|
|
127
129
|
queueEvent(workspaceId, `plan-done:${s.id}`, s.id, note);
|
|
@@ -136,7 +138,7 @@ async function planWritten(s: Session): Promise<void> {
|
|
|
136
138
|
if (!goOn) {
|
|
137
139
|
try {
|
|
138
140
|
const planned = text ? planFor(fresh, text) : null;
|
|
139
|
-
(await import("
|
|
141
|
+
(await import("../../notify/push/events")).pushPlanAwaiting(
|
|
140
142
|
s.id,
|
|
141
143
|
s.name,
|
|
142
144
|
planned?.pieces.length ?? 1
|
|
@@ -146,7 +148,7 @@ async function planWritten(s: Session): Promise<void> {
|
|
|
146
148
|
}
|
|
147
149
|
return;
|
|
148
150
|
}
|
|
149
|
-
await
|
|
151
|
+
await continuePlanThere(fresh);
|
|
150
152
|
if (auto)
|
|
151
153
|
recordAutoApproval({
|
|
152
154
|
source: "plan",
|
|
@@ -161,28 +163,29 @@ async function planWritten(s: Session): Promise<void> {
|
|
|
161
163
|
* Carries a written plan on: approves it on the machine that planned it, or
|
|
162
164
|
* hands it to the move home. Returns what it will implement on.
|
|
163
165
|
*/
|
|
164
|
-
export async function
|
|
166
|
+
export async function continuePlanThere(
|
|
165
167
|
s: Session,
|
|
166
168
|
message?: string
|
|
167
169
|
): Promise<string> {
|
|
168
170
|
const link = requireHostLink(s.host_id!);
|
|
169
|
-
const split = checkedSplit(s, readCopy(s.id));
|
|
171
|
+
const split = checkedSplit(s, await readCopy(s.id));
|
|
170
172
|
if (implementTarget(s, split) === "there") {
|
|
171
|
-
const out = await
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
173
|
+
const out = await client.approvePlan(link, s.id, message);
|
|
174
|
+
setPlan(
|
|
175
|
+
db,
|
|
176
|
+
s.id,
|
|
177
|
+
{ plan_phase: "implementing" },
|
|
178
|
+
{ plan_phase: "awaiting" }
|
|
175
179
|
);
|
|
176
|
-
|
|
177
|
-
`UPDATE sessions SET plan_phase = 'implementing' WHERE id = ? AND plan_phase = 'awaiting'`
|
|
178
|
-
).run(s.id);
|
|
179
|
-
forgetHostTasks(link.hostId);
|
|
180
|
+
client.forget(link.hostId);
|
|
180
181
|
return out.model ?? link.hostName;
|
|
181
182
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
183
|
+
setPlan(
|
|
184
|
+
db,
|
|
185
|
+
s.id,
|
|
186
|
+
{ plan_phase: "handoff", plan_handoff_message: message ?? null },
|
|
187
|
+
{ plan_phase: "awaiting" }
|
|
188
|
+
);
|
|
186
189
|
const workspaceId = sessionWorkspace(s);
|
|
187
190
|
if (orchestratorOf(s.started_by) && workspaceId)
|
|
188
191
|
queueEvent(
|
|
@@ -197,18 +200,19 @@ export async function continueRemotePlan(
|
|
|
197
200
|
return "this machine, once it is free";
|
|
198
201
|
}
|
|
199
202
|
|
|
200
|
-
const handingOff = new Set<string>();
|
|
203
|
+
export const handingOff = new Set<string>();
|
|
201
204
|
|
|
202
|
-
async function finishArrival(
|
|
205
|
+
export async function finishArrival(
|
|
206
|
+
id: string,
|
|
207
|
+
message?: string
|
|
208
|
+
): Promise<boolean> {
|
|
203
209
|
await implementArrived(id, message || undefined);
|
|
204
|
-
db
|
|
205
|
-
`UPDATE sessions SET plan_handoff_message = NULL WHERE id = ?`
|
|
206
|
-
).run(id);
|
|
210
|
+
setPlan(db, id, { plan_handoff_message: null });
|
|
207
211
|
return true;
|
|
208
212
|
}
|
|
209
213
|
|
|
210
214
|
// Fired, not awaited: a move takes minutes and the queue tick must not wait.
|
|
211
|
-
function fire(id: string, run: () => Promise<unknown>) {
|
|
215
|
+
export function fire(id: string, run: () => Promise<unknown>) {
|
|
212
216
|
if (handingOff.has(id)) return;
|
|
213
217
|
void run().catch((e) => console.error(`Moving ${id} here failed:`, e));
|
|
214
218
|
}
|
|
@@ -253,10 +257,11 @@ export async function handoffHome(
|
|
|
253
257
|
const edits = s.plan_edits ?? null;
|
|
254
258
|
const arrived = await moveTask(id, LOCAL);
|
|
255
259
|
// The arrived row keeps the pending hand-off until implementing has
|
|
256
|
-
// started, so a restart in between is picked up by
|
|
257
|
-
db.
|
|
258
|
-
|
|
259
|
-
|
|
260
|
+
// started, so a restart in between is picked up by syncMirrors.
|
|
261
|
+
setPlan(db, arrived.id, {
|
|
262
|
+
plan_edits: edits ?? undefined,
|
|
263
|
+
plan_handoff_message: message ?? "",
|
|
264
|
+
});
|
|
260
265
|
handingOff.add(arrived.id);
|
|
261
266
|
try {
|
|
262
267
|
return await finishArrival(arrived.id, message);
|
|
@@ -267,42 +272,3 @@ export async function handoffHome(
|
|
|
267
272
|
handingOff.delete(id);
|
|
268
273
|
}
|
|
269
274
|
}
|
|
270
|
-
|
|
271
|
-
/** Every mirror whose plan is being written, or approved and coming here. */
|
|
272
|
-
export async function syncRemotePlans(): Promise<void> {
|
|
273
|
-
const rows = db
|
|
274
|
-
.prepare(
|
|
275
|
-
`SELECT * FROM sessions
|
|
276
|
-
WHERE plan_waits_for_caller = 1 AND plan_phase IN ('planning', 'handoff')
|
|
277
|
-
AND task_status = 'running' AND archived_at IS NULL`
|
|
278
|
-
)
|
|
279
|
-
.all() as Session[];
|
|
280
|
-
for (const s of rows) {
|
|
281
|
-
if (!isRemoteHost(s.host_id)) continue;
|
|
282
|
-
try {
|
|
283
|
-
if (s.plan_phase === "planning") await planWritten(s);
|
|
284
|
-
else fire(s.id, () => handoffHome(s.id));
|
|
285
|
-
} catch (e) {
|
|
286
|
-
console.error(`Plan on ${s.host_id} for ${s.id} failed:`, e);
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
// A task that arrived but never began implementing (a restart between).
|
|
290
|
-
const arrived = db
|
|
291
|
-
.prepare(
|
|
292
|
-
`SELECT id, plan_handoff_message FROM sessions
|
|
293
|
-
WHERE plan_phase = 'awaiting' AND plan_handoff_message IS NOT NULL
|
|
294
|
-
AND task_status = 'running' AND archived_at IS NULL AND peer_mirror IS NOT 1`
|
|
295
|
-
)
|
|
296
|
-
.all() as { id: string; plan_handoff_message: string }[];
|
|
297
|
-
for (const a of arrived) {
|
|
298
|
-
if (isRemoteHost(getSession(a.id)?.host_id)) continue;
|
|
299
|
-
fire(a.id, async () => {
|
|
300
|
-
handingOff.add(a.id);
|
|
301
|
-
try {
|
|
302
|
-
await finishArrival(a.id, a.plan_handoff_message);
|
|
303
|
-
} finally {
|
|
304
|
-
handingOff.delete(a.id);
|
|
305
|
-
}
|
|
306
|
-
});
|
|
307
|
-
}
|
|
308
|
-
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** The task routes a linked machine's AgentOS answers, and how long to wait for each. */
|
|
2
|
+
|
|
3
|
+
export const taskPath = (id: string, leaf: string) =>
|
|
4
|
+
`/api/tasks/${encodeURIComponent(id)}/${leaf}`;
|
|
5
|
+
|
|
6
|
+
export const LIST = "/api/tasks";
|
|
7
|
+
export const IMPORT = "/api/tasks/import";
|
|
8
|
+
export const HELD_REPLIES = "/api/tasks/held-replies";
|
|
9
|
+
export const arrivedPath = (from: string) =>
|
|
10
|
+
`/api/tasks/arrived?from=${encodeURIComponent(from)}`;
|
|
11
|
+
|
|
12
|
+
export const TIMEOUT = {
|
|
13
|
+
list: 8000,
|
|
14
|
+
start: 180000,
|
|
15
|
+
plan: 30000,
|
|
16
|
+
approvePlan: 60000,
|
|
17
|
+
message: 20000,
|
|
18
|
+
hold: 20000,
|
|
19
|
+
chat: 10000,
|
|
20
|
+
stop: 30000,
|
|
21
|
+
done: 120000,
|
|
22
|
+
merge: 300000,
|
|
23
|
+
replies: 8000,
|
|
24
|
+
exportOut: 300000,
|
|
25
|
+
importBundle: 600000,
|
|
26
|
+
} as const;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tasks that run on another machine's AgentOS. That machine schedules them;
|
|
3
|
+
* this one keeps a mirror row (same id, the host's id) so the sidebar lists
|
|
4
|
+
* them and the terminal attaches through that machine's AgentOS.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { db, type Project, type Session } from "../../db";
|
|
8
|
+
import { isRemoteHost } from "../../hosts";
|
|
9
|
+
import { requireHostLink } from "../../hosts/remote-api";
|
|
10
|
+
import { orchestratedStart } from "../held";
|
|
11
|
+
import { projectRef } from "../project-ref";
|
|
12
|
+
import { orchestratorOf } from "../starter";
|
|
13
|
+
import type { TaskStatus } from "../state";
|
|
14
|
+
import * as client from "./client";
|
|
15
|
+
import { setPlan, setTask, startTaskClock } from "../../db/session-parts";
|
|
16
|
+
|
|
17
|
+
export function isMirror(session: Session): boolean {
|
|
18
|
+
return isRemoteHost(session.host_id);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function mirrorTask(
|
|
22
|
+
hostId: string,
|
|
23
|
+
projectId: string | null,
|
|
24
|
+
remote: Session,
|
|
25
|
+
startedBy?: string | null
|
|
26
|
+
): void {
|
|
27
|
+
db.transaction(() => {
|
|
28
|
+
const inserted = db
|
|
29
|
+
.prepare(
|
|
30
|
+
`INSERT INTO sessions (id, name, tmux_name, working_directory, model, group_path,
|
|
31
|
+
agent_type, auto_approve, project_id, host_id, worktree_path, branch_name,
|
|
32
|
+
base_branch, name_source, view)
|
|
33
|
+
VALUES (?, ?, ?, ?, ?, 'sessions', 'claude', 1, ?, ?, ?, ?, ?, 'user', ?)
|
|
34
|
+
ON CONFLICT(id) DO NOTHING`
|
|
35
|
+
)
|
|
36
|
+
.run(
|
|
37
|
+
remote.id,
|
|
38
|
+
remote.name,
|
|
39
|
+
remote.tmux_name,
|
|
40
|
+
remote.working_directory,
|
|
41
|
+
remote.model,
|
|
42
|
+
projectId,
|
|
43
|
+
hostId,
|
|
44
|
+
remote.worktree_path,
|
|
45
|
+
remote.branch_name,
|
|
46
|
+
remote.base_branch,
|
|
47
|
+
// As that machine runs it: one that predates chat tasks says terminal.
|
|
48
|
+
remote.view === "chat" ? "chat" : "terminal"
|
|
49
|
+
);
|
|
50
|
+
if (inserted.changes)
|
|
51
|
+
setTask(db, remote.id, {
|
|
52
|
+
task_prompt: remote.task_prompt,
|
|
53
|
+
task_status: "running",
|
|
54
|
+
});
|
|
55
|
+
// What carryTaskState gives a moved task's mirror: the orchestrator that
|
|
56
|
+
// holds it, and when it began (so its PR is judged from then).
|
|
57
|
+
if (startedBy) {
|
|
58
|
+
db.prepare(
|
|
59
|
+
`UPDATE sessions SET started_by = COALESCE(started_by, ?) WHERE id = ?`
|
|
60
|
+
).run(startedBy, remote.id);
|
|
61
|
+
startTaskClock(db, remote.id);
|
|
62
|
+
}
|
|
63
|
+
})();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function startRemoteTask(
|
|
67
|
+
hostId: string,
|
|
68
|
+
project: Project,
|
|
69
|
+
opts: {
|
|
70
|
+
id: string;
|
|
71
|
+
prompt: string;
|
|
72
|
+
name?: string;
|
|
73
|
+
model?: string;
|
|
74
|
+
baseBranch?: string;
|
|
75
|
+
view?: "chat" | "terminal";
|
|
76
|
+
// The orchestrator that started it: that machine holds the task for it.
|
|
77
|
+
startedBy?: string | null;
|
|
78
|
+
// It plans first: there it plans, stops, and waits for this machine.
|
|
79
|
+
plan?: { only: boolean; reason: string | null };
|
|
80
|
+
}
|
|
81
|
+
): Promise<Session> {
|
|
82
|
+
const link = requireHostLink(hostId);
|
|
83
|
+
const { startedBy: by, plan, ...rest } = opts;
|
|
84
|
+
const startedBy = orchestratorOf(by);
|
|
85
|
+
if (startedBy)
|
|
86
|
+
await client.requireCapability(
|
|
87
|
+
link,
|
|
88
|
+
"held-start",
|
|
89
|
+
"an orchestrator's task",
|
|
90
|
+
"hold"
|
|
91
|
+
);
|
|
92
|
+
if (plan)
|
|
93
|
+
await client.requireCapability(link, "remote-plan", "a plan step", "plan");
|
|
94
|
+
const { session } = await client.start(link, {
|
|
95
|
+
project: await projectRef(project),
|
|
96
|
+
...rest,
|
|
97
|
+
...(plan && {
|
|
98
|
+
plan: true,
|
|
99
|
+
plan_only: true,
|
|
100
|
+
plan_waits: true,
|
|
101
|
+
plan_reason: plan.reason ?? undefined,
|
|
102
|
+
}),
|
|
103
|
+
orchestrated: startedBy ? orchestratedStart(startedBy) : undefined,
|
|
104
|
+
});
|
|
105
|
+
mirrorTask(hostId, project.id, session, startedBy);
|
|
106
|
+
if (plan)
|
|
107
|
+
setPlan(db, session.id, {
|
|
108
|
+
plan_phase: "planning",
|
|
109
|
+
plan_only: plan.only ? 1 : null,
|
|
110
|
+
plan_decided: 1,
|
|
111
|
+
plan_reason: plan.reason,
|
|
112
|
+
plan_waits_for_caller: 1,
|
|
113
|
+
});
|
|
114
|
+
client.forget(hostId);
|
|
115
|
+
return db
|
|
116
|
+
.prepare(`SELECT * FROM session_rows WHERE id = ?`)
|
|
117
|
+
.get(session.id) as Session;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function setMirrorStatus(id: string, status: TaskStatus): void {
|
|
121
|
+
setTask(db, id, { task_status: status }, { task_status: "running" });
|
|
122
|
+
}
|
|
@@ -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]);
|