@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.
Files changed (177) 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 +5 -1
  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 -4
  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 +35 -0
  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 +5 -2
  23. package/app/globals.css +28 -0
  24. package/components/Chat/ChatPanel.tsx +2 -11
  25. package/components/Chat/EventTurns.tsx +1 -7
  26. package/components/Chat/Markdown.tsx +1 -0
  27. package/components/Chat/Messages.tsx +1 -1
  28. package/lib/approvals/enable.ts +9 -2
  29. package/lib/away/report.ts +4 -4
  30. package/lib/away/run.ts +2 -2
  31. package/lib/bookmarks/store.ts +3 -3
  32. package/lib/bus/held-reply.ts +2 -2
  33. package/lib/bus/index.ts +3 -3
  34. package/lib/chat/compact-context.ts +1 -1
  35. package/lib/chat/events.ts +4 -0
  36. package/lib/chat/group.ts +3 -1
  37. package/lib/chat/hold.ts +3 -3
  38. package/lib/chat/reading-order.ts +50 -0
  39. package/lib/chat/refresh.ts +1 -1
  40. package/lib/chat/registry.ts +3 -3
  41. package/lib/chat/runner.ts +1 -1
  42. package/lib/chat/turns.ts +22 -3
  43. package/lib/chat/worker/host.ts +3 -0
  44. package/lib/chat/worker/main.ts +1 -1
  45. package/lib/db/index.ts +5 -1
  46. package/lib/db/lumifyhub.ts +8 -9
  47. package/lib/db/machine-scope.ts +12 -0
  48. package/lib/db/migrations/20261015T1600_plan_waits_for_caller.ts +19 -0
  49. package/lib/db/migrations/20261015T1800_split_sessions_tables.ts +172 -0
  50. package/lib/db/migrations/README.md +7 -0
  51. package/lib/db/queries.ts +16 -31
  52. package/lib/db/session-parts/columns.ts +186 -0
  53. package/lib/db/session-parts/fill.ts +33 -0
  54. package/lib/db/session-parts/index.ts +9 -0
  55. package/lib/db/session-parts/pauses.ts +11 -0
  56. package/lib/db/session-parts/placement.ts +11 -0
  57. package/lib/db/session-parts/plans.ts +31 -0
  58. package/lib/db/session-parts/prs.ts +31 -0
  59. package/lib/db/session-parts/tasks.ts +27 -0
  60. package/lib/db/session-parts/view.ts +43 -0
  61. package/lib/db/session-parts/write.ts +69 -0
  62. package/lib/db/types.ts +6 -1
  63. package/lib/done/archive.ts +2 -2
  64. package/lib/done/auto-archive.ts +1 -1
  65. package/lib/done/bulk.ts +1 -1
  66. package/lib/done/index.ts +8 -14
  67. package/lib/env-setup.ts +17 -4
  68. package/lib/health/evidence.ts +1 -1
  69. package/lib/health/facts.ts +1 -1
  70. package/lib/health/pieces.ts +1 -1
  71. package/lib/hosts/peer-actions.ts +2 -2
  72. package/lib/hosts/peer-mirror.ts +2 -2
  73. package/lib/hosts/peer-sync.ts +69 -32
  74. package/lib/learning-cards/input.ts +1 -1
  75. package/lib/learning-cards/store.ts +1 -1
  76. package/lib/models/plan-step.ts +83 -46
  77. package/lib/models/resume.ts +5 -7
  78. package/lib/notify/index.ts +1 -1
  79. package/lib/notify/push/events.ts +1 -1
  80. package/lib/notify/push/index.ts +1 -1
  81. package/lib/orchestrator/act.ts +1 -1
  82. package/lib/orchestrator/armed.ts +17 -15
  83. package/lib/orchestrator/ask-approvals.ts +1 -1
  84. package/lib/orchestrator/ask-heads.ts +1 -1
  85. package/lib/orchestrator/ask-settle.ts +1 -1
  86. package/lib/orchestrator/brakes.ts +1 -1
  87. package/lib/orchestrator/digest.ts +2 -2
  88. package/lib/orchestrator/external-pr.ts +2 -2
  89. package/lib/orchestrator/facts.ts +4 -28
  90. package/lib/orchestrator/held-asks.ts +3 -3
  91. package/lib/orchestrator/home.ts +2 -2
  92. package/lib/orchestrator/overview.ts +1 -1
  93. package/lib/orchestrator/pr-wake.ts +3 -5
  94. package/lib/orchestrator/questions.ts +1 -1
  95. package/lib/orchestrator/quick-store.ts +1 -1
  96. package/lib/orchestrator/quick.ts +10 -7
  97. package/lib/orchestrator/read.ts +1 -1
  98. package/lib/orchestrator/rebase.ts +8 -11
  99. package/lib/orchestrator/reconcile.ts +2 -2
  100. package/lib/orchestrator/signoff.ts +3 -1
  101. package/lib/orchestrator/targets.ts +1 -1
  102. package/lib/orchestrator/task-state.ts +4 -2
  103. package/lib/plans/features.ts +1 -1
  104. package/lib/plans/release.ts +1 -1
  105. package/lib/ports.ts +1 -1
  106. package/lib/resurrect/find.ts +1 -1
  107. package/lib/resurrect/index.ts +24 -18
  108. package/lib/schedules/store.ts +1 -1
  109. package/lib/session-audit/sources.ts +3 -3
  110. package/lib/session-logs/index.ts +1 -1
  111. package/lib/session-titles.ts +1 -1
  112. package/lib/sessions/setup-progress.ts +1 -1
  113. package/lib/sessions/stop.ts +1 -1
  114. package/lib/sessions/worktree-setup.ts +2 -1
  115. package/lib/simulator/index.ts +1 -1
  116. package/lib/simulator/task-devices.ts +1 -1
  117. package/lib/stacks/board.ts +1 -1
  118. package/lib/stacks/land.ts +1 -1
  119. package/lib/stacks/restack.ts +1 -1
  120. package/lib/stacks/start.ts +2 -2
  121. package/lib/stacks/tick.ts +1 -1
  122. package/lib/stacks/view.ts +1 -1
  123. package/lib/status/incremental.ts +1 -1
  124. package/lib/tasks/contract/actions.ts +35 -0
  125. package/lib/tasks/contract/authorize.ts +82 -0
  126. package/lib/tasks/contract/client.ts +296 -0
  127. package/lib/tasks/{held-remote.ts → contract/held-calls.ts} +35 -66
  128. package/lib/tasks/contract/index.ts +28 -0
  129. package/lib/tasks/contract/plan-path.ts +7 -0
  130. package/lib/tasks/contract/plan.ts +274 -0
  131. package/lib/tasks/contract/routes.ts +26 -0
  132. package/lib/tasks/contract/start.ts +122 -0
  133. package/lib/tasks/contract/sync.ts +126 -0
  134. package/lib/tasks/contract/types.ts +41 -0
  135. package/lib/tasks/{remote-views.ts → contract/views.ts} +11 -18
  136. package/lib/tasks/finish.ts +10 -9
  137. package/lib/tasks/held-replies.ts +7 -13
  138. package/lib/tasks/held.ts +36 -17
  139. package/lib/tasks/hold-checks.ts +134 -0
  140. package/lib/tasks/holds.ts +281 -0
  141. package/lib/tasks/import.ts +26 -14
  142. package/lib/tasks/index.ts +56 -22
  143. package/lib/tasks/launch-gate.ts +1 -1
  144. package/lib/tasks/move-bundle.ts +17 -2
  145. package/lib/tasks/move-carry.ts +15 -7
  146. package/lib/tasks/move-flow.ts +29 -36
  147. package/lib/tasks/move-guard.ts +10 -2
  148. package/lib/tasks/move-recover.ts +6 -25
  149. package/lib/tasks/move.ts +28 -27
  150. package/lib/tasks/moved-away.ts +1 -1
  151. package/lib/tasks/pause-hold.ts +13 -5
  152. package/lib/tasks/pause.ts +21 -54
  153. package/lib/tasks/placement.ts +16 -16
  154. package/lib/tasks/plan-view.ts +11 -3
  155. package/lib/tasks/queue-candidates.ts +151 -0
  156. package/lib/tasks/queue-placement.ts +155 -0
  157. package/lib/tasks/queue-rows.ts +236 -0
  158. package/lib/tasks/queue-start.ts +213 -0
  159. package/lib/tasks/queue-tick.ts +148 -0
  160. package/lib/tasks/queue-types.ts +141 -0
  161. package/lib/tasks/queue-view.ts +112 -0
  162. package/lib/tasks/queue.ts +67 -1102
  163. package/lib/tasks/schedule.ts +44 -147
  164. package/lib/tasks/session.ts +7 -4
  165. package/lib/tasks/start.ts +4 -4
  166. package/lib/tasks/starter.ts +8 -11
  167. package/lib/usage/calls.ts +1 -1
  168. package/lib/usage/compare.ts +1 -1
  169. package/lib/usage/quality.ts +1 -1
  170. package/lib/usage/turns.ts +1 -1
  171. package/lib/voice/vocab.ts +1 -1
  172. package/package.json +1 -1
  173. package/scripts/backfill-session-logs.ts +1 -1
  174. package/scripts/check +55 -7
  175. package/lib/tasks/auto-hold.ts +0 -17
  176. package/lib/tasks/held-routes.ts +0 -33
  177. package/lib/tasks/remote.ts +0 -238
