@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
package/lib/tasks/move-flow.ts
CHANGED
|
@@ -16,19 +16,15 @@ import { holderRefusal } from "./held";
|
|
|
16
16
|
import { isPaused } from "../orchestrator/pause";
|
|
17
17
|
import { sessionWorkspace } from "../models/resolve";
|
|
18
18
|
import { arrived as arrivedHere, importTask } from "./import";
|
|
19
|
-
import
|
|
20
|
-
|
|
21
|
-
requireCapability,
|
|
22
|
-
postIdempotent,
|
|
23
|
-
unknownOutcome,
|
|
24
|
-
} from "./remote";
|
|
25
|
-
import { arrivedThere, settleMovedOut, tell } from "./move-recover";
|
|
19
|
+
import * as client from "./contract/client";
|
|
20
|
+
import { arrivedThere, settleMovedOut } from "./move-recover";
|
|
26
21
|
import {
|
|
27
22
|
finishProgress,
|
|
28
23
|
moveSteps,
|
|
29
24
|
startProgress,
|
|
30
25
|
stepProgress,
|
|
31
26
|
} from "./move-progress";
|
|
27
|
+
import { setTask } from "../db/session-parts";
|
|
32
28
|
|
|
33
29
|
const LIVE = new Set(["running", "moving"]);
|
|
34
30
|
|
|
@@ -89,7 +85,7 @@ export async function moveTask(id: string, toHostId: string): Promise<Session> {
|
|
|
89
85
|
}
|
|
90
86
|
|
|
91
87
|
const requireChatMoves = (link: HostLink) =>
|
|
92
|
-
requireCapability(link, "chat-move", "chat tasks");
|
|
88
|
+
client.requireCapability(link, "chat-move", "chat tasks");
|
|
93
89
|
|
|
94
90
|
const unconfirmed = (where: string, err: unknown) =>
|
|
95
91
|
new Error(
|
|
@@ -101,7 +97,11 @@ async function moveOut(session: Session, hostId: string): Promise<Session> {
|
|
|
101
97
|
const chat = session.view === "chat";
|
|
102
98
|
if (chat) await requireChatMoves(link);
|
|
103
99
|
if (isOrchestrated(session))
|
|
104
|
-
await requireCapability(
|
|
100
|
+
await client.requireCapability(
|
|
101
|
+
link,
|
|
102
|
+
"orchestrated-move",
|
|
103
|
+
"orchestrator tasks"
|
|
104
|
+
);
|
|
105
105
|
startProgress(
|
|
106
106
|
session.id,
|
|
107
107
|
link.hostName,
|
|
@@ -118,15 +118,10 @@ async function moveOut(session: Session, hostId: string): Promise<Session> {
|
|
|
118
118
|
stepProgress(session.id, "arrive");
|
|
119
119
|
let there: Session;
|
|
120
120
|
try {
|
|
121
|
-
({ session: there } = await
|
|
122
|
-
link,
|
|
123
|
-
"/api/tasks/import",
|
|
124
|
-
bundle,
|
|
125
|
-
600000
|
|
126
|
-
));
|
|
121
|
+
({ session: there } = await client.importBundle(link, bundle));
|
|
127
122
|
} catch (err) {
|
|
128
123
|
// It may be running there already: resuming here could run it twice.
|
|
129
|
-
if (unknownOutcome(err)) throw unconfirmed(link.hostName, err);
|
|
124
|
+
if (client.unknownOutcome(err)) throw unconfirmed(link.hostName, err);
|
|
130
125
|
await resumeTask(session.id);
|
|
131
126
|
throw err;
|
|
132
127
|
}
|
|
@@ -134,45 +129,43 @@ async function moveOut(session: Session, hostId: string): Promise<Session> {
|
|
|
134
129
|
}
|
|
135
130
|
|
|
136
131
|
function mirrorRunning(id: string): void {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
132
|
+
setTask(
|
|
133
|
+
db,
|
|
134
|
+
id,
|
|
135
|
+
{ task_status: "running", moved_to: null },
|
|
136
|
+
{ task_status: "moving" }
|
|
137
|
+
);
|
|
141
138
|
}
|
|
142
139
|
|
|
143
140
|
async function moveIn(mirror: Session): Promise<Session> {
|
|
144
141
|
const link = requireHostLink(mirror.host_id);
|
|
145
142
|
if (mirror.view === "chat") await requireChatMoves(link);
|
|
146
143
|
startProgress(mirror.id, "this machine", moveSteps("in", link.hostName));
|
|
147
|
-
const path = `/api/tasks/${encodeURIComponent(mirror.id)}`;
|
|
148
144
|
// A retry after it arrived here but that machine didn't hear: tidy up.
|
|
149
145
|
let here = arrivedHere(mirror.id);
|
|
150
146
|
if (!here) {
|
|
151
147
|
// Moving, so nothing reads the mirror as a task to judge, or sign off,
|
|
152
148
|
// while it leaves (a refusal below puts it back).
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
149
|
+
setTask(
|
|
150
|
+
db,
|
|
151
|
+
mirror.id,
|
|
152
|
+
{ task_status: "moving", moved_to: "this machine" },
|
|
153
|
+
{ task_status: "running" }
|
|
154
|
+
);
|
|
157
155
|
// That machine resumes the agent itself if its half fails there.
|
|
158
156
|
stepProgress(mirror.id, "export");
|
|
159
157
|
let bundle: TaskBundle;
|
|
160
158
|
try {
|
|
161
|
-
|
|
162
|
-
link,
|
|
163
|
-
`${path}/export`,
|
|
164
|
-
{ to: os.hostname() },
|
|
165
|
-
300000
|
|
166
|
-
));
|
|
159
|
+
bundle = await client.exportOut(link, mirror.id, os.hostname());
|
|
167
160
|
} catch (err) {
|
|
168
161
|
mirrorRunning(mirror.id);
|
|
169
|
-
if (unknownOutcome(err)) throw unconfirmed(link.hostName, err);
|
|
162
|
+
if (client.unknownOutcome(err)) throw unconfirmed(link.hostName, err);
|
|
170
163
|
throw err;
|
|
171
164
|
}
|
|
172
165
|
try {
|
|
173
166
|
here = await importTask(bundle);
|
|
174
167
|
} catch (err) {
|
|
175
|
-
await
|
|
168
|
+
await client.resume(link, mirror.id, { force: true }).catch((e) => {
|
|
176
169
|
throw new Error(
|
|
177
170
|
`${(err as Error).message}; and ${link.hostName} didn't resume it: ${e.message}`
|
|
178
171
|
);
|
|
@@ -186,7 +179,7 @@ async function moveIn(mirror: Session): Promise<Session> {
|
|
|
186
179
|
// "moving" there, which nothing can sign off), the mirror stays, so Move
|
|
187
180
|
// here again comes back to this.
|
|
188
181
|
try {
|
|
189
|
-
await
|
|
182
|
+
await client.confirmMoved(link, mirror.id, os.hostname());
|
|
190
183
|
} catch (err) {
|
|
191
184
|
throw new Error(
|
|
192
185
|
`It's running here now, but ${link.hostName} didn't confirm (${(err as Error).message}). Press Move here again to tidy up.`
|
|
@@ -197,8 +190,8 @@ async function moveIn(mirror: Session): Promise<Session> {
|
|
|
197
190
|
mirror.id,
|
|
198
191
|
mirror.host_id
|
|
199
192
|
);
|
|
200
|
-
|
|
193
|
+
client.forget(link.hostId);
|
|
201
194
|
return db
|
|
202
|
-
.prepare(`SELECT * FROM
|
|
195
|
+
.prepare(`SELECT * FROM session_rows WHERE id = ?`)
|
|
203
196
|
.get(here.id) as Session;
|
|
204
197
|
}
|
package/lib/tasks/move-guard.ts
CHANGED
|
@@ -29,7 +29,15 @@ export function moveRefusal(session: Session): string | null {
|
|
|
29
29
|
return "Its repo is encrypted with git-crypt and the key never leaves this machine, so it stays here";
|
|
30
30
|
if (escalations(session.id).length)
|
|
31
31
|
return "It's waiting on you (an escalated gate), so it stays here";
|
|
32
|
-
if (
|
|
32
|
+
if (
|
|
33
|
+
session.plan_only &&
|
|
34
|
+
session.plan_phase !== "implementing" &&
|
|
35
|
+
// A plan another machine placed here moves to it once written.
|
|
36
|
+
!(
|
|
37
|
+
session.plan_waits_for_caller &&
|
|
38
|
+
(session.plan_phase === "awaiting" || session.plan_phase === "handoff")
|
|
39
|
+
)
|
|
40
|
+
)
|
|
33
41
|
return "It's a plan-only task that hasn't been approved, so it stays here";
|
|
34
42
|
const ask = db
|
|
35
43
|
.prepare(
|
|
@@ -41,7 +49,7 @@ export function moveRefusal(session: Session): string | null {
|
|
|
41
49
|
.prepare(
|
|
42
50
|
`SELECT lh_card_id,
|
|
43
51
|
EXISTS (SELECT 1 FROM stack_items WHERE session_id = s.id) AS stacked
|
|
44
|
-
FROM
|
|
52
|
+
FROM session_rows s WHERE id = ?`
|
|
45
53
|
)
|
|
46
54
|
.get(session.id) as
|
|
47
55
|
| { lh_card_id: string | null; stacked: number }
|
|
@@ -8,38 +8,19 @@
|
|
|
8
8
|
|
|
9
9
|
import { db, type Session } from "../db";
|
|
10
10
|
import { hostIdNamed } from "../hosts";
|
|
11
|
-
import {
|
|
11
|
+
import { hostLink, type HostLink } from "../hosts/remote-api";
|
|
12
12
|
import { getTaskSession } from "./session";
|
|
13
13
|
import { markMoved, resumeTask } from "./move";
|
|
14
|
-
import
|
|
14
|
+
import * as client from "./contract/client";
|
|
15
|
+
import { mirrorTask } from "./contract/start";
|
|
15
16
|
import { carryTaskState } from "./move-carry";
|
|
16
17
|
|
|
17
|
-
/** POST, repeated when the outcome is unknown (the calls are idempotent). */
|
|
18
|
-
export async function tell(
|
|
19
|
-
link: HostLink,
|
|
20
|
-
path: string,
|
|
21
|
-
body: object = {}
|
|
22
|
-
): Promise<void> {
|
|
23
|
-
for (let i = 0; ; i++) {
|
|
24
|
-
try {
|
|
25
|
-
await hostApi(link, path, { body });
|
|
26
|
-
return;
|
|
27
|
-
} catch (err) {
|
|
28
|
-
if (!unknownOutcome(err) || i === 2) throw err;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
18
|
/** The task a move from this session made there, or null. Throws if unsure. */
|
|
34
19
|
export async function arrivedThere(
|
|
35
20
|
link: HostLink,
|
|
36
21
|
sessionId: string
|
|
37
22
|
): Promise<Session | null> {
|
|
38
|
-
|
|
39
|
-
link,
|
|
40
|
-
`/api/tasks/arrived?from=${encodeURIComponent(sessionId)}`
|
|
41
|
-
);
|
|
42
|
-
return session;
|
|
23
|
+
return client.arrived(link, sessionId);
|
|
43
24
|
}
|
|
44
25
|
|
|
45
26
|
export function settleMovedOut(
|
|
@@ -53,9 +34,9 @@ export function settleMovedOut(
|
|
|
53
34
|
mirrorTask(hostId, session.project_id, there);
|
|
54
35
|
carryTaskState(session.id, there.id);
|
|
55
36
|
})();
|
|
56
|
-
|
|
37
|
+
client.forget(hostId);
|
|
57
38
|
return db
|
|
58
|
-
.prepare(`SELECT * FROM
|
|
39
|
+
.prepare(`SELECT * FROM session_rows WHERE id = ?`)
|
|
59
40
|
.get(there.id) as Session;
|
|
60
41
|
}
|
|
61
42
|
|
package/lib/tasks/move.ts
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
latestSessionId,
|
|
37
37
|
readTranscript,
|
|
38
38
|
} from "./transcript";
|
|
39
|
+
import { setTask } from "../db/session-parts";
|
|
39
40
|
|
|
40
41
|
export * from "./move-bundle";
|
|
41
42
|
|
|
@@ -88,13 +89,13 @@ async function pushOwnBranch(cwd: string, branch: string): Promise<void> {
|
|
|
88
89
|
* arrived, and then it would run on both.
|
|
89
90
|
*/
|
|
90
91
|
function claimMoving(id: string, to: string): { fresh: boolean } {
|
|
91
|
-
const fresh =
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (fresh
|
|
92
|
+
const fresh = setTask(
|
|
93
|
+
db,
|
|
94
|
+
id,
|
|
95
|
+
{ task_status: "moving", moved_to: to },
|
|
96
|
+
{ task_status: "running" }
|
|
97
|
+
);
|
|
98
|
+
if (fresh) return { fresh: true };
|
|
98
99
|
const { task_status, moved_to } = getTaskSession(id);
|
|
99
100
|
if (task_status === "moving" && moved_to === to) return { fresh: false };
|
|
100
101
|
throw new Error(
|
|
@@ -140,10 +141,7 @@ function orchestratedBundle(session: Session): OrchestratedBundle {
|
|
|
140
141
|
modelProvider: session.model_provider ?? null,
|
|
141
142
|
modelOverrides: session.model_overrides ?? null,
|
|
142
143
|
// moveRefusal keeps an unapproved plan-only task here.
|
|
143
|
-
planPhase: (session.plan_phase ?? null) as
|
|
144
|
-
| "planning"
|
|
145
|
-
| "implementing"
|
|
146
|
-
| null,
|
|
144
|
+
planPhase: (session.plan_phase ?? null) as OrchestratedBundle["planPhase"],
|
|
147
145
|
planFile: readPlanFile(session.working_directory),
|
|
148
146
|
orchestrator: orchestratorOf(session),
|
|
149
147
|
};
|
|
@@ -215,6 +213,10 @@ export async function exportTask(id: string, to: string): Promise<TaskBundle> {
|
|
|
215
213
|
: null,
|
|
216
214
|
chat: chatBundle,
|
|
217
215
|
orchestrated: isOrchestrated(now) ? orchestratedBundle(now) : undefined,
|
|
216
|
+
planWait:
|
|
217
|
+
now.plan_waits_for_caller && now.plan_phase === "awaiting"
|
|
218
|
+
? { file: readPlanFile(cwd) }
|
|
219
|
+
: undefined,
|
|
218
220
|
};
|
|
219
221
|
} finally {
|
|
220
222
|
packed();
|
|
@@ -249,12 +251,12 @@ export async function exportOrResume(
|
|
|
249
251
|
}
|
|
250
252
|
|
|
251
253
|
export function markMoved(id: string, to: string): void {
|
|
252
|
-
const moved =
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
254
|
+
const moved = setTask(
|
|
255
|
+
db,
|
|
256
|
+
id,
|
|
257
|
+
{ task_status: "moved", moved_to: to },
|
|
258
|
+
{ task_status: ["running", "moving"] }
|
|
259
|
+
);
|
|
258
260
|
// It goes on there with a copy of its own.
|
|
259
261
|
if (moved) void dropSessionDatabase(id);
|
|
260
262
|
}
|
|
@@ -268,25 +270,24 @@ export function markMoved(id: string, to: string): void {
|
|
|
268
270
|
*/
|
|
269
271
|
export async function resumeTask(id: string, note?: string): Promise<void> {
|
|
270
272
|
if (exporting.has(id)) throw new Error("It's moving right now");
|
|
271
|
-
const claimed =
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
273
|
+
const claimed = setTask(
|
|
274
|
+
db,
|
|
275
|
+
id,
|
|
276
|
+
{ task_status: "running" },
|
|
277
|
+
{ task_status: "moving" }
|
|
278
|
+
);
|
|
276
279
|
const session = getTaskSession(id);
|
|
277
|
-
if (claimed
|
|
280
|
+
if (!claimed) {
|
|
278
281
|
if (session.task_status === "running") return;
|
|
279
282
|
throw new Error(`Task is already ${session.task_status}`);
|
|
280
283
|
}
|
|
281
284
|
try {
|
|
282
285
|
await relaunch(session, note);
|
|
283
286
|
} catch (err) {
|
|
284
|
-
db
|
|
285
|
-
`UPDATE sessions SET task_status = 'moving' WHERE id = ? AND task_status = 'running'`
|
|
286
|
-
).run(id);
|
|
287
|
+
setTask(db, id, { task_status: "moving" }, { task_status: "running" });
|
|
287
288
|
throw err;
|
|
288
289
|
}
|
|
289
|
-
db
|
|
290
|
+
setTask(db, id, { moved_to: null });
|
|
290
291
|
}
|
|
291
292
|
|
|
292
293
|
async function relaunch(session: Session, note?: string): Promise<void> {
|
package/lib/tasks/moved-away.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { settleWorktree, type WorktreeFate } from "../done/worktree";
|
|
|
12
12
|
import { archiveSession } from "../done/archive";
|
|
13
13
|
|
|
14
14
|
export async function cleanUpMovedAway(id: string): Promise<WorktreeFate> {
|
|
15
|
-
const s = db.prepare(`SELECT * FROM
|
|
15
|
+
const s = db.prepare(`SELECT * FROM session_rows WHERE id = ?`).get(id) as
|
|
16
16
|
| Session
|
|
17
17
|
| undefined;
|
|
18
18
|
if (!s || s.task_status !== "moved" || !s.held_by_device)
|
package/lib/tasks/pause-hold.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// can hold a message for it without loading the pause machinery.
|
|
3
3
|
|
|
4
4
|
import { db, type TaskPause } from "../db";
|
|
5
|
+
import { setPause } from "../db/session-parts";
|
|
5
6
|
|
|
6
7
|
export interface PauseHold {
|
|
7
8
|
pauseId: number;
|
|
@@ -11,7 +12,7 @@ export interface PauseHold {
|
|
|
11
12
|
export function pauseHoldOf(sessionId: string): PauseHold | null {
|
|
12
13
|
const row = db
|
|
13
14
|
.prepare(
|
|
14
|
-
`SELECT pause_id, paused_until FROM
|
|
15
|
+
`SELECT pause_id, paused_until FROM session_rows
|
|
15
16
|
WHERE id = ? AND pause_state IN ('pausing', 'paused') AND pause_id IS NOT NULL`
|
|
16
17
|
)
|
|
17
18
|
.get(sessionId) as
|
|
@@ -45,10 +46,17 @@ export function endPause(
|
|
|
45
46
|
`UPDATE task_pauses SET state = 'ended', ended_at = datetime('now'), end_reason = ?
|
|
46
47
|
WHERE id = ? AND state != 'ended'`
|
|
47
48
|
).run(reason, row.id);
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
setPause(
|
|
50
|
+
db,
|
|
51
|
+
row.session_id,
|
|
52
|
+
{
|
|
53
|
+
pause_state: null,
|
|
54
|
+
paused_until: null,
|
|
55
|
+
pause_id: null,
|
|
56
|
+
pause_note: null,
|
|
57
|
+
},
|
|
58
|
+
{ pause_id: row.id }
|
|
59
|
+
);
|
|
52
60
|
})();
|
|
53
61
|
}
|
|
54
62
|
|
package/lib/tasks/pause.ts
CHANGED
|
@@ -34,9 +34,7 @@ import { openBySubject, workSubject } from "../orchestrator/asks";
|
|
|
34
34
|
import { queueEventOnce } from "../orchestrator/events";
|
|
35
35
|
import { isPaused } from "../orchestrator/pause";
|
|
36
36
|
import { isMerging } from "./finish";
|
|
37
|
-
import {
|
|
38
|
-
import { localView } from "../load/machines";
|
|
39
|
-
import { machineHold } from "./schedule";
|
|
37
|
+
import { loadHoldFor, resumeHoldFor } from "./hold-checks";
|
|
40
38
|
import {
|
|
41
39
|
continuePrompt,
|
|
42
40
|
durationText,
|
|
@@ -52,6 +50,7 @@ import {
|
|
|
52
50
|
type PauseView,
|
|
53
51
|
type Priority,
|
|
54
52
|
} from "./schedule-types";
|
|
53
|
+
import { setPause } from "../db/session-parts";
|
|
55
54
|
|
|
56
55
|
export class PauseRefused extends Error {}
|
|
57
56
|
|
|
@@ -74,7 +73,7 @@ const nudge = () =>
|
|
|
74
73
|
void import("./queue").then((q) => q.tickQueueSoon()).catch(() => undefined);
|
|
75
74
|
|
|
76
75
|
const sessionOf = (id: string) =>
|
|
77
|
-
db.prepare(`SELECT * FROM
|
|
76
|
+
db.prepare(`SELECT * FROM session_rows WHERE id = ?`).get(id) as
|
|
78
77
|
| Session
|
|
79
78
|
| undefined;
|
|
80
79
|
|
|
@@ -167,10 +166,9 @@ export function requestPause(sessionId: string, req: PauseRequest): TaskPause {
|
|
|
167
166
|
until ? toSqliteTime(until) : null,
|
|
168
167
|
existing.id
|
|
169
168
|
);
|
|
170
|
-
db
|
|
171
|
-
until ? toSqliteTime(until) : null,
|
|
172
|
-
|
|
173
|
-
);
|
|
169
|
+
setPause(db, sessionId, {
|
|
170
|
+
paused_until: until ? toSqliteTime(until) : null,
|
|
171
|
+
});
|
|
174
172
|
// "Pause now" takes over: the first driver is superseded, not raced.
|
|
175
173
|
drive(sessionId, existing.id, mode === "now");
|
|
176
174
|
return pauseRow(existing.id)!;
|
|
@@ -192,9 +190,11 @@ export function requestPause(sessionId: string, req: PauseRequest): TaskPause {
|
|
|
192
190
|
until ? toSqliteTime(until) : null
|
|
193
191
|
).lastInsertRowid
|
|
194
192
|
);
|
|
195
|
-
db
|
|
196
|
-
|
|
197
|
-
|
|
193
|
+
setPause(db, sessionId, {
|
|
194
|
+
pause_state: "pausing",
|
|
195
|
+
paused_until: until ? toSqliteTime(until) : null,
|
|
196
|
+
pause_id: id,
|
|
197
|
+
});
|
|
198
198
|
drive(sessionId, id);
|
|
199
199
|
return pauseRow(id)!;
|
|
200
200
|
}
|
|
@@ -309,10 +309,7 @@ function markPaused(sessionId: string, pauseId: number): void {
|
|
|
309
309
|
WHERE id = ? AND state = 'pausing'`
|
|
310
310
|
)
|
|
311
311
|
.run(pauseId).changes;
|
|
312
|
-
if (n)
|
|
313
|
-
db.prepare(`UPDATE sessions SET pause_state = 'paused' WHERE id = ?`).run(
|
|
314
|
-
sessionId
|
|
315
|
-
);
|
|
312
|
+
if (n) setPause(db, sessionId, { pause_state: "paused" });
|
|
316
313
|
return n;
|
|
317
314
|
})();
|
|
318
315
|
if (!changed) return;
|
|
@@ -361,10 +358,7 @@ export function setResumeTime(sessionId: string, until: Date | null): void {
|
|
|
361
358
|
db.prepare(
|
|
362
359
|
`UPDATE task_pauses SET until_at = ?, resume_requested_at = NULL WHERE id = ? AND state IN ('pausing','paused')`
|
|
363
360
|
).run(value, row.id);
|
|
364
|
-
db
|
|
365
|
-
value,
|
|
366
|
-
sessionId
|
|
367
|
-
);
|
|
361
|
+
setPause(db, sessionId, { paused_until: value });
|
|
368
362
|
})();
|
|
369
363
|
}
|
|
370
364
|
|
|
@@ -395,39 +389,14 @@ export function requestResume(sessionId: string, actor: PauseBy): void {
|
|
|
395
389
|
);
|
|
396
390
|
}
|
|
397
391
|
|
|
398
|
-
|
|
399
|
-
// red (or still settling) machine holds a resume; amber doesn't.
|
|
400
|
-
export function loadHoldFor(s: Session): string | null {
|
|
401
|
-
const project = s.project_id ? getProject(s.project_id) : null;
|
|
402
|
-
if (!project || isRemoteHost(s.host_id ?? project.host_id)) return null;
|
|
403
|
-
const hold = machineHold(
|
|
404
|
-
{
|
|
405
|
-
heavy: false,
|
|
406
|
-
needsUnmet: false,
|
|
407
|
-
remote: false,
|
|
408
|
-
priority: DEFAULT_PRIORITY,
|
|
409
|
-
},
|
|
410
|
-
localView(),
|
|
411
|
-
Date.now()
|
|
412
|
-
);
|
|
413
|
-
return hold?.code === "load_red" ? hold.text.replace(/^Waiting: /, "") : null;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
// Why a resume this actor asked for must wait: this machine's load, and for
|
|
417
|
-
// the orchestrator also what holds its own starts (it is paused, or the
|
|
418
|
-
// usage window). Saad's own resume skips the second.
|
|
419
|
-
export function resumeHoldFor(s: Session, actor: PauseBy): string | null {
|
|
420
|
-
const load = loadHoldFor(s);
|
|
421
|
-
if (load) return load;
|
|
422
|
-
if (actor !== "orchestrator") return null;
|
|
423
|
-
const ws = s.project_id ? getProject(s.project_id)?.workspace_id : null;
|
|
424
|
-
return autoHold(ws ?? null)?.text.replace(/^Waiting: /, "") ?? null;
|
|
425
|
-
}
|
|
392
|
+
export { loadHoldFor, resumeHoldFor };
|
|
426
393
|
|
|
427
394
|
export function setPauseNote(sessionId: string, note: string | null): void {
|
|
428
|
-
db
|
|
429
|
-
`
|
|
430
|
-
|
|
395
|
+
const row = db
|
|
396
|
+
.prepare(`SELECT pause_note FROM session_rows WHERE id = ?`)
|
|
397
|
+
.get(sessionId) as { pause_note: string | null } | undefined;
|
|
398
|
+
if ((row?.pause_note ?? "") === (note ?? "")) return;
|
|
399
|
+
setPause(db, sessionId, { pause_note: note }, { pause_state: "paused" });
|
|
431
400
|
}
|
|
432
401
|
|
|
433
402
|
export type ResumeOutcome =
|
|
@@ -482,7 +451,7 @@ export function dueResumes(): DueResume[] {
|
|
|
482
451
|
.prepare(
|
|
483
452
|
`SELECT p.id AS pause_id, p.session_id, p.by_kind, p.until_at, p.resume_requested_at, p.resume_by_kind,
|
|
484
453
|
s.project_id, s.priority
|
|
485
|
-
FROM task_pauses p JOIN
|
|
454
|
+
FROM task_pauses p JOIN session_rows s ON s.id = p.session_id
|
|
486
455
|
WHERE p.state = 'paused' AND p.resume_requested_at IS NOT NULL
|
|
487
456
|
AND s.task_status IN ('running') AND s.archived_at IS NULL`
|
|
488
457
|
)
|
|
@@ -612,9 +581,7 @@ export async function claimResume(
|
|
|
612
581
|
}),
|
|
613
582
|
Date.now()
|
|
614
583
|
);
|
|
615
|
-
db
|
|
616
|
-
`UPDATE sessions SET pause_state = 'resuming', pause_note = NULL WHERE id = ?`
|
|
617
|
-
).run(sessionId);
|
|
584
|
+
setPause(db, sessionId, { pause_state: "resuming", pause_note: null });
|
|
618
585
|
return true;
|
|
619
586
|
})();
|
|
620
587
|
if (!claimed) return "gone";
|
package/lib/tasks/placement.ts
CHANGED
|
@@ -12,12 +12,8 @@ import path from "path";
|
|
|
12
12
|
import { db } from "../db";
|
|
13
13
|
import { loadProjectConfig } from "../project-config";
|
|
14
14
|
import { MACHINE_NEEDS } from "../project-config/schema";
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
type Candidate,
|
|
18
|
-
type Hold,
|
|
19
|
-
type MachineView,
|
|
20
|
-
} from "./schedule";
|
|
15
|
+
import { machineHold, placeable, type MachineView } from "./holds";
|
|
16
|
+
import type { Candidate } from "./schedule";
|
|
21
17
|
|
|
22
18
|
export type Need = (typeof MACHINE_NEEDS)[number];
|
|
23
19
|
export type NeedsSource = "task" | "brief" | "config" | "inferred" | "none";
|
|
@@ -138,8 +134,6 @@ export function unplaceable(r: {
|
|
|
138
134
|
host_id: string | null;
|
|
139
135
|
}): string | null {
|
|
140
136
|
if (r.host_id) return "it was pinned to a machine";
|
|
141
|
-
if (r.plan_only) return "plan-only tasks run on this machine";
|
|
142
|
-
if (r.plan) return "plan-first tasks run on this machine";
|
|
143
137
|
if (r.agent_type && r.agent_type !== "claude")
|
|
144
138
|
return "only Claude tasks run on another machine";
|
|
145
139
|
if (r.model_overrides) return "per-role models run on this machine";
|
|
@@ -153,11 +147,24 @@ export interface PeerOption {
|
|
|
153
147
|
capabilities: string[];
|
|
154
148
|
// It can hold an orchestrator's task (false: an older AgentOS). Unset: yes.
|
|
155
149
|
holds?: boolean;
|
|
150
|
+
// It can run a plan step that stops for this machine (false: an older
|
|
151
|
+
// AgentOS). Unset: yes.
|
|
152
|
+
plans?: boolean;
|
|
156
153
|
// The project exists there (projectMachines said null).
|
|
157
154
|
hasProject: boolean;
|
|
158
155
|
loadPerCore: number;
|
|
159
156
|
}
|
|
160
157
|
|
|
158
|
+
// A start that plans first: the plan phase only reads code, so it needs no
|
|
159
|
+
// simulator or Mac, and the plan-only skip of load holds doesn't apply to
|
|
160
|
+
// where it goes, only to whether it may wait. The candidate as a plan phase
|
|
161
|
+
// sees it.
|
|
162
|
+
export const asPlanPhase = <
|
|
163
|
+
T extends Pick<Candidate, "heavy" | "needsUnmet" | "planOnly">,
|
|
164
|
+
>(
|
|
165
|
+
c: T
|
|
166
|
+
): T => ({ ...c, heavy: false, needsUnmet: false, planOnly: false });
|
|
167
|
+
|
|
161
168
|
export interface Placement {
|
|
162
169
|
hostId: string;
|
|
163
170
|
name: string;
|
|
@@ -165,13 +172,6 @@ export interface Placement {
|
|
|
165
172
|
why: string;
|
|
166
173
|
}
|
|
167
174
|
|
|
168
|
-
const LOAD_HOLDS = new Set<Hold["code"]>([
|
|
169
|
-
"load_red",
|
|
170
|
-
"load_amber",
|
|
171
|
-
"needs",
|
|
172
|
-
"heavy",
|
|
173
|
-
]);
|
|
174
|
-
|
|
175
175
|
// True when the home machine's load or needs rule this start out, which is
|
|
176
176
|
// the only time a linked machine is worth asking.
|
|
177
177
|
export const wantsPlacing = (
|
|
@@ -181,7 +181,7 @@ export const wantsPlacing = (
|
|
|
181
181
|
): boolean => {
|
|
182
182
|
if (c.remote || home.reading) return false;
|
|
183
183
|
const hold = machineHold(c, home, now);
|
|
184
|
-
return !!hold &&
|
|
184
|
+
return !!hold && placeable(hold);
|
|
185
185
|
};
|
|
186
186
|
|
|
187
187
|
// Null: start it at home (or hold it there for a reason placing doesn't
|
package/lib/tasks/plan-view.ts
CHANGED
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
type TaskPlan,
|
|
18
18
|
} from "./loop-types";
|
|
19
19
|
import { expandHome } from "./session";
|
|
20
|
+
import { isRemoteHost } from "../hosts";
|
|
21
|
+
import { remotePlanPath } from "./contract/plan-path";
|
|
20
22
|
|
|
21
23
|
const TITLE_MAX = 120;
|
|
22
24
|
|
|
@@ -131,7 +133,10 @@ export function taskPlan(session: Session): TaskPlan | null {
|
|
|
131
133
|
};
|
|
132
134
|
|
|
133
135
|
if (phase && session.worktree_path) {
|
|
134
|
-
|
|
136
|
+
// A plan written on a linked machine is read from the copy kept here.
|
|
137
|
+
const file = isRemoteHost(session.host_id)
|
|
138
|
+
? remotePlanPath(session.id)
|
|
139
|
+
: path.join(expandHome(session.worktree_path), PLAN_FILE);
|
|
135
140
|
const text = readHead(file, PLAN_TEXT_MAX);
|
|
136
141
|
if (text !== null || phase === "planning") {
|
|
137
142
|
const body = text ?? "";
|
|
@@ -141,13 +146,16 @@ export function taskPlan(session: Session): TaskPlan | null {
|
|
|
141
146
|
status:
|
|
142
147
|
phase === "planning"
|
|
143
148
|
? "writing"
|
|
144
|
-
: phase === "awaiting"
|
|
149
|
+
: phase === "awaiting" || phase === "handoff"
|
|
145
150
|
? "ready"
|
|
146
151
|
: "approved",
|
|
147
152
|
source: "step",
|
|
148
153
|
title: planTitle(body),
|
|
149
154
|
path: text !== null ? PLAN_FILE : null,
|
|
150
|
-
pieces:
|
|
155
|
+
pieces:
|
|
156
|
+
phase === "awaiting" || phase === "handoff"
|
|
157
|
+
? piecesView(session, file)
|
|
158
|
+
: null,
|
|
151
159
|
...capped(body),
|
|
152
160
|
};
|
|
153
161
|
}
|