@saadnvd1/agent-os 0.3.637 → 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.
Files changed (154) hide show
  1. package/app/api/sessions/[id]/pr/route.ts +3 -3
  2. package/app/api/sessions/[id]/setup/route.ts +3 -3
  3. package/app/api/tasks/[id]/approve-plan/route.ts +3 -4
  4. package/app/api/tasks/[id]/arm/route.ts +6 -5
  5. package/app/api/tasks/[id]/chat-facts/route.ts +2 -2
  6. package/app/api/tasks/[id]/chat-tail/route.ts +2 -2
  7. package/app/api/tasks/[id]/checks/route.ts +1 -1
  8. package/app/api/tasks/[id]/diff/route.ts +1 -1
  9. package/app/api/tasks/[id]/done/route.ts +2 -2
  10. package/app/api/tasks/[id]/drop/route.ts +6 -2
  11. package/app/api/tasks/[id]/export/route.ts +3 -5
  12. package/app/api/tasks/[id]/merge/route.ts +3 -3
  13. package/app/api/tasks/[id]/message/route.ts +2 -2
  14. package/app/api/tasks/[id]/model/route.ts +5 -1
  15. package/app/api/tasks/[id]/move/route.ts +5 -1
  16. package/app/api/tasks/[id]/moved/route.ts +6 -8
  17. package/app/api/tasks/[id]/plan/route.ts +4 -6
  18. package/app/api/tasks/[id]/plan-edits/route.ts +6 -5
  19. package/app/api/tasks/[id]/priority/route.ts +4 -1
  20. package/app/api/tasks/[id]/resume/route.ts +3 -4
  21. package/app/api/tasks/[id]/stop/route.ts +2 -2
  22. package/app/api/tasks/route.ts +1 -1
  23. package/lib/approvals/enable.ts +1 -1
  24. package/lib/away/report.ts +4 -4
  25. package/lib/away/run.ts +2 -2
  26. package/lib/bookmarks/store.ts +3 -3
  27. package/lib/bus/held-reply.ts +2 -2
  28. package/lib/bus/index.ts +3 -3
  29. package/lib/chat/compact-context.ts +1 -1
  30. package/lib/chat/hold.ts +3 -3
  31. package/lib/chat/refresh.ts +1 -1
  32. package/lib/chat/registry.ts +3 -3
  33. package/lib/chat/runner.ts +1 -1
  34. package/lib/chat/worker/main.ts +1 -1
  35. package/lib/db/index.ts +5 -1
  36. package/lib/db/lumifyhub.ts +8 -9
  37. package/lib/db/machine-scope.ts +12 -0
  38. package/lib/db/migrations/20261015T1800_split_sessions_tables.ts +172 -0
  39. package/lib/db/migrations/README.md +7 -0
  40. package/lib/db/queries.ts +16 -31
  41. package/lib/db/session-parts/columns.ts +186 -0
  42. package/lib/db/session-parts/fill.ts +33 -0
  43. package/lib/db/session-parts/index.ts +9 -0
  44. package/lib/db/session-parts/pauses.ts +11 -0
  45. package/lib/db/session-parts/placement.ts +11 -0
  46. package/lib/db/session-parts/plans.ts +31 -0
  47. package/lib/db/session-parts/prs.ts +31 -0
  48. package/lib/db/session-parts/tasks.ts +27 -0
  49. package/lib/db/session-parts/view.ts +43 -0
  50. package/lib/db/session-parts/write.ts +69 -0
  51. package/lib/done/archive.ts +2 -2
  52. package/lib/done/auto-archive.ts +1 -1
  53. package/lib/done/bulk.ts +1 -1
  54. package/lib/done/index.ts +8 -14
  55. package/lib/health/evidence.ts +1 -1
  56. package/lib/health/facts.ts +1 -1
  57. package/lib/health/pieces.ts +1 -1
  58. package/lib/hosts/peer-actions.ts +2 -2
  59. package/lib/hosts/peer-mirror.ts +2 -2
  60. package/lib/hosts/peer-sync.ts +69 -32
  61. package/lib/learning-cards/input.ts +1 -1
  62. package/lib/learning-cards/store.ts +1 -1
  63. package/lib/models/plan-step.ts +10 -24
  64. package/lib/models/resume.ts +5 -7
  65. package/lib/notify/index.ts +1 -1
  66. package/lib/notify/push/events.ts +1 -1
  67. package/lib/notify/push/index.ts +1 -1
  68. package/lib/orchestrator/act.ts +1 -1
  69. package/lib/orchestrator/armed.ts +17 -15
  70. package/lib/orchestrator/ask-approvals.ts +1 -1
  71. package/lib/orchestrator/ask-heads.ts +1 -1
  72. package/lib/orchestrator/ask-settle.ts +1 -1
  73. package/lib/orchestrator/brakes.ts +1 -1
  74. package/lib/orchestrator/digest.ts +2 -2
  75. package/lib/orchestrator/external-pr.ts +2 -2
  76. package/lib/orchestrator/facts.ts +4 -28
  77. package/lib/orchestrator/held-asks.ts +3 -3
  78. package/lib/orchestrator/home.ts +2 -2
  79. package/lib/orchestrator/overview.ts +1 -1
  80. package/lib/orchestrator/pr-wake.ts +3 -5
  81. package/lib/orchestrator/questions.ts +1 -1
  82. package/lib/orchestrator/quick-store.ts +1 -1
  83. package/lib/orchestrator/quick.ts +10 -7
  84. package/lib/orchestrator/read.ts +1 -1
  85. package/lib/orchestrator/rebase.ts +8 -11
  86. package/lib/orchestrator/reconcile.ts +2 -2
  87. package/lib/orchestrator/signoff.ts +3 -1
  88. package/lib/orchestrator/targets.ts +1 -1
  89. package/lib/orchestrator/task-state.ts +4 -2
  90. package/lib/plans/features.ts +1 -1
  91. package/lib/plans/release.ts +1 -1
  92. package/lib/ports.ts +1 -1
  93. package/lib/resurrect/find.ts +1 -1
  94. package/lib/resurrect/index.ts +24 -18
  95. package/lib/schedules/store.ts +1 -1
  96. package/lib/session-audit/sources.ts +3 -3
  97. package/lib/session-logs/index.ts +1 -1
  98. package/lib/session-titles.ts +1 -1
  99. package/lib/sessions/setup-progress.ts +1 -1
  100. package/lib/sessions/stop.ts +1 -1
  101. package/lib/sessions/worktree-setup.ts +2 -1
  102. package/lib/simulator/index.ts +1 -1
  103. package/lib/simulator/task-devices.ts +1 -1
  104. package/lib/stacks/board.ts +1 -1
  105. package/lib/stacks/land.ts +1 -1
  106. package/lib/stacks/restack.ts +1 -1
  107. package/lib/stacks/start.ts +2 -2
  108. package/lib/stacks/tick.ts +1 -1
  109. package/lib/stacks/view.ts +1 -1
  110. package/lib/status/incremental.ts +1 -1
  111. package/lib/tasks/contract/actions.ts +35 -0
  112. package/lib/tasks/contract/authorize.ts +82 -0
  113. package/lib/tasks/contract/client.ts +296 -0
  114. package/lib/tasks/{held-remote.ts → contract/held-calls.ts} +35 -66
  115. package/lib/tasks/contract/index.ts +28 -0
  116. package/lib/tasks/{remote-plan-path.ts → contract/plan-path.ts} +2 -2
  117. package/lib/tasks/{remote-plan.ts → contract/plan.ts} +76 -110
  118. package/lib/tasks/contract/routes.ts +26 -0
  119. package/lib/tasks/contract/start.ts +122 -0
  120. package/lib/tasks/contract/sync.ts +126 -0
  121. package/lib/tasks/contract/types.ts +41 -0
  122. package/lib/tasks/{remote-views.ts → contract/views.ts} +11 -18
  123. package/lib/tasks/finish.ts +10 -9
  124. package/lib/tasks/held-replies.ts +7 -13
  125. package/lib/tasks/held.ts +14 -18
  126. package/lib/tasks/import.ts +19 -15
  127. package/lib/tasks/index.ts +34 -22
  128. package/lib/tasks/launch-gate.ts +1 -1
  129. package/lib/tasks/move-carry.ts +15 -7
  130. package/lib/tasks/move-flow.ts +29 -36
  131. package/lib/tasks/move-guard.ts +1 -1
  132. package/lib/tasks/move-recover.ts +6 -25
  133. package/lib/tasks/move.ts +23 -23
  134. package/lib/tasks/moved-away.ts +1 -1
  135. package/lib/tasks/pause-hold.ts +13 -5
  136. package/lib/tasks/pause.ts +19 -23
  137. package/lib/tasks/plan-view.ts +1 -1
  138. package/lib/tasks/queue-placement.ts +1 -1
  139. package/lib/tasks/queue-rows.ts +4 -4
  140. package/lib/tasks/queue-start.ts +5 -3
  141. package/lib/tasks/queue-tick.ts +2 -2
  142. package/lib/tasks/queue.ts +14 -6
  143. package/lib/tasks/session.ts +7 -4
  144. package/lib/tasks/start.ts +4 -4
  145. package/lib/tasks/starter.ts +8 -11
  146. package/lib/usage/calls.ts +1 -1
  147. package/lib/usage/compare.ts +1 -1
  148. package/lib/usage/quality.ts +1 -1
  149. package/lib/usage/turns.ts +1 -1
  150. package/lib/voice/vocab.ts +1 -1
  151. package/package.json +1 -1
  152. package/scripts/backfill-session-logs.ts +1 -1
  153. package/lib/tasks/held-routes.ts +0 -33
  154. package/lib/tasks/remote.ts +0 -255
