@saadnvd1/agent-os 0.3.631 → 0.3.639
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/api/sessions/[id]/pr/route.ts +3 -3
- package/app/api/sessions/[id]/setup/route.ts +3 -3
- package/app/api/tasks/[id]/approve-plan/route.ts +5 -1
- package/app/api/tasks/[id]/arm/route.ts +6 -5
- package/app/api/tasks/[id]/chat-facts/route.ts +2 -2
- package/app/api/tasks/[id]/chat-tail/route.ts +2 -2
- package/app/api/tasks/[id]/checks/route.ts +1 -1
- package/app/api/tasks/[id]/diff/route.ts +1 -1
- package/app/api/tasks/[id]/done/route.ts +2 -2
- package/app/api/tasks/[id]/drop/route.ts +6 -2
- package/app/api/tasks/[id]/export/route.ts +3 -4
- package/app/api/tasks/[id]/merge/route.ts +3 -3
- package/app/api/tasks/[id]/message/route.ts +2 -2
- package/app/api/tasks/[id]/model/route.ts +5 -1
- package/app/api/tasks/[id]/move/route.ts +5 -1
- package/app/api/tasks/[id]/moved/route.ts +6 -8
- package/app/api/tasks/[id]/plan/route.ts +35 -0
- package/app/api/tasks/[id]/plan-edits/route.ts +6 -5
- package/app/api/tasks/[id]/priority/route.ts +4 -1
- package/app/api/tasks/[id]/resume/route.ts +3 -4
- package/app/api/tasks/[id]/stop/route.ts +2 -2
- package/app/api/tasks/route.ts +5 -2
- package/app/globals.css +28 -0
- package/components/Chat/ChatPanel.tsx +2 -11
- package/components/Chat/EventTurns.tsx +1 -7
- package/components/Chat/Markdown.tsx +1 -0
- package/components/Chat/Messages.tsx +1 -1
- package/lib/approvals/enable.ts +9 -2
- package/lib/away/report.ts +4 -4
- package/lib/away/run.ts +2 -2
- package/lib/bookmarks/store.ts +3 -3
- package/lib/bus/held-reply.ts +2 -2
- package/lib/bus/index.ts +3 -3
- package/lib/chat/compact-context.ts +1 -1
- package/lib/chat/events.ts +4 -0
- package/lib/chat/group.ts +3 -1
- package/lib/chat/hold.ts +3 -3
- package/lib/chat/reading-order.ts +50 -0
- package/lib/chat/refresh.ts +1 -1
- package/lib/chat/registry.ts +3 -3
- package/lib/chat/runner.ts +1 -1
- package/lib/chat/turns.ts +22 -3
- package/lib/chat/worker/host.ts +3 -0
- package/lib/chat/worker/main.ts +1 -1
- package/lib/db/index.ts +5 -1
- package/lib/db/lumifyhub.ts +8 -9
- package/lib/db/machine-scope.ts +12 -0
- package/lib/db/migrations/20261015T1600_plan_waits_for_caller.ts +19 -0
- package/lib/db/migrations/20261015T1800_split_sessions_tables.ts +172 -0
- package/lib/db/migrations/README.md +7 -0
- package/lib/db/queries.ts +16 -31
- package/lib/db/session-parts/columns.ts +186 -0
- package/lib/db/session-parts/fill.ts +33 -0
- package/lib/db/session-parts/index.ts +9 -0
- package/lib/db/session-parts/pauses.ts +11 -0
- package/lib/db/session-parts/placement.ts +11 -0
- package/lib/db/session-parts/plans.ts +31 -0
- package/lib/db/session-parts/prs.ts +31 -0
- package/lib/db/session-parts/tasks.ts +27 -0
- package/lib/db/session-parts/view.ts +43 -0
- package/lib/db/session-parts/write.ts +69 -0
- package/lib/db/types.ts +6 -1
- package/lib/done/archive.ts +2 -2
- package/lib/done/auto-archive.ts +1 -1
- package/lib/done/bulk.ts +1 -1
- package/lib/done/index.ts +8 -14
- package/lib/env-setup.ts +17 -4
- package/lib/health/evidence.ts +1 -1
- package/lib/health/facts.ts +1 -1
- package/lib/health/pieces.ts +1 -1
- package/lib/hosts/peer-actions.ts +2 -2
- package/lib/hosts/peer-mirror.ts +2 -2
- package/lib/hosts/peer-sync.ts +69 -32
- package/lib/learning-cards/input.ts +1 -1
- package/lib/learning-cards/store.ts +1 -1
- package/lib/models/plan-step.ts +83 -46
- package/lib/models/resume.ts +5 -7
- package/lib/notify/index.ts +1 -1
- package/lib/notify/push/events.ts +1 -1
- package/lib/notify/push/index.ts +1 -1
- package/lib/orchestrator/act.ts +1 -1
- package/lib/orchestrator/armed.ts +17 -15
- package/lib/orchestrator/ask-approvals.ts +1 -1
- package/lib/orchestrator/ask-heads.ts +1 -1
- package/lib/orchestrator/ask-settle.ts +1 -1
- package/lib/orchestrator/brakes.ts +1 -1
- package/lib/orchestrator/digest.ts +2 -2
- package/lib/orchestrator/external-pr.ts +2 -2
- package/lib/orchestrator/facts.ts +4 -28
- package/lib/orchestrator/held-asks.ts +3 -3
- package/lib/orchestrator/home.ts +2 -2
- package/lib/orchestrator/overview.ts +1 -1
- package/lib/orchestrator/pr-wake.ts +3 -5
- package/lib/orchestrator/questions.ts +1 -1
- package/lib/orchestrator/quick-store.ts +1 -1
- package/lib/orchestrator/quick.ts +10 -7
- package/lib/orchestrator/read.ts +1 -1
- package/lib/orchestrator/rebase.ts +8 -11
- package/lib/orchestrator/reconcile.ts +2 -2
- package/lib/orchestrator/signoff.ts +3 -1
- package/lib/orchestrator/targets.ts +1 -1
- package/lib/orchestrator/task-state.ts +4 -2
- package/lib/plans/features.ts +1 -1
- package/lib/plans/release.ts +1 -1
- package/lib/ports.ts +1 -1
- package/lib/resurrect/find.ts +1 -1
- package/lib/resurrect/index.ts +24 -18
- package/lib/schedules/store.ts +1 -1
- package/lib/session-audit/sources.ts +3 -3
- package/lib/session-logs/index.ts +1 -1
- package/lib/session-titles.ts +1 -1
- package/lib/sessions/setup-progress.ts +1 -1
- package/lib/sessions/stop.ts +1 -1
- package/lib/sessions/worktree-setup.ts +2 -1
- package/lib/simulator/index.ts +1 -1
- package/lib/simulator/task-devices.ts +1 -1
- package/lib/stacks/board.ts +1 -1
- package/lib/stacks/land.ts +1 -1
- package/lib/stacks/restack.ts +1 -1
- package/lib/stacks/start.ts +2 -2
- package/lib/stacks/tick.ts +1 -1
- package/lib/stacks/view.ts +1 -1
- package/lib/status/incremental.ts +1 -1
- package/lib/tasks/contract/actions.ts +35 -0
- package/lib/tasks/contract/authorize.ts +82 -0
- package/lib/tasks/contract/client.ts +296 -0
- package/lib/tasks/{held-remote.ts → contract/held-calls.ts} +35 -66
- package/lib/tasks/contract/index.ts +28 -0
- package/lib/tasks/contract/plan-path.ts +7 -0
- package/lib/tasks/contract/plan.ts +274 -0
- package/lib/tasks/contract/routes.ts +26 -0
- package/lib/tasks/contract/start.ts +122 -0
- package/lib/tasks/contract/sync.ts +126 -0
- package/lib/tasks/contract/types.ts +41 -0
- package/lib/tasks/{remote-views.ts → contract/views.ts} +11 -18
- package/lib/tasks/finish.ts +10 -9
- package/lib/tasks/held-replies.ts +7 -13
- package/lib/tasks/held.ts +36 -17
- package/lib/tasks/hold-checks.ts +134 -0
- package/lib/tasks/holds.ts +281 -0
- package/lib/tasks/import.ts +26 -14
- package/lib/tasks/index.ts +56 -22
- package/lib/tasks/launch-gate.ts +1 -1
- package/lib/tasks/move-bundle.ts +17 -2
- package/lib/tasks/move-carry.ts +15 -7
- package/lib/tasks/move-flow.ts +29 -36
- package/lib/tasks/move-guard.ts +10 -2
- package/lib/tasks/move-recover.ts +6 -25
- package/lib/tasks/move.ts +28 -27
- package/lib/tasks/moved-away.ts +1 -1
- package/lib/tasks/pause-hold.ts +13 -5
- package/lib/tasks/pause.ts +21 -54
- package/lib/tasks/placement.ts +16 -16
- package/lib/tasks/plan-view.ts +11 -3
- package/lib/tasks/queue-candidates.ts +151 -0
- package/lib/tasks/queue-placement.ts +155 -0
- package/lib/tasks/queue-rows.ts +236 -0
- package/lib/tasks/queue-start.ts +213 -0
- package/lib/tasks/queue-tick.ts +148 -0
- package/lib/tasks/queue-types.ts +141 -0
- package/lib/tasks/queue-view.ts +112 -0
- package/lib/tasks/queue.ts +67 -1102
- package/lib/tasks/schedule.ts +44 -147
- package/lib/tasks/session.ts +7 -4
- package/lib/tasks/start.ts +4 -4
- package/lib/tasks/starter.ts +8 -11
- package/lib/usage/calls.ts +1 -1
- package/lib/usage/compare.ts +1 -1
- package/lib/usage/quality.ts +1 -1
- package/lib/usage/turns.ts +1 -1
- package/lib/voice/vocab.ts +1 -1
- package/package.json +1 -1
- package/scripts/backfill-session-logs.ts +1 -1
- package/scripts/check +55 -7
- package/lib/tasks/auto-hold.ts +0 -17
- package/lib/tasks/held-routes.ts +0 -33
- package/lib/tasks/remote.ts +0 -238
|
@@ -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/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
|
+
}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one client for a task running on a linked machine: every call the Mac
|
|
3
|
+
* makes to that machine's task API, with its path, body, timeout and retry in
|
|
4
|
+
* one place. Callers hold a HostLink and say what they want; none of them
|
|
5
|
+
* builds a task URL.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { MERGE_METHODS, type MergeMethod } from "../merge-methods";
|
|
9
|
+
import type { Session } from "../../db";
|
|
10
|
+
import {
|
|
11
|
+
HostApiError,
|
|
12
|
+
cleanRemoteBlock,
|
|
13
|
+
cleanRemoteText,
|
|
14
|
+
hostApi,
|
|
15
|
+
type HostLink,
|
|
16
|
+
} from "../../hosts/remote-api";
|
|
17
|
+
import type { OrchestratedBundle } from "../move-bundle";
|
|
18
|
+
import type { TaskBundle } from "../move-bundle";
|
|
19
|
+
import {
|
|
20
|
+
HELD_REPLIES,
|
|
21
|
+
IMPORT,
|
|
22
|
+
LIST,
|
|
23
|
+
TIMEOUT,
|
|
24
|
+
arrivedPath,
|
|
25
|
+
taskPath,
|
|
26
|
+
} from "./routes";
|
|
27
|
+
import type { ChatFacts, HostTasks } from "./types";
|
|
28
|
+
|
|
29
|
+
export const unknownOutcome = (err: unknown) =>
|
|
30
|
+
err instanceof HostApiError && !err.refused;
|
|
31
|
+
|
|
32
|
+
/** POST a call that's safe to repeat; once more if we can't tell it landed. */
|
|
33
|
+
export async function postIdempotent<T>(
|
|
34
|
+
link: HostLink,
|
|
35
|
+
path: string,
|
|
36
|
+
body: unknown,
|
|
37
|
+
timeout: number
|
|
38
|
+
): Promise<T> {
|
|
39
|
+
try {
|
|
40
|
+
return await hostApi<T>(link, path, { body, timeout });
|
|
41
|
+
} catch (err) {
|
|
42
|
+
if (!unknownOutcome(err)) throw err;
|
|
43
|
+
return hostApi<T>(link, path, { body, timeout });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** POST, repeated when the outcome is unknown (the calls are idempotent). */
|
|
48
|
+
export async function tell(
|
|
49
|
+
link: HostLink,
|
|
50
|
+
path: string,
|
|
51
|
+
body: object = {}
|
|
52
|
+
): Promise<void> {
|
|
53
|
+
for (let i = 0; ; i++) {
|
|
54
|
+
try {
|
|
55
|
+
await hostApi(link, path, { body });
|
|
56
|
+
return;
|
|
57
|
+
} catch (err) {
|
|
58
|
+
if (!unknownOutcome(err) || i === 2) throw err;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const cache = new Map<string, { at: number; list: Promise<HostTasks> }>();
|
|
64
|
+
|
|
65
|
+
/** The machine's task list; the last answer within 3 s unless `fresh`. */
|
|
66
|
+
export function list(link: HostLink, fresh = false): Promise<HostTasks> {
|
|
67
|
+
const hit = cache.get(link.hostId);
|
|
68
|
+
if (!fresh && hit && Date.now() - hit.at < 3000) return hit.list;
|
|
69
|
+
const next = hostApi<HostTasks>(link, LIST, { timeout: TIMEOUT.list });
|
|
70
|
+
cache.set(link.hostId, { at: Date.now(), list: next });
|
|
71
|
+
next.catch(() => cache.delete(link.hostId));
|
|
72
|
+
return next;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const forget = (hostId: string) => cache.delete(hostId);
|
|
76
|
+
|
|
77
|
+
export async function view(
|
|
78
|
+
link: HostLink,
|
|
79
|
+
id: string,
|
|
80
|
+
fresh = false
|
|
81
|
+
): Promise<HostTasks["tasks"][number] | undefined> {
|
|
82
|
+
const { tasks } = await list(link, fresh);
|
|
83
|
+
return (Array.isArray(tasks) ? tasks : []).find((t) => t?.id === id);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* A machine on an older AgentOS would take a chat task as a terminal one,
|
|
88
|
+
* or an orchestrator's task as a plain one that anyone there could merge, or
|
|
89
|
+
* refuse to hand one over.
|
|
90
|
+
*/
|
|
91
|
+
export async function requireCapability(
|
|
92
|
+
link: HostLink,
|
|
93
|
+
capability: string,
|
|
94
|
+
what: string,
|
|
95
|
+
verb = "move"
|
|
96
|
+
): Promise<void> {
|
|
97
|
+
const { capabilities = [] } = await list(link, true).catch((err) => {
|
|
98
|
+
throw new Error(
|
|
99
|
+
`Can't ask ${link.hostName} whether it takes ${what}: ${(err as Error).message}`
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
if (!capabilities.includes(capability))
|
|
103
|
+
throw new Error(
|
|
104
|
+
`${link.hostName}'s AgentOS can't ${verb} ${what} yet; update it first`
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export const start = (link: HostLink, body: object) =>
|
|
109
|
+
postIdempotent<{ session: Session }>(link, LIST, body, TIMEOUT.start);
|
|
110
|
+
|
|
111
|
+
export async function plan(link: HostLink, id: string): Promise<string | null> {
|
|
112
|
+
const { text } = await hostApi<{ text: string | null }>(
|
|
113
|
+
link,
|
|
114
|
+
taskPath(id, "plan"),
|
|
115
|
+
{ timeout: TIMEOUT.plan }
|
|
116
|
+
);
|
|
117
|
+
return text;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export const approvePlan = (link: HostLink, id: string, message?: string) =>
|
|
121
|
+
hostApi<{ model?: string }>(link, taskPath(id, "approve-plan"), {
|
|
122
|
+
body: message ? { message } : {},
|
|
123
|
+
timeout: TIMEOUT.approvePlan,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
export async function hold(
|
|
127
|
+
link: HostLink,
|
|
128
|
+
id: string,
|
|
129
|
+
orchestrated: OrchestratedBundle
|
|
130
|
+
): Promise<void> {
|
|
131
|
+
await hostApi(link, taskPath(id, "hold"), {
|
|
132
|
+
body: { orchestrated },
|
|
133
|
+
timeout: TIMEOUT.hold,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function message(
|
|
138
|
+
link: HostLink,
|
|
139
|
+
id: string,
|
|
140
|
+
body: { id?: string; line: string; from: string }
|
|
141
|
+
): Promise<"queued" | "delivered"> {
|
|
142
|
+
const { state } = await hostApi<{ state?: unknown }>(
|
|
143
|
+
link,
|
|
144
|
+
taskPath(id, "message"),
|
|
145
|
+
{ body, timeout: TIMEOUT.message }
|
|
146
|
+
);
|
|
147
|
+
return state === "queued" ? "queued" : "delivered";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export async function chatTail(
|
|
151
|
+
link: HostLink,
|
|
152
|
+
id: string,
|
|
153
|
+
lines: number
|
|
154
|
+
): Promise<string | null> {
|
|
155
|
+
const { text } = await hostApi<{ text?: unknown }>(
|
|
156
|
+
link,
|
|
157
|
+
`${taskPath(id, "chat-tail")}?lines=${lines}`,
|
|
158
|
+
{ timeout: TIMEOUT.chat }
|
|
159
|
+
);
|
|
160
|
+
// Whole lines, as a local read has them (read.ts caps and trims them).
|
|
161
|
+
return typeof text === "string" ? cleanRemoteBlock(text, 16000) : null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export async function chatFacts(
|
|
165
|
+
link: HostLink,
|
|
166
|
+
id: string
|
|
167
|
+
): Promise<ChatFacts | null> {
|
|
168
|
+
const res = await hostApi<{ blocked?: unknown; turn?: unknown }>(
|
|
169
|
+
link,
|
|
170
|
+
taskPath(id, "chat-facts"),
|
|
171
|
+
{ timeout: TIMEOUT.chat }
|
|
172
|
+
);
|
|
173
|
+
if (res.blocked !== null && typeof res.blocked !== "string") return null;
|
|
174
|
+
if (res.turn !== null && typeof res.turn !== "string") return null;
|
|
175
|
+
return {
|
|
176
|
+
blocked: res.blocked ? cleanRemoteBlock(res.blocked, 2000) : null,
|
|
177
|
+
turn: res.turn,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export async function stop(link: HostLink, id: string): Promise<string> {
|
|
182
|
+
const { text } = await hostApi<{ text?: unknown }>(
|
|
183
|
+
link,
|
|
184
|
+
taskPath(id, "stop"),
|
|
185
|
+
{
|
|
186
|
+
body: {},
|
|
187
|
+
timeout: TIMEOUT.stop,
|
|
188
|
+
}
|
|
189
|
+
);
|
|
190
|
+
return cleanRemoteText(text);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export async function done(link: HostLink, id: string): Promise<void> {
|
|
194
|
+
await hostApi(link, taskPath(id, "done"), {
|
|
195
|
+
body: {},
|
|
196
|
+
timeout: TIMEOUT.done,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** A merge returns the method that machine merged with; null when it doesn't say. */
|
|
201
|
+
export async function merge(
|
|
202
|
+
link: HostLink,
|
|
203
|
+
id: string,
|
|
204
|
+
pin?: { head: string; base?: string | null },
|
|
205
|
+
label = "it"
|
|
206
|
+
): Promise<{ head: string | null; method: MergeMethod | null }> {
|
|
207
|
+
// An AgentOS that predates pinned merges would ignore the head and merge
|
|
208
|
+
// whatever the PR is at: refuse before asking it.
|
|
209
|
+
if (pin)
|
|
210
|
+
await requireCapability(link, "pinned-merge", "a merge to a commit", "pin");
|
|
211
|
+
const res = await hostApi<{ head?: string | null; method?: unknown }>(
|
|
212
|
+
link,
|
|
213
|
+
taskPath(id, "merge"),
|
|
214
|
+
{
|
|
215
|
+
// base: what the gates here were judged against; the machine that
|
|
216
|
+
// merges compares the PR with it, not with a base only it recorded.
|
|
217
|
+
body: {
|
|
218
|
+
...(pin && { head: pin.head }),
|
|
219
|
+
...(pin?.base && { base: pin.base }),
|
|
220
|
+
},
|
|
221
|
+
timeout: TIMEOUT.merge,
|
|
222
|
+
}
|
|
223
|
+
).catch(async (err) => {
|
|
224
|
+
if (!unknownOutcome(err)) throw err;
|
|
225
|
+
// A proxy or a dropped connection after the merge ran answers 502 with
|
|
226
|
+
// nothing from AgentOS: ask what state the task is in before failing.
|
|
227
|
+
const merged = await view(link, id, true)
|
|
228
|
+
.then((t) => t?.state === "merged")
|
|
229
|
+
.catch(() => false);
|
|
230
|
+
if (!merged)
|
|
231
|
+
throw new Error(
|
|
232
|
+
`${(err as Error).message}: couldn't confirm whether ${link.hostName} merged ${label}; check its PR before signing off again`
|
|
233
|
+
);
|
|
234
|
+
return { head: pin?.head ?? null, method: undefined };
|
|
235
|
+
});
|
|
236
|
+
forget(link.hostId);
|
|
237
|
+
return {
|
|
238
|
+
head: res.head ?? null,
|
|
239
|
+
method: MERGE_METHODS.find((m) => m === res.method) ?? null,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export async function drop(link: HostLink, id: string): Promise<void> {
|
|
244
|
+
await hostApi(link, taskPath(id, "drop"), {
|
|
245
|
+
body: {},
|
|
246
|
+
timeout: TIMEOUT.merge,
|
|
247
|
+
});
|
|
248
|
+
forget(link.hostId);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export const resume = (link: HostLink, id: string, body: object = {}) =>
|
|
252
|
+
tell(link, taskPath(id, "resume"), body);
|
|
253
|
+
|
|
254
|
+
export const confirmMoved = (link: HostLink, id: string, to: string) =>
|
|
255
|
+
tell(link, taskPath(id, "moved"), { to });
|
|
256
|
+
|
|
257
|
+
export async function exportOut(
|
|
258
|
+
link: HostLink,
|
|
259
|
+
id: string,
|
|
260
|
+
to: string
|
|
261
|
+
): Promise<TaskBundle> {
|
|
262
|
+
const { bundle } = await postIdempotent<{ bundle: TaskBundle }>(
|
|
263
|
+
link,
|
|
264
|
+
taskPath(id, "export"),
|
|
265
|
+
{ to },
|
|
266
|
+
TIMEOUT.exportOut
|
|
267
|
+
);
|
|
268
|
+
return bundle;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export const importBundle = (link: HostLink, bundle: TaskBundle) =>
|
|
272
|
+
postIdempotent<{ session: Session }>(
|
|
273
|
+
link,
|
|
274
|
+
IMPORT,
|
|
275
|
+
bundle,
|
|
276
|
+
TIMEOUT.importBundle
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
export async function arrived(
|
|
280
|
+
link: HostLink,
|
|
281
|
+
from: string
|
|
282
|
+
): Promise<Session | null> {
|
|
283
|
+
const { session } = await hostApi<{ session: Session | null }>(
|
|
284
|
+
link,
|
|
285
|
+
arrivedPath(from)
|
|
286
|
+
);
|
|
287
|
+
return session;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export const replies = (link: HostLink) =>
|
|
291
|
+
hostApi<{ messages?: unknown }>(link, HELD_REPLIES, {
|
|
292
|
+
timeout: TIMEOUT.replies,
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
export const ackReplies = (link: HostLink, ids: number[]) =>
|
|
296
|
+
hostApi(link, HELD_REPLIES, { body: { ids }, timeout: TIMEOUT.replies });
|