@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
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export type SweepKind = "settled" | "orphan" | "test-leak";
|
|
2
|
+
|
|
3
|
+
export interface SweepRemoved {
|
|
4
|
+
path: string;
|
|
5
|
+
kind: SweepKind;
|
|
6
|
+
why: string;
|
|
7
|
+
bytes: number;
|
|
8
|
+
sessionId?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface SweepSkipped {
|
|
12
|
+
path: string;
|
|
13
|
+
kind: SweepKind;
|
|
14
|
+
reason: string;
|
|
15
|
+
sessionId?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface SweepSummary {
|
|
19
|
+
removed: SweepRemoved[];
|
|
20
|
+
skipped: SweepSkipped[];
|
|
21
|
+
freedBytes: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface SweepRun {
|
|
25
|
+
id: number;
|
|
26
|
+
startedAt: string;
|
|
27
|
+
finishedAt: string | null;
|
|
28
|
+
dryRun: boolean;
|
|
29
|
+
removed: number;
|
|
30
|
+
freedBytes: number;
|
|
31
|
+
skipped: number;
|
|
32
|
+
details: SweepSummary | null;
|
|
33
|
+
error: string | null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type DaysSource = "project" | "global" | "default";
|
|
37
|
+
|
|
38
|
+
export interface ResolvedDays {
|
|
39
|
+
days: number;
|
|
40
|
+
from: DaysSource;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// A directory the sweep may remove, once it has been judged.
|
|
44
|
+
export interface Candidate {
|
|
45
|
+
path: string;
|
|
46
|
+
kind: SweepKind;
|
|
47
|
+
sessionId?: string;
|
|
48
|
+
// The main checkout git knows it by, when it is a registered worktree.
|
|
49
|
+
repo?: string;
|
|
50
|
+
// The PR head a merge took, when the session has one.
|
|
51
|
+
prNumber?: number | null;
|
|
52
|
+
branch?: string | null;
|
|
53
|
+
}
|
package/package.json
CHANGED
|
@@ -47,7 +47,7 @@ async function main() {
|
|
|
47
47
|
const { commitLogs } = await import("../lib/session-logs/hq");
|
|
48
48
|
const rows = db
|
|
49
49
|
.prepare(
|
|
50
|
-
`SELECT id, name FROM
|
|
50
|
+
`SELECT id, name FROM session_rows
|
|
51
51
|
WHERE task_prompt IS NOT NULL AND peer_mirror = 0
|
|
52
52
|
AND (task_status = 'merged' OR (pr_status = 'merged' AND task_status != 'dropped'))
|
|
53
53
|
ORDER BY COALESCE(merged_at, updated_at)`
|
|
@@ -234,7 +234,7 @@ function tmux(args: string[]): void {
|
|
|
234
234
|
|
|
235
235
|
async function seedDb(): Promise<void> {
|
|
236
236
|
process.env.DB_PATH = DB_PATH;
|
|
237
|
-
const { getDb } = await import("../../lib/db");
|
|
237
|
+
const { getDb, insertSessionRow } = await import("../../lib/db");
|
|
238
238
|
const db = getDb();
|
|
239
239
|
const projectIds = new Map<string, string>();
|
|
240
240
|
const workspaceIds = new Map<string, string>();
|
|
@@ -261,24 +261,25 @@ async function seedDb(): Promise<void> {
|
|
|
261
261
|
sessionIds.set(s.key, id);
|
|
262
262
|
const tmuxName = `claude-${id}`;
|
|
263
263
|
const when = `-${s.minutesAgo} minutes`;
|
|
264
|
-
|
|
265
|
-
`
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
).run(
|
|
264
|
+
const at = (
|
|
265
|
+
db.prepare(`SELECT datetime('now', ?) AS t`).get(when) as { t: string }
|
|
266
|
+
).t;
|
|
267
|
+
insertSessionRow(db, {
|
|
269
268
|
id,
|
|
270
|
-
s.name,
|
|
271
|
-
tmuxName,
|
|
272
|
-
`~/code/${s.project}`,
|
|
273
|
-
projectIds.get(s.project),
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
s.
|
|
277
|
-
s.task?.
|
|
278
|
-
s.task
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
269
|
+
name: s.name,
|
|
270
|
+
tmux_name: tmuxName,
|
|
271
|
+
working_directory: `~/code/${s.project}`,
|
|
272
|
+
project_id: projectIds.get(s.project),
|
|
273
|
+
agent_type: "claude",
|
|
274
|
+
model: "opus",
|
|
275
|
+
view: s.view ?? "terminal",
|
|
276
|
+
task_prompt: s.task?.prompt ?? null,
|
|
277
|
+
task_status: s.task?.status ?? null,
|
|
278
|
+
branch_name: s.task?.branch ?? null,
|
|
279
|
+
base_branch: s.task ? "main" : null,
|
|
280
|
+
created_at: at,
|
|
281
|
+
updated_at: at,
|
|
282
|
+
});
|
|
282
283
|
if (s.chat) {
|
|
283
284
|
const insert = db.prepare(
|
|
284
285
|
`INSERT INTO chat_items (session_id, item_id, seq, data) VALUES (?, ?, ?, ?)`
|
package/server.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { recordBoot } from "./lib/health/boots";
|
|
|
18
18
|
import { realDeps, schedulesEnabled, startScheduler } from "./lib/schedules";
|
|
19
19
|
import { startSettling } from "./lib/recipes";
|
|
20
20
|
import { startAutoArchive } from "./lib/done/auto-archive";
|
|
21
|
+
import { startWorktreeSweep } from "./lib/worktree-sweep/start";
|
|
21
22
|
import { startUsageRollup, usageRollupEnabled } from "./lib/usage/nightly";
|
|
22
23
|
import { resumePhoneOutbox } from "./lib/notify";
|
|
23
24
|
import { resumePushOutbox } from "./lib/notify/push";
|
|
@@ -445,6 +446,8 @@ app.prepare().then(async () => {
|
|
|
445
446
|
startSettling();
|
|
446
447
|
// Sessions that sat in Done for 30 days are archived.
|
|
447
448
|
startAutoArchive();
|
|
449
|
+
// Settled worktrees older than the setting are removed; branches stay.
|
|
450
|
+
startWorktreeSweep();
|
|
448
451
|
resumePhoneOutbox();
|
|
449
452
|
resumePushOutbox();
|
|
450
453
|
// The usage ledger's daily rollup and its ratchet alerts.
|
package/lib/tasks/held-routes.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The routes only the device that moved a task here may call on it
|
|
3
|
-
* (held.ts): message, stop, done and read. Each is refused for any other
|
|
4
|
-
* device, and for a task that isn't held.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { NextRequest, NextResponse } from "next/server";
|
|
8
|
-
import { db, type Session } from "../db";
|
|
9
|
-
import { callingDevice } from "../security/device-me";
|
|
10
|
-
import { holderRefusal } from "./held";
|
|
11
|
-
|
|
12
|
-
type Ctx = { params: Promise<{ id: string }> };
|
|
13
|
-
|
|
14
|
-
export async function heldRoute(
|
|
15
|
-
request: NextRequest,
|
|
16
|
-
{ params }: Ctx,
|
|
17
|
-
run: (task: Session) => Promise<object>
|
|
18
|
-
): Promise<NextResponse> {
|
|
19
|
-
const id = (await params).id;
|
|
20
|
-
const task = db.prepare(`SELECT * FROM sessions WHERE id = ?`).get(id) as
|
|
21
|
-
| Session
|
|
22
|
-
| undefined;
|
|
23
|
-
if (!task?.held_by_device)
|
|
24
|
-
return NextResponse.json({ error: "No such held task" }, { status: 404 });
|
|
25
|
-
const refusal = holderRefusal(task, callingDevice(request));
|
|
26
|
-
if (refusal) return NextResponse.json({ error: refusal }, { status: 403 });
|
|
27
|
-
try {
|
|
28
|
-
return NextResponse.json(await run(task));
|
|
29
|
-
} catch (error) {
|
|
30
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
31
|
-
return NextResponse.json({ error: message }, { status: 409 });
|
|
32
|
-
}
|
|
33
|
-
}
|
package/lib/tasks/remote.ts
DELETED
|
@@ -1,255 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tasks that run on another machine's AgentOS. That machine schedules them;
|
|
3
|
-
* this one keeps a mirror row (same id, the host's id) so the sidebar lists
|
|
4
|
-
* them and the terminal attaches through that machine's AgentOS, and asks it for their
|
|
5
|
-
* state, sign-off and drop.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { MERGE_METHODS, type MergeMethod } from "./merge-methods";
|
|
9
|
-
import { db, type Project, type Session } from "../db";
|
|
10
|
-
import {
|
|
11
|
-
HostApiError,
|
|
12
|
-
hostApi,
|
|
13
|
-
requireHostLink,
|
|
14
|
-
type HostLink,
|
|
15
|
-
} from "../hosts/remote-api";
|
|
16
|
-
import { isRemoteHost } from "../hosts";
|
|
17
|
-
import { projectRef } from "./project-ref";
|
|
18
|
-
import { orchestratedStart } from "./held";
|
|
19
|
-
import type { TaskStatus } from "./state";
|
|
20
|
-
import type { TaskView } from "./index";
|
|
21
|
-
import { orchestratorOf } from "./starter";
|
|
22
|
-
|
|
23
|
-
// What this machine's task API does, for another machine deciding whether
|
|
24
|
-
// it can trust it with a pinned merge or a move; keyed-start: a session
|
|
25
|
-
// start takes a key, so it's safe to retry (lib/sessions/launch.ts).
|
|
26
|
-
export const TASK_CAPABILITIES = [
|
|
27
|
-
"pinned-merge",
|
|
28
|
-
"move",
|
|
29
|
-
"chat-turn",
|
|
30
|
-
"keyed-start",
|
|
31
|
-
"chat-move",
|
|
32
|
-
// Holds an orchestrator's task for the machine that sent it, and answers
|
|
33
|
-
// its messages, reads, stops and done (lib/tasks/held.ts).
|
|
34
|
-
"orchestrated-move",
|
|
35
|
-
// A held task's chat facts and its messages for its orchestrator.
|
|
36
|
-
"held-chat-facts",
|
|
37
|
-
"held-replies",
|
|
38
|
-
// A keyed start that carries an orchestrator's description holds the task
|
|
39
|
-
// for the device that sent it, as a move does (lib/tasks/held.ts).
|
|
40
|
-
"held-start",
|
|
41
|
-
// A plan placed here by another machine stops at the plan for that machine
|
|
42
|
-
// to decide what follows (lib/tasks/remote-plan.ts).
|
|
43
|
-
"remote-plan",
|
|
44
|
-
] as const;
|
|
45
|
-
|
|
46
|
-
export interface HostTasks {
|
|
47
|
-
tasks: TaskView[];
|
|
48
|
-
moved?: { id: string; movedTo: string | null }[];
|
|
49
|
-
capabilities?: string[];
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function isMirror(session: Session): boolean {
|
|
53
|
-
return isRemoteHost(session.host_id);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function mirrorTask(
|
|
57
|
-
hostId: string,
|
|
58
|
-
projectId: string | null,
|
|
59
|
-
remote: Session,
|
|
60
|
-
startedBy?: string | null
|
|
61
|
-
): void {
|
|
62
|
-
db.prepare(
|
|
63
|
-
`INSERT INTO sessions (id, name, tmux_name, working_directory, model, group_path,
|
|
64
|
-
agent_type, auto_approve, project_id, host_id, worktree_path, branch_name,
|
|
65
|
-
base_branch, task_prompt, task_status, name_source, view)
|
|
66
|
-
VALUES (?, ?, ?, ?, ?, 'sessions', 'claude', 1, ?, ?, ?, ?, ?, ?, 'running', 'user', ?)
|
|
67
|
-
ON CONFLICT(id) DO NOTHING`
|
|
68
|
-
).run(
|
|
69
|
-
remote.id,
|
|
70
|
-
remote.name,
|
|
71
|
-
remote.tmux_name,
|
|
72
|
-
remote.working_directory,
|
|
73
|
-
remote.model,
|
|
74
|
-
projectId,
|
|
75
|
-
hostId,
|
|
76
|
-
remote.worktree_path,
|
|
77
|
-
remote.branch_name,
|
|
78
|
-
remote.base_branch,
|
|
79
|
-
remote.task_prompt,
|
|
80
|
-
// As that machine runs it: one that predates chat tasks says terminal.
|
|
81
|
-
remote.view === "chat" ? "chat" : "terminal"
|
|
82
|
-
);
|
|
83
|
-
// What carryTaskState gives a moved task's mirror: the orchestrator that
|
|
84
|
-
// holds it, and when it began (so its PR is judged from then).
|
|
85
|
-
if (startedBy)
|
|
86
|
-
db.prepare(
|
|
87
|
-
`UPDATE sessions SET started_by = COALESCE(started_by, ?),
|
|
88
|
-
task_since = COALESCE(task_since, created_at) WHERE id = ?`
|
|
89
|
-
).run(startedBy, remote.id);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* A machine on an older AgentOS would take a chat task as a terminal one,
|
|
94
|
-
* or an orchestrator's task as a plain one that anyone there could merge, or
|
|
95
|
-
* refuse to hand one over.
|
|
96
|
-
*/
|
|
97
|
-
export async function requireCapability(
|
|
98
|
-
link: HostLink,
|
|
99
|
-
capability: string,
|
|
100
|
-
what: string,
|
|
101
|
-
verb = "move"
|
|
102
|
-
): Promise<void> {
|
|
103
|
-
const { capabilities = [] } = await hostTasks(link, true).catch((err) => {
|
|
104
|
-
throw new Error(
|
|
105
|
-
`Can't ask ${link.hostName} whether it takes ${what}: ${(err as Error).message}`
|
|
106
|
-
);
|
|
107
|
-
});
|
|
108
|
-
if (!capabilities.includes(capability))
|
|
109
|
-
throw new Error(
|
|
110
|
-
`${link.hostName}'s AgentOS can't ${verb} ${what} yet; update it first`
|
|
111
|
-
);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export const unknownOutcome = (err: unknown) =>
|
|
115
|
-
err instanceof HostApiError && !err.refused;
|
|
116
|
-
|
|
117
|
-
/** POST a call that's safe to repeat; once more if we can't tell it landed. */
|
|
118
|
-
export async function postIdempotent<T>(
|
|
119
|
-
link: HostLink,
|
|
120
|
-
path: string,
|
|
121
|
-
body: unknown,
|
|
122
|
-
timeout: number
|
|
123
|
-
): Promise<T> {
|
|
124
|
-
try {
|
|
125
|
-
return await hostApi<T>(link, path, { body, timeout });
|
|
126
|
-
} catch (err) {
|
|
127
|
-
if (!unknownOutcome(err)) throw err;
|
|
128
|
-
return hostApi<T>(link, path, { body, timeout });
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export async function startRemoteTask(
|
|
133
|
-
hostId: string,
|
|
134
|
-
project: Project,
|
|
135
|
-
opts: {
|
|
136
|
-
id: string;
|
|
137
|
-
prompt: string;
|
|
138
|
-
name?: string;
|
|
139
|
-
model?: string;
|
|
140
|
-
baseBranch?: string;
|
|
141
|
-
view?: "chat" | "terminal";
|
|
142
|
-
// The orchestrator that started it: that machine holds the task for it.
|
|
143
|
-
startedBy?: string | null;
|
|
144
|
-
// It plans first: there it plans, stops, and waits for this machine.
|
|
145
|
-
plan?: { only: boolean; reason: string | null };
|
|
146
|
-
}
|
|
147
|
-
): Promise<Session> {
|
|
148
|
-
const link = requireHostLink(hostId);
|
|
149
|
-
const { startedBy: by, plan, ...rest } = opts;
|
|
150
|
-
const startedBy = orchestratorOf(by);
|
|
151
|
-
if (startedBy)
|
|
152
|
-
await requireCapability(
|
|
153
|
-
link,
|
|
154
|
-
"held-start",
|
|
155
|
-
"an orchestrator's task",
|
|
156
|
-
"hold"
|
|
157
|
-
);
|
|
158
|
-
if (plan) await requireCapability(link, "remote-plan", "a plan step", "plan");
|
|
159
|
-
const { session } = await postIdempotent<{ session: Session }>(
|
|
160
|
-
link,
|
|
161
|
-
"/api/tasks",
|
|
162
|
-
{
|
|
163
|
-
project: await projectRef(project),
|
|
164
|
-
...rest,
|
|
165
|
-
...(plan && {
|
|
166
|
-
plan: true,
|
|
167
|
-
plan_only: true,
|
|
168
|
-
plan_waits: true,
|
|
169
|
-
plan_reason: plan.reason ?? undefined,
|
|
170
|
-
}),
|
|
171
|
-
orchestrated: startedBy ? orchestratedStart(startedBy) : undefined,
|
|
172
|
-
},
|
|
173
|
-
180000
|
|
174
|
-
);
|
|
175
|
-
mirrorTask(hostId, project.id, session, startedBy);
|
|
176
|
-
if (plan)
|
|
177
|
-
db.prepare(
|
|
178
|
-
`UPDATE sessions SET plan_phase = 'planning', plan_only = ?, plan_decided = 1,
|
|
179
|
-
plan_reason = ?, plan_waits_for_caller = 1 WHERE id = ?`
|
|
180
|
-
).run(plan.only ? 1 : null, plan.reason, session.id);
|
|
181
|
-
forgetHostTasks(hostId);
|
|
182
|
-
return db
|
|
183
|
-
.prepare(`SELECT * FROM sessions WHERE id = ?`)
|
|
184
|
-
.get(session.id) as Session;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const cache = new Map<string, { at: number; list: Promise<HostTasks> }>();
|
|
188
|
-
|
|
189
|
-
export function hostTasks(link: HostLink, fresh = false): Promise<HostTasks> {
|
|
190
|
-
const hit = cache.get(link.hostId);
|
|
191
|
-
if (!fresh && hit && Date.now() - hit.at < 3000) return hit.list;
|
|
192
|
-
const list = hostApi<HostTasks>(link, "/api/tasks", { timeout: 8000 });
|
|
193
|
-
cache.set(link.hostId, { at: Date.now(), list });
|
|
194
|
-
list.catch(() => cache.delete(link.hostId));
|
|
195
|
-
return list;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
export const forgetHostTasks = (hostId: string) => cache.delete(hostId);
|
|
199
|
-
|
|
200
|
-
export function setMirrorStatus(id: string, status: TaskStatus): void {
|
|
201
|
-
db.prepare(
|
|
202
|
-
`UPDATE sessions SET task_status = ? WHERE id = ? AND task_status = 'running'`
|
|
203
|
-
).run(status, id);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Sign off or drop on the machine that runs it. A merge returns the method
|
|
208
|
-
* that machine merged with; null when it doesn't say (an older AgentOS).
|
|
209
|
-
*/
|
|
210
|
-
export async function remoteTaskAction(
|
|
211
|
-
session: Session,
|
|
212
|
-
action: "merge" | "drop",
|
|
213
|
-
head?: string,
|
|
214
|
-
base?: string | null
|
|
215
|
-
): Promise<MergeMethod | null> {
|
|
216
|
-
const link = requireHostLink(session.host_id);
|
|
217
|
-
// An AgentOS that predates pinned merges would ignore the head and merge
|
|
218
|
-
// whatever the PR is at: refuse before asking it.
|
|
219
|
-
if (head) {
|
|
220
|
-
const { capabilities = [] } = await hostTasks(link, true);
|
|
221
|
-
if (!capabilities.includes("pinned-merge"))
|
|
222
|
-
throw new Error(
|
|
223
|
-
`${link.hostName}'s AgentOS can't pin a merge to a commit; update it first`
|
|
224
|
-
);
|
|
225
|
-
}
|
|
226
|
-
// base: what the gates here were judged against; the machine that merges
|
|
227
|
-
// compares the PR with it, not with a base only it recorded.
|
|
228
|
-
const res = await hostApi<{ head?: string | null; method?: unknown }>(
|
|
229
|
-
link,
|
|
230
|
-
`/api/tasks/${encodeURIComponent(session.id)}/${action}`,
|
|
231
|
-
{
|
|
232
|
-
body: { ...(head && { head }), ...(head && base && { base }) },
|
|
233
|
-
timeout: 300000,
|
|
234
|
-
}
|
|
235
|
-
).catch(async (err) => {
|
|
236
|
-
if (action !== "merge" || !unknownOutcome(err)) throw err;
|
|
237
|
-
// A proxy or a dropped connection after the merge ran answers 502 with
|
|
238
|
-
// nothing from AgentOS: ask what state the task is in before failing.
|
|
239
|
-
const merged = await hostTasks(link, true)
|
|
240
|
-
.then((l) => l.tasks.find((t) => t.id === session.id)?.state === "merged")
|
|
241
|
-
.catch(() => false);
|
|
242
|
-
if (!merged)
|
|
243
|
-
throw new Error(
|
|
244
|
-
`${(err as Error).message}: couldn't confirm whether ${link.hostName} merged ${session.name}; check its PR before signing off again`
|
|
245
|
-
);
|
|
246
|
-
return { head: head ?? null, method: undefined };
|
|
247
|
-
});
|
|
248
|
-
forgetHostTasks(link.hostId);
|
|
249
|
-
setMirrorStatus(session.id, action === "merge" ? "merged" : "dropped");
|
|
250
|
-
if (head && res.head !== head)
|
|
251
|
-
throw new Error(
|
|
252
|
-
`${link.hostName} merged without confirming it was ${head.slice(0, 7)}`
|
|
253
|
-
);
|
|
254
|
-
return MERGE_METHODS.find((m) => m === res.method) ?? null;
|
|
255
|
-
}
|