@@ -13,59 +13,33 @@ import {
13
13
  PRIORITY_LABEL,
14
14
  type Priority,
15
15
  type QueueExplainView,
16
- type WaitCode,
17
16
  } from "./schedule-types";
17
+ import {
18
+ PRIORITY_POINTS,
19
+ holdsFor,
20
+ type Hold,
21
+ type MachineView,
22
+ } from "./holds";
18
23
 
19
24
  export { PRIORITIES, DEFAULT_PRIORITY } from "./schedule-types";
20
25
  export type { Priority, WaitCode } from "./schedule-types";
26
+ export {
27
+ AMBER_MIN_PRIORITY,
28
+ HEAVY_START_GAP_MS,
29
+ LOAD_SETTLE_MS,
30
+ PRIORITY_POINTS,
31
+ START_GAP_MS,
32
+ machineHold,
33
+ slotText,
34
+ type Hold,
35
+ type MachineLevel,
36
+ type MachineView,
37
+ } from "./holds";
21
38
 
22
- export const PRIORITY_POINTS: Record<Priority, number> = {
23
- urgent: 400,
24
- blocking: 300,
25
- normal: 200,
26
- background: 100,
27
- };
28
39
  export const AGE_POINTS_PER_MIN = 0.5;
29
40
  // Reached after five hours of waiting.
30
41
  export const AGE_CAP = 150;
31
42
  export const RESUME_BONUS = 150;
32
- // Out of red this long before automatic starts resume.
33
- export const LOAD_SETTLE_MS = 120_000;
34
- // On amber, only this priority and above start.
35
- export const AMBER_MIN_PRIORITY: Priority = "blocking";
36
- // Between automatic starts on one machine.
37
- export const START_GAP_MS = 60_000;
38
- // Between starts of tasks that boot a simulator.
39
- export const HEAVY_START_GAP_MS = 5 * 60_000;
40
-
41
- export type MachineLevel = "green" | "amber" | "red";
42
-
43
- // What the machine a candidate would start on looks like right now.
44
- export interface MachineView {
45
- // Shown in hold texts, e.g. "this machine".
46
- label: string;
47
- // False: no load holds at all (AGENTOS_LOAD=off).
48
- enabled: boolean;
49
- // No sample read yet since AgentOS started.
50
- reading: boolean;
51
- level: MachineLevel;
52
- // Not red, and calm for LOAD_SETTLE_MS since it last was.
53
- settled: boolean;
54
- // Seconds of calm still needed, when not settled and not red.
55
- settleLeftMs: number;
56
- booted: number;
57
- maxBooted: number;
58
- // Heavy tasks started within HEAVY_START_GAP_MS that hold no lease yet.
59
- heavyStarting: number;
60
- lastAutoStartAt: number | null;
61
- lastHeavyStartAt: number | null;
62
- }
63
-
64
- export interface Hold {
65
- code: WaitCode;
66
- text: string;
67
- }
68
-
69
43
  export interface Candidate {
70
44
  id: string;
71
45
  kind: "start" | "resume";
@@ -88,7 +62,7 @@ export interface Candidate {
88
62
  // `after X` hold, from the caller (it needs the database).
89
63
  before: Hold | null;
90
64
  // Orchestrator Pause and the usage window, which hold after `after`.
91
- auto: Hold | null;
65
+ auto: Hold | readonly Hold[] | null;
92
66
  // "After any": its set, the ones that have finished, the ones other waiters
93
67
  // already used, and whether it was already released (a retry).
94
68
  any: {
@@ -110,6 +84,8 @@ export interface Plan {
110
84
  // The finish an "after any" waiter takes, by candidate id (null: none left).
111
85
  consume: Map<string, string | null>;
112
86
  explain: Map<string, QueueExplainView>;
87
+ // Every hold on each candidate, in order (empty: it starts).
88
+ holds: Map<string, Hold[]>;
113
89
  }
114
90
 
115
91
  export function scoreParts(
@@ -124,75 +100,6 @@ export function scoreParts(
124
100
  return { priority, age, resume, score: priority + age + resume };
125
101
  }
126
102
 
127
- const priorityRank = (p: Priority) => PRIORITY_POINTS[p] ?? 0;
128
-
129
- const seconds = (ms: number) => Math.max(1, Math.ceil(ms / 1000));
130
-
131
- // The machine holds, in order. Null: it may start here now.
132
- export function machineHold(
133
- c: Pick<Candidate, "heavy" | "needsUnmet" | "remote" | "priority"> &
134
- Partial<Pick<Candidate, "planOnly">>,
135
- m: MachineView,
136
- now: number
137
- ): Hold | null {
138
- if (c.remote || c.planOnly) return null;
139
- if (c.needsUnmet) return { code: "needs", text: `Waiting: needs a Mac` };
140
- if (!m.enabled) return null;
141
- if (m.reading)
142
- return { code: "load_red", text: "Waiting: reading machine load" };
143
- if (m.level === "red")
144
- return { code: "load_red", text: `Waiting: ${m.label} load red` };
145
- if (!m.settled)
146
- return {
147
- code: "load_red",
148
- text: `Waiting: ${m.label} load was red; calm for ${seconds(m.settleLeftMs)}s more`,
149
- };
150
- if (
151
- m.level === "amber" &&
152
- priorityRank(c.priority) < priorityRank(AMBER_MIN_PRIORITY)
153
- )
154
- return {
155
- code: "load_amber",
156
- text: `Waiting: ${m.label} load amber; ${c.priority} work starts on green`,
157
- };
158
- if (c.heavy) {
159
- if (m.level !== "green")
160
- return {
161
- code: "heavy",
162
- text: `Waiting: ${m.label} load amber; simulator work starts on green`,
163
- };
164
- if (m.heavyStarting > 0)
165
- return {
166
- code: "heavy",
167
- text: `Waiting: a simulator is booting on ${m.label}`,
168
- };
169
- if (m.booted >= m.maxBooted)
170
- return {
171
- code: "heavy",
172
- text: `Waiting: ${m.booted} of ${m.maxBooted} simulators in use`,
173
- };
174
- if (
175
- m.lastHeavyStartAt !== null &&
176
- now - m.lastHeavyStartAt < HEAVY_START_GAP_MS
177
- )
178
- return {
179
- code: "heavy",
180
- text: `Waiting: a simulator task just started on ${m.label} (next in ${seconds(HEAVY_START_GAP_MS - (now - m.lastHeavyStartAt))}s)`,
181
- };
182
- }
183
- if (m.lastAutoStartAt !== null && now - m.lastAutoStartAt < START_GAP_MS)
184
- return {
185
- code: "spacing",
186
- text: `Waiting: spacing starts (next in ${seconds(START_GAP_MS - (now - m.lastAutoStartAt))}s)`,
187
- };
188
- return null;
189
- }
190
-
191
- export function slotText(rank: number, of: number, priority: Priority): string {
192
- const word = priority === DEFAULT_PRIORITY ? "" : ` · ${priority}`;
193
- return `#${rank} of ${of}${word}`;
194
- }
195
-
196
103
  export function priorityWord(priority: Priority): string {
197
104
  return PRIORITY_LABEL[priority];
198
105
  }
@@ -204,7 +111,12 @@ export function planStarts(
204
111
  machine: MachineView,
205
112
  now: number
206
113
  ): Plan {
207
- const plan: Plan = { start: [], consume: new Map(), explain: new Map() };
114
+ const plan: Plan = {
115
+ start: [],
116
+ consume: new Map(),
117
+ explain: new Map(),
118
+ holds: new Map(),
119
+ };
208
120
  const m: MachineView = { ...machine };
209
121
  const byGroup = new Map<string, Candidate[]>();
210
122
  for (const c of candidates)
@@ -218,26 +130,27 @@ export function planStarts(
218
130
  const scored = list
219
131
  .map((c) => ({ c, ...scoreParts(c, now) }))
220
132
  .sort((a, b) => b.score - a.score || a.c.position - b.c.position);
221
- scored.forEach(({ c, score, priority: _p }, index) => {
222
- const rank = index + 1;
223
- const hold = holdOf(c);
224
- const view = (h: Hold | null): QueueExplainView => ({
225
- code: h?.code ?? null,
226
- text: h?.text ?? null,
133
+ scored.forEach(({ c, score }, index) => {
134
+ const holds = holdsFor(c, {
135
+ machine: m,
136
+ now,
137
+ used,
138
+ inFlight,
139
+ limit: group.limit,
140
+ rank: index + 1,
141
+ of: scored.length,
142
+ });
143
+ const hold = holds[0] ?? null;
144
+ plan.holds.set(c.id, holds);
145
+ plan.explain.set(c.id, {
146
+ code: hold?.code ?? null,
147
+ text: hold?.text ?? null,
227
148
  score: Math.round(score * 10) / 10,
228
- rank,
149
+ rank: index + 1,
229
150
  of: scored.length,
230
151
  priority: c.priority,
231
152
  });
232
- if (hold) return void plan.explain.set(c.id, view(hold));
233
- if (!c.planOnly && inFlight >= group.limit)
234
- return void plan.explain.set(
235
- c.id,
236
- view({
237
- code: "slot",
238
- text: slotText(rank, scored.length, c.priority),
239
- })
240
- );
153
+ if (hold) return;
241
154
  // Passed every hold: it starts.
242
155
  if (c.any && !c.any.released) {
243
156
  const free = c.any.finished.find((id) => !used.has(id)) ?? null;
@@ -253,23 +166,7 @@ export function planStarts(
253
166
  }
254
167
  }
255
168
  plan.start.push(c.id);
256
- plan.explain.set(c.id, view(null));
257
169
  });
258
170
  }
259
171
  return plan;
260
-
261
- function holdOf(c: Candidate): Hold | null {
262
- if (c.before) return c.before;
263
- if (c.any && !c.any.released) {
264
- const free = c.any.finished.some((id) => !used.has(id));
265
- const spent =
266
- c.any.ids.length > 0 && c.any.finished.length === c.any.ids.length;
267
- if (!free && !spent)
268
- return {
269
- code: "after_any",
270
- text: `Waiting: after any running task (${c.any.label})`,
271
- };
272
- }
273
- return c.auto ?? machineHold(c, m, now);
274
- }
275
172
  }
@@ -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.631",
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)`
package/scripts/check CHANGED
@@ -2,18 +2,66 @@
2
2
  # The full guard, the same run locally and in CI:
3
3
  # scripts/check banned terms, typecheck, lint, format, tests
4
4
  # scripts/check --build ...and a production build, held to its JS budget
5
+ # CI runs the same groups as parallel jobs:
6
+ # --only static,test,build run just these groups (static = migrations,
7
+ # terms, typecheck, lint, format)
8
+ # --parallel run typecheck, lint and format at once
9
+ # -- <args> passed to `vitest run` (e.g. --shard=1/3)
5
10
  set -euo pipefail
6
11
  cd "$(dirname "$0")/.."
7
12
 
13
+ usage() { sed -n '2,10p' "$0" | sed 's/^# \{0,1\}//' >&2; exit 2; }
14
+
15
+ groups="static,test" parallel=0 vitest_args=()
16
+ while [ $# -gt 0 ]; do
17
+ case "$1" in
18
+ --build) groups="$groups,build" ;;
19
+ --only) [ $# -ge 2 ] || usage; groups="$2"; shift ;;
20
+ --parallel) parallel=1 ;;
21
+ --) shift; vitest_args=("$@"); break ;;
22
+ *) usage ;;
23
+ esac
24
+ shift
25
+ done
26
+ for g in ${groups//,/ }; do
27
+ case "$g" in static|test|build) ;; *) usage ;; esac
28
+ done
29
+ has() { [[ ",$groups," == *",$1,"* ]]; }
30
+
8
31
  step() { printf '\n▸ %s\n' "$1"; }
9
32
 
10
- step "migrations"; node scripts/gen-migrations-index.mjs && node scripts/gen-migrations-index.mjs --check
11
- step "terms"; npx tsx scripts/hooks/banned-terms.ts --tree
12
- step "typecheck"; npx tsc --noEmit
13
- step "lint"; npx eslint . --max-warnings=40
14
- step "format"; npx prettier --check . --log-level warn
15
- step "test"; npx vitest run
16
- if [ "${1:-}" = "--build" ]; then
33
+ if has static; then
34
+ step "migrations"; node scripts/gen-migrations-index.mjs && node scripts/gen-migrations-index.mjs --check
35
+ step "terms"; npx tsx scripts/hooks/banned-terms.ts --tree
36
+ if [ "$parallel" = 1 ]; then
37
+ out=$(mktemp -d); trap 'rm -rf "$out"' EXIT
38
+ pids=()
39
+ npx tsc --noEmit >"$out/typecheck" 2>&1 & pids+=($!)
40
+ npx eslint . --max-warnings=40 --concurrency=auto >"$out/lint" 2>&1 & pids+=($!)
41
+ npx prettier --check . --log-level warn >"$out/format" 2>&1 & pids+=($!)
42
+ failed=()
43
+ names=(typecheck lint format)
44
+ for i in 0 1 2; do
45
+ wait "${pids[$i]}" || failed+=("${names[$i]}")
46
+ step "${names[$i]}"; cat "$out/${names[$i]}"
47
+ done
48
+ if [ ${#failed[@]} -gt 0 ]; then
49
+ printf '\n✗ failed: %s\n' "${failed[*]}" >&2
50
+ exit 1
51
+ fi
52
+ else
53
+ step "typecheck"; npx tsc --noEmit
54
+ step "lint"; npx eslint . --max-warnings=40 --concurrency=auto
55
+ step "format"; npx prettier --check . --log-level warn
56
+ fi
57
+ fi
58
+ if has test; then
59
+ step "test"; npx vitest run ${vitest_args[@]+"${vitest_args[@]}"}
60
+ fi
61
+ if has build; then
62
+ # The build imports the generated migrations index, so it needs it even
63
+ # when the static group (which checks it) did not run.
64
+ step "migrations"; node scripts/gen-migrations-index.mjs
17
65
  step "build"; npx next build
18
66
  step "bundle"; node scripts/check-bundle.mjs
19
67
  fi
@@ -1,17 +0,0 @@
1
- import { brakesEnabled } from "../orchestrator/brakes";
2
- import { isPaused } from "../orchestrator/pause";
3
- import { readUsage, windowRefusal } from "../orchestrator/usage";
4
- import type { Hold } from "./schedule";
5
-
6
- // Why an automatic start has to wait, or null. A start now by a person
7
- // isn't held.
8
- export function autoHold(
9
- workspaceId: string | null,
10
- planOnly = false
11
- ): Hold | null {
12
- if (workspaceId && isPaused(workspaceId))
13
- return { code: "orch_paused", text: "Waiting: the orchestrator is paused" };
14
- if (planOnly) return null;
15
- const window = brakesEnabled() ? windowRefusal(readUsage()) : null;
16
- return window ? { code: "window", text: `Waiting: ${window}` } : null;
17
- }
@@ -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
- }