@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
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 = db
92
- .prepare(
93
- `UPDATE sessions SET task_status = 'moving', moved_to = ?
94
- WHERE id = ? AND task_status = 'running'`
95
- )
96
- .run(to, id);
97
- if (fresh.changes === 1) return { fresh: true };
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 = db
254
- .prepare(
255
- `UPDATE sessions SET task_status = 'moved', moved_to = ?
256
- WHERE id = ? AND task_status IN ('running', 'moving')`
257
- )
258
- .run(to, id).changes;
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 = db
273
- .prepare(
274
- `UPDATE sessions SET task_status = 'running' WHERE id = ? AND task_status = 'moving'`
275
- )
276
- .run(id);
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.changes !== 1) {
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.prepare(
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.prepare(`UPDATE sessions SET moved_to = NULL WHERE id = ?`).run(id);
290
+ setTask(db, id, { moved_to: null });
291
291
  }
292
292
 
293
293
  async function relaunch(session: Session, note?: string): Promise<void> {
@@ -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 sessions WHERE id = ?`).get(id) as
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)
@@ -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 sessions
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
- db.prepare(
49
- `UPDATE sessions SET pause_state = NULL, paused_until = NULL, pause_id = NULL, pause_note = NULL
50
- WHERE id = ? AND pause_id = ?`
51
- ).run(row.session_id, row.id);
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
 
@@ -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 sessions WHERE id = ?`).get(id) as
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.prepare(`UPDATE sessions SET paused_until = ? WHERE id = ?`).run(
169
- until ? toSqliteTime(until) : null,
170
- sessionId
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.prepare(
194
- `UPDATE sessions SET pause_state = 'pausing', paused_until = ?, pause_id = ? WHERE id = ?`
195
- ).run(until ? toSqliteTime(until) : null, id, sessionId);
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.prepare(`UPDATE sessions SET paused_until = ? WHERE id = ?`).run(
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.prepare(
400
- `UPDATE sessions SET pause_note = ? WHERE id = ? AND pause_state = 'paused' AND COALESCE(pause_note, '') != COALESCE(?, '')`
401
- ).run(note, sessionId, note);
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 sessions s ON s.id = p.session_id
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.prepare(
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";
@@ -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 "./remote-plan-path";
21
+ import { remotePlanPath } from "./contract/plan-path";
22
22
 
23
23
  const TITLE_MAX = 120;
24
24
 
@@ -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 "./remote";
13
+ import { list as hostTasks } from "./contract/client";
14
14
  import {
15
15
  asPlanPhase,
16
16
  place,
@@ -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 sessions WHERE id = ?`).get(id) as
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 sessions s JOIN projects p ON p.id = s.project_id
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 sessions s JOIN projects p ON p.id = s.project_id
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 sessions WHERE task_status IS NOT NULL
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
  )
@@ -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.prepare(
209
- `UPDATE sessions SET priority = COALESCE(?, priority), placed_note = COALESCE(?, placed_note) WHERE id = ?`
210
- ).run(priority, note, r.id);
209
+ db.transaction(() => {
210
+ setTask(db, r.id, { priority: priority ?? undefined });
211
+ setPlacement(db, r.id, { placed_note: note ?? undefined });
212
+ })();
211
213
  }
@@ -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("./remote-plan")
132
- .then((m) => m.syncRemotePlans())
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;
@@ -66,6 +66,7 @@ export type {
66
66
  QueuedTaskView,
67
67
  StartOutcome,
68
68
  } from "./queue-types";
69
+ import { setTask } from "../db/session-parts";
69
70
  export { afterMet, hasFinished, resolveAfter, slotState } from "./queue-rows";
70
71
  export { findQueued, listQueue, queuedView } from "./queue-view";
71
72
  export { tickOnce, tickQueue, tickQueueSoon } from "./queue-tick";
@@ -260,11 +261,18 @@ export function setPriority(id: string, priority: Priority): void {
260
261
  `UPDATE task_queue SET priority = ? WHERE id = ? AND status IN ('queued', 'failed')`
261
262
  )
262
263
  .run(priority, id).changes;
263
- const running = db
264
- .prepare(
265
- `UPDATE sessions SET priority = ? WHERE id = ? AND task_status IS NOT NULL AND archived_at IS NULL`
266
- )
267
- .run(priority === DEFAULT_PRIORITY ? null : priority, id).changes;
264
+ const running = db.transaction(() => {
265
+ const live = db
266
+ .prepare(
267
+ `SELECT 1 FROM session_rows WHERE id = ? AND task_status IS NOT NULL AND archived_at IS NULL`
268
+ )
269
+ .get(id);
270
+ return live
271
+ ? setTask(db, id, {
272
+ priority: priority === DEFAULT_PRIORITY ? null : priority,
273
+ })
274
+ : false;
275
+ })();
268
276
  if (!queued && !running) throw new Error("No such queued or running task");
269
277
  tickQueueSoon();
270
278
  }
@@ -272,7 +280,7 @@ export function setPriority(id: string, priority: Priority): void {
272
280
  // A task started directly (never queued) keeps the priority it was given.
273
281
  export function stampPriority(id: string, priority: Priority | undefined) {
274
282
  if (!priority || priority === DEFAULT_PRIORITY) return;
275
- db.prepare(`UPDATE sessions SET priority = ? WHERE id = ?`).run(priority, id);
283
+ setTask(db, id, { priority });
276
284
  }
277
285
 
278
286
  export function removeQueued(id: string): void {
@@ -26,6 +26,7 @@ import {
26
26
  import { notifyStatusChanged } from "../status/hub";
27
27
  import type { TaskPR } from "./state";
28
28
  import { markStatusDirty } from "../status/dirty";
29
+ import { setPr } from "../db/session-parts";
29
30
 
30
31
  export const expandHome = (p: string) => p.replace(/^~/, os.homedir());
31
32
 
@@ -33,7 +34,7 @@ export const expandHome = (p: string) => p.replace(/^~/, os.homedir());
33
34
  export function taskSessions(includeArchived = true): Session[] {
34
35
  return db
35
36
  .prepare(
36
- `SELECT ${sessionListColumns(db)} FROM sessions
37
+ `SELECT ${sessionListColumns(db)} FROM session_rows
37
38
  WHERE task_status IS NOT NULL AND task_status != 'moved'
38
39
  ${includeArchived ? "" : "AND archived_at IS NULL"}
39
40
  ORDER BY created_at DESC`
@@ -205,9 +206,11 @@ export async function prFor(
205
206
  if (pr) pushPrChange(session, pr);
206
207
  }
207
208
  if (pr) {
208
- db.prepare(
209
- `UPDATE sessions SET pr_url = ?, pr_number = ?, pr_status = ? WHERE id = ?`
210
- ).run(pr.url, pr.number, pr.state.toLowerCase(), session.id);
209
+ setPr(db, session.id, {
210
+ pr_url: pr.url,
211
+ pr_number: pr.number,
212
+ pr_status: pr.state.toLowerCase() as "open" | "merged" | "closed",
213
+ });
211
214
  // Merged on GitHub rather than through AgentOS: its log is due too.
212
215
  if (pr.state === "MERGED") {
213
216
  if (session.pr_status !== "merged") pushMerged(session.id);
@@ -107,7 +107,7 @@ export async function finishTaskStart(
107
107
  earlier?: TaskSetup
108
108
  ): Promise<TaskSetup> {
109
109
  const session = db
110
- .prepare(`SELECT * FROM sessions WHERE id = ?`)
110
+ .prepare(`SELECT * FROM session_rows WHERE id = ?`)
111
111
  .get(sessionId) as Session | undefined;
112
112
  if (!session?.worktree_path) throw new Error(`No task ${sessionId}`);
113
113
  const project = session.project_id ? getProject(session.project_id) : null;
@@ -133,7 +133,7 @@ export async function finishTaskStart(
133
133
  // The setup's own error is kept, for the agent's prompt when it launches.
134
134
  if (held) return done({ ...setup, status: "held" });
135
135
  const now = db
136
- .prepare(`SELECT task_status, archived_at FROM sessions WHERE id = ?`)
136
+ .prepare(`SELECT task_status, archived_at FROM session_rows WHERE id = ?`)
137
137
  .get(sessionId) as
138
138
  | { task_status: string | null; archived_at: string | null }
139
139
  | undefined;
@@ -226,7 +226,7 @@ export async function resumeTaskStarts(
226
226
  ): Promise<string[]> {
227
227
  const rows = db
228
228
  .prepare(
229
- `SELECT id, tmux_name, view, setup_ms, setup_error FROM sessions
229
+ `SELECT id, tmux_name, view, setup_ms, setup_error FROM session_rows
230
230
  WHERE setup_status = 'running' AND task_status = 'running'
231
231
  AND archived_at IS NULL`
232
232
  )
@@ -269,7 +269,7 @@ export async function resumeTaskStarts(
269
269
  export function resumeHeldStarts(): string[] {
270
270
  const rows = db
271
271
  .prepare(
272
- `SELECT id, setup_ms, setup_error FROM sessions WHERE setup_status = 'held'
272
+ `SELECT id, setup_ms, setup_error FROM session_rows WHERE setup_status = 'held'
273
273
  AND task_status = 'running' AND archived_at IS NULL`
274
274
  )
275
275
  .all() as ({ id: string } & SetupColumns)[];
@@ -4,6 +4,7 @@
4
4
 
5
5
  import path from "path";
6
6
  import { db } from "../db";
7
+ import { setPlacement } from "../db/session-parts";
7
8
 
8
9
  export type StartVia =
9
10
  | "session"
@@ -59,7 +60,7 @@ export function callerSession(input: {
59
60
  const cwd = path.resolve(input.cwd);
60
61
  const rows = db
61
62
  .prepare(
62
- `SELECT id, role, worktree_path FROM sessions
63
+ `SELECT id, role, worktree_path FROM session_rows
63
64
  WHERE worktree_path IS NOT NULL AND archived_at IS NULL
64
65
  AND task_status IS NOT NULL
65
66
  AND (host_id IS NULL OR host_id = 'local')`
@@ -111,14 +112,10 @@ export function orchestratorOf(startedBy: string | null | undefined) {
111
112
  }
112
113
 
113
114
  export function recordStart(id: string, s: Starter): void {
114
- db.prepare(
115
- `UPDATE sessions SET started_by = ?, parent_session_id = (SELECT id FROM sessions WHERE id = ?), started_via = ?,
116
- started_by_device = COALESCE(?, started_by_device) WHERE id = ?`
117
- ).run(
118
- s.startedBy,
119
- s.parentSessionId,
120
- s.startedVia,
121
- s.startedByDevice ?? null,
122
- id
123
- );
115
+ db.transaction(() => {
116
+ db.prepare(
117
+ `UPDATE sessions SET started_by = ?, parent_session_id = (SELECT id FROM sessions WHERE id = ?), started_via = ? WHERE id = ?`
118
+ ).run(s.startedBy, s.parentSessionId, s.startedVia, id);
119
+ setPlacement(db, id, { started_by_device: s.startedByDevice ?? undefined });
120
+ })();
124
121
  }
@@ -97,7 +97,7 @@ export function sessionContext(sessionId: string): SessionContext {
97
97
  const s = db
98
98
  .prepare(
99
99
  `SELECT name, workspace_id, project_id, model_provider, role, task_status,
100
- plan_phase, started_by FROM sessions WHERE id = ?`
100
+ plan_phase, started_by FROM session_rows WHERE id = ?`
101
101
  )
102
102
  .get(sessionId) as
103
103
  | {
@@ -225,7 +225,7 @@ export function taskOutcomes(ids: string[]): Map<string, TaskOutcome> {
225
225
  const marks = ids.map(() => "?").join(",");
226
226
  const tasks = db
227
227
  .prepare(
228
- `SELECT id, task_status, plan_decided FROM sessions
228
+ `SELECT id, task_status, plan_decided FROM session_rows
229
229
  WHERE id IN (${marks}) AND task_status IS NOT NULL`
230
230
  )
231
231
  .all(...ids) as {
@@ -82,7 +82,7 @@ export function mergedTasks(from: number, to: number): MergedTask[] {
82
82
  db
83
83
  .prepare(
84
84
  `SELECT id, project_id AS projectId, pr_number AS prNumber, merged_at AS mergedAt
85
- FROM sessions WHERE task_status = 'merged' AND merged_at >= ? AND merged_at < ?`
85
+ FROM session_rows WHERE task_status = 'merged' AND merged_at >= ? AND merged_at < ?`
86
86
  )
87
87
  .all(sqlTime(from), sqlTime(to)) as MergedTask[]
88
88
  ).filter((t) => t.mergedAt);
@@ -61,7 +61,7 @@ export function recordTurn(
61
61
  const current = db
62
62
  .prepare(
63
63
  `SELECT name, workspace_id, model, model_provider, role, task_status,
64
- plan_phase, started_by FROM sessions WHERE id = ?`
64
+ plan_phase, started_by FROM session_rows WHERE id = ?`
65
65
  )
66
66
  .get(session.id) as
67
67
  | Pick<
@@ -44,7 +44,7 @@ export function vocabFor(workspaceId: string): string[] {
44
44
  workspaceId
45
45
  );
46
46
  const sessions = names(
47
- `SELECT s.name FROM sessions s
47
+ `SELECT s.name FROM session_rows s
48
48
  JOIN projects p ON p.id = s.project_id
49
49
  WHERE p.workspace_id = ?
50
50
  AND (s.task_status IS NULL OR s.task_status NOT IN ('merged','dropped','done','moved'))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saadnvd1/agent-os",
3
- "version": "0.3.637",
3
+ "version": "0.3.639",
4
4
  "description": "Self-hosted web UI for managing Claude Code sessions",
5
5
  "bin": {
6
6
  "agent-os": "scripts/agent-os"
@@ -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 sessions
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)`
@@ -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
- }