@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
|
@@ -1,25 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* An orchestrator's calls on a task it
|
|
3
|
-
* machine's AgentOS answers for the device that sent the task (held.ts).
|
|
4
|
-
*
|
|
2
|
+
* An orchestrator's calls on a task it put on a linked machine, which that
|
|
3
|
+
* machine's AgentOS answers for the device that sent the task (held.ts). A
|
|
4
|
+
* machine that doesn't hold the task yet is asked to, once, and the call
|
|
5
|
+
* repeated. The orchestrator polls and calls; the other machine never reaches
|
|
6
|
+
* back.
|
|
5
7
|
*/
|
|
6
8
|
|
|
7
|
-
import { db, type Session } from "
|
|
9
|
+
import { db, type Session } from "../../db";
|
|
8
10
|
import {
|
|
9
11
|
HostApiError,
|
|
10
|
-
cleanRemoteBlock,
|
|
11
|
-
cleanRemoteText,
|
|
12
|
-
hostApi,
|
|
13
12
|
hostLink,
|
|
14
13
|
requireHostLink,
|
|
15
|
-
} from "
|
|
16
|
-
import type { Delivery } from "
|
|
17
|
-
import { wakeLine } from "
|
|
18
|
-
import { orchestratedStart } from "
|
|
19
|
-
import { orchestratorOf } from "
|
|
20
|
-
|
|
21
|
-
const path = (id: string, leaf: string) =>
|
|
22
|
-
`/api/tasks/${encodeURIComponent(id)}/${leaf}`;
|
|
14
|
+
} from "../../hosts/remote-api";
|
|
15
|
+
import type { Delivery } from "../../bus/delivery";
|
|
16
|
+
import { wakeLine } from "../../bus/format";
|
|
17
|
+
import { orchestratedStart } from "../held";
|
|
18
|
+
import { orchestratorOf } from "../starter";
|
|
19
|
+
import * as client from "./client";
|
|
23
20
|
|
|
24
21
|
/**
|
|
25
22
|
* A task its orchestrator put on a linked machine, whether by moving it or
|
|
@@ -54,16 +51,11 @@ async function adoptHeld(task: Held): Promise<boolean> {
|
|
|
54
51
|
if (adopted.has(task.id)) return false;
|
|
55
52
|
adopted.set(task.id, now);
|
|
56
53
|
try {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
task.task_since ?? null
|
|
63
|
-
),
|
|
64
|
-
},
|
|
65
|
-
timeout: 20000,
|
|
66
|
-
});
|
|
54
|
+
await client.hold(
|
|
55
|
+
requireHostLink(task.host_id),
|
|
56
|
+
task.id,
|
|
57
|
+
orchestratedStart(task.started_by!, task.task_since ?? null)
|
|
58
|
+
);
|
|
67
59
|
} catch {
|
|
68
60
|
return false;
|
|
69
61
|
}
|
|
@@ -72,6 +64,8 @@ async function adoptHeld(task: Held): Promise<boolean> {
|
|
|
72
64
|
return true;
|
|
73
65
|
}
|
|
74
66
|
|
|
67
|
+
// compat: task-contract-1. A task the queue placed before machines held them
|
|
68
|
+
// is unheld there; drop this heal once every linked machine holds on start.
|
|
75
69
|
/** The call, held first if that machine says it isn't, once. */
|
|
76
70
|
async function withHold<T>(task: Held, call: () => Promise<T>): Promise<T> {
|
|
77
71
|
try {
|
|
@@ -142,16 +136,13 @@ export async function relayToHeldTask(
|
|
|
142
136
|
try {
|
|
143
137
|
const link = requireHostLink(to.host_id);
|
|
144
138
|
const send = () =>
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
from: fromName,
|
|
150
|
-
},
|
|
151
|
-
timeout: 20000,
|
|
139
|
+
client.message(link, to.id, {
|
|
140
|
+
id: messageId ? `bus-${messageId}` : undefined,
|
|
141
|
+
line,
|
|
142
|
+
from: fromName,
|
|
152
143
|
});
|
|
153
|
-
const
|
|
154
|
-
return { state
|
|
144
|
+
const state = await (heal ? withHold(to, send) : send());
|
|
145
|
+
return { state };
|
|
155
146
|
} catch (error) {
|
|
156
147
|
const why = error instanceof Error ? error.message : String(error);
|
|
157
148
|
return { state: "failed", why: why.slice(0, 300) };
|
|
@@ -164,15 +155,9 @@ export async function heldChatTail(
|
|
|
164
155
|
lines: number
|
|
165
156
|
): Promise<string | null> {
|
|
166
157
|
const link = requireHostLink(task.host_id);
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
`${path(task.id, "chat-tail")}?lines=${lines}`,
|
|
171
|
-
{ timeout: 10000 }
|
|
172
|
-
)
|
|
173
|
-
).catch(() => ({ text: undefined }));
|
|
174
|
-
// Whole lines, as a local read has them (read.ts caps and trims them).
|
|
175
|
-
return typeof text === "string" ? cleanRemoteBlock(text, 16000) : null;
|
|
158
|
+
return withHold(task, () => client.chatTail(link, task.id, lines)).catch(
|
|
159
|
+
() => null
|
|
160
|
+
);
|
|
176
161
|
}
|
|
177
162
|
|
|
178
163
|
/**
|
|
@@ -184,38 +169,22 @@ export async function heldChatFacts(
|
|
|
184
169
|
task: Held
|
|
185
170
|
): Promise<{ blocked: string | null; turn: string | null } | null> {
|
|
186
171
|
const link = requireHostLink(task.host_id);
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
path(task.id, "chat-facts"),
|
|
191
|
-
{ timeout: 10000 }
|
|
192
|
-
)
|
|
193
|
-
).catch(() => null);
|
|
194
|
-
if (!res || (res.blocked !== null && typeof res.blocked !== "string"))
|
|
195
|
-
return null;
|
|
196
|
-
if (res.turn !== null && typeof res.turn !== "string") return null;
|
|
197
|
-
return {
|
|
198
|
-
blocked: res.blocked ? cleanRemoteBlock(res.blocked, 2000) : null,
|
|
199
|
-
turn: res.turn,
|
|
200
|
-
};
|
|
172
|
+
return withHold(task, () => client.chatFacts(link, task.id)).catch(
|
|
173
|
+
() => null
|
|
174
|
+
);
|
|
201
175
|
}
|
|
202
176
|
|
|
203
177
|
/** Stops its agent there; its conversation and worktree are kept. */
|
|
204
178
|
export async function stopHeldTask(task: Held): Promise<string> {
|
|
205
179
|
const link = requireHostLink(task.host_id);
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
timeout: 30000,
|
|
210
|
-
})
|
|
180
|
+
return (
|
|
181
|
+
(await withHold(task, () => client.stop(link, task.id))) ||
|
|
182
|
+
`Stopped ${task.name}.`
|
|
211
183
|
);
|
|
212
|
-
return cleanRemoteText(text) || `Stopped ${task.name}.`;
|
|
213
184
|
}
|
|
214
185
|
|
|
215
186
|
/** Done there: its agent stops and its worktree goes. Never a merge. */
|
|
216
187
|
export async function doneHeldTask(task: Held): Promise<void> {
|
|
217
188
|
const link = requireHostLink(task.host_id);
|
|
218
|
-
await withHold(task, () =>
|
|
219
|
-
hostApi(link, path(task.id, "done"), { body: {}, timeout: 120000 })
|
|
220
|
-
);
|
|
189
|
+
await withHold(task, () => client.done(link, task.id));
|
|
221
190
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a task runs on a linked machine: one client for its API
|
|
3
|
+
* (client.ts), one authorization rule per route (authorize.ts), one loop that
|
|
4
|
+
* keeps mirrors current (sync.ts). Local code paths call these through the
|
|
5
|
+
* entry points they already had (signOff, dropTask, deliver, ...).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export * as taskClient from "./client";
|
|
9
|
+
export { TASK_CAPABILITIES, type HostTasks } from "./types";
|
|
10
|
+
export {
|
|
11
|
+
isMirror,
|
|
12
|
+
mirrorTask,
|
|
13
|
+
startRemoteTask,
|
|
14
|
+
setMirrorStatus,
|
|
15
|
+
} from "./start";
|
|
16
|
+
export { mergeThere, dropThere } from "./actions";
|
|
17
|
+
export { mirrorViews } from "./views";
|
|
18
|
+
export { syncMirrors } from "./sync";
|
|
19
|
+
export { taskRoute, refuse, callerRefusal, type Rule } from "./authorize";
|
|
20
|
+
export {
|
|
21
|
+
isHeldRemote,
|
|
22
|
+
relayToHeldTask,
|
|
23
|
+
heldChatTail,
|
|
24
|
+
heldChatFacts,
|
|
25
|
+
stopHeldTask,
|
|
26
|
+
doneHeldTask,
|
|
27
|
+
flushUndelivered,
|
|
28
|
+
} from "./held-calls";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { plansDir } from "../../plans/features";
|
|
3
|
+
|
|
4
|
+
// A plan written on a linked machine, copied here so this machine's
|
|
5
|
+
// orchestrator and plan view can read it (contract/plan.ts).
|
|
6
|
+
export const remotePlanPath = (sessionId: string) =>
|
|
7
|
+
path.join(plansDir(), "remote", `${sessionId}.md`);
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A plan placed on a linked machine (queue placement, remote.ts). Planning
|
|
3
|
+
* only reads code, so it runs wherever there is room; what follows the plan
|
|
4
|
+
* is decided here, on the machine whose scheduler, needs and workspace apply.
|
|
5
|
+
* The other machine plans, stops, and waits. This machine then copies the
|
|
6
|
+
* plan, says it is written, and either approves it there (the task is built
|
|
7
|
+
* where it planned) or moves the task here once this machine can take it
|
|
8
|
+
* (it needs a Mac, or its plan is split into pieces, which register here).
|
|
9
|
+
* Moving the task reuses the ordinary move (move-flow.ts).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import fs from "fs/promises";
|
|
13
|
+
import path from "path";
|
|
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";
|
|
39
|
+
|
|
40
|
+
export { remotePlanPath };
|
|
41
|
+
|
|
42
|
+
const getSession = (id: string) =>
|
|
43
|
+
db.prepare(`SELECT * FROM session_rows WHERE id = ?`).get(id) as
|
|
44
|
+
| Session
|
|
45
|
+
| undefined;
|
|
46
|
+
|
|
47
|
+
async function readCopy(id: string): Promise<string | null> {
|
|
48
|
+
try {
|
|
49
|
+
return await fs.readFile(remotePlanPath(id), "utf8");
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function checkedSplit(s: Session, text: string | null) {
|
|
56
|
+
try {
|
|
57
|
+
return text ? splitOf(s, text) : null;
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Where a written plan is built: "there" (approved on the machine that
|
|
65
|
+
* planned it) or "home" (moved here first). Pieces are checked against this
|
|
66
|
+
* machine's projects, and work that needs a Mac can't be built on a machine
|
|
67
|
+
* without one, so those come home; everything else stays where it is, since
|
|
68
|
+
* the machine that planned it is the one with room.
|
|
69
|
+
*/
|
|
70
|
+
export function implementTarget(
|
|
71
|
+
s: Session,
|
|
72
|
+
split: ReturnType<typeof splitOf>
|
|
73
|
+
): "there" | "home" {
|
|
74
|
+
const project = s.project_id ? getProject(s.project_id) : null;
|
|
75
|
+
// The project lives there: nothing here to build it in.
|
|
76
|
+
if (!project || isRemoteHost(project.host_id)) return "there";
|
|
77
|
+
if (split) return "home";
|
|
78
|
+
const { needs } = needsOf({
|
|
79
|
+
projectId: project.id,
|
|
80
|
+
projectPath: expandHome(project.working_directory),
|
|
81
|
+
prompt: s.task_prompt ?? "",
|
|
82
|
+
});
|
|
83
|
+
const caps = peerSource(s.host_id!, Date.now()).capabilities;
|
|
84
|
+
if (meetsNeeds(needs, caps)) return "there";
|
|
85
|
+
return meetsNeeds(needs, machineCapabilities()) ? "home" : "there";
|
|
86
|
+
}
|
|
87
|
+
|
|
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> {
|
|
96
|
+
const link = hostLink(s.host_id);
|
|
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" }
|
|
104
|
+
);
|
|
105
|
+
if (!claimed) return;
|
|
106
|
+
if (text !== null) {
|
|
107
|
+
await fs.mkdir(path.dirname(remotePlanPath(s.id)), { recursive: true });
|
|
108
|
+
await fs.writeFile(remotePlanPath(s.id), text);
|
|
109
|
+
}
|
|
110
|
+
const fresh = getSession(s.id)!;
|
|
111
|
+
const split = checkedSplit(fresh, text);
|
|
112
|
+
const errors = split?.errors ?? [];
|
|
113
|
+
// As for a task planned here: a plan-first task goes on by itself, a
|
|
114
|
+
// plan-only one waits unless no-approvals is on, and pieces that couldn't
|
|
115
|
+
// be registered always hold it.
|
|
116
|
+
const auto = !!fresh.plan_only && noApprovalsOn();
|
|
117
|
+
const goOn = (!fresh.plan_only || auto) && !errors.length;
|
|
118
|
+
const target = goOn ? implementTarget(fresh, split) : null;
|
|
119
|
+
const next =
|
|
120
|
+
target === "there"
|
|
121
|
+
? `implementing on ${hostLink(s.host_id)?.hostName ?? "that machine"}`
|
|
122
|
+
: target === "home"
|
|
123
|
+
? "moving here to implement once this machine is free"
|
|
124
|
+
: "waiting for approval, not implementing (approve_plan to go ahead, drop to reject)";
|
|
125
|
+
const note = planNoteFor(fresh, remotePlanPath(s.id), next);
|
|
126
|
+
setPlan(db, s.id, { plan_note: note });
|
|
127
|
+
const workspaceId = sessionWorkspace(fresh);
|
|
128
|
+
if (orchestratorOf(fresh.started_by) && workspaceId) {
|
|
129
|
+
queueEvent(workspaceId, `plan-done:${s.id}`, s.id, note);
|
|
130
|
+
if (errors.length)
|
|
131
|
+
queueEvent(
|
|
132
|
+
workspaceId,
|
|
133
|
+
`plan-pieces:${s.id}`,
|
|
134
|
+
s.id,
|
|
135
|
+
`Plan of "${s.name}": couldn't register its pieces: ${errors.join("; ")}. It is held, not building. Message the task to fix the plan, or approve_plan to build it as one piece.`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
if (!goOn) {
|
|
139
|
+
try {
|
|
140
|
+
const planned = text ? planFor(fresh, text) : null;
|
|
141
|
+
(await import("../../notify/push/events")).pushPlanAwaiting(
|
|
142
|
+
s.id,
|
|
143
|
+
s.name,
|
|
144
|
+
planned?.pieces.length ?? 1
|
|
145
|
+
);
|
|
146
|
+
} catch (e) {
|
|
147
|
+
console.error("plan push failed:", e);
|
|
148
|
+
}
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
await continuePlanThere(fresh);
|
|
152
|
+
if (auto)
|
|
153
|
+
recordAutoApproval({
|
|
154
|
+
source: "plan",
|
|
155
|
+
workspaceId,
|
|
156
|
+
sessionId: s.id,
|
|
157
|
+
subject: `plan for ${s.name}`,
|
|
158
|
+
detail: "implementing without waiting for approve_plan",
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Carries a written plan on: approves it on the machine that planned it, or
|
|
164
|
+
* hands it to the move home. Returns what it will implement on.
|
|
165
|
+
*/
|
|
166
|
+
export async function continuePlanThere(
|
|
167
|
+
s: Session,
|
|
168
|
+
message?: string
|
|
169
|
+
): Promise<string> {
|
|
170
|
+
const link = requireHostLink(s.host_id!);
|
|
171
|
+
const split = checkedSplit(s, await readCopy(s.id));
|
|
172
|
+
if (implementTarget(s, split) === "there") {
|
|
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" }
|
|
179
|
+
);
|
|
180
|
+
client.forget(link.hostId);
|
|
181
|
+
return out.model ?? link.hostName;
|
|
182
|
+
}
|
|
183
|
+
setPlan(
|
|
184
|
+
db,
|
|
185
|
+
s.id,
|
|
186
|
+
{ plan_phase: "handoff", plan_handoff_message: message ?? null },
|
|
187
|
+
{ plan_phase: "awaiting" }
|
|
188
|
+
);
|
|
189
|
+
const workspaceId = sessionWorkspace(s);
|
|
190
|
+
if (orchestratorOf(s.started_by) && workspaceId)
|
|
191
|
+
queueEvent(
|
|
192
|
+
workspaceId,
|
|
193
|
+
`plan-handoff:${s.id}`,
|
|
194
|
+
s.id,
|
|
195
|
+
`Task "${s.name}": plan approved; it moves here to be built once this machine is free (${split ? "its plan is in pieces" : "it needs this machine"}).`
|
|
196
|
+
);
|
|
197
|
+
void handoffHome(s.id).catch((e) =>
|
|
198
|
+
console.error(`Moving ${s.id} here failed:`, e)
|
|
199
|
+
);
|
|
200
|
+
return "this machine, once it is free";
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export const handingOff = new Set<string>();
|
|
204
|
+
|
|
205
|
+
export async function finishArrival(
|
|
206
|
+
id: string,
|
|
207
|
+
message?: string
|
|
208
|
+
): Promise<boolean> {
|
|
209
|
+
await implementArrived(id, message || undefined);
|
|
210
|
+
setPlan(db, id, { plan_handoff_message: null });
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Fired, not awaited: a move takes minutes and the queue tick must not wait.
|
|
215
|
+
export function fire(id: string, run: () => Promise<unknown>) {
|
|
216
|
+
if (handingOff.has(id)) return;
|
|
217
|
+
void run().catch((e) => console.error(`Moving ${id} here failed:`, e));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Moves an approved plan's task here, when this machine can take it, and
|
|
222
|
+
* starts implementing. False while the machine is loaded (tried again on the
|
|
223
|
+
* next look) or another call has it.
|
|
224
|
+
*/
|
|
225
|
+
export async function handoffHome(
|
|
226
|
+
id: string,
|
|
227
|
+
now = Date.now()
|
|
228
|
+
): Promise<boolean> {
|
|
229
|
+
const s = getSession(id);
|
|
230
|
+
if (!s || s.plan_phase !== "handoff" || !isRemoteHost(s.host_id))
|
|
231
|
+
return false;
|
|
232
|
+
if (handingOff.has(id)) return false;
|
|
233
|
+
const project = s.project_id ? getProject(s.project_id) : null;
|
|
234
|
+
if (!project) return false;
|
|
235
|
+
// What an orchestrator started stays under its Pause and brakes.
|
|
236
|
+
const workspaceId = sessionWorkspace(s);
|
|
237
|
+
if (workspaceId && orchestratorOf(s.started_by)) {
|
|
238
|
+
if (isPaused(workspaceId)) return false;
|
|
239
|
+
if ((await brakesOn(workspaceId)).length) return false;
|
|
240
|
+
}
|
|
241
|
+
const { needs } = needsOf({
|
|
242
|
+
projectId: project.id,
|
|
243
|
+
projectPath: expandHome(project.working_directory),
|
|
244
|
+
prompt: s.task_prompt ?? "",
|
|
245
|
+
});
|
|
246
|
+
const holds = localHolds(
|
|
247
|
+
{
|
|
248
|
+
needs,
|
|
249
|
+
priority: isPriority(s.priority) ? s.priority : DEFAULT_PRIORITY,
|
|
250
|
+
},
|
|
251
|
+
now
|
|
252
|
+
);
|
|
253
|
+
if (holds.length) return false;
|
|
254
|
+
handingOff.add(id);
|
|
255
|
+
try {
|
|
256
|
+
const message = s.plan_handoff_message ?? undefined;
|
|
257
|
+
const edits = s.plan_edits ?? null;
|
|
258
|
+
const arrived = await moveTask(id, LOCAL);
|
|
259
|
+
// The arrived row keeps the pending hand-off until implementing has
|
|
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
|
+
});
|
|
265
|
+
handingOff.add(arrived.id);
|
|
266
|
+
try {
|
|
267
|
+
return await finishArrival(arrived.id, message);
|
|
268
|
+
} finally {
|
|
269
|
+
handingOff.delete(arrived.id);
|
|
270
|
+
}
|
|
271
|
+
} finally {
|
|
272
|
+
handingOff.delete(id);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
@@ -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
|
+
}
|