@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
@@ -57,7 +57,7 @@ export interface PushOptions {
57
57
 
58
58
  function isMirrored(sessionId: string): boolean {
59
59
  const s = db
60
- .prepare(`SELECT host_id, peer_mirror FROM sessions WHERE id = ?`)
60
+ .prepare(`SELECT host_id, peer_mirror FROM session_rows WHERE id = ?`)
61
61
  .get(sessionId) as
62
62
  | { host_id: string | null; peer_mirror: number | null }
63
63
  | undefined;
@@ -439,7 +439,7 @@ export async function resurrect(
439
439
  if (!reason.trim()) throw new Error("Say why it's restored");
440
440
  const id = resolveEnded(ref, { workspaceId });
441
441
  const row = db
442
- .prepare(`SELECT task_status, archived_at FROM sessions WHERE id = ?`)
442
+ .prepare(`SELECT task_status, archived_at FROM session_rows WHERE id = ?`)
443
443
  .get(id) as { task_status: string | null; archived_at: string | null };
444
444
  const task =
445
445
  row.task_status === "dropped" ||
@@ -28,6 +28,7 @@ import { review } from "./review";
28
28
  import { judge, mergeJudged } from "./signoff";
29
29
  import { workspaceTask } from "./targets";
30
30
  import { markStatusDirty } from "../status/dirty";
31
+ import { setPr } from "../db/session-parts";
31
32
 
32
33
  // Judged again at least this often, for what the fingerprint can't see.
33
34
  export const ARMED_RECHECK_MS = 5 * 60 * 1000;
@@ -35,10 +36,18 @@ export const ARMED_RECHECK_MS = 5 * 60 * 1000;
35
36
  const short = (sha: string) => sha.slice(0, 7);
36
37
 
37
38
  export function setArmed(sessionId: string, armed: boolean): void {
38
- db.prepare(
39
- `UPDATE sessions SET merge_armed_at = ${armed ? "COALESCE(merge_armed_at, datetime('now'))" : "NULL"},
40
- merge_armed_seen = NULL WHERE id = ?`
41
- ).run(sessionId);
39
+ db.transaction(() => {
40
+ const row = db
41
+ .prepare(`SELECT merge_armed_at FROM session_rows WHERE id = ?`)
42
+ .get(sessionId) as { merge_armed_at: string | null } | undefined;
43
+ setPr(db, sessionId, {
44
+ merge_armed_at: !armed
45
+ ? null
46
+ : (row?.merge_armed_at ??
47
+ new Date().toISOString().slice(0, 19).replace("T", " ")),
48
+ merge_armed_seen: null,
49
+ });
50
+ })();
42
51
  looked.delete(sessionId);
43
52
  markStatusDirty(sessionId);
44
53
  notifyStatusChanged();
@@ -57,7 +66,7 @@ export function armingWorkspace(task: Session): string | null {
57
66
  function armedTasks(workspaceId: string): Session[] {
58
67
  return db
59
68
  .prepare(
60
- `SELECT s.* FROM sessions s JOIN projects p ON p.id = s.project_id
69
+ `SELECT s.* FROM session_rows s JOIN projects p ON p.id = s.project_id
61
70
  WHERE p.workspace_id = ? AND s.merge_armed_at IS NOT NULL
62
71
  AND s.task_status = 'running' AND s.peer_mirror = 0
63
72
  AND s.held_by_device IS NULL`
@@ -70,7 +79,7 @@ function armedTasks(workspaceId: string): Session[] {
70
79
  function stillArmed(workspaceId: string, taskId: string): boolean {
71
80
  if (isPaused(workspaceId)) return false;
72
81
  const row = db
73
- .prepare(`SELECT merge_armed_at FROM sessions WHERE id = ?`)
82
+ .prepare(`SELECT merge_armed_at FROM session_rows WHERE id = ?`)
74
83
  .get(taskId) as { merge_armed_at: string | null } | undefined;
75
84
  return !!row?.merge_armed_at;
76
85
  }
@@ -255,11 +264,7 @@ async function mergeOne(
255
264
  if (counted.ok) return merge(workspaceId, task, counted, deps);
256
265
  const toSaad = holdingEscalations(task.id).length > 0;
257
266
  if (toSaad) setArmed(task.id, false);
258
- else
259
- db.prepare(`UPDATE sessions SET merge_armed_seen = ? WHERE id = ?`).run(
260
- seen,
261
- task.id
262
- );
267
+ else setPr(db, task.id, { merge_armed_seen: seen });
263
268
  event(
264
269
  workspaceId,
265
270
  task,
@@ -283,10 +288,7 @@ async function merge(
283
288
  const why = error instanceof Error ? error.message : String(error);
284
289
  const seen = `${verdict.sha}:merge:${why}`;
285
290
  if (task.merge_armed_seen === seen) return;
286
- db.prepare(`UPDATE sessions SET merge_armed_seen = ? WHERE id = ?`).run(
287
- seen,
288
- task.id
289
- );
291
+ setPr(db, task.id, { merge_armed_seen: seen });
290
292
  event(
291
293
  workspaceId,
292
294
  task,
@@ -76,7 +76,7 @@ export function releaseInterruptedClaims(): number {
76
76
  `UPDATE orchestrator_asks SET used_at = NULL
77
77
  WHERE status = 'approved' AND used_at IS NOT NULL
78
78
  AND COALESCE(answer, '') != 'approve (void)'
79
- AND (subject IN (SELECT 'task:' || id FROM sessions WHERE task_status = 'running')
79
+ AND (subject IN (SELECT 'task:' || id FROM session_rows WHERE task_status = 'running')
80
80
  -- A PR no task owns: a merge that happened can't happen again.
81
81
  OR subject LIKE 'pr:%')`
82
82
  )
@@ -34,7 +34,7 @@ export async function followAskHeads(
34
34
  )
35
35
  continue;
36
36
  const task = db
37
- .prepare(`SELECT * FROM sessions WHERE id = ?`)
37
+ .prepare(`SELECT * FROM session_rows WHERE id = ?`)
38
38
  .get(f!.id) as Session | undefined;
39
39
  if (!task || hostLink(task.host_id)) continue;
40
40
  try {
@@ -44,7 +44,7 @@ function workspaceTasks(workspaceId: string): TaskRow[] {
44
44
  return db
45
45
  .prepare(
46
46
  `SELECT s.id, s.task_status, s.pr_status, s.pr_url, s.pr_number
47
- FROM sessions s JOIN projects p ON p.id = s.project_id
47
+ FROM session_rows s JOIN projects p ON p.id = s.project_id
48
48
  WHERE p.workspace_id = ? AND s.task_status IS NOT NULL`
49
49
  )
50
50
  .all(workspaceId) as TaskRow[];
@@ -81,7 +81,7 @@ export async function runningCount(
81
81
  // minutes, here or there: it holds its slot.
82
82
  const moving = db
83
83
  .prepare(
84
- `SELECT s.id FROM sessions s JOIN projects p ON p.id = s.project_id
84
+ `SELECT s.id FROM session_rows s JOIN projects p ON p.id = s.project_id
85
85
  WHERE p.workspace_id = ? AND s.task_status = 'moving' AND s.peer_mirror = 0`
86
86
  )
87
87
  .all(workspaceId) as { id: string }[];
@@ -167,7 +167,7 @@ function tasksSection(
167
167
  .prepare(
168
168
  `SELECT s.id, s.name, p.name AS project, s.pr_number, s.pr_status, s.view,
169
169
  s.merge_armed_at AS armed_at, s.pause_state, s.paused_until, s.pause_note, s.placed_note
170
- FROM sessions s JOIN projects p ON p.id = s.project_id
170
+ FROM session_rows s JOIN projects p ON p.id = s.project_id
171
171
  WHERE p.workspace_id = ? AND s.role IS NULL AND s.archived_at IS NULL
172
172
  AND s.task_status = 'running'
173
173
  ORDER BY s.created_at, s.id`
@@ -223,7 +223,7 @@ function queueSection(workspaceId: string): Section {
223
223
  AND b.held_queued = 1 AND b.delivered_at IS NULL AND b.read_at IS NULL) AS held,
224
224
  p.name AS project, a.name AS after_name
225
225
  FROM task_queue q JOIN projects p ON p.id = q.project_id
226
- LEFT JOIN sessions a ON a.id = q.after_task
226
+ LEFT JOIN session_rows a ON a.id = q.after_task
227
227
  WHERE p.workspace_id = ? AND q.status IN ('queued', 'starting', 'failed')
228
228
  ORDER BY q.position`
229
229
  )
@@ -94,7 +94,7 @@ const unique = (repos: WorkspaceRepo[]) => [
94
94
  function tasksWithPR(workspaceId: string, ref: PRRef): WorkspaceTask[] {
95
95
  const rows = db
96
96
  .prepare(
97
- `SELECT s.*, p.name AS project_name FROM sessions s
97
+ `SELECT s.*, p.name AS project_name FROM session_rows s
98
98
  JOIN projects p ON p.id = s.project_id
99
99
  WHERE p.workspace_id = ? AND s.pr_number = ? AND s.task_status IS NOT NULL
100
100
  ORDER BY s.created_at DESC`
@@ -125,7 +125,7 @@ async function ownerOf(
125
125
  .prepare(
126
126
  `SELECT s.*, p.name AS project_name, p.workspace_id AS owner_workspace,
127
127
  p.working_directory AS project_dir, p.host_id AS project_host
128
- FROM sessions s JOIN projects p ON p.id = s.project_id
128
+ FROM session_rows s JOIN projects p ON p.id = s.project_id
129
129
  WHERE s.task_status IS NOT NULL
130
130
  AND (s.pr_number = ? OR (s.branch_name = ? AND s.task_status IN ('running', 'moving', 'moved')))
131
131
  ORDER BY s.created_at DESC`
@@ -7,7 +7,7 @@
7
7
 
8
8
  import { db, type Session, type StackItemStatus } from "../db";
9
9
  import { statusDetector } from "../status-detector";
10
- import { cleanRemoteText, hostLink } from "../hosts/remote-api";
10
+ import { hostLink } from "../hosts/remote-api";
11
11
  import { peerStatus } from "../hosts/peer-sessions";
12
12
  import { chatCompacting, chatState } from "../chat/runner";
13
13
  import { chatActivityLine, chatStall, type Stall } from "../chat/activity";
@@ -18,14 +18,13 @@ import { sessionRowInfo } from "../session-meta";
18
18
  import { taskView, type TaskPR, type TaskState } from "../tasks";
19
19
  import { prFor, storedPR } from "../tasks/session";
20
20
  import { lastActiveOf } from "../tasks/last-active";
21
- import { hostTasks } from "../tasks/remote";
21
+ import * as taskClient from "../tasks/contract/client";
22
22
  import { deriveTaskState } from "../tasks";
23
23
  import { waitingState } from "./task-state";
24
24
  import { commitTime, repoOf } from "./repo";
25
25
  import { CHECKLESS_MS, checklessFor, ciSettleIn } from "./task-state";
26
26
  import { repoHasCI } from "./repo-ci";
27
27
  import { markStatusDirty } from "../status/dirty";
28
- import { queueEvent } from "./events";
29
28
 
30
29
  export type FactStatus = "running" | "waiting" | "idle" | "dead";
31
30
 
@@ -85,7 +84,7 @@ export function workspaceSessions(
85
84
  ): (Session & { project_name: string })[] {
86
85
  return db
87
86
  .prepare(
88
- `SELECT s.*, p.name AS project_name FROM sessions s
87
+ `SELECT s.*, p.name AS project_name FROM session_rows s
89
88
  JOIN projects p ON p.id = s.project_id
90
89
  WHERE p.workspace_id = ? AND s.role IS NULL AND s.archived_at IS NULL
91
90
  -- A linked machine's own sessions are its orchestrator's to run.
@@ -271,7 +270,6 @@ async function remoteTaskFacts(
271
270
  prFor(s).catch(() => null),
272
271
  waitingState(s),
273
272
  ]);
274
- await announceRemotePlan(workspaceId, s);
275
273
  const status = peerStatus(s.host_id, s.id) ?? "running";
276
274
  const state = deriveTaskState({
277
275
  taskStatus: "running",
@@ -288,34 +286,12 @@ async function remoteTaskFacts(
288
286
  };
289
287
  }
290
288
 
291
- // The plan-written line a held task stored on its machine (plan-step.ts),
292
- // once per distinct plan: the mirror keeps the last one it told about.
293
- async function announceRemotePlan(
294
- workspaceId: string,
295
- s: Session
296
- ): Promise<void> {
297
- const link = hostLink(s.host_id);
298
- if (!link) return;
299
- const view = await hostTasks(link)
300
- .then(({ tasks }) => tasks.find((t) => t?.id === s.id))
301
- .catch(() => undefined);
302
- const note =
303
- typeof view?.planNote === "string"
304
- ? cleanRemoteText(view.planNote).slice(0, 2000)
305
- : "";
306
- if (!note || note === s.plan_note) return;
307
- db.prepare(`UPDATE sessions SET plan_note = ? WHERE id = ?`).run(note, s.id);
308
- queueEvent(workspaceId, `plan-done:${s.id}`, s.id, note);
309
- }
310
-
311
289
  // A task on another machine is as active as that machine says; one it can't
312
290
  // reach is taken as active just now, so nothing reads it as abandoned.
313
291
  async function lastActiveFor(s: Session): Promise<number> {
314
292
  const link = hostLink(s.host_id);
315
293
  if (!link) return lastActiveOf(s);
316
- const view = await hostTasks(link)
317
- .then(({ tasks }) => tasks.find((t) => t?.id === s.id))
318
- .catch(() => undefined);
294
+ const view = await taskClient.view(link, s.id).catch(() => undefined);
319
295
  return typeof view?.lastActiveAt === "number"
320
296
  ? view.lastActiveAt
321
297
  : Date.now();
@@ -58,9 +58,9 @@ async function raiseFor(
58
58
  now: number
59
59
  ): Promise<void> {
60
60
  const f = facts.find((x) => x.id === taskId);
61
- const task = db.prepare(`SELECT * FROM sessions WHERE id = ?`).get(taskId) as
62
- | Session
63
- | undefined;
61
+ const task = db
62
+ .prepare(`SELECT * FROM session_rows WHERE id = ?`)
63
+ .get(taskId) as Session | undefined;
64
64
  // Gone, merged or dropped: nothing left to ask. An open ask or an
65
65
  // unspent approval already covers it (followAskHeads, heldVerdict).
66
66
  if (
@@ -24,7 +24,7 @@ export function getOrchestrator(workspaceId: string): Session | null {
24
24
  return (
25
25
  (db
26
26
  .prepare(
27
- `SELECT * FROM sessions WHERE role = 'orchestrator' AND workspace_id = ?`
27
+ `SELECT * FROM session_rows WHERE role = 'orchestrator' AND workspace_id = ?`
28
28
  )
29
29
  .get(workspaceId) as Session | undefined) ?? null
30
30
  );
@@ -32,7 +32,7 @@ export function getOrchestrator(workspaceId: string): Session | null {
32
32
 
33
33
  export function listOrchestrators(): Session[] {
34
34
  return db
35
- .prepare(`SELECT * FROM sessions WHERE role = 'orchestrator'`)
35
+ .prepare(`SELECT * FROM session_rows WHERE role = 'orchestrator'`)
36
36
  .all() as Session[];
37
37
  }
38
38
 
@@ -25,7 +25,7 @@ function inReview(workspaceId: string): number {
25
25
  return (
26
26
  db
27
27
  .prepare(
28
- `SELECT COUNT(*) AS n FROM sessions s JOIN projects p ON p.id = s.project_id
28
+ `SELECT COUNT(*) AS n FROM session_rows s JOIN projects p ON p.id = s.project_id
29
29
  WHERE p.workspace_id = ? AND s.role IS NULL
30
30
  AND s.task_status = 'running' AND s.pr_status = 'open'`
31
31
  )
@@ -41,6 +41,7 @@ import type { SessionFacts } from "./facts";
41
41
  import { isPaused } from "./pause";
42
42
  import { repoOf } from "./repo";
43
43
  import { untrusted, UNTRUSTED_RULE } from "./untrusted";
44
+ import { setPr } from "../db/session-parts";
44
45
 
45
46
  export const COMMENT_WAKE_CAP = 10;
46
47
  // A first look at a PR this young takes its whole history as new.
@@ -93,7 +94,7 @@ const polled = new Map<string, number>();
93
94
  const failures = new Map<string, number>();
94
95
 
95
96
  const sessionRow = (id: string) =>
96
- db.prepare(`SELECT * FROM sessions WHERE id = ?`).get(id) as
97
+ db.prepare(`SELECT * FROM session_rows WHERE id = ?`).get(id) as
97
98
  | Session
98
99
  | undefined;
99
100
 
@@ -350,10 +351,7 @@ function watchSince(task: Session, print: Fingerprint, now: number): string {
350
351
  Number.isFinite(created) && now - created < YOUNG_PR_MS
351
352
  ? new Date(created).toISOString()
352
353
  : new Date(now).toISOString();
353
- db.prepare(`UPDATE sessions SET pr_wake_since = ? WHERE id = ?`).run(
354
- since,
355
- task.id
356
- );
354
+ setPr(db, task.id, { pr_wake_since: since });
357
355
  task.pr_wake_since = since;
358
356
  return since;
359
357
  }
@@ -46,7 +46,7 @@ function settleQuestions(workspaceId: string): void {
46
46
  `UPDATE orchestrator_questions
47
47
  SET status = 'cleared', resolved_by = 'orchestrator', resolved_at = datetime('now')
48
48
  WHERE workspace_id = ? AND status = 'open' AND task IN (
49
- SELECT id FROM sessions WHERE task_status IN ('merged', 'dropped', 'done'))`
49
+ SELECT id FROM session_rows WHERE task_status IN ('merged', 'dropped', 'done'))`
50
50
  ).run(workspaceId);
51
51
  }
52
52
 
@@ -9,7 +9,7 @@ export function quickWorktrees(orchestratorId: string): string[] {
9
9
  return (
10
10
  db
11
11
  .prepare(
12
- `SELECT worktree_path AS dir FROM sessions
12
+ `SELECT worktree_path AS dir FROM session_rows
13
13
  WHERE quick_of = ? AND task_status = 'running'
14
14
  AND worktree_path IS NOT NULL AND worktree_path != ''`
15
15
  )
@@ -29,6 +29,7 @@ import { addNote } from "./notes";
29
29
  import { quickWorktrees } from "./quick-store";
30
30
  import { findWorkspaceSession } from "./read";
31
31
  import { workspaceProject } from "./targets";
32
+ import { recordPr, setTask } from "../db/session-parts";
32
33
 
33
34
  // Open quick changes one orchestrator may hold at once.
34
35
  export const MAX_OPEN_QUICK = 5;
@@ -105,8 +106,13 @@ export async function startQuickChange(
105
106
  .updateSessionWorktree(db)
106
107
  .run(wt.worktreePath, wt.branchName, baseBranch, null, id);
107
108
  db.prepare(
108
- `UPDATE sessions SET task_prompt = ?, task_status = 'running', name_source = 'user', view = 'chat', quick_of = ? WHERE id = ?`
109
- ).run(`Quick change: ${label}`, owner.id, id);
109
+ `UPDATE sessions SET name_source = 'user', view = 'chat' WHERE id = ?`
110
+ ).run(id);
111
+ setTask(db, id, {
112
+ task_prompt: `Quick change: ${label}`,
113
+ task_status: "running",
114
+ quick_of: owner.id,
115
+ });
110
116
  })();
111
117
  } catch (e) {
112
118
  await deleteWorktree(wt.worktreePath, projectPath, true).catch(() => {});
@@ -208,10 +214,7 @@ export async function quickPR(
208
214
  );
209
215
  const head = (await revParse(wt, "HEAD")).trim();
210
216
  const goal = `${title.trim()}\n\n${body.trim()}`;
211
- db.prepare(`UPDATE sessions SET task_prompt = ? WHERE id = ?`).run(
212
- goal,
213
- s.id
214
- );
217
+ setTask(db, s.id, { task_prompt: goal });
215
218
  const existing = await prFor(s, true);
216
219
  if (existing?.state === "OPEN") {
217
220
  forgetPR(s.id);
@@ -238,7 +241,7 @@ export async function quickPR(
238
241
  60_000
239
242
  );
240
243
  const m = /https:\/\/\S+\/pull\/(\d+)/.exec(out);
241
- if (m) queries.updateSessionPR(db).run(m[0], Number(m[1]), "open", s.id);
244
+ if (m) recordPr(db, s.id, { url: m[0], number: Number(m[1]), state: "open" });
242
245
  forgetPR(s.id);
243
246
  addNote(
244
247
  workspaceId,
@@ -10,7 +10,7 @@ import { hostLink } from "../hosts/remote-api";
10
10
  import { peerPane } from "../hosts/peer-sessions";
11
11
  import { workspaceSessions } from "./facts";
12
12
  import { untrusted } from "./untrusted";
13
- import { heldChatTail, isHeldRemote } from "../tasks/held-remote";
13
+ import { heldChatTail, isHeldRemote } from "../tasks/contract/held-calls";
14
14
  import {
15
15
  findExternalSession,
16
16
  knownExternalSession,
@@ -39,6 +39,7 @@ import { statusOf, type SessionFacts } from "./facts";
39
39
  import { isPaused } from "./pause";
40
40
  import { repoOf } from "./repo";
41
41
  import { reviewAt, taskReviewTarget } from "./review";
42
+ import { setPr } from "../db/session-parts";
42
43
 
43
44
  const short = (sha: string) => sha.slice(0, 7);
44
45
 
@@ -72,7 +73,7 @@ const defaults: RebaseDeps = {
72
73
  const tried = new Map<string, string>();
73
74
 
74
75
  const sessionRow = (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
 
@@ -207,9 +208,10 @@ async function rebaseOne(
207
208
  },
208
209
  { from: pr.head, to, unpushed: false }
209
210
  );
210
- db.prepare(
211
- `UPDATE sessions SET rebased_from = ?, rebased_to = ? WHERE id = ?`
212
- ).run(heads.restacked_from, heads.restacked_to, task.id);
211
+ setPr(db, task.id, {
212
+ rebased_from: heads.restacked_from,
213
+ rebased_to: heads.restacked_to,
214
+ });
213
215
  // Only the task's own branch, and only if origin still has the head
214
216
  // that was judged. --no-verify: CI runs on the PR.
215
217
  const pushed = await git(
@@ -225,9 +227,7 @@ async function rebaseOne(
225
227
  );
226
228
  if (pushed !== null) {
227
229
  console.error(`Pushing ${task.name}'s rebase:`, pushed);
228
- db.prepare(
229
- `UPDATE sessions SET rebased_from = ?, rebased_to = ? WHERE id = ?`
230
- ).run(...before, task.id);
230
+ setPr(db, task.id, { rebased_from: before[0], rebased_to: before[1] });
231
231
  return askToRebase(
232
232
  workspaceId,
233
233
  task,
@@ -362,10 +362,7 @@ async function askToRebase(
362
362
  deps: RebaseDeps
363
363
  ): Promise<void> {
364
364
  // Claimed first: a restart mid-send doesn't ask twice.
365
- db.prepare(`UPDATE sessions SET rebase_asked_head = ? WHERE id = ?`).run(
366
- pr.head,
367
- task.id
368
- );
365
+ setPr(db, task.id, { rebase_asked_head: pr.head });
369
366
  // A quick change has no agent to ask: it's the orchestrator's to redo.
370
367
  if (task.quick_of) {
371
368
  queueEvent(
@@ -114,7 +114,7 @@ async function ownedBy(
114
114
  ): Promise<boolean> {
115
115
  const rows = db
116
116
  .prepare(
117
- `SELECT s.pr_url, s.branch_name, p.working_directory FROM sessions s
117
+ `SELECT s.pr_url, s.branch_name, p.working_directory FROM session_rows s
118
118
  LEFT JOIN projects p ON p.id = s.project_id
119
119
  WHERE s.task_status IS NOT NULL AND (LOWER(s.pr_url) = ? OR s.branch_name = ?)`
120
120
  )
@@ -145,7 +145,7 @@ export async function classifyAgentPrs(
145
145
  let raised = 0;
146
146
  for (const pr of open) {
147
147
  const task = db
148
- .prepare(`SELECT * FROM sessions WHERE id = ?`)
148
+ .prepare(`SELECT * FROM session_rows WHERE id = ?`)
149
149
  .get(pr.session_id) as Session | undefined;
150
150
  const workspaceId = task ? sessionWorkspace(task) : null;
151
151
  if (!task || !workspaceId) {
@@ -331,7 +331,9 @@ export async function refundIfRefused(
331
331
  await merge();
332
332
  } catch (e) {
333
333
  const merged = db
334
- .prepare(`SELECT 1 FROM sessions WHERE id = ? AND task_status = 'merged'`)
334
+ .prepare(
335
+ `SELECT 1 FROM session_rows WHERE id = ? AND task_status = 'merged'`
336
+ )
335
337
  .get(taskId);
336
338
  if (approval && !merged) refundApproval(approval);
337
339
  throw e;
@@ -56,7 +56,7 @@ export function workspaceTask(
56
56
  if (pr !== null) {
57
57
  const rows = db
58
58
  .prepare(
59
- `SELECT s.*, p.name AS project_name FROM sessions s
59
+ `SELECT s.*, p.name AS project_name FROM session_rows s
60
60
  JOIN projects p ON p.id = s.project_id
61
61
  WHERE p.workspace_id = ? AND s.pr_number = ? AND s.task_status IS NOT NULL
62
62
  ORDER BY s.created_at DESC`
@@ -9,8 +9,8 @@ import { blockedReason } from "../tasks/state";
9
9
  import { checkWaitingPatterns, statusDetector } from "../status-detector";
10
10
  import { getCheck, putCheck } from "./checks";
11
11
  import { cleanRemoteText, hostLink, type HostLink } from "../hosts/remote-api";
12
- import { heldChatFacts, isHeldRemote } from "../tasks/held-remote";
13
- import { hostTasks } from "../tasks/remote";
12
+ import { heldChatFacts, isHeldRemote } from "../tasks/contract/held-calls";
13
+ import { list as hostTasks } from "../tasks/contract/client";
14
14
  import { peerPane } from "../hosts/peer-sessions";
15
15
  import { untrusted } from "./untrusted";
16
16
 
@@ -104,6 +104,8 @@ async function peerChatWaiting(
104
104
  : `its chat on ${link.hostName} couldn't say whether a question is open`,
105
105
  };
106
106
  }
107
+ // compat: task-contract-1. A machine without held-chat-facts is asked
108
+ // through its task list; drop this once every linked machine has it.
107
109
  const view = await hostTasks(link, true)
108
110
  .then(({ tasks, capabilities }) =>
109
111
  // One that can't say its chat's turn can't rule out an open question.
@@ -160,7 +160,7 @@ export function pieceOfSession(sessionId: string): FeaturePiece | undefined {
160
160
  export function prOf(p: FeaturePiece): number | null {
161
161
  if (!p.session_id) return null;
162
162
  const s = db
163
- .prepare(`SELECT pr_number, project_id FROM sessions WHERE id = ?`)
163
+ .prepare(`SELECT pr_number, project_id FROM session_rows WHERE id = ?`)
164
164
  .get(p.session_id) as
165
165
  | { pr_number: number | null; project_id: string | null }
166
166
  | undefined;
@@ -38,7 +38,7 @@ export interface ReleaseDeps {
38
38
 
39
39
  const sessionOf = (id: string | null) =>
40
40
  id
41
- ? (db.prepare(`SELECT * FROM sessions WHERE id = ?`).get(id) as
41
+ ? (db.prepare(`SELECT * FROM session_rows WHERE id = ?`).get(id) as
42
42
  | Session
43
43
  | undefined)
44
44
  : undefined;
package/lib/ports.ts CHANGED
@@ -70,7 +70,7 @@ async function reclaimEnded(
70
70
  ): Promise<void> {
71
71
  const rows = db
72
72
  .prepare(
73
- `SELECT id, tmux_name, task_status, host_id FROM sessions
73
+ `SELECT id, tmux_name, task_status, host_id FROM session_rows
74
74
  WHERE port_slot IS NOT NULL
75
75
  AND (archived_at IS NOT NULL OR task_status IN ('done', 'merged'))`
76
76
  )
@@ -65,7 +65,7 @@ function rows(scope: EndedScope, limit: number, kind?: EndedKind): Row[] {
65
65
  `SELECT s.id, s.name, s.tmux_name, s.project_id, p.name AS project_name,
66
66
  s.task_status, s.archived_at, s.plan_phase,
67
67
  COALESCE(s.task_ended_at, s.archived_at, s.settled_at, s.updated_at) AS ended_at
68
- FROM sessions s LEFT JOIN projects p ON p.id = s.project_id
68
+ FROM session_rows s LEFT JOIN projects p ON p.id = s.project_id
69
69
  WHERE ${where.join(" AND ")}
70
70
  ORDER BY ended_at DESC LIMIT ?`
71
71
  )