@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
|
@@ -8,38 +8,19 @@
|
|
|
8
8
|
|
|
9
9
|
import { db, type Session } from "../db";
|
|
10
10
|
import { hostIdNamed } from "../hosts";
|
|
11
|
-
import {
|
|
11
|
+
import { hostLink, type HostLink } from "../hosts/remote-api";
|
|
12
12
|
import { getTaskSession } from "./session";
|
|
13
13
|
import { markMoved, resumeTask } from "./move";
|
|
14
|
-
import
|
|
14
|
+
import * as client from "./contract/client";
|
|
15
|
+
import { mirrorTask } from "./contract/start";
|
|
15
16
|
import { carryTaskState } from "./move-carry";
|
|
16
17
|
|
|
17
|
-
/** POST, repeated when the outcome is unknown (the calls are idempotent). */
|
|
18
|
-
export async function tell(
|
|
19
|
-
link: HostLink,
|
|
20
|
-
path: string,
|
|
21
|
-
body: object = {}
|
|
22
|
-
): Promise<void> {
|
|
23
|
-
for (let i = 0; ; i++) {
|
|
24
|
-
try {
|
|
25
|
-
await hostApi(link, path, { body });
|
|
26
|
-
return;
|
|
27
|
-
} catch (err) {
|
|
28
|
-
if (!unknownOutcome(err) || i === 2) throw err;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
18
|
/** The task a move from this session made there, or null. Throws if unsure. */
|
|
34
19
|
export async function arrivedThere(
|
|
35
20
|
link: HostLink,
|
|
36
21
|
sessionId: string
|
|
37
22
|
): Promise<Session | null> {
|
|
38
|
-
|
|
39
|
-
link,
|
|
40
|
-
`/api/tasks/arrived?from=${encodeURIComponent(sessionId)}`
|
|
41
|
-
);
|
|
42
|
-
return session;
|
|
23
|
+
return client.arrived(link, sessionId);
|
|
43
24
|
}
|
|
44
25
|
|
|
45
26
|
export function settleMovedOut(
|
|
@@ -53,9 +34,9 @@ export function settleMovedOut(
|
|
|
53
34
|
mirrorTask(hostId, session.project_id, there);
|
|
54
35
|
carryTaskState(session.id, there.id);
|
|
55
36
|
})();
|
|
56
|
-
|
|
37
|
+
client.forget(hostId);
|
|
57
38
|
return db
|
|
58
|
-
.prepare(`SELECT * FROM
|
|
39
|
+
.prepare(`SELECT * FROM session_rows WHERE id = ?`)
|
|
59
40
|
.get(there.id) as Session;
|
|
60
41
|
}
|
|
61
42
|
|
|
@@ -6,7 +6,7 @@ import fs from "fs";
|
|
|
6
6
|
import os from "os";
|
|
7
7
|
import path from "path";
|
|
8
8
|
import { randomUUID } from "crypto";
|
|
9
|
-
import { db, type Session } from "../db";
|
|
9
|
+
import { db, insertSessionRow, type Session } from "../db";
|
|
10
10
|
import { createProject } from "../projects";
|
|
11
11
|
import { writeTranscript } from "./transcript";
|
|
12
12
|
import { gitSync } from "../git-exec";
|
|
@@ -17,7 +17,7 @@ export const git = (cwd: string, ...args: string[]) =>
|
|
|
17
17
|
gitSync(cwd, args).trim();
|
|
18
18
|
|
|
19
19
|
export const row = (id: string) =>
|
|
20
|
-
db.prepare(`SELECT * FROM
|
|
20
|
+
db.prepare(`SELECT * FROM session_rows WHERE id = ?`).get(id) as Session;
|
|
21
21
|
|
|
22
22
|
export interface MoveFixture {
|
|
23
23
|
tmp: string;
|
|
@@ -71,11 +71,20 @@ export async function seedTask(
|
|
|
71
71
|
git(f.repo, "worktree", "add", "-q", "-b", branch, cwd, "main");
|
|
72
72
|
fs.writeFileSync(path.join(cwd, "work.txt"), "half done\n");
|
|
73
73
|
const id = randomUUID();
|
|
74
|
-
db
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
74
|
+
insertSessionRow(db, {
|
|
75
|
+
id,
|
|
76
|
+
name: "Fix it",
|
|
77
|
+
tmux_name: `claude-${id}`,
|
|
78
|
+
working_directory: cwd,
|
|
79
|
+
project_id: f.projectId,
|
|
80
|
+
task_status: "running",
|
|
81
|
+
task_prompt: "fix the thing",
|
|
82
|
+
branch_name: branch,
|
|
83
|
+
worktree_path: cwd,
|
|
84
|
+
base_branch: "main",
|
|
85
|
+
claude_session_id: CLAUDE_ID,
|
|
86
|
+
model: "sonnet",
|
|
87
|
+
});
|
|
79
88
|
await writeTranscript(
|
|
80
89
|
cwd,
|
|
81
90
|
CLAUDE_ID,
|
package/lib/tasks/move.ts
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
latestSessionId,
|
|
37
37
|
readTranscript,
|
|
38
38
|
} from "./transcript";
|
|
39
|
+
import { setTask } from "../db/session-parts";
|
|
39
40
|
|
|
40
41
|
export * from "./move-bundle";
|
|
41
42
|
|
|
@@ -88,13 +89,13 @@ async function pushOwnBranch(cwd: string, branch: string): Promise<void> {
|
|
|
88
89
|
* arrived, and then it would run on both.
|
|
89
90
|
*/
|
|
90
91
|
function claimMoving(id: string, to: string): { fresh: boolean } {
|
|
91
|
-
const fresh =
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (fresh
|
|
92
|
+
const fresh = setTask(
|
|
93
|
+
db,
|
|
94
|
+
id,
|
|
95
|
+
{ task_status: "moving", moved_to: to },
|
|
96
|
+
{ task_status: "running" }
|
|
97
|
+
);
|
|
98
|
+
if (fresh) return { fresh: true };
|
|
98
99
|
const { task_status, moved_to } = getTaskSession(id);
|
|
99
100
|
if (task_status === "moving" && moved_to === to) return { fresh: false };
|
|
100
101
|
throw new Error(
|
|
@@ -250,12 +251,12 @@ export async function exportOrResume(
|
|
|
250
251
|
}
|
|
251
252
|
|
|
252
253
|
export function markMoved(id: string, to: string): void {
|
|
253
|
-
const moved =
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
254
|
+
const moved = setTask(
|
|
255
|
+
db,
|
|
256
|
+
id,
|
|
257
|
+
{ task_status: "moved", moved_to: to },
|
|
258
|
+
{ task_status: ["running", "moving"] }
|
|
259
|
+
);
|
|
259
260
|
// It goes on there with a copy of its own.
|
|
260
261
|
if (moved) void dropSessionDatabase(id);
|
|
261
262
|
}
|
|
@@ -269,25 +270,24 @@ export function markMoved(id: string, to: string): void {
|
|
|
269
270
|
*/
|
|
270
271
|
export async function resumeTask(id: string, note?: string): Promise<void> {
|
|
271
272
|
if (exporting.has(id)) throw new Error("It's moving right now");
|
|
272
|
-
const claimed =
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
273
|
+
const claimed = setTask(
|
|
274
|
+
db,
|
|
275
|
+
id,
|
|
276
|
+
{ task_status: "running" },
|
|
277
|
+
{ task_status: "moving" }
|
|
278
|
+
);
|
|
277
279
|
const session = getTaskSession(id);
|
|
278
|
-
if (claimed
|
|
280
|
+
if (!claimed) {
|
|
279
281
|
if (session.task_status === "running") return;
|
|
280
282
|
throw new Error(`Task is already ${session.task_status}`);
|
|
281
283
|
}
|
|
282
284
|
try {
|
|
283
285
|
await relaunch(session, note);
|
|
284
286
|
} catch (err) {
|
|
285
|
-
db
|
|
286
|
-
`UPDATE sessions SET task_status = 'moving' WHERE id = ? AND task_status = 'running'`
|
|
287
|
-
).run(id);
|
|
287
|
+
setTask(db, id, { task_status: "moving" }, { task_status: "running" });
|
|
288
288
|
throw err;
|
|
289
289
|
}
|
|
290
|
-
db
|
|
290
|
+
setTask(db, id, { moved_to: null });
|
|
291
291
|
}
|
|
292
292
|
|
|
293
293
|
async function relaunch(session: Session, note?: string): Promise<void> {
|
package/lib/tasks/moved-away.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { settleWorktree, type WorktreeFate } from "../done/worktree";
|
|
|
12
12
|
import { archiveSession } from "../done/archive";
|
|
13
13
|
|
|
14
14
|
export async function cleanUpMovedAway(id: string): Promise<WorktreeFate> {
|
|
15
|
-
const s = db.prepare(`SELECT * FROM
|
|
15
|
+
const s = db.prepare(`SELECT * FROM session_rows WHERE id = ?`).get(id) as
|
|
16
16
|
| Session
|
|
17
17
|
| undefined;
|
|
18
18
|
if (!s || s.task_status !== "moved" || !s.held_by_device)
|
package/lib/tasks/pause-hold.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// can hold a message for it without loading the pause machinery.
|
|
3
3
|
|
|
4
4
|
import { db, type TaskPause } from "../db";
|
|
5
|
+
import { setPause } from "../db/session-parts";
|
|
5
6
|
|
|
6
7
|
export interface PauseHold {
|
|
7
8
|
pauseId: number;
|
|
@@ -11,7 +12,7 @@ export interface PauseHold {
|
|
|
11
12
|
export function pauseHoldOf(sessionId: string): PauseHold | null {
|
|
12
13
|
const row = db
|
|
13
14
|
.prepare(
|
|
14
|
-
`SELECT pause_id, paused_until FROM
|
|
15
|
+
`SELECT pause_id, paused_until FROM session_rows
|
|
15
16
|
WHERE id = ? AND pause_state IN ('pausing', 'paused') AND pause_id IS NOT NULL`
|
|
16
17
|
)
|
|
17
18
|
.get(sessionId) as
|
|
@@ -45,10 +46,17 @@ export function endPause(
|
|
|
45
46
|
`UPDATE task_pauses SET state = 'ended', ended_at = datetime('now'), end_reason = ?
|
|
46
47
|
WHERE id = ? AND state != 'ended'`
|
|
47
48
|
).run(reason, row.id);
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
setPause(
|
|
50
|
+
db,
|
|
51
|
+
row.session_id,
|
|
52
|
+
{
|
|
53
|
+
pause_state: null,
|
|
54
|
+
paused_until: null,
|
|
55
|
+
pause_id: null,
|
|
56
|
+
pause_note: null,
|
|
57
|
+
},
|
|
58
|
+
{ pause_id: row.id }
|
|
59
|
+
);
|
|
52
60
|
})();
|
|
53
61
|
}
|
|
54
62
|
|
package/lib/tasks/pause.ts
CHANGED
|
@@ -50,6 +50,7 @@ import {
|
|
|
50
50
|
type PauseView,
|
|
51
51
|
type Priority,
|
|
52
52
|
} from "./schedule-types";
|
|
53
|
+
import { setPause } from "../db/session-parts";
|
|
53
54
|
|
|
54
55
|
export class PauseRefused extends Error {}
|
|
55
56
|
|
|
@@ -72,7 +73,7 @@ const nudge = () =>
|
|
|
72
73
|
void import("./queue").then((q) => q.tickQueueSoon()).catch(() => undefined);
|
|
73
74
|
|
|
74
75
|
const sessionOf = (id: string) =>
|
|
75
|
-
db.prepare(`SELECT * FROM
|
|
76
|
+
db.prepare(`SELECT * FROM session_rows WHERE id = ?`).get(id) as
|
|
76
77
|
| Session
|
|
77
78
|
| undefined;
|
|
78
79
|
|
|
@@ -165,10 +166,9 @@ export function requestPause(sessionId: string, req: PauseRequest): TaskPause {
|
|
|
165
166
|
until ? toSqliteTime(until) : null,
|
|
166
167
|
existing.id
|
|
167
168
|
);
|
|
168
|
-
db
|
|
169
|
-
until ? toSqliteTime(until) : null,
|
|
170
|
-
|
|
171
|
-
);
|
|
169
|
+
setPause(db, sessionId, {
|
|
170
|
+
paused_until: until ? toSqliteTime(until) : null,
|
|
171
|
+
});
|
|
172
172
|
// "Pause now" takes over: the first driver is superseded, not raced.
|
|
173
173
|
drive(sessionId, existing.id, mode === "now");
|
|
174
174
|
return pauseRow(existing.id)!;
|
|
@@ -190,9 +190,11 @@ export function requestPause(sessionId: string, req: PauseRequest): TaskPause {
|
|
|
190
190
|
until ? toSqliteTime(until) : null
|
|
191
191
|
).lastInsertRowid
|
|
192
192
|
);
|
|
193
|
-
db
|
|
194
|
-
|
|
195
|
-
|
|
193
|
+
setPause(db, sessionId, {
|
|
194
|
+
pause_state: "pausing",
|
|
195
|
+
paused_until: until ? toSqliteTime(until) : null,
|
|
196
|
+
pause_id: id,
|
|
197
|
+
});
|
|
196
198
|
drive(sessionId, id);
|
|
197
199
|
return pauseRow(id)!;
|
|
198
200
|
}
|
|
@@ -307,10 +309,7 @@ function markPaused(sessionId: string, pauseId: number): void {
|
|
|
307
309
|
WHERE id = ? AND state = 'pausing'`
|
|
308
310
|
)
|
|
309
311
|
.run(pauseId).changes;
|
|
310
|
-
if (n)
|
|
311
|
-
db.prepare(`UPDATE sessions SET pause_state = 'paused' WHERE id = ?`).run(
|
|
312
|
-
sessionId
|
|
313
|
-
);
|
|
312
|
+
if (n) setPause(db, sessionId, { pause_state: "paused" });
|
|
314
313
|
return n;
|
|
315
314
|
})();
|
|
316
315
|
if (!changed) return;
|
|
@@ -359,10 +358,7 @@ export function setResumeTime(sessionId: string, until: Date | null): void {
|
|
|
359
358
|
db.prepare(
|
|
360
359
|
`UPDATE task_pauses SET until_at = ?, resume_requested_at = NULL WHERE id = ? AND state IN ('pausing','paused')`
|
|
361
360
|
).run(value, row.id);
|
|
362
|
-
db
|
|
363
|
-
value,
|
|
364
|
-
sessionId
|
|
365
|
-
);
|
|
361
|
+
setPause(db, sessionId, { paused_until: value });
|
|
366
362
|
})();
|
|
367
363
|
}
|
|
368
364
|
|
|
@@ -396,9 +392,11 @@ export function requestResume(sessionId: string, actor: PauseBy): void {
|
|
|
396
392
|
export { loadHoldFor, resumeHoldFor };
|
|
397
393
|
|
|
398
394
|
export function setPauseNote(sessionId: string, note: string | null): void {
|
|
399
|
-
db
|
|
400
|
-
`
|
|
401
|
-
|
|
395
|
+
const row = db
|
|
396
|
+
.prepare(`SELECT pause_note FROM session_rows WHERE id = ?`)
|
|
397
|
+
.get(sessionId) as { pause_note: string | null } | undefined;
|
|
398
|
+
if ((row?.pause_note ?? "") === (note ?? "")) return;
|
|
399
|
+
setPause(db, sessionId, { pause_note: note }, { pause_state: "paused" });
|
|
402
400
|
}
|
|
403
401
|
|
|
404
402
|
export type ResumeOutcome =
|
|
@@ -453,7 +451,7 @@ export function dueResumes(): DueResume[] {
|
|
|
453
451
|
.prepare(
|
|
454
452
|
`SELECT p.id AS pause_id, p.session_id, p.by_kind, p.until_at, p.resume_requested_at, p.resume_by_kind,
|
|
455
453
|
s.project_id, s.priority
|
|
456
|
-
FROM task_pauses p JOIN
|
|
454
|
+
FROM task_pauses p JOIN session_rows s ON s.id = p.session_id
|
|
457
455
|
WHERE p.state = 'paused' AND p.resume_requested_at IS NOT NULL
|
|
458
456
|
AND s.task_status IN ('running') AND s.archived_at IS NULL`
|
|
459
457
|
)
|
|
@@ -583,9 +581,7 @@ export async function claimResume(
|
|
|
583
581
|
}),
|
|
584
582
|
Date.now()
|
|
585
583
|
);
|
|
586
|
-
db
|
|
587
|
-
`UPDATE sessions SET pause_state = 'resuming', pause_note = NULL WHERE id = ?`
|
|
588
|
-
).run(sessionId);
|
|
584
|
+
setPause(db, sessionId, { pause_state: "resuming", pause_note: null });
|
|
589
585
|
return true;
|
|
590
586
|
})();
|
|
591
587
|
if (!claimed) return "gone";
|
package/lib/tasks/plan-view.ts
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
} from "./loop-types";
|
|
19
19
|
import { expandHome } from "./session";
|
|
20
20
|
import { isRemoteHost } from "../hosts";
|
|
21
|
-
import { remotePlanPath } from "./
|
|
21
|
+
import { remotePlanPath } from "./contract/plan-path";
|
|
22
22
|
|
|
23
23
|
const TITLE_MAX = 120;
|
|
24
24
|
|
package/lib/tasks/pr-activity.ts
CHANGED
|
@@ -275,9 +275,19 @@ export async function failedLog(
|
|
|
275
275
|
repo: Repo,
|
|
276
276
|
url: string | null | undefined
|
|
277
277
|
): Promise<FailedLog | null> {
|
|
278
|
+
const raw = await failedJobLog(dir, repo, url);
|
|
279
|
+
return raw === null ? null : trimLog(raw);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// The same log whole, as gh prints it.
|
|
283
|
+
export async function failedJobLog(
|
|
284
|
+
dir: string,
|
|
285
|
+
repo: Repo,
|
|
286
|
+
url: string | null | undefined
|
|
287
|
+
): Promise<string | null> {
|
|
278
288
|
const job = jobOf(url);
|
|
279
289
|
if (!job) return null;
|
|
280
|
-
|
|
290
|
+
return run(
|
|
281
291
|
"gh",
|
|
282
292
|
[
|
|
283
293
|
"run",
|
|
@@ -291,5 +301,4 @@ export async function failedLog(
|
|
|
291
301
|
dir,
|
|
292
302
|
60000
|
|
293
303
|
);
|
|
294
|
-
return trimLog(raw);
|
|
295
304
|
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// `aos branches prune`: the stale remote branches AgentOS tasks left behind.
|
|
2
|
+
// A branch is stale when it is a task's recorded branch, its latest PR is
|
|
3
|
+
// merged or closed at the branch's current commit, and no open PR builds on
|
|
4
|
+
// it. Dry run by default; deleting goes through the same checks as a merge.
|
|
5
|
+
|
|
6
|
+
import { db } from "../db";
|
|
7
|
+
import { run } from "./gh";
|
|
8
|
+
import { PROTECTED_BRANCHES, deleteTaskBranch } from "./remote-branch";
|
|
9
|
+
|
|
10
|
+
export interface PruneRepo {
|
|
11
|
+
dir: string;
|
|
12
|
+
slug: string;
|
|
13
|
+
projectIds: string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface StaleBranch {
|
|
17
|
+
branch: string;
|
|
18
|
+
pr: number;
|
|
19
|
+
state: "MERGED" | "CLOSED";
|
|
20
|
+
head: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface RepoPrune {
|
|
24
|
+
slug: string;
|
|
25
|
+
stale: StaleBranch[];
|
|
26
|
+
kept: { branch: string; why: string }[];
|
|
27
|
+
deleted: string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface ListedPR {
|
|
31
|
+
number: number;
|
|
32
|
+
state: string;
|
|
33
|
+
headRefName: string;
|
|
34
|
+
headRefOid: string;
|
|
35
|
+
baseRefName: string;
|
|
36
|
+
isCrossRepository: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const taskBranches = (projectIds: string[]): Set<string> => {
|
|
40
|
+
if (!projectIds.length) return new Set();
|
|
41
|
+
const rows = db
|
|
42
|
+
.prepare(
|
|
43
|
+
`SELECT DISTINCT branch_name FROM session_rows
|
|
44
|
+
WHERE branch_name IS NOT NULL
|
|
45
|
+
AND project_id IN (${projectIds.map(() => "?").join(", ")})`
|
|
46
|
+
)
|
|
47
|
+
.all(...projectIds) as { branch_name: string }[];
|
|
48
|
+
return new Set(rows.map((r) => r.branch_name));
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
async function remoteBranches(repo: PruneRepo): Promise<Map<string, string>> {
|
|
52
|
+
const out = await run(
|
|
53
|
+
"gh",
|
|
54
|
+
[
|
|
55
|
+
"api",
|
|
56
|
+
`repos/${repo.slug}/branches`,
|
|
57
|
+
"--paginate",
|
|
58
|
+
"--jq",
|
|
59
|
+
".[] | [.name, .commit.sha] | @tsv",
|
|
60
|
+
],
|
|
61
|
+
repo.dir,
|
|
62
|
+
120000
|
|
63
|
+
);
|
|
64
|
+
const tips = new Map<string, string>();
|
|
65
|
+
for (const line of out.split("\n")) {
|
|
66
|
+
const [name, sha] = line.split("\t");
|
|
67
|
+
if (name && sha) tips.set(name, sha);
|
|
68
|
+
}
|
|
69
|
+
return tips;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function pruneRepo(
|
|
73
|
+
repo: PruneRepo,
|
|
74
|
+
opts: { yes?: boolean } = {}
|
|
75
|
+
): Promise<RepoPrune> {
|
|
76
|
+
const [tips, prsOut, defaultOut] = await Promise.all([
|
|
77
|
+
remoteBranches(repo),
|
|
78
|
+
run(
|
|
79
|
+
"gh",
|
|
80
|
+
[
|
|
81
|
+
"pr",
|
|
82
|
+
"list",
|
|
83
|
+
"--repo",
|
|
84
|
+
repo.slug,
|
|
85
|
+
"--state",
|
|
86
|
+
"all",
|
|
87
|
+
"--limit",
|
|
88
|
+
"1000",
|
|
89
|
+
"--json",
|
|
90
|
+
"number,state,headRefName,headRefOid,baseRefName,isCrossRepository",
|
|
91
|
+
],
|
|
92
|
+
repo.dir,
|
|
93
|
+
120000
|
|
94
|
+
),
|
|
95
|
+
run(
|
|
96
|
+
"gh",
|
|
97
|
+
[
|
|
98
|
+
"repo",
|
|
99
|
+
"view",
|
|
100
|
+
repo.slug,
|
|
101
|
+
"--json",
|
|
102
|
+
"defaultBranchRef",
|
|
103
|
+
"--jq",
|
|
104
|
+
".defaultBranchRef.name",
|
|
105
|
+
],
|
|
106
|
+
repo.dir,
|
|
107
|
+
20000
|
|
108
|
+
),
|
|
109
|
+
]);
|
|
110
|
+
const prs = JSON.parse(prsOut) as ListedPR[];
|
|
111
|
+
const defaultBranch = defaultOut.trim();
|
|
112
|
+
const mine = taskBranches(repo.projectIds);
|
|
113
|
+
const result: RepoPrune = {
|
|
114
|
+
slug: repo.slug,
|
|
115
|
+
stale: [],
|
|
116
|
+
kept: [],
|
|
117
|
+
deleted: [],
|
|
118
|
+
};
|
|
119
|
+
const keep = (branch: string, why: string) =>
|
|
120
|
+
result.kept.push({ branch, why });
|
|
121
|
+
|
|
122
|
+
for (const [branch, tip] of [...tips].sort()) {
|
|
123
|
+
if (branch === defaultBranch || PROTECTED_BRANCHES.has(branch)) continue;
|
|
124
|
+
if (!mine.has(branch)) {
|
|
125
|
+
keep(branch, "not an AgentOS task's branch");
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const own = prs
|
|
129
|
+
.filter((p) => p.headRefName === branch && !p.isCrossRepository)
|
|
130
|
+
.sort((a, b) => b.number - a.number);
|
|
131
|
+
if (!own.length) {
|
|
132
|
+
keep(branch, "no PR");
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const open = prs.find(
|
|
136
|
+
(p) =>
|
|
137
|
+
p.state === "OPEN" &&
|
|
138
|
+
(p.baseRefName === branch ||
|
|
139
|
+
(p.headRefName === branch && !p.isCrossRepository))
|
|
140
|
+
);
|
|
141
|
+
if (open) {
|
|
142
|
+
keep(branch, `open PR #${open.number}`);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const latest = own[0];
|
|
146
|
+
if (latest.headRefOid !== tip) {
|
|
147
|
+
keep(branch, `moved to ${tip.slice(0, 7)} after PR #${latest.number}`);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
result.stale.push({
|
|
151
|
+
branch,
|
|
152
|
+
pr: latest.number,
|
|
153
|
+
state: latest.state as StaleBranch["state"],
|
|
154
|
+
head: latest.headRefOid,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (opts.yes)
|
|
159
|
+
for (const s of result.stale) {
|
|
160
|
+
const out = await deleteTaskBranch({
|
|
161
|
+
repo: repo.dir,
|
|
162
|
+
slug: repo.slug,
|
|
163
|
+
branch: s.branch,
|
|
164
|
+
prHead: s.head,
|
|
165
|
+
keep: [defaultBranch],
|
|
166
|
+
});
|
|
167
|
+
if (out.deleted) result.deleted.push(s.branch);
|
|
168
|
+
else keep(s.branch, out.why);
|
|
169
|
+
}
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function pruneText(results: RepoPrune[], yes: boolean): string {
|
|
174
|
+
const lines: string[] = [];
|
|
175
|
+
let total = 0;
|
|
176
|
+
for (const r of results) {
|
|
177
|
+
lines.push(`${r.slug}:`);
|
|
178
|
+
const list = yes ? r.deleted : r.stale.map((s) => s.branch);
|
|
179
|
+
total += list.length;
|
|
180
|
+
for (const s of r.stale)
|
|
181
|
+
if (!yes || r.deleted.includes(s.branch))
|
|
182
|
+
lines.push(
|
|
183
|
+
` ${yes ? "deleted" : "stale"} ${s.branch} (PR #${s.pr} ${s.state.toLowerCase()})`
|
|
184
|
+
);
|
|
185
|
+
for (const k of r.kept) lines.push(` kept ${k.branch} (${k.why})`);
|
|
186
|
+
}
|
|
187
|
+
lines.push(
|
|
188
|
+
yes
|
|
189
|
+
? `Deleted ${total} branch${total === 1 ? "" : "es"}.`
|
|
190
|
+
: `${total} stale branch${total === 1 ? "" : "es"}. Dry run: aos branches prune --yes deletes them.`
|
|
191
|
+
);
|
|
192
|
+
return lines.join("\n");
|
|
193
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { getProject } from "../projects";
|
|
2
|
+
import { repoSlug } from "../git-remote";
|
|
3
|
+
import { scopeProjectIds, type DoneScope } from "../done/bulk";
|
|
4
|
+
import { expandHome } from "./session";
|
|
5
|
+
import type { PruneRepo } from "./prune-branches";
|
|
6
|
+
|
|
7
|
+
// The scope's local GitHub clones, one entry per repository (projects that
|
|
8
|
+
// share one are merged, so its branches are listed once).
|
|
9
|
+
export async function prunableRepos(scope: DoneScope): Promise<PruneRepo[]> {
|
|
10
|
+
const bySlug = new Map<string, PruneRepo>();
|
|
11
|
+
for (const id of scopeProjectIds(scope)) {
|
|
12
|
+
const p = getProject(id);
|
|
13
|
+
if (!p || (p.host_id && p.host_id !== "local")) continue;
|
|
14
|
+
const dir = expandHome(p.working_directory);
|
|
15
|
+
const slug = await repoSlug(dir);
|
|
16
|
+
if (!slug) continue;
|
|
17
|
+
const hit = bySlug.get(slug);
|
|
18
|
+
if (hit) hit.projectIds.push(id);
|
|
19
|
+
else bySlug.set(slug, { dir, slug, projectIds: [id] });
|
|
20
|
+
}
|
|
21
|
+
return [...bySlug.values()];
|
|
22
|
+
}
|
|
@@ -10,7 +10,7 @@ import { peerReading, peerSource, refreshPeers } from "../hosts/peer-load";
|
|
|
10
10
|
import { projectMachines } from "../hosts/project-machines";
|
|
11
11
|
import { viewOf } from "../load/machines";
|
|
12
12
|
import { usesGitCrypt } from "../worktree-add";
|
|
13
|
-
import { hostTasks } from "./
|
|
13
|
+
import { list as hostTasks } from "./contract/client";
|
|
14
14
|
import {
|
|
15
15
|
asPlanPhase,
|
|
16
16
|
place,
|
package/lib/tasks/queue-rows.ts
CHANGED
|
@@ -18,7 +18,7 @@ export const row = (id: string) =>
|
|
|
18
18
|
| undefined;
|
|
19
19
|
|
|
20
20
|
export const sessionOf = (id: string) =>
|
|
21
|
-
db.prepare(`SELECT * FROM
|
|
21
|
+
db.prepare(`SELECT * FROM session_rows WHERE id = ?`).get(id) as
|
|
22
22
|
| Session
|
|
23
23
|
| undefined;
|
|
24
24
|
|
|
@@ -60,7 +60,7 @@ export function runningIn(
|
|
|
60
60
|
return (
|
|
61
61
|
db
|
|
62
62
|
.prepare(
|
|
63
|
-
`SELECT s.id FROM
|
|
63
|
+
`SELECT s.id FROM session_rows s JOIN projects p ON p.id = s.project_id
|
|
64
64
|
WHERE ${where} AND s.archived_at IS NULL AND s.quick_of IS NULL
|
|
65
65
|
AND (s.pause_state IS NULL OR s.pause_state != 'paused')
|
|
66
66
|
${forSlots ? `AND NOT (COALESCE(s.plan_only, 0) = 1 AND COALESCE(s.plan_phase, '') != 'implementing')` : ""}
|
|
@@ -127,7 +127,7 @@ export function resolveAfter(
|
|
|
127
127
|
const scope = workspaceId ? `AND p.workspace_id = @ws` : "";
|
|
128
128
|
const candidates = db
|
|
129
129
|
.prepare(
|
|
130
|
-
`SELECT s.id, s.name FROM
|
|
130
|
+
`SELECT s.id, s.name FROM session_rows s JOIN projects p ON p.id = s.project_id
|
|
131
131
|
WHERE s.archived_at IS NULL AND s.task_status IN ('running', 'moving') ${scope}
|
|
132
132
|
UNION ALL
|
|
133
133
|
SELECT q.id, COALESCE(q.name, q.prompt) AS name FROM task_queue q
|
|
@@ -227,7 +227,7 @@ export function lineSignature(): string {
|
|
|
227
227
|
.prepare(
|
|
228
228
|
`SELECT COUNT(*) AS n, COALESCE(MAX(position), 0) AS p,
|
|
229
229
|
COALESCE(SUM(priority = 'normal'), 0) AS pr,
|
|
230
|
-
(SELECT COUNT(*) FROM
|
|
230
|
+
(SELECT COUNT(*) FROM session_rows WHERE task_status IS NOT NULL
|
|
231
231
|
AND task_status NOT IN ('merged', 'dropped')) AS running
|
|
232
232
|
FROM task_queue WHERE status IN ('queued', 'starting')`
|
|
233
233
|
)
|
package/lib/tasks/queue-start.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
type QueueRow,
|
|
28
28
|
type StartOutcome,
|
|
29
29
|
} from "./queue-types";
|
|
30
|
+
import { setPlacement, setTask } from "../db/session-parts";
|
|
30
31
|
|
|
31
32
|
// Tells the orchestrator that queued it that it started.
|
|
32
33
|
export function announce(
|
|
@@ -205,7 +206,8 @@ function stampStarted(r: QueueRow, placed: Placement | null): void {
|
|
|
205
206
|
const priority =
|
|
206
207
|
r.priority && r.priority !== DEFAULT_PRIORITY ? r.priority : null;
|
|
207
208
|
if (!note && !priority) return;
|
|
208
|
-
db.
|
|
209
|
-
|
|
210
|
-
|
|
209
|
+
db.transaction(() => {
|
|
210
|
+
setTask(db, r.id, { priority: priority ?? undefined });
|
|
211
|
+
setPlacement(db, r.id, { placed_note: note ?? undefined });
|
|
212
|
+
})();
|
|
211
213
|
}
|
package/lib/tasks/queue-tick.ts
CHANGED
|
@@ -128,8 +128,8 @@ export function tickQueue(): Promise<void> {
|
|
|
128
128
|
tickState.ticking ??= (async () => {
|
|
129
129
|
try {
|
|
130
130
|
// Plans written on a linked machine, and approved plans coming here.
|
|
131
|
-
await import("./
|
|
132
|
-
.then((m) => m.
|
|
131
|
+
await import("./contract/sync")
|
|
132
|
+
.then((m) => m.syncMirrors())
|
|
133
133
|
.catch((error: unknown) => console.error("[queue] plans:", error));
|
|
134
134
|
for (let i = 0; i < 3; i++) {
|
|
135
135
|
if ((await tickOnce()) !== "stale") break;
|