@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/resurrect/index.ts
CHANGED
|
@@ -17,7 +17,11 @@ import { unarchiveSession } from "../done/archive";
|
|
|
17
17
|
import { reopenSession } from "../sidebar/shelf-state";
|
|
18
18
|
import { peerSessionLink } from "../hosts/peer-actions";
|
|
19
19
|
import { hostApi, hostLink } from "../hosts/remote-api";
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
forget as forgetHostTasks,
|
|
22
|
+
unknownOutcome,
|
|
23
|
+
} from "../tasks/contract/client";
|
|
24
|
+
import { isMirror } from "../tasks/contract/start";
|
|
21
25
|
import { heldRefusal } from "../tasks/held";
|
|
22
26
|
import { expandHome, projectPathFor } from "../tasks/session";
|
|
23
27
|
import { run } from "../tasks/gh";
|
|
@@ -40,6 +44,9 @@ import { launchClaude } from "../agents/launch";
|
|
|
40
44
|
import { PLAN_FILE, preparePlanDir } from "../models/plan-step";
|
|
41
45
|
import { planFromTranscript } from "./plan";
|
|
42
46
|
import { dropSnapshot, restoreWorktree, type RestoredAs } from "./snapshot";
|
|
47
|
+
import { restoreSwept, sweptNote } from "./swept";
|
|
48
|
+
import { forgetSwept } from "../worktree-sweep/store";
|
|
49
|
+
import { setPr, setTask } from "../db/session-parts";
|
|
43
50
|
|
|
44
51
|
export interface ResurrectOptions {
|
|
45
52
|
// The paired device asking: a task held for another machine's orchestrator
|
|
@@ -64,6 +71,7 @@ const ENDED_TASK = new Set(["dropped", "done"]);
|
|
|
64
71
|
// Restores in flight here, so two clicks can't both build a worktree.
|
|
65
72
|
const g = globalThis as unknown as { __agentosResurrecting?: Set<string> };
|
|
66
73
|
const inFlight = (g.__agentosResurrecting ??= new Set());
|
|
74
|
+
export const restoring = (id: string) => inFlight.has(id);
|
|
67
75
|
|
|
68
76
|
interface PrInfo {
|
|
69
77
|
number: number;
|
|
@@ -119,12 +127,22 @@ export async function resurrectSession(
|
|
|
119
127
|
// restoring it is the whole of it.
|
|
120
128
|
if (!endedTask || (s.archived_at && s.task_status !== "dropped")) {
|
|
121
129
|
if (s.archived_at) {
|
|
130
|
+
const swept = await restoreSwept(s);
|
|
122
131
|
unarchiveSession(id);
|
|
123
|
-
return outcome(
|
|
132
|
+
return outcome(
|
|
133
|
+
s,
|
|
134
|
+
"unarchived",
|
|
135
|
+
`${s.name} is back in the sidebar${swept ? `; ${swept}` : ""}.`
|
|
136
|
+
);
|
|
124
137
|
}
|
|
125
138
|
if (s.settled_at) {
|
|
139
|
+
const swept = await restoreSwept(s);
|
|
126
140
|
reopenSession(id);
|
|
127
|
-
return outcome(
|
|
141
|
+
return outcome(
|
|
142
|
+
s,
|
|
143
|
+
"reopened",
|
|
144
|
+
`${s.name} reopened${swept ? `; ${swept}` : ""}.`
|
|
145
|
+
);
|
|
128
146
|
}
|
|
129
147
|
throw new Error(`${s.name} isn't dropped, done or archived.`);
|
|
130
148
|
}
|
|
@@ -157,9 +175,12 @@ async function onItsMachine(s: Session): Promise<ResurrectOutcome> {
|
|
|
157
175
|
`/api/sessions/${encodeURIComponent(s.id)}/resurrect`,
|
|
158
176
|
{ body: {}, timeout: 120_000 }
|
|
159
177
|
);
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
178
|
+
setTask(
|
|
179
|
+
db,
|
|
180
|
+
s.id,
|
|
181
|
+
{ task_status: "running" },
|
|
182
|
+
{ task_status: ["dropped", "done"] }
|
|
183
|
+
);
|
|
163
184
|
forgetHostTasks(link.hostId);
|
|
164
185
|
return outcome(
|
|
165
186
|
s,
|
|
@@ -172,7 +193,7 @@ async function onItsMachine(s: Session): Promise<ResurrectOutcome> {
|
|
|
172
193
|
}
|
|
173
194
|
const before = db
|
|
174
195
|
.prepare(
|
|
175
|
-
`SELECT * FROM
|
|
196
|
+
`SELECT * FROM session_rows WHERE host_id = 'local' AND task_status = 'moved'
|
|
176
197
|
AND project_id IS ? AND branch_name IS ? ORDER BY updated_at DESC LIMIT 1`
|
|
177
198
|
)
|
|
178
199
|
.get(s.project_id, s.branch_name) as Session | undefined;
|
|
@@ -217,7 +238,7 @@ async function restoreTask(
|
|
|
217
238
|
if (s.branch_name) {
|
|
218
239
|
const busy = db
|
|
219
240
|
.prepare(
|
|
220
|
-
`SELECT name FROM
|
|
241
|
+
`SELECT name FROM session_rows WHERE project_id = ? AND branch_name = ? AND id != ?
|
|
221
242
|
AND task_status IN ('running', 'moving') AND host_id = 'local'`
|
|
222
243
|
)
|
|
223
244
|
.get(project.id, s.branch_name, s.id) as { name: string } | undefined;
|
|
@@ -238,11 +259,12 @@ async function restoreTask(
|
|
|
238
259
|
if (inFlight.has(s.id))
|
|
239
260
|
throw new Error(`${s.name} is being restored already.`);
|
|
240
261
|
inFlight.add(s.id);
|
|
241
|
-
const claimed =
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
262
|
+
const claimed = setTask(
|
|
263
|
+
db,
|
|
264
|
+
s.id,
|
|
265
|
+
{ task_status: "running", task_ended_at: null },
|
|
266
|
+
{ task_status: prev }
|
|
267
|
+
);
|
|
246
268
|
if (!claimed) {
|
|
247
269
|
inFlight.delete(s.id);
|
|
248
270
|
throw new Error(`${s.name} is being restored already.`);
|
|
@@ -250,9 +272,7 @@ async function restoreTask(
|
|
|
250
272
|
try {
|
|
251
273
|
return await bringBack(s, prev, repo, oldCwd, pr);
|
|
252
274
|
} catch (err) {
|
|
253
|
-
db.
|
|
254
|
-
`UPDATE sessions SET task_status = ? WHERE id = ? AND task_status = 'running'`
|
|
255
|
-
).run(prev, s.id);
|
|
275
|
+
setTask(db, s.id, { task_status: prev }, { task_status: "running" });
|
|
256
276
|
throw err;
|
|
257
277
|
} finally {
|
|
258
278
|
inFlight.delete(s.id);
|
|
@@ -310,10 +330,11 @@ async function bringBack(
|
|
|
310
330
|
if (!restored) {
|
|
311
331
|
// Nothing to make a worktree from: the conversation alone, in the project.
|
|
312
332
|
db.prepare(
|
|
313
|
-
`UPDATE sessions SET
|
|
333
|
+
`UPDATE sessions SET worktree_path = NULL,
|
|
314
334
|
working_directory = ?, settled_at = NULL, snoozed_until = NULL, snoozed_at = NULL,
|
|
315
335
|
archived_at = NULL, active_at = datetime('now') WHERE id = ?`
|
|
316
336
|
).run(cwd, s.id);
|
|
337
|
+
setTask(db, s.id, { task_status: null, task_ended_at: null });
|
|
317
338
|
notes.push(
|
|
318
339
|
"its branch and worktree were gone, so only the conversation came back, in the project folder"
|
|
319
340
|
);
|
|
@@ -335,9 +356,7 @@ async function bringBack(
|
|
|
335
356
|
try {
|
|
336
357
|
await gitPush(restored.path, ["-u", "origin", s.branch_name!]);
|
|
337
358
|
await run("gh", ["pr", "reopen", String(pr.number)], repo);
|
|
338
|
-
db.
|
|
339
|
-
s.id
|
|
340
|
-
);
|
|
359
|
+
setPr(db, s.id, { pr_status: "open" });
|
|
341
360
|
reopened = pr.number;
|
|
342
361
|
} catch (err) {
|
|
343
362
|
notes.push(
|
|
@@ -351,6 +370,11 @@ async function bringBack(
|
|
|
351
370
|
if (restored && after.view !== "chat") await relaunch(after, restored.path);
|
|
352
371
|
syncTaskCardInBackground(after, "working", null);
|
|
353
372
|
void dropSnapshot(repo, s.id);
|
|
373
|
+
const swept = sweptNote(s.id);
|
|
374
|
+
if (swept) {
|
|
375
|
+
notes.unshift(swept);
|
|
376
|
+
forgetSwept(s.id);
|
|
377
|
+
}
|
|
354
378
|
const text = `${s.name} is back${restored ? ` in ${restored.path}` : ""}${reopened ? `, PR #${reopened} reopened` : ""}${notes.length ? `; ${notes.join("; ")}` : ""}.`;
|
|
355
379
|
return outcome(s, "task", text, {
|
|
356
380
|
worktree: restored?.mode ?? "chat-only",
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A session whose worktree the age clean-up removed: bringing it back
|
|
3
|
+
* recreates the worktree from its branch, or says plainly that it's gone.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from "fs";
|
|
7
|
+
import { db, type Session } from "../db";
|
|
8
|
+
import { getProject } from "../projects";
|
|
9
|
+
import { expandHome, projectPathFor } from "../tasks/session";
|
|
10
|
+
import { forgetSwept, sweptAt } from "../worktree-sweep/store";
|
|
11
|
+
import { restoreWorktree } from "./snapshot";
|
|
12
|
+
|
|
13
|
+
const day = (t: string) => t.slice(0, 10);
|
|
14
|
+
|
|
15
|
+
// "its worktree was cleaned up on 2026-10-15" for a session the sweep removed
|
|
16
|
+
// the worktree of; null for any other.
|
|
17
|
+
export function sweptNote(id: string): string | null {
|
|
18
|
+
const at = sweptAt(id);
|
|
19
|
+
return at ? `its worktree was cleaned up on ${day(at)}` : null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* For a session coming back without a task restore (unarchive, reopen):
|
|
24
|
+
* rebuild a swept worktree. The note says what happened; null when the
|
|
25
|
+
* session wasn't swept or its worktree is still there.
|
|
26
|
+
*/
|
|
27
|
+
export async function restoreSwept(s: Session): Promise<string | null> {
|
|
28
|
+
const note = sweptNote(s.id);
|
|
29
|
+
if (!note || !s.worktree_path) return null;
|
|
30
|
+
const old = expandHome(s.worktree_path);
|
|
31
|
+
if (fs.existsSync(old)) {
|
|
32
|
+
forgetSwept(s.id);
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
const project = s.project_id ? getProject(s.project_id) : null;
|
|
36
|
+
const repo = project ? projectPathFor(s) : null;
|
|
37
|
+
const restored =
|
|
38
|
+
repo && s.branch_name && fs.existsSync(repo)
|
|
39
|
+
? await restoreWorktree({
|
|
40
|
+
sessionId: s.id,
|
|
41
|
+
repo,
|
|
42
|
+
branch: s.branch_name,
|
|
43
|
+
base: s.base_branch,
|
|
44
|
+
oldPath: old,
|
|
45
|
+
allowFresh: false,
|
|
46
|
+
}).catch(() => null)
|
|
47
|
+
: null;
|
|
48
|
+
if (restored) {
|
|
49
|
+
db.prepare(
|
|
50
|
+
`UPDATE sessions SET worktree_path = ?, working_directory = ? WHERE id = ?`
|
|
51
|
+
).run(restored.path, restored.path, s.id);
|
|
52
|
+
forgetSwept(s.id);
|
|
53
|
+
return `${note} and recreated from ${s.branch_name}`;
|
|
54
|
+
}
|
|
55
|
+
// No branch to build from: the conversation alone, in the project folder.
|
|
56
|
+
db.prepare(
|
|
57
|
+
`UPDATE sessions SET worktree_path = NULL, working_directory = ? WHERE id = ?`
|
|
58
|
+
).run(repo ?? s.working_directory, s.id);
|
|
59
|
+
forgetSwept(s.id);
|
|
60
|
+
return `${note} and its branch is gone, so only the conversation came back`;
|
|
61
|
+
}
|
package/lib/schedules/store.ts
CHANGED
|
@@ -163,7 +163,7 @@ export function findSchedule(ref: string): Schedule {
|
|
|
163
163
|
|
|
164
164
|
export function getSessionRow(id: string): Session | null {
|
|
165
165
|
return (
|
|
166
|
-
(db.prepare(`SELECT * FROM
|
|
166
|
+
(db.prepare(`SELECT * FROM session_rows WHERE id = ?`).get(id) as
|
|
167
167
|
| Session
|
|
168
168
|
| undefined) ?? null
|
|
169
169
|
);
|
|
@@ -81,7 +81,7 @@ export function loadSessions(
|
|
|
81
81
|
`SELECT s.id, s.name, s.created_at, s.role, s.view, s.task_prompt,
|
|
82
82
|
length(s.task_brief) AS task_brief_chars, s.working_directory,
|
|
83
83
|
s.worktree_path, s.claude_session_id, s.workspace_id, p.name AS project
|
|
84
|
-
FROM
|
|
84
|
+
FROM session_rows s LEFT JOIN projects p ON p.id = s.project_id
|
|
85
85
|
WHERE coalesce(s.host_id, 'local') = 'local'`
|
|
86
86
|
)
|
|
87
87
|
.all() as SessionRow[];
|
|
@@ -297,8 +297,8 @@ export function loadSignals(
|
|
|
297
297
|
}>(
|
|
298
298
|
`SELECT m.from_id, m.to_id, m.body, f.role AS from_role, (t.task_prompt IS NOT NULL) AS to_task
|
|
299
299
|
FROM bus_messages m
|
|
300
|
-
LEFT JOIN
|
|
301
|
-
LEFT JOIN
|
|
300
|
+
LEFT JOIN session_rows f ON f.id = m.from_id
|
|
301
|
+
LEFT JOIN session_rows t ON t.id = m.to_id
|
|
302
302
|
WHERE m.created_at >= ? AND m.created_at < ?`,
|
|
303
303
|
a,
|
|
304
304
|
b
|
|
@@ -120,7 +120,7 @@ export async function writeLog(
|
|
|
120
120
|
id: string,
|
|
121
121
|
opts: CaptureOpts & { plan?: string | null } = {}
|
|
122
122
|
): Promise<boolean> {
|
|
123
|
-
const row = db.prepare(`SELECT * FROM
|
|
123
|
+
const row = db.prepare(`SELECT * FROM session_rows WHERE id = ?`).get(id) as
|
|
124
124
|
| Session
|
|
125
125
|
| undefined;
|
|
126
126
|
const existing = getLog(id);
|
package/lib/session-titles.ts
CHANGED
|
@@ -283,7 +283,7 @@ export function titleChatFromMessage(
|
|
|
283
283
|
): Promise<void> | void {
|
|
284
284
|
const s = db
|
|
285
285
|
.prepare(
|
|
286
|
-
`SELECT name, name_source, working_directory, task_prompt FROM
|
|
286
|
+
`SELECT name, name_source, working_directory, task_prompt FROM session_rows WHERE id = ?`
|
|
287
287
|
)
|
|
288
288
|
.get(sessionId) as
|
|
289
289
|
| {
|
|
@@ -110,7 +110,7 @@ export function getSetup(sessionId: string): SetupView | null {
|
|
|
110
110
|
function savedSetup(sessionId: string): string | null {
|
|
111
111
|
const row = db
|
|
112
112
|
.prepare(
|
|
113
|
-
`SELECT setup_status FROM
|
|
113
|
+
`SELECT setup_status FROM session_rows WHERE id = ? AND task_status IS NULL`
|
|
114
114
|
)
|
|
115
115
|
.get(sessionId) as { setup_status: string | null } | undefined;
|
|
116
116
|
return row?.setup_status ?? null;
|
package/lib/sessions/stop.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { endTmuxSession } from "../tmux/end";
|
|
|
2
2
|
import { hostLink } from "../hosts/remote-api";
|
|
3
3
|
import { stopChatTurn } from "../chat/stop";
|
|
4
4
|
import type { Session } from "../db";
|
|
5
|
-
import { isHeldRemote, stopHeldTask } from "../tasks/held-
|
|
5
|
+
import { isHeldRemote, stopHeldTask } from "../tasks/contract/held-calls";
|
|
6
6
|
import { releaseOnEnd } from "../simulator";
|
|
7
7
|
import { forgetPlanWait } from "../models/plan-step";
|
|
8
8
|
|
|
@@ -175,7 +175,8 @@ export function failInterruptedSetups(): number {
|
|
|
175
175
|
THEN COALESCE((SELECT working_directory FROM projects WHERE id = sessions.project_id), working_directory)
|
|
176
176
|
ELSE working_directory END,
|
|
177
177
|
setup_error = 'AgentOS restarted before setup finished. Your message is still queued: send it when you are ready.'
|
|
178
|
-
WHERE setup_status = 'running'
|
|
178
|
+
WHERE setup_status = 'running'
|
|
179
|
+
AND id IN (SELECT id FROM session_rows WHERE task_status IS NULL)`
|
|
179
180
|
)
|
|
180
181
|
.run().changes;
|
|
181
182
|
}
|
package/lib/simulator/index.ts
CHANGED
|
@@ -70,7 +70,7 @@ export { simulatorAvailable };
|
|
|
70
70
|
export { shutTaskSimulators, shutFinishedTaskSimulators } from "./task-devices";
|
|
71
71
|
|
|
72
72
|
const session = (id: string) =>
|
|
73
|
-
db.prepare(`SELECT * FROM
|
|
73
|
+
db.prepare(`SELECT * FROM session_rows WHERE id = ?`).get(id) as
|
|
74
74
|
| Session
|
|
75
75
|
| undefined;
|
|
76
76
|
|
|
@@ -148,7 +148,7 @@ export async function shutFinishedTaskSimulators(): Promise<number> {
|
|
|
148
148
|
db
|
|
149
149
|
.prepare(
|
|
150
150
|
`SELECT id, branch_name, worktree_path, task_status, host_id
|
|
151
|
-
FROM
|
|
151
|
+
FROM session_rows WHERE task_status IS NOT NULL`
|
|
152
152
|
)
|
|
153
153
|
.all() as (TaskRef & { host_id: string })[]
|
|
154
154
|
).filter((t) => !isRemoteHost(t.host_id));
|
package/lib/stacks/board.ts
CHANGED
|
@@ -44,7 +44,7 @@ export function toPlanCards(
|
|
|
44
44
|
function runningTasks(boardId: string): Map<string, string> {
|
|
45
45
|
const rows = db
|
|
46
46
|
.prepare(
|
|
47
|
-
`SELECT id, lh_card_id FROM
|
|
47
|
+
`SELECT id, lh_card_id FROM session_rows
|
|
48
48
|
WHERE lh_board_id = ? AND lh_card_id IS NOT NULL AND task_status = 'running'`
|
|
49
49
|
)
|
|
50
50
|
.all(boardId) as Array<{ id: string; lh_card_id: string }>;
|
package/lib/stacks/land.ts
CHANGED
|
@@ -67,7 +67,7 @@ export const LAND_DEFAULTS: LandDeps = {
|
|
|
67
67
|
|
|
68
68
|
const sessionOf = (id: string | null) =>
|
|
69
69
|
(id
|
|
70
|
-
? db.prepare(`SELECT * FROM
|
|
70
|
+
? db.prepare(`SELECT * FROM session_rows WHERE id = ?`).get(id)
|
|
71
71
|
: undefined) as Session | undefined;
|
|
72
72
|
|
|
73
73
|
class Stop extends Error {}
|
package/lib/stacks/restack.ts
CHANGED
|
@@ -109,7 +109,7 @@ const defaults: RestackDeps = {
|
|
|
109
109
|
|
|
110
110
|
const sessionOf = (id: string | null) =>
|
|
111
111
|
id
|
|
112
|
-
? ((db.prepare(`SELECT * FROM
|
|
112
|
+
? ((db.prepare(`SELECT * FROM session_rows WHERE id = ?`).get(id) as
|
|
113
113
|
| Session
|
|
114
114
|
| undefined) ?? null)
|
|
115
115
|
: null;
|
package/lib/stacks/start.ts
CHANGED
|
@@ -25,7 +25,7 @@ async function stackedBase(
|
|
|
25
25
|
item: StackItemRow
|
|
26
26
|
) {
|
|
27
27
|
const session = db
|
|
28
|
-
.prepare(`SELECT * FROM
|
|
28
|
+
.prepare(`SELECT * FROM session_rows WHERE id = ?`)
|
|
29
29
|
.get(parent.session_id) as Session | undefined;
|
|
30
30
|
const branch = session?.branch_name;
|
|
31
31
|
if (!branch) throw new Error(`${itemName(parent)} has no branch to stack on`);
|
|
@@ -52,7 +52,7 @@ async function stackedBase(
|
|
|
52
52
|
export function existingTask(cardId: string): string | null {
|
|
53
53
|
const row = db
|
|
54
54
|
.prepare(
|
|
55
|
-
`SELECT id FROM
|
|
55
|
+
`SELECT id FROM session_rows WHERE lh_card_id = ? AND task_status = 'running'
|
|
56
56
|
ORDER BY created_at DESC LIMIT 1`
|
|
57
57
|
)
|
|
58
58
|
.get(cardId) as { id: string } | undefined;
|
package/lib/stacks/testing.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
// Test fixtures: a project, task sessions and a stack in the test database.
|
|
2
2
|
|
|
3
3
|
import { randomUUID } from "crypto";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
db,
|
|
6
|
+
insertSessionRow,
|
|
7
|
+
stackQueries as q,
|
|
8
|
+
type StackItemStatus,
|
|
9
|
+
} from "../db";
|
|
5
10
|
import { createProject } from "../projects";
|
|
6
11
|
|
|
7
12
|
export interface SeedItem {
|
|
@@ -28,22 +33,18 @@ export function seedStack(repo: string, specs: SeedItem[]) {
|
|
|
28
33
|
if (!s.branch) continue;
|
|
29
34
|
const id = randomUUID();
|
|
30
35
|
sessions.set(s.key, id);
|
|
31
|
-
db
|
|
32
|
-
`INSERT INTO sessions (id, name, tmux_name, working_directory, project_id, task_status,
|
|
33
|
-
branch_name, worktree_path, base_branch, pr_number)
|
|
34
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
35
|
-
).run(
|
|
36
|
+
insertSessionRow(db, {
|
|
36
37
|
id,
|
|
37
|
-
s.key,
|
|
38
|
-
`claude-${id}`,
|
|
39
|
-
s.worktree ?? repo,
|
|
40
|
-
project.id,
|
|
41
|
-
s.status === "merged" ? "merged" : "running",
|
|
42
|
-
s.branch,
|
|
43
|
-
s.worktree ?? null,
|
|
44
|
-
s.baseBranch ?? null,
|
|
45
|
-
s.pr ?? null
|
|
46
|
-
);
|
|
38
|
+
name: s.key,
|
|
39
|
+
tmux_name: `claude-${id}`,
|
|
40
|
+
working_directory: s.worktree ?? repo,
|
|
41
|
+
project_id: project.id,
|
|
42
|
+
task_status: s.status === "merged" ? "merged" : "running",
|
|
43
|
+
branch_name: s.branch,
|
|
44
|
+
worktree_path: s.worktree ?? null,
|
|
45
|
+
base_branch: s.baseBranch ?? null,
|
|
46
|
+
pr_number: s.pr ?? null,
|
|
47
|
+
});
|
|
47
48
|
}
|
|
48
49
|
q.create(
|
|
49
50
|
db,
|
package/lib/stacks/tick.ts
CHANGED
|
@@ -39,7 +39,7 @@ export function setStartGate(gate: StartGate | null): void {
|
|
|
39
39
|
const LIVE = new Set(["running", "pr"]);
|
|
40
40
|
|
|
41
41
|
const sessionOf = (id: string) =>
|
|
42
|
-
db.prepare(`SELECT * FROM
|
|
42
|
+
db.prepare(`SELECT * FROM session_rows WHERE id = ?`).get(id) as
|
|
43
43
|
| Session
|
|
44
44
|
| undefined;
|
|
45
45
|
|
package/lib/stacks/view.ts
CHANGED
|
@@ -51,7 +51,7 @@ const prUrlOf = (sessionId: string | null) =>
|
|
|
51
51
|
sessionId
|
|
52
52
|
? ((
|
|
53
53
|
db
|
|
54
|
-
.prepare(`SELECT pr_url FROM
|
|
54
|
+
.prepare(`SELECT pr_url FROM session_rows WHERE id = ?`)
|
|
55
55
|
.get(sessionId) as { pr_url: string | null } | undefined
|
|
56
56
|
)?.pr_url ?? null)
|
|
57
57
|
: null;
|
|
@@ -60,7 +60,7 @@ export function resetStatusCaches(): void {
|
|
|
60
60
|
function loadRows(dirty: Set<string>): void {
|
|
61
61
|
const rows = getDb()
|
|
62
62
|
.prepare(
|
|
63
|
-
`SELECT s.*, p.workspace_id AS project_workspace_id FROM
|
|
63
|
+
`SELECT s.*, p.workspace_id AS project_workspace_id FROM session_rows s
|
|
64
64
|
LEFT JOIN projects p ON p.id = s.project_id
|
|
65
65
|
WHERE s.archived_at IS NULL`
|
|
66
66
|
)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Session } from "../../db";
|
|
2
|
+
import { requireHostLink } from "../../hosts/remote-api";
|
|
3
|
+
import type { MergeMethod } from "../merge-methods";
|
|
4
|
+
import * as client from "./client";
|
|
5
|
+
import { setMirrorStatus } from "./start";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Sign off on the machine that runs it. It returns the method that machine
|
|
9
|
+
* merged with; null when it doesn't say (an older AgentOS).
|
|
10
|
+
*/
|
|
11
|
+
export async function mergeThere(
|
|
12
|
+
session: Session,
|
|
13
|
+
head?: string,
|
|
14
|
+
base?: string | null
|
|
15
|
+
): Promise<MergeMethod | null> {
|
|
16
|
+
const link = requireHostLink(session.host_id);
|
|
17
|
+
const res = await client.merge(
|
|
18
|
+
link,
|
|
19
|
+
session.id,
|
|
20
|
+
head ? { head, base } : undefined,
|
|
21
|
+
session.name
|
|
22
|
+
);
|
|
23
|
+
setMirrorStatus(session.id, "merged");
|
|
24
|
+
if (head && res.head !== head)
|
|
25
|
+
throw new Error(
|
|
26
|
+
`${link.hostName} merged without confirming it was ${head.slice(0, 7)}`
|
|
27
|
+
);
|
|
28
|
+
return res.method;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Drop on the machine that runs it. */
|
|
32
|
+
export async function dropThere(session: Session): Promise<void> {
|
|
33
|
+
await client.drop(requireHostLink(session.host_id), session.id);
|
|
34
|
+
setMirrorStatus(session.id, "dropped");
|
|
35
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who may call a task route on the machine that runs the task. One rule per
|
|
3
|
+
* route, applied the same way everywhere:
|
|
4
|
+
*
|
|
5
|
+
* - holder: a task an orchestrator on another machine holds (held.ts); only
|
|
6
|
+
* the device that holds it, and a task nobody holds is "No such held task".
|
|
7
|
+
* - open: the holder, when there is one; any caller otherwise. A mirror row
|
|
8
|
+
* or a task started here is never held, so this machine's own web UI
|
|
9
|
+
* (which sends no device) is never refused on them.
|
|
10
|
+
* - plan: open, and a plan another machine placed here is read and approved
|
|
11
|
+
* by that machine only.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
15
|
+
import { db, type Session } from "../../db";
|
|
16
|
+
import { callingDevice } from "../../security/device-me";
|
|
17
|
+
import { heldRefusal, holderRefusal, planWaitRefusal } from "../held";
|
|
18
|
+
|
|
19
|
+
export type Rule = "holder" | "open" | "plan";
|
|
20
|
+
|
|
21
|
+
type Ctx = { params: Promise<{ id: string }> };
|
|
22
|
+
|
|
23
|
+
export interface Refusal {
|
|
24
|
+
status: 403 | 404;
|
|
25
|
+
error: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function callerRefusal(
|
|
29
|
+
id: string,
|
|
30
|
+
device: string | null | undefined,
|
|
31
|
+
rule: Rule
|
|
32
|
+
): Refusal | null {
|
|
33
|
+
if (rule === "holder") {
|
|
34
|
+
const task = db
|
|
35
|
+
.prepare(
|
|
36
|
+
`SELECT held_by_device, held_from FROM session_rows WHERE id = ?`
|
|
37
|
+
)
|
|
38
|
+
.get(id) as Pick<Session, "held_by_device" | "held_from"> | undefined;
|
|
39
|
+
if (!task?.held_by_device)
|
|
40
|
+
return { status: 404, error: "No such held task" };
|
|
41
|
+
const error = holderRefusal(task, device);
|
|
42
|
+
return error ? { status: 403, error } : null;
|
|
43
|
+
}
|
|
44
|
+
const error =
|
|
45
|
+
heldRefusal(id, device) ??
|
|
46
|
+
(rule === "plan" ? planWaitRefusal(id, device) : null);
|
|
47
|
+
return error ? { status: 403, error } : null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The refusal as a response, or null when the caller may go on. */
|
|
51
|
+
export function refuse(
|
|
52
|
+
request: NextRequest,
|
|
53
|
+
id: string,
|
|
54
|
+
rule: Rule
|
|
55
|
+
): NextResponse | null {
|
|
56
|
+
const refusal = callerRefusal(id, callingDevice(request), rule);
|
|
57
|
+
return refusal
|
|
58
|
+
? NextResponse.json({ error: refusal.error }, { status: refusal.status })
|
|
59
|
+
: null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function taskRoute(
|
|
63
|
+
request: NextRequest,
|
|
64
|
+
{ params }: Ctx,
|
|
65
|
+
rule: Rule,
|
|
66
|
+
run: (task: Session) => Promise<object>
|
|
67
|
+
): Promise<NextResponse> {
|
|
68
|
+
const id = (await params).id;
|
|
69
|
+
const refused = refuse(request, id, rule);
|
|
70
|
+
if (refused) return refused;
|
|
71
|
+
const task = db.prepare(`SELECT * FROM session_rows WHERE id = ?`).get(id) as
|
|
72
|
+
| Session
|
|
73
|
+
| undefined;
|
|
74
|
+
if (!task)
|
|
75
|
+
return NextResponse.json({ error: "No such task" }, { status: 404 });
|
|
76
|
+
try {
|
|
77
|
+
return NextResponse.json(await run(task));
|
|
78
|
+
} catch (error) {
|
|
79
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
80
|
+
return NextResponse.json({ error: message }, { status: 409 });
|
|
81
|
+
}
|
|
82
|
+
}
|