@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
package/lib/tasks/finish.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { db, type Session } from "../db";
|
|
2
2
|
import { endPauseOnFinish } from "./pause-hold";
|
|
3
|
-
import {
|
|
3
|
+
import { dropThere, mergeThere } from "./contract/actions";
|
|
4
|
+
import { isMirror } from "./contract/start";
|
|
4
5
|
import { deleteWorktree } from "../worktrees";
|
|
5
6
|
import { settleWorktree, type MergedAs } from "../done/worktree";
|
|
6
7
|
import {
|
|
@@ -15,7 +16,8 @@ import { resolveMergedTaskAsks } from "../orchestrator/ask-settle";
|
|
|
15
16
|
import { endSessionAgent } from "../sessions/end";
|
|
16
17
|
import { run } from "./gh";
|
|
17
18
|
import { pushMerged } from "../notify/push/events";
|
|
18
|
-
import { gitFetch
|
|
19
|
+
import { gitFetch } from "../git-exec";
|
|
20
|
+
import { deleteTaskBranch } from "./remote-branch";
|
|
19
21
|
import {
|
|
20
22
|
mergePolicy,
|
|
21
23
|
type MergeMethod,
|
|
@@ -37,9 +39,7 @@ import { captureMergedTask } from "../session-logs";
|
|
|
37
39
|
import { sessionEnded } from "../experiments/hooks";
|
|
38
40
|
import { snapshotWorktree } from "../resurrect/snapshot";
|
|
39
41
|
import { keepSessionFiles } from "../session-artifacts/collect";
|
|
40
|
-
|
|
41
|
-
// Never deleted, whatever a setting says.
|
|
42
|
-
export const PROTECTED_BRANCHES = new Set(["main", "master", "HEAD"]);
|
|
42
|
+
import { setPr, setTask } from "../db/session-parts";
|
|
43
43
|
|
|
44
44
|
export const sessionMergePolicy = (session: Session): MergePolicy =>
|
|
45
45
|
mergePolicy(session.project_id ? getProject(session.project_id) : null);
|
|
@@ -50,7 +50,14 @@ export const sessionMergePolicy = (session: Session): MergePolicy =>
|
|
|
50
50
|
async function cleanup(
|
|
51
51
|
session: Session,
|
|
52
52
|
repo: string,
|
|
53
|
-
opts: {
|
|
53
|
+
opts: {
|
|
54
|
+
keepRemoteBranch?: boolean;
|
|
55
|
+
merged?: MergedAs;
|
|
56
|
+
// The head the PR merged or closed at.
|
|
57
|
+
prHead?: string | null;
|
|
58
|
+
// A drop of a task that never had a PR.
|
|
59
|
+
noPr?: boolean;
|
|
60
|
+
} = {}
|
|
54
61
|
): Promise<void> {
|
|
55
62
|
const keepRemoteBranch = opts.keepRemoteBranch ?? false;
|
|
56
63
|
await endSessionAgent(session);
|
|
@@ -66,14 +73,14 @@ async function cleanup(
|
|
|
66
73
|
}
|
|
67
74
|
// A stacked child's PR targets this branch: deleting it on origin would
|
|
68
75
|
// make GitHub close that PR, and a closed PR can't be retargeted.
|
|
69
|
-
if (
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
);
|
|
76
|
+
if (session.branch_name && !keepRemoteBranch) {
|
|
77
|
+
await deleteTaskBranch({
|
|
78
|
+
repo,
|
|
79
|
+
branch: session.branch_name,
|
|
80
|
+
prHead: opts.prHead,
|
|
81
|
+
noPr: opts.noPr,
|
|
82
|
+
keep: [session.base_branch],
|
|
83
|
+
});
|
|
77
84
|
}
|
|
78
85
|
await gitFetch(repo, ["--prune", "--quiet"]).catch(() => {});
|
|
79
86
|
forgetPR(session.id);
|
|
@@ -110,6 +117,7 @@ async function finishMerge(
|
|
|
110
117
|
await cleanup(session, repo, {
|
|
111
118
|
keepRemoteBranch: stuck || !policy.delete_remote_branch,
|
|
112
119
|
merged: { prHead },
|
|
120
|
+
prHead,
|
|
113
121
|
});
|
|
114
122
|
// After cleanup's fetch, so the base branch holds the merged files.
|
|
115
123
|
inBackground(`re-publish docs after task ${session.id}`, () =>
|
|
@@ -141,7 +149,7 @@ export async function signOffTask(
|
|
|
141
149
|
// Its own machine checks the gates and merges.
|
|
142
150
|
// That machine's settings chose the method; null if it didn't say.
|
|
143
151
|
if (isMirror(session))
|
|
144
|
-
return
|
|
152
|
+
return mergeThere(session, opts.head, session.base_branch);
|
|
145
153
|
const repo = projectPathFor(session);
|
|
146
154
|
if (!repo) throw new Error("Task has no project");
|
|
147
155
|
const refusal = signOffRefusal(id);
|
|
@@ -178,9 +186,10 @@ export async function signOffTask(
|
|
|
178
186
|
method: policy.method,
|
|
179
187
|
head: opts.head,
|
|
180
188
|
});
|
|
181
|
-
db.
|
|
182
|
-
|
|
183
|
-
|
|
189
|
+
db.transaction(() => {
|
|
190
|
+
setTask(db, id, { task_status: "merged" });
|
|
191
|
+
setPr(db, id, { pr_status: "merged" });
|
|
192
|
+
})();
|
|
184
193
|
endPauseOnFinish(id);
|
|
185
194
|
db.prepare(
|
|
186
195
|
`UPDATE stack_items SET status = 'merged', error = NULL WHERE session_id = ?`
|
|
@@ -225,7 +234,7 @@ export async function dropTask(id: string): Promise<void> {
|
|
|
225
234
|
if (session.task_status !== "running")
|
|
226
235
|
throw new Error(`Task is already ${session.task_status}`);
|
|
227
236
|
if (isMirror(session)) {
|
|
228
|
-
await
|
|
237
|
+
await dropThere(session);
|
|
229
238
|
return;
|
|
230
239
|
}
|
|
231
240
|
const repo = projectPathFor(session);
|
|
@@ -247,14 +256,12 @@ export async function dropTask(id: string): Promise<void> {
|
|
|
247
256
|
if (pr?.state === "OPEN") {
|
|
248
257
|
await run("gh", ["pr", "close", String(pr.number)], repo).catch(() => {});
|
|
249
258
|
}
|
|
250
|
-
db
|
|
251
|
-
id
|
|
252
|
-
);
|
|
259
|
+
setTask(db, id, { task_status: "dropped" });
|
|
253
260
|
endPauseOnFinish(id);
|
|
254
261
|
db.prepare(
|
|
255
262
|
`UPDATE stack_items SET status = 'dropped' WHERE session_id = ?`
|
|
256
263
|
).run(id);
|
|
257
264
|
syncTaskCardInBackground(session, "dropped", pr);
|
|
258
265
|
await snapshot;
|
|
259
|
-
await cleanup(session, repo);
|
|
266
|
+
await cleanup(session, repo, { prHead: pr?.head, noPr: !pr });
|
|
260
267
|
}
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
import { db, type Session } from "../db";
|
|
10
10
|
import { sendMessage } from "../bus";
|
|
11
|
-
import {
|
|
11
|
+
import type { HostLink } from "../hosts/remote-api";
|
|
12
|
+
import * as client from "./contract/client";
|
|
12
13
|
|
|
13
14
|
const PULL_EVERY_MS = 5000;
|
|
14
15
|
const MISSING_FOR_MS = 10 * 60 * 1000;
|
|
@@ -27,13 +28,13 @@ interface Pulled {
|
|
|
27
28
|
body: string;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
// Held means an orchestrator started it, as isHeldRemote reads it.
|
|
31
|
+
// Held means an orchestrator started it, as isHeldRemote reads it (contract/held-calls.ts).
|
|
31
32
|
const HELD_BY = `started_by IN (SELECT id FROM sessions WHERE role = 'orchestrator')`;
|
|
32
33
|
|
|
33
34
|
const heldMirror = (hostId: string, id: string) =>
|
|
34
35
|
db
|
|
35
36
|
.prepare(
|
|
36
|
-
`SELECT * FROM
|
|
37
|
+
`SELECT * FROM session_rows WHERE id = ? AND host_id = ?
|
|
37
38
|
AND ${HELD_BY} AND task_status IN ('running', 'moving')`
|
|
38
39
|
)
|
|
39
40
|
.get(id, hostId) as Session | undefined;
|
|
@@ -41,7 +42,7 @@ const heldMirror = (hostId: string, id: string) =>
|
|
|
41
42
|
export function hasHeldMirrors(hostId: string): boolean {
|
|
42
43
|
return !!db
|
|
43
44
|
.prepare(
|
|
44
|
-
`SELECT 1 FROM
|
|
45
|
+
`SELECT 1 FROM session_rows WHERE host_id = ? AND ${HELD_BY}
|
|
45
46
|
AND task_prompt IS NOT NULL AND task_status IN ('running', 'moving')`
|
|
46
47
|
)
|
|
47
48
|
.get(hostId);
|
|
@@ -58,11 +59,7 @@ export async function pullHeldReplies(link: HostLink): Promise<number> {
|
|
|
58
59
|
try {
|
|
59
60
|
let messages: Pulled[];
|
|
60
61
|
try {
|
|
61
|
-
const res = await
|
|
62
|
-
link,
|
|
63
|
-
"/api/tasks/held-replies",
|
|
64
|
-
{ timeout: 8000 }
|
|
65
|
-
);
|
|
62
|
+
const res = await client.replies(link);
|
|
66
63
|
messages = (Array.isArray(res.messages) ? res.messages : []).filter(
|
|
67
64
|
(m): m is Pulled =>
|
|
68
65
|
!!m &&
|
|
@@ -98,10 +95,7 @@ export async function pullHeldReplies(link: HostLink): Promise<number> {
|
|
|
98
95
|
}
|
|
99
96
|
}
|
|
100
97
|
if (handled.length)
|
|
101
|
-
await
|
|
102
|
-
body: { ids: handled },
|
|
103
|
-
timeout: 8000,
|
|
104
|
-
}).catch(() => undefined);
|
|
98
|
+
await client.ackReplies(link, handled).catch(() => undefined);
|
|
105
99
|
return delivered;
|
|
106
100
|
} finally {
|
|
107
101
|
busy.delete(hostId);
|
package/lib/tasks/held.ts
CHANGED
|
@@ -10,6 +10,7 @@ import os from "os";
|
|
|
10
10
|
import { db, type Session } from "../db";
|
|
11
11
|
import { isRemoteHost } from "../hosts";
|
|
12
12
|
import type { OrchestratedBundle } from "./move-bundle";
|
|
13
|
+
import { setPlacement, setTask } from "../db/session-parts";
|
|
13
14
|
|
|
14
15
|
export function holderRefusal(
|
|
15
16
|
session: Pick<Session, "held_by_device" | "held_from">,
|
|
@@ -26,7 +27,7 @@ export function heldRefusal(
|
|
|
26
27
|
device: string | null | undefined
|
|
27
28
|
): string | null {
|
|
28
29
|
const session = db
|
|
29
|
-
.prepare(`SELECT held_by_device, held_from FROM
|
|
30
|
+
.prepare(`SELECT held_by_device, held_from FROM session_rows WHERE id = ?`)
|
|
30
31
|
.get(id) as Pick<Session, "held_by_device" | "held_from"> | undefined;
|
|
31
32
|
return session ? holderRefusal(session, device) : null;
|
|
32
33
|
}
|
|
@@ -41,17 +42,15 @@ export function holdFor(
|
|
|
41
42
|
since?: string | null;
|
|
42
43
|
}
|
|
43
44
|
): void {
|
|
44
|
-
db.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
held.
|
|
52
|
-
|
|
53
|
-
id
|
|
54
|
-
);
|
|
45
|
+
db.transaction(() => {
|
|
46
|
+
setPlacement(db, id, {
|
|
47
|
+
held_by_device: holder,
|
|
48
|
+
held_from: held.heldFrom,
|
|
49
|
+
held_for: held.orchestrator?.id ?? null,
|
|
50
|
+
held_for_name: held.orchestrator?.name ?? null,
|
|
51
|
+
});
|
|
52
|
+
setTask(db, id, { task_since: held.since ?? undefined });
|
|
53
|
+
})();
|
|
55
54
|
}
|
|
56
55
|
|
|
57
56
|
/**
|
|
@@ -81,10 +80,7 @@ const UNRECORDED_STARTER = "unrecorded";
|
|
|
81
80
|
|
|
82
81
|
/** Records the paired device that started a task here, the only one that may later hold it. */
|
|
83
82
|
export function recordStarter(id: string, device: string): void {
|
|
84
|
-
db
|
|
85
|
-
device,
|
|
86
|
-
id
|
|
87
|
-
);
|
|
83
|
+
setPlacement(db, id, { started_by_device: device });
|
|
88
84
|
}
|
|
89
85
|
|
|
90
86
|
type Holdable = Pick<
|
|
@@ -124,7 +120,7 @@ export function holdExisting(
|
|
|
124
120
|
const s = db
|
|
125
121
|
.prepare(
|
|
126
122
|
`SELECT held_by_device, started_by, started_by_device, task_status, host_id, task_prompt
|
|
127
|
-
FROM
|
|
123
|
+
FROM session_rows WHERE id = ?`
|
|
128
124
|
)
|
|
129
125
|
.get(id) as Holdable | undefined;
|
|
130
126
|
if (!s?.task_prompt || s.task_status !== "running") return false;
|
|
@@ -145,7 +141,7 @@ export function planWaitRefusal(
|
|
|
145
141
|
): string | null {
|
|
146
142
|
const s = db
|
|
147
143
|
.prepare(
|
|
148
|
-
`SELECT plan_waits_for_caller, started_by_device, host_id FROM
|
|
144
|
+
`SELECT plan_waits_for_caller, started_by_device, host_id FROM session_rows WHERE id = ?`
|
|
149
145
|
)
|
|
150
146
|
.get(id) as
|
|
151
147
|
| Pick<Session, "plan_waits_for_caller" | "started_by_device" | "host_id">
|
package/lib/tasks/import.ts
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
validateBundle,
|
|
39
39
|
type TaskBundle,
|
|
40
40
|
} from "./move-bundle";
|
|
41
|
+
import { setPlan, setTask } from "../db/session-parts";
|
|
41
42
|
|
|
42
43
|
// Imports in flight in this process, by move id.
|
|
43
44
|
const g = globalThis as unknown as { __agentosImporting?: Set<string> };
|
|
@@ -47,7 +48,7 @@ const importing = (g.__agentosImporting ??= new Set());
|
|
|
47
48
|
export const arrived = (moveId: string) =>
|
|
48
49
|
db
|
|
49
50
|
.prepare(
|
|
50
|
-
`SELECT * FROM
|
|
51
|
+
`SELECT * FROM session_rows WHERE moved_from = ? AND host_id = 'local'
|
|
51
52
|
AND task_status IN ('running', 'moving', 'moved', 'merged', 'done')`
|
|
52
53
|
)
|
|
53
54
|
.get(moveId) as Session | undefined;
|
|
@@ -56,7 +57,7 @@ export const arrived = (moveId: string) =>
|
|
|
56
57
|
function refuseBusyBranch(projectId: string, branch: string): void {
|
|
57
58
|
const busy = db
|
|
58
59
|
.prepare(
|
|
59
|
-
`SELECT id FROM
|
|
60
|
+
`SELECT id FROM session_rows WHERE project_id = ? AND branch_name = ?
|
|
60
61
|
AND task_status IN ('running', 'moving') AND host_id = 'local'`
|
|
61
62
|
)
|
|
62
63
|
.get(projectId, branch);
|
|
@@ -166,26 +167,29 @@ async function arrive(
|
|
|
166
167
|
.run(worktreePath, bundle.branch, baseBranch, null, id);
|
|
167
168
|
// A chat's worker reads its brief from the row; a terminal's is passed.
|
|
168
169
|
db.prepare(
|
|
169
|
-
`UPDATE sessions SET
|
|
170
|
-
|
|
171
|
-
|
|
170
|
+
`UPDATE sessions SET name_source = 'user', claude_session_id = ? WHERE id = ?`
|
|
171
|
+
).run(claude?.sessionId ?? null, id);
|
|
172
|
+
setTask(db, id, {
|
|
173
|
+
task_prompt: bundle.prompt,
|
|
174
|
+
task_brief: brief,
|
|
175
|
+
task_status: "running",
|
|
176
|
+
moved_from: bundle.moveId,
|
|
177
|
+
});
|
|
172
178
|
if (bundle.chat) unpackChat(id, bundle.chat, paths);
|
|
173
|
-
if (bundle.planWait)
|
|
174
|
-
db.prepare(
|
|
175
|
-
`UPDATE sessions SET plan_phase = 'awaiting' WHERE id = ?`
|
|
176
|
-
).run(id);
|
|
179
|
+
if (bundle.planWait) setPlan(db, id, { plan_phase: "awaiting" });
|
|
177
180
|
if (orch) {
|
|
178
|
-
db.
|
|
179
|
-
|
|
180
|
-
).run(orch.planPhase, orch.since, id);
|
|
181
|
+
setPlan(db, id, { plan_phase: orch.planPhase });
|
|
182
|
+
setTask(db, id, { task_since: orch.since });
|
|
181
183
|
// Held for the orchestrator's device. The orchestrator's own state
|
|
182
184
|
// (started_by, its gates) never comes from the bundle: it stays on its
|
|
183
185
|
// machine, and a task coming back takes it from the mirror's record.
|
|
184
186
|
if (holder) {
|
|
185
187
|
holdFor(id, holder, orch);
|
|
186
|
-
db.prepare(
|
|
187
|
-
|
|
188
|
-
|
|
188
|
+
db.prepare(`UPDATE sessions SET model_provider = ? WHERE id = ?`).run(
|
|
189
|
+
orch.modelProvider,
|
|
190
|
+
id
|
|
191
|
+
);
|
|
192
|
+
setTask(db, id, { model_overrides: orch.modelOverrides });
|
|
189
193
|
}
|
|
190
194
|
}
|
|
191
195
|
carryTaskState(bundle.moveId, id);
|
package/lib/tasks/index.ts
CHANGED
|
@@ -59,12 +59,13 @@ import { finishTaskStart } from "./start";
|
|
|
59
59
|
import { isRemoteHost } from "../hosts";
|
|
60
60
|
import { DEFAULT_START_VIEW, type StartView } from "../sessions/launch";
|
|
61
61
|
import { viewOf } from "./move-targets";
|
|
62
|
-
import { isMirror, startRemoteTask } from "./
|
|
63
|
-
import {
|
|
62
|
+
import { isMirror, startRemoteTask } from "./contract/start";
|
|
63
|
+
import { mirrorViews } from "./contract/views";
|
|
64
64
|
import { chatStateNow } from "../chat/runner";
|
|
65
65
|
import { saidSinceLastMessage } from "../chat/store";
|
|
66
66
|
|
|
67
67
|
import { lastActiveOf } from "./last-active";
|
|
68
|
+
import { setPlan, setTask } from "../db/session-parts";
|
|
68
69
|
export * from "./state";
|
|
69
70
|
export { codeReviewGate, codeReviewRefusal } from "./code-review";
|
|
70
71
|
export { signOffTask, dropTask, signingOff, mergeSettled } from "./finish";
|
|
@@ -258,7 +259,7 @@ async function startTask(
|
|
|
258
259
|
if (opts.planOnly && view !== "chat")
|
|
259
260
|
throw new Error("A plan-only task needs a chat, not a terminal");
|
|
260
261
|
// The plan step is this machine's to run and decide: it plans there,
|
|
261
|
-
// stops, and what follows is chosen here (
|
|
262
|
+
// stops, and what follows is chosen here (contract/plan.ts).
|
|
262
263
|
const planned =
|
|
263
264
|
!!(opts.plan || opts.planOnly) &&
|
|
264
265
|
taskRouting(
|
|
@@ -280,7 +281,7 @@ async function startTask(
|
|
|
280
281
|
? { only: !!opts.planOnly, reason: opts.planReason?.trim() || null }
|
|
281
282
|
: undefined,
|
|
282
283
|
});
|
|
283
|
-
// The mirror's started_by stays orchestrator-only: a held
|
|
284
|
+
// The mirror's started_by stays orchestrator-only: a held task on a linked machine is
|
|
284
285
|
// relayed, stopped and answered through it. A plain calling task is its
|
|
285
286
|
// parent and started_via only.
|
|
286
287
|
recordStart(session.id, {
|
|
@@ -366,30 +367,41 @@ async function startTask(
|
|
|
366
367
|
.run(wt.worktreePath, wt.branchName, baseBranch, null, id);
|
|
367
368
|
// Everything the launch needs is on the row, so a restart can resume it.
|
|
368
369
|
db.prepare(
|
|
369
|
-
`UPDATE sessions SET
|
|
370
|
-
model_provider = ?, model_overrides = ?, plan_phase = ?, plan_only = ?,
|
|
371
|
-
plan_decided = ?, plan_reason = ?, plan_waits_for_caller = ? WHERE id = ?`
|
|
370
|
+
`UPDATE sessions SET setup_status = 'running', name_source = ?, view = ?, model_provider = ? WHERE id = ?`
|
|
372
371
|
).run(
|
|
373
|
-
prompt,
|
|
374
|
-
buildTaskBrief({
|
|
375
|
-
branch: wt.branchName,
|
|
376
|
-
baseBranch,
|
|
377
|
-
stack: opts.base?.stack,
|
|
378
|
-
test: declaredTest(wt.worktreePath),
|
|
379
|
-
}),
|
|
380
372
|
naming.source,
|
|
381
373
|
view,
|
|
382
374
|
routing.first.provider.id === ANTHROPIC
|
|
383
375
|
? null
|
|
384
376
|
: routing.first.provider.id,
|
|
385
|
-
routing.override ? JSON.stringify(routing.override) : null,
|
|
386
|
-
routing.plan ? "planning" : null,
|
|
387
|
-
routing.planOnly ? 1 : null,
|
|
388
|
-
routing.planOnly ? 1 : opts.plan === undefined ? null : opts.plan ? 1 : 0,
|
|
389
|
-
opts.planReason?.trim() || null,
|
|
390
|
-
routing.planOnly && opts.planWaits ? 1 : null,
|
|
391
377
|
id
|
|
392
378
|
);
|
|
379
|
+
setTask(db, id, {
|
|
380
|
+
task_prompt: prompt,
|
|
381
|
+
task_brief: buildTaskBrief({
|
|
382
|
+
branch: wt.branchName,
|
|
383
|
+
baseBranch,
|
|
384
|
+
stack: opts.base?.stack,
|
|
385
|
+
test: declaredTest(wt.worktreePath),
|
|
386
|
+
}),
|
|
387
|
+
task_status: "running",
|
|
388
|
+
model_overrides: routing.override
|
|
389
|
+
? JSON.stringify(routing.override)
|
|
390
|
+
: null,
|
|
391
|
+
});
|
|
392
|
+
setPlan(db, id, {
|
|
393
|
+
plan_phase: routing.plan ? "planning" : null,
|
|
394
|
+
plan_only: routing.planOnly ? 1 : null,
|
|
395
|
+
plan_decided: routing.planOnly
|
|
396
|
+
? 1
|
|
397
|
+
: opts.plan === undefined
|
|
398
|
+
? null
|
|
399
|
+
: opts.plan
|
|
400
|
+
? 1
|
|
401
|
+
: 0,
|
|
402
|
+
plan_reason: opts.planReason?.trim() || null,
|
|
403
|
+
plan_waits_for_caller: routing.planOnly && opts.planWaits ? 1 : null,
|
|
404
|
+
});
|
|
393
405
|
recordStart(id, {
|
|
394
406
|
startedBy: opts.startedBy ?? null,
|
|
395
407
|
parentSessionId: opts.parentSessionId ?? null,
|
|
@@ -532,7 +544,7 @@ export async function taskView(session: Session): Promise<TaskView> {
|
|
|
532
544
|
export function movedTasks(): { id: string; movedTo: string | null }[] {
|
|
533
545
|
return db
|
|
534
546
|
.prepare(
|
|
535
|
-
`SELECT id, moved_to AS movedTo FROM
|
|
547
|
+
`SELECT id, moved_to AS movedTo FROM session_rows
|
|
536
548
|
WHERE task_status = 'moved' AND host_id = 'local'
|
|
537
549
|
ORDER BY updated_at DESC LIMIT 500`
|
|
538
550
|
)
|
|
@@ -544,7 +556,7 @@ export async function listTasks(): Promise<TaskView[]> {
|
|
|
544
556
|
const sessions = taskSessions(false);
|
|
545
557
|
const [local, remote] = await Promise.all([
|
|
546
558
|
Promise.all(sessions.filter((s) => !isMirror(s)).map(taskView)),
|
|
547
|
-
|
|
559
|
+
mirrorViews(sessions.filter(isMirror)),
|
|
548
560
|
]);
|
|
549
561
|
const costs = taskCosts(local.map((t) => t.id));
|
|
550
562
|
for (const t of local) {
|
package/lib/tasks/launch-gate.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { db } from "../db";
|
|
|
9
9
|
export function launchPending(sessionId: string): boolean {
|
|
10
10
|
const row = db
|
|
11
11
|
.prepare(
|
|
12
|
-
`SELECT 1 FROM
|
|
12
|
+
`SELECT 1 FROM session_rows WHERE id = ? AND task_status = 'running'
|
|
13
13
|
AND setup_status IN ('running', 'held')`
|
|
14
14
|
)
|
|
15
15
|
.get(sessionId);
|
package/lib/tasks/move-carry.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import type Database from "better-sqlite3";
|
|
11
11
|
import { db } from "../db";
|
|
12
12
|
import { orchestratorOf } from "./starter";
|
|
13
|
+
import { fillMissing } from "../db/session-parts";
|
|
13
14
|
|
|
14
15
|
// table, column, and how the id sits in it: the whole value, `task:<id>`, or
|
|
15
16
|
// the tail of a `<condition>:<id>` key.
|
|
@@ -100,15 +101,22 @@ export function carryTaskState(
|
|
|
100
101
|
.run(fromId.length, toId, `%:${fromId}`);
|
|
101
102
|
}
|
|
102
103
|
const from = handle
|
|
103
|
-
.prepare(`SELECT * FROM
|
|
104
|
+
.prepare(`SELECT * FROM session_rows WHERE id = ?`)
|
|
104
105
|
.get(fromId) as Record<string, unknown> | undefined;
|
|
105
106
|
if (!from) return;
|
|
106
|
-
const
|
|
107
|
-
.map((c) => `${c} = COALESCE(${c}, @${c})`)
|
|
108
|
-
.join(", ");
|
|
109
|
-
handle.prepare(`UPDATE sessions SET ${sets} WHERE id = @id`).run({
|
|
107
|
+
const carried = {
|
|
110
108
|
...orchestrationOf(from),
|
|
111
109
|
task_since: (from.task_since as string | null) ?? from.created_at ?? null,
|
|
112
|
-
|
|
113
|
-
|
|
110
|
+
} as Record<string, string | number | null>;
|
|
111
|
+
handle
|
|
112
|
+
.prepare(
|
|
113
|
+
`UPDATE sessions SET started_by = COALESCE(started_by, @started_by),
|
|
114
|
+
model_provider = COALESCE(model_provider, @model_provider) WHERE id = @id`
|
|
115
|
+
)
|
|
116
|
+
.run({
|
|
117
|
+
started_by: carried.started_by,
|
|
118
|
+
model_provider: carried.model_provider,
|
|
119
|
+
id: toId,
|
|
120
|
+
});
|
|
121
|
+
fillMissing(handle, toId, carried);
|
|
114
122
|
}
|
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
|
@@ -49,7 +49,7 @@ export function moveRefusal(session: Session): string | null {
|
|
|
49
49
|
.prepare(
|
|
50
50
|
`SELECT lh_card_id,
|
|
51
51
|
EXISTS (SELECT 1 FROM stack_items WHERE session_id = s.id) AS stacked
|
|
52
|
-
FROM
|
|
52
|
+
FROM session_rows s WHERE id = ?`
|
|
53
53
|
)
|
|
54
54
|
.get(session.id) as
|
|
55
55
|
| { lh_card_id: string | null; stacked: number }
|