@@ -17,7 +17,11 @@ import { unarchiveSession } from "../done/archive";
17
17
  import { reopenSession } from "../sidebar/shelf-state";
18
18
  import { peerSessionLink } from "../hosts/peer-actions";
19
19
  import { hostApi, hostLink } from "../hosts/remote-api";
20
- import { forgetHostTasks, isMirror, unknownOutcome } from "../tasks/remote";
20
+ import {
21
+ forget as forgetHostTasks,
22
+ unknownOutcome,
23
+ } from "../tasks/contract/client";
24
+ import { isMirror } from "../tasks/contract/start";
21
25
  import { heldRefusal } from "../tasks/held";
22
26
  import { expandHome, projectPathFor } from "../tasks/session";
23
27
  import { run } from "../tasks/gh";
@@ -40,6 +44,7 @@ import { launchClaude } from "../agents/launch";
40
44
  import { PLAN_FILE, preparePlanDir } from "../models/plan-step";
41
45
  import { planFromTranscript } from "./plan";
42
46
  import { dropSnapshot, restoreWorktree, type RestoredAs } from "./snapshot";
47
+ import { setPr, setTask } from "../db/session-parts";
43
48
 
44
49
  export interface ResurrectOptions {
45
50
  // The paired device asking: a task held for another machine's orchestrator
@@ -157,9 +162,12 @@ async function onItsMachine(s: Session): Promise<ResurrectOutcome> {
157
162
  `/api/sessions/${encodeURIComponent(s.id)}/resurrect`,
158
163
  { body: {}, timeout: 120_000 }
159
164
  );
160
- db.prepare(
161
- `UPDATE sessions SET task_status = 'running' WHERE id = ? AND task_status IN ('dropped', 'done')`
162
- ).run(s.id);
165
+ setTask(
166
+ db,
167
+ s.id,
168
+ { task_status: "running" },
169
+ { task_status: ["dropped", "done"] }
170
+ );
163
171
  forgetHostTasks(link.hostId);
164
172
  return outcome(
165
173
  s,
@@ -172,7 +180,7 @@ async function onItsMachine(s: Session): Promise<ResurrectOutcome> {
172
180
  }
173
181
  const before = db
174
182
  .prepare(
175
- `SELECT * FROM sessions WHERE host_id = 'local' AND task_status = 'moved'
183
+ `SELECT * FROM session_rows WHERE host_id = 'local' AND task_status = 'moved'
176
184
  AND project_id IS ? AND branch_name IS ? ORDER BY updated_at DESC LIMIT 1`
177
185
  )
178
186
  .get(s.project_id, s.branch_name) as Session | undefined;
@@ -217,7 +225,7 @@ async function restoreTask(
217
225
  if (s.branch_name) {
218
226
  const busy = db
219
227
  .prepare(
220
- `SELECT name FROM sessions WHERE project_id = ? AND branch_name = ? AND id != ?
228
+ `SELECT name FROM session_rows WHERE project_id = ? AND branch_name = ? AND id != ?
221
229
  AND task_status IN ('running', 'moving') AND host_id = 'local'`
222
230
  )
223
231
  .get(project.id, s.branch_name, s.id) as { name: string } | undefined;
@@ -238,11 +246,12 @@ async function restoreTask(
238
246
  if (inFlight.has(s.id))
239
247
  throw new Error(`${s.name} is being restored already.`);
240
248
  inFlight.add(s.id);
241
- const claimed = db
242
- .prepare(
243
- `UPDATE sessions SET task_status = 'running', task_ended_at = NULL WHERE id = ? AND task_status = ?`
244
- )
245
- .run(s.id, prev).changes;
249
+ const claimed = setTask(
250
+ db,
251
+ s.id,
252
+ { task_status: "running", task_ended_at: null },
253
+ { task_status: prev }
254
+ );
246
255
  if (!claimed) {
247
256
  inFlight.delete(s.id);
248
257
  throw new Error(`${s.name} is being restored already.`);
@@ -250,9 +259,7 @@ async function restoreTask(
250
259
  try {
251
260
  return await bringBack(s, prev, repo, oldCwd, pr);
252
261
  } catch (err) {
253
- db.prepare(
254
- `UPDATE sessions SET task_status = ? WHERE id = ? AND task_status = 'running'`
255
- ).run(prev, s.id);
262
+ setTask(db, s.id, { task_status: prev }, { task_status: "running" });
256
263
  throw err;
257
264
  } finally {
258
265
  inFlight.delete(s.id);
@@ -310,10 +317,11 @@ async function bringBack(
310
317
  if (!restored) {
311
318
  // Nothing to make a worktree from: the conversation alone, in the project.
312
319
  db.prepare(
313
- `UPDATE sessions SET task_status = NULL, task_ended_at = NULL, worktree_path = NULL,
320
+ `UPDATE sessions SET worktree_path = NULL,
314
321
  working_directory = ?, settled_at = NULL, snoozed_until = NULL, snoozed_at = NULL,
315
322
  archived_at = NULL, active_at = datetime('now') WHERE id = ?`
316
323
  ).run(cwd, s.id);
324
+ setTask(db, s.id, { task_status: null, task_ended_at: null });
317
325
  notes.push(
318
326
  "its branch and worktree were gone, so only the conversation came back, in the project folder"
319
327
  );
@@ -335,9 +343,7 @@ async function bringBack(
335
343
  try {
336
344
  await gitPush(restored.path, ["-u", "origin", s.branch_name!]);
337
345
  await run("gh", ["pr", "reopen", String(pr.number)], repo);
338
- db.prepare(`UPDATE sessions SET pr_status = 'open' WHERE id = ?`).run(
339
- s.id
340
- );
346
+ setPr(db, s.id, { pr_status: "open" });
341
347
  reopened = pr.number;
342
348
  } catch (err) {
343
349
  notes.push(
@@ -163,7 +163,7 @@ export function findSchedule(ref: string): Schedule {
163
163
 
164
164
  export function getSessionRow(id: string): Session | null {
165
165
  return (
166
- (db.prepare(`SELECT * FROM sessions WHERE id = ?`).get(id) as
166
+ (db.prepare(`SELECT * FROM session_rows WHERE id = ?`).get(id) as
167
167
  | Session
168
168
  | undefined) ?? null
169
169
  );
@@ -81,7 +81,7 @@ export function loadSessions(
81
81
  `SELECT s.id, s.name, s.created_at, s.role, s.view, s.task_prompt,
82
82
  length(s.task_brief) AS task_brief_chars, s.working_directory,
83
83
  s.worktree_path, s.claude_session_id, s.workspace_id, p.name AS project
84
- FROM sessions s LEFT JOIN projects p ON p.id = s.project_id
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 sessions f ON f.id = m.from_id
301
- LEFT JOIN sessions t ON t.id = m.to_id
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 sessions WHERE id = ?`).get(id) as
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);
@@ -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 sessions WHERE id = ?`
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 sessions WHERE id = ? AND task_status IS NULL`
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;
@@ -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-remote";
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' AND task_status IS NULL`
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
  }
@@ -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 sessions WHERE id = ?`).get(id) as
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 sessions WHERE task_status IS NOT NULL`
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));
@@ -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 sessions
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 }>;
@@ -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 sessions WHERE id = ?`).get(id)
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 {}
@@ -109,7 +109,7 @@ const defaults: RestackDeps = {
109
109
 
110
110
  const sessionOf = (id: string | null) =>
111
111
  id
112
- ? ((db.prepare(`SELECT * FROM sessions WHERE id = ?`).get(id) as
112
+ ? ((db.prepare(`SELECT * FROM session_rows WHERE id = ?`).get(id) as
113
113
  | Session
114
114
  | undefined) ?? null)
115
115
  : null;
@@ -25,7 +25,7 @@ async function stackedBase(
25
25
  item: StackItemRow
26
26
  ) {
27
27
  const session = db
28
- .prepare(`SELECT * FROM sessions WHERE id = ?`)
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 sessions WHERE lh_card_id = ? AND task_status = 'running'
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;
@@ -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 sessions WHERE id = ?`).get(id) as
42
+ db.prepare(`SELECT * FROM session_rows WHERE id = ?`).get(id) as
43
43
  | Session
44
44
  | undefined;
45
45
 
@@ -51,7 +51,7 @@ const prUrlOf = (sessionId: string | null) =>
51
51
  sessionId
52
52
  ? ((
53
53
  db
54
- .prepare(`SELECT pr_url FROM sessions WHERE id = ?`)
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 sessions s
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
+ }