@saadnvd1/agent-os 0.3.596 → 0.3.600

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 (150) hide show
  1. package/app/api/bus/spawn/route.ts +35 -16
  2. package/app/api/lumifyhub/cards/[cardId]/run/route.ts +3 -0
  3. package/app/api/tasks/route.ts +3 -0
  4. package/app/page.tsx +2 -1
  5. package/components/SessionList/SessionRow.tsx +2 -1
  6. package/lib/agents/spawn.ts +16 -4
  7. package/lib/chat/refresh.ts +2 -1
  8. package/lib/chat/runner.ts +29 -0
  9. package/lib/chat/store.ts +53 -0
  10. package/lib/db/migration-types.ts +15 -0
  11. package/lib/db/migrations/0001_add_group_path_to_sessions.ts +11 -0
  12. package/lib/db/migrations/0002_add_agent_type_to_sessions.ts +11 -0
  13. package/lib/db/migrations/0003_add_worktree_columns_to_sessions.ts +12 -0
  14. package/lib/db/migrations/0004_add_pr_tracking_to_sessions.ts +11 -0
  15. package/lib/db/migrations/0005_add_group_path_index.ts +11 -0
  16. package/lib/db/migrations/0006_add_orchestration_columns_to_sessions.ts +16 -0
  17. package/lib/db/migrations/0007_add_auto_approve_to_sessions.ts +11 -0
  18. package/lib/db/migrations/0008_add_dev_server_columns.ts +19 -0
  19. package/lib/db/migrations/0009_add_project_id_to_sessions.ts +17 -0
  20. package/lib/db/migrations/0010_add_project_id_to_dev_servers.ts +36 -0
  21. package/lib/db/migrations/0011_add_tmux_name_to_sessions.ts +13 -0
  22. package/lib/db/migrations/0012_add_initial_prompt_to_projects.ts +9 -0
  23. package/lib/db/migrations/0013_add_project_repositories_table.ts +22 -0
  24. package/lib/db/migrations/0014_add_hosts.ts +23 -0
  25. package/lib/db/migrations/0015_add_workspaces.ts +18 -0
  26. package/lib/db/migrations/0016_add_task_columns_to_sessions.ts +13 -0
  27. package/lib/db/migrations/0017_add_bus_messages.ts +24 -0
  28. package/lib/db/migrations/0018_add_chat.ts +23 -0
  29. package/lib/db/migrations/0019_add_lumifyhub.ts +36 -0
  30. package/lib/db/migrations/0020_add_lumifyhub_published_docs.ts +18 -0
  31. package/lib/db/migrations/0021_add_chat_access.ts +14 -0
  32. package/lib/db/migrations/0022_add_last_seen.ts +10 -0
  33. package/lib/db/migrations/0023_mark_existing_sessions_seen.ts +12 -0
  34. package/lib/db/migrations/0024_add_stacks.ts +49 -0
  35. package/lib/db/migrations/0025_add_stack_item_attempts.ts +16 -0
  36. package/lib/db/migrations/0026_add_workspace_orchestrator.ts +47 -0
  37. package/lib/db/migrations/0027_add_devices_and_settings.ts +28 -0
  38. package/lib/db/migrations/0028_add_orchestrator_acting.ts +70 -0
  39. package/lib/db/migrations/0029_add_orchestrator_asks_and_pause.ts +81 -0
  40. package/lib/db/migrations/0030_add_sessions_archived_at.ts +14 -0
  41. package/lib/db/migrations/0031_add_stack_item_restacked_heads.ts +22 -0
  42. package/lib/db/migrations/0032_add_sessions_pinned.ts +12 -0
  43. package/lib/db/migrations/0033_add_program_status.ts +18 -0
  44. package/lib/db/migrations/0034_add_session_names.ts +21 -0
  45. package/lib/db/migrations/0035_add_artifacts.ts +23 -0
  46. package/lib/db/migrations/0036_add_chat_queue.ts +26 -0
  47. package/lib/db/migrations/0037_add_chat_plan_and_turns.ts +39 -0
  48. package/lib/db/migrations/0038_add_schedules.ts +56 -0
  49. package/lib/db/migrations/0039_add_session_name_source.ts +21 -0
  50. package/lib/db/migrations/0040_add_schedule_targets_and_phone_notify.ts +41 -0
  51. package/lib/db/migrations/0041_add_task_setup.ts +17 -0
  52. package/lib/db/migrations/0042_add_host_links_and_moved_tasks.ts +29 -0
  53. package/lib/db/migrations/0043_scratch_project.ts +14 -0
  54. package/lib/db/migrations/0044_chat_item_kind.ts +21 -0
  55. package/lib/db/migrations/0045_change_versions.ts +13 -0
  56. package/lib/db/migrations/0046_session_port_slots.ts +21 -0
  57. package/lib/db/migrations/0047_pin_orchestrators.ts +12 -0
  58. package/lib/db/migrations/0048_session_database.ts +15 -0
  59. package/lib/db/migrations/0049_peer_mirror.ts +14 -0
  60. package/lib/db/migrations/0050_task_queue.ts +44 -0
  61. package/lib/db/migrations/0051_chat_queue_sent_by.ts +16 -0
  62. package/lib/db/migrations/0052_sessions_chat_restarted.ts +17 -0
  63. package/lib/db/migrations/0053_brief_amendments_failure_sha_ask_raised_by.ts +37 -0
  64. package/lib/db/migrations/0054_sessions_chat_tools_digest.ts +15 -0
  65. package/lib/db/migrations/0055_sessions_merge_armed_and_rebase.ts +25 -0
  66. package/lib/db/migrations/0056_orchestrator_rules.ts +39 -0
  67. package/lib/db/migrations/0057_task_pr_wakes.ts +34 -0
  68. package/lib/db/migrations/0058_away_mode.ts +71 -0
  69. package/lib/db/migrations/0059_gate_failure_commits.ts +33 -0
  70. package/lib/db/migrations/0060_check_runs_and_compactions.ts +44 -0
  71. package/lib/db/migrations/0061_model_routing.ts +30 -0
  72. package/lib/db/migrations/0062_sessions_quick_of.ts +19 -0
  73. package/lib/db/migrations/0063_one_off_schedules.ts +16 -0
  74. package/lib/db/migrations/0064_chat_compactions_phase.ts +15 -0
  75. package/lib/db/migrations/0065_orchestrator_triage.ts +23 -0
  76. package/lib/db/migrations/0066_workspace_orch_mcp_servers.ts +16 -0
  77. package/lib/db/migrations/0067_orchestrator_event_log_suppressed.ts +14 -0
  78. package/lib/db/migrations/0068_usage_calls_and_rollups.ts +74 -0
  79. package/lib/db/migrations/0069_session_logs.ts +26 -0
  80. package/lib/db/migrations/0070_chat_items_running.ts +15 -0
  81. package/lib/db/migrations/0071_devices_push.ts +29 -0
  82. package/lib/db/migrations/0072_push_outbox.ts +33 -0
  83. package/lib/db/migrations/0073_orchestrated_task_moves.ts +27 -0
  84. package/lib/db/migrations/0074_held_task_replies.ts +27 -0
  85. package/lib/db/migrations/0075_plan_only_tasks.ts +17 -0
  86. package/lib/db/migrations/0076_welcome_backfill.ts +16 -0
  87. package/lib/db/migrations/0077_simulator_leases.ts +21 -0
  88. package/lib/db/migrations/0078_orchestrator_questions.ts +34 -0
  89. package/lib/db/migrations/0079_task_queue_agent.ts +13 -0
  90. package/lib/db/migrations/0080_recipes.ts +129 -0
  91. package/lib/db/migrations/0081_schedules_into_recipes.ts +18 -0
  92. package/lib/db/migrations/0082_session_shelf_times.ts +23 -0
  93. package/lib/db/migrations/0083_native_devices_can_approve.ts +18 -0
  94. package/lib/db/migrations/0084_approval_audit.ts +20 -0
  95. package/lib/db/migrations/0085_experiment_usage.ts +35 -0
  96. package/lib/db/migrations/0086_learning_cards.ts +28 -0
  97. package/lib/db/migrations/0087_plan_pieces_reconcile.ts +29 -0
  98. package/lib/db/migrations/0088_shared_links.ts +25 -0
  99. package/lib/db/migrations/0089_passkeys_device_keys.ts +23 -0
  100. package/lib/db/migrations/20261014T0900_task_scheduling.ts +57 -0
  101. package/lib/db/migrations/20261014T1235_placed_mirror_started_by.ts +19 -0
  102. package/lib/db/migrations/20261014T1600_queued_task_messages.ts +21 -0
  103. package/lib/db/migrations/20261014T1800_task_starter_device.ts +20 -0
  104. package/lib/db/migrations/20261014T1900_pause_note.ts +13 -0
  105. package/lib/db/migrations/20261014T2000_chat_bookmarks.ts +33 -0
  106. package/lib/db/migrations/20261014T2100_gate_failure_ask_held.ts +18 -0
  107. package/lib/db/migrations/20261014T2110_task_start_record.ts +21 -0
  108. package/lib/db/migrations/20261014T2200_end_orphaned_tasks.ts +23 -0
  109. package/lib/db/migrations/README.md +35 -0
  110. package/lib/db/migrations-legacy-checksums.json +9 -8
  111. package/lib/db/migrations-split-snapshot.json +1759 -0
  112. package/lib/db/migrations.ts +76 -2337
  113. package/lib/db/types.ts +2 -0
  114. package/lib/learning-cards/apply.ts +7 -1
  115. package/lib/models/plan-step.ts +25 -8
  116. package/lib/models/resolve.ts +1 -1
  117. package/lib/models/resume.ts +1 -1
  118. package/lib/models/roles.ts +0 -16
  119. package/lib/models/session-role.ts +19 -0
  120. package/lib/orchestrator/act.ts +22 -7
  121. package/lib/orchestrator/amendments.ts +10 -3
  122. package/lib/orchestrator/serve.ts +1 -0
  123. package/lib/orchestrator/testing.ts +8 -0
  124. package/lib/orchestrator/tool-schemas.ts +9 -0
  125. package/lib/orchestrator/tools.ts +1 -1
  126. package/lib/plans/claim.ts +131 -0
  127. package/lib/plans/release.ts +5 -2
  128. package/lib/recipes/run.ts +6 -0
  129. package/lib/schedules/start.ts +1 -0
  130. package/lib/sessions/fork.ts +10 -0
  131. package/lib/sessions/launch.ts +5 -1
  132. package/lib/stacks/start.ts +1 -0
  133. package/lib/tasks/held-remote.ts +4 -2
  134. package/lib/tasks/held-replies.ts +5 -2
  135. package/lib/tasks/held.ts +2 -1
  136. package/lib/tasks/index.ts +85 -46
  137. package/lib/tasks/move-carry.ts +12 -1
  138. package/lib/tasks/move-guard.ts +2 -1
  139. package/lib/tasks/move.ts +7 -5
  140. package/lib/tasks/pause.ts +1 -1
  141. package/lib/tasks/queue.ts +54 -31
  142. package/lib/tasks/remote.ts +3 -1
  143. package/lib/tasks/starter.ts +124 -0
  144. package/lib/usage/calls.ts +2 -1
  145. package/lib/usage/turns.ts +2 -1
  146. package/package.json +6 -3
  147. package/scripts/check +1 -0
  148. package/scripts/gen-migrations-index.mjs +44 -0
  149. package/scripts/screenshots/demo.ts +5 -1
  150. package/scripts/screenshots/run.ts +5 -1
@@ -5,17 +5,14 @@ import { startOrQueue, stampPriority } from "@/lib/tasks/queue";
5
5
  import { isPriority } from "@/lib/tasks/schedule-types";
6
6
  import { hostIdNamed } from "@/lib/hosts";
7
7
  import { startView } from "@/lib/sessions/launch";
8
- import { db } from "@/lib/db";
9
-
10
- // The calling session, when it's an orchestrator: what it starts runs on the
11
- // implementation model. A made-up `from` can only pick that cheaper role.
12
- function orchestratorFrom(from: unknown): string | null {
13
- if (typeof from !== "string" || !from) return null;
14
- const row = db
15
- .prepare(`SELECT 1 FROM sessions WHERE id = ? AND role = 'orchestrator'`)
16
- .get(from);
17
- return row ? from : null;
18
- }
8
+ import { callerSession, starterOf } from "@/lib/tasks/starter";
9
+ import { callingDevice } from "@/lib/security/device-me";
10
+ import {
11
+ findPiece,
12
+ pieceLabel,
13
+ pieceRefusal,
14
+ waitingPiecesFor,
15
+ } from "@/lib/plans/claim";
19
16
 
20
17
  // mode "session" starts an interactive agent; "task" starts one that ends in
21
18
  // a PR, or queues it (over the workspace's limit, or with `after`).
@@ -34,8 +31,15 @@ export async function POST(request: NextRequest) {
34
31
  priority,
35
32
  machine,
36
33
  from,
34
+ cwd,
35
+ piece,
37
36
  } = await request.json();
38
- const startedBy = orchestratorFrom(from);
37
+ // Whoever called: a session (by `from`, else the worktree it runs in) or a person.
38
+ const starter = starterOf(
39
+ callerSession({ from, cwd }),
40
+ callingDevice(request)
41
+ );
42
+ const { startedBy } = starter;
39
43
  const given = typeof name === "string" ? name : undefined;
40
44
  if (after !== undefined && mode !== "task")
41
45
  throw new Error("--after works for tasks only");
@@ -47,16 +51,29 @@ export async function POST(request: NextRequest) {
47
51
  throw new Error("--priority is urgent, blocking, normal or background");
48
52
  if (machine !== undefined && machine !== "mac" && machine !== "any")
49
53
  throw new Error('machine is "mac" or "any"');
54
+ if (piece !== undefined && mode !== "task")
55
+ throw new Error("--piece works for tasks only");
50
56
  if (mode === "task") {
57
+ const projectId = findProject(String(project ?? "")).id;
58
+ // A task whose plan has pieces waiting here must say which it builds.
59
+ const waiting = startedBy ? waitingPiecesFor(startedBy, projectId) : [];
60
+ let pieceId: number | undefined;
61
+ if (piece !== undefined) {
62
+ if (typeof piece !== "string" || !piece.trim())
63
+ throw new Error("--piece needs a piece name");
64
+ pieceId = findPiece(waiting, piece).id;
65
+ } else if (waiting.length)
66
+ throw new Error(pieceRefusal(waiting, projectId));
51
67
  const task = {
52
- projectId: findProject(String(project ?? "")).id,
68
+ projectId,
69
+ ...starter,
70
+ pieceId,
53
71
  prompt: String(prompt ?? ""),
54
72
  name: given,
55
73
  model,
56
74
  hostId: on ? hostIdNamed(String(on)) : undefined,
57
75
  view: startView(view),
58
76
  planOnly: planOnly === true ? true : undefined,
59
- startedBy,
60
77
  };
61
78
  const out = await startOrQueue(
62
79
  {
@@ -71,9 +88,10 @@ export async function POST(request: NextRequest) {
71
88
  return session;
72
89
  }
73
90
  );
91
+ const builds = pieceId ? pieceLabel(pieceId) : undefined;
74
92
  return "queued" in out
75
- ? NextResponse.json({ queued: out.queued }, { status: 202 })
76
- : NextResponse.json({ session: out.started }, { status: 201 });
93
+ ? NextResponse.json({ queued: out.queued, builds }, { status: 202 })
94
+ : NextResponse.json({ session: out.started, builds }, { status: 201 });
77
95
  }
78
96
  const session = await spawnSession({
79
97
  project: String(project ?? ""),
@@ -81,6 +99,7 @@ export async function POST(request: NextRequest) {
81
99
  name: given,
82
100
  model,
83
101
  startedBy,
102
+ parentSessionId: starter.parentSessionId,
84
103
  });
85
104
  return NextResponse.json({ session }, { status: 201 });
86
105
  } catch (error) {
@@ -3,6 +3,7 @@ import { body, respond } from "@/lib/lumifyhub/http";
3
3
  import { lumifyHubId } from "@/lib/lumifyhub/ids";
4
4
  import { cardForTask, promptFromCard } from "@/lib/lumifyhub/task-cards";
5
5
  import { createTask } from "@/lib/tasks";
6
+ import { callingDevice } from "@/lib/security/device-me";
6
7
 
7
8
  type RouteParams = { params: Promise<{ cardId: string }> };
8
9
 
@@ -20,6 +21,8 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
20
21
  prompt: promptFromCard(card),
21
22
  name: card.title,
22
23
  cardId: card.id,
24
+ startedVia: "person",
25
+ startedByDevice: callingDevice(request) ?? undefined,
23
26
  });
24
27
  return { session };
25
28
  });
@@ -85,6 +85,8 @@ export async function POST(request: NextRequest) {
85
85
  hostId: typeof hostId === "string" && hostId ? hostId : undefined,
86
86
  baseBranch: typeof baseBranch === "string" ? baseBranch : undefined,
87
87
  view: startView(view),
88
+ startedVia: "person" as const,
89
+ startedByDevice: callingDevice(request) ?? undefined,
88
90
  };
89
91
  if (id !== undefined) {
90
92
  // An orchestrator's task is held for the paired device that sent it,
@@ -105,6 +107,7 @@ export async function POST(request: NextRequest) {
105
107
  id,
106
108
  ...opts,
107
109
  held,
110
+ startedVia: "remote",
108
111
  startedByDevice: device ?? undefined,
109
112
  });
110
113
  return NextResponse.json({ session }, { status: 201 });
package/app/page.tsx CHANGED
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
 
3
+ import { isFork } from "@/lib/sessions/fork";
3
4
  import { toast } from "sonner";
4
5
 
5
6
  import { subscribe } from "valtio";
@@ -299,7 +300,7 @@ function HomeContent() {
299
300
 
300
301
  // Get parent session ID for forking
301
302
  let parentSessionId: string | null = null;
302
- if (!session.claude_session_id && session.parent_session_id) {
303
+ if (!session.claude_session_id && isFork(session)) {
303
304
  const parentSession = sessions.find(
304
305
  (s) => s.id === session.parent_session_id
305
306
  );
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
 
3
+ import { isFork } from "@/lib/sessions/fork";
3
4
  import { memo, useState } from "react";
4
5
  import { useSnapshot } from "valtio";
5
6
  import {
@@ -99,7 +100,7 @@ export const SessionRow = memo(function SessionRow({
99
100
  />
100
101
  </span>
101
102
  )}
102
- {session.parent_session_id && (
103
+ {isFork(session) && (
103
104
  <GitFork className="text-muted-foreground/60 h-3 w-3 shrink-0" />
104
105
  )}
105
106
  </>
@@ -1,4 +1,5 @@
1
1
  import { randomUUID } from "crypto";
2
+ import { orchestratorOf, recordStart } from "../tasks/starter";
2
3
  import os from "os";
3
4
  import { db, queries, type Session } from "../db";
4
5
  import { getProject, getAllProjects } from "../projects";
@@ -35,6 +36,7 @@ export async function spawnSession(opts: {
35
36
  // The orchestrator session that starts it: the implementation role's
36
37
  // model unless `model` is given.
37
38
  startedBy?: string | null;
39
+ parentSessionId?: string | null;
38
40
  }): Promise<Session> {
39
41
  const prompt = opts.prompt.trim();
40
42
  if (!prompt) throw new Error("Give the new session a prompt");
@@ -46,7 +48,7 @@ export async function spawnSession(opts: {
46
48
 
47
49
  const id = randomUUID();
48
50
  const tmuxName = `claude-${id}`;
49
- const routed = opts.startedBy
51
+ const routed = orchestratorOf(opts.startedBy)
50
52
  ? resolveRole("implement", {
51
53
  workspaceId: project.workspace_id,
52
54
  override: parseOverride(opts.model),
@@ -76,9 +78,19 @@ export async function spawnSession(opts: {
76
78
  project.id,
77
79
  "local"
78
80
  );
79
- db.prepare(
80
- `UPDATE sessions SET name_source = ?, started_by = ? WHERE id = ?`
81
- ).run(naming.source, opts.startedBy ?? null, id);
81
+ db.prepare(`UPDATE sessions SET name_source = ? WHERE id = ?`).run(
82
+ naming.source,
83
+ id
84
+ );
85
+ recordStart(id, {
86
+ startedBy: opts.startedBy ?? null,
87
+ parentSessionId: opts.parentSessionId ?? opts.startedBy ?? null,
88
+ startedVia: orchestratorOf(opts.startedBy)
89
+ ? "orchestrator"
90
+ : opts.startedBy
91
+ ? "session"
92
+ : "person",
93
+ });
82
94
  if (routed) setSessionModel(id, routed);
83
95
  void naming.refine?.(id);
84
96
  await launchClaude({ sessionId: id, tmuxName, cwd, model, prompt });
@@ -13,7 +13,8 @@
13
13
 
14
14
  import { db } from "../db";
15
15
  import { effectiveModels } from "../models/config";
16
- import { sessionRole, type ModelRole } from "../models/roles";
16
+ import { type ModelRole } from "../models/roles";
17
+ import { sessionRole } from "../models/session-role";
17
18
  import { MAX_REFRESH_TOKENS, MIN_REFRESH_TOKENS } from "./refresh-limits";
18
19
 
19
20
  export const ORCHESTRATOR_KEY = "orchestrator.refresh_tokens";
@@ -41,10 +41,13 @@ import {
41
41
  setChatPlan,
42
42
  } from "./settings";
43
43
  import {
44
+ endOrphanedTasks,
45
+ endRunningTasks,
44
46
  getItem,
45
47
  hasItem,
46
48
  itemsOfKind,
47
49
  recentTasks,
50
+ sessionsWithRunningTasks,
48
51
  saveItem,
49
52
  settle,
50
53
  } from "./store";
@@ -167,8 +170,26 @@ function planEnv(sessionId: string): PlanHandoffEnv {
167
170
  };
168
171
  }
169
172
 
173
+ // Tell watchers of tasks that just ended, and drop them from the live count.
174
+ function emitEnded(
175
+ sessionId: string,
176
+ live: Live | undefined,
177
+ ended: ReturnType<typeof endRunningTasks>
178
+ ): void {
179
+ for (const item of ended) {
180
+ live?.activity.tasks.delete(item.taskId);
181
+ emit(sessionId, { type: "item", item });
182
+ }
183
+ }
184
+
170
185
  function onWorkerEvent(sessionId: string, live: Live, e: WorkerEvent): void {
171
186
  track(live, e);
187
+ if (
188
+ e.type === "item" &&
189
+ e.item.kind === "tool" &&
190
+ (e.item.status === "stopped" || e.item.status === "error")
191
+ )
192
+ emitEnded(sessionId, live, endOrphanedTasks(sessionId));
172
193
  if (e.type === "item" && e.item.kind === "task")
173
194
  retireIfStale(sessionId, live);
174
195
  if (e.type === "item") {
@@ -297,7 +318,11 @@ async function ensureLive(sessionId: string, spawn = true): Promise<Live> {
297
318
  );
298
319
  },
299
320
  };
321
+ // No worker running: its agent ended, and with it every job it ran.
322
+ const hadWorker = runningWorkers().includes(sessionId);
300
323
  let { client, hello } = await connectWorker(sessionId, spawn, handlers);
324
+ if (!hadWorker) emitEnded(sessionId, undefined, endRunningTasks(sessionId));
325
+ emitEnded(sessionId, undefined, endOrphanedTasks(sessionId));
301
326
  let activity = restoreActivity(sessionId, hello.state);
302
327
  // An idle worker left over from before a redeploy: retire it, and start
303
328
  // a fresh one only when a message needs it. One still running background
@@ -316,6 +341,7 @@ async function ensureLive(sessionId: string, spawn = true): Promise<Live> {
316
341
  { code: RETIRED }
317
342
  );
318
343
  ({ client, hello } = await connectWorker(sessionId, true, handlers));
344
+ emitEnded(sessionId, undefined, endRunningTasks(sessionId));
319
345
  activity = restoreActivity(sessionId, hello.state);
320
346
  }
321
347
  live = {
@@ -965,6 +991,9 @@ export async function reattachChats(): Promise<void> {
965
991
  })
966
992
  )
967
993
  );
994
+ // A task saved as running with no worker behind it has no job either.
995
+ for (const id of sessionsWithRunningTasks())
996
+ if (!live.has(id)) endRunningTasks(id);
968
997
  // Turns cut off while no worker was left to finish them, then queues.
969
998
  // One at a time, and only what was queued recently: a backlog from long
970
999
  // ago isn't sent unasked by a restart (it stays on screen to send).
package/lib/chat/store.ts CHANGED
@@ -235,6 +235,59 @@ export function runningItems<K extends "tool" | "task">(
235
235
  ).map((r) => JSON.parse(r.data));
236
236
  }
237
237
 
238
+ const LOST = "Its agent ended before this finished";
239
+
240
+ function endTasks(
241
+ sessionId: string,
242
+ which: (task: Extract<ChatItem, { kind: "task" }>) => boolean
243
+ ): Extract<ChatItem, { kind: "task" }>[] {
244
+ const ended: Extract<ChatItem, { kind: "task" }>[] = [];
245
+ for (const task of runningItems(sessionId, "task")) {
246
+ if (!which(task)) continue;
247
+ const item = {
248
+ ...task,
249
+ status: "stopped" as const,
250
+ endedAt: Date.now(),
251
+ summary: task.summary ?? LOST,
252
+ };
253
+ saveItem(sessionId, item);
254
+ ended.push(item);
255
+ }
256
+ return ended;
257
+ }
258
+
259
+ // Its agent is gone (a fresh worker, no worker at restart): nothing it ran
260
+ // in the background is still going. Returns what it ended.
261
+ export const endRunningTasks = (sessionId: string) =>
262
+ endTasks(sessionId, () => true);
263
+
264
+ // A task whose tool call was stopped or errored has no job behind it.
265
+ export function endOrphanedTasks(sessionId: string) {
266
+ const dead = new Set(
267
+ runningItems(sessionId, "task").flatMap((t) => {
268
+ const tool = t.toolUseId ? getItem(sessionId, t.toolUseId) : undefined;
269
+ return tool?.kind === "tool" &&
270
+ (tool.status === "stopped" || tool.status === "error")
271
+ ? [t.id]
272
+ : [];
273
+ })
274
+ );
275
+ return endTasks(sessionId, (t) => dead.has(t.id));
276
+ }
277
+
278
+ // Chats with a task saved as running.
279
+ export function sessionsWithRunningTasks(): string[] {
280
+ flushItems();
281
+ return (
282
+ db
283
+ .prepare(
284
+ `SELECT DISTINCT session_id FROM chat_items WHERE kind = 'task'
285
+ AND json_extract(data, '$.status') = 'running'`
286
+ )
287
+ .all() as { session_id: string }[]
288
+ ).map((r) => r.session_id);
289
+ }
290
+
238
291
  // Tool calls saved as still running.
239
292
  export const runningTools = (sessionId: string) =>
240
293
  runningItems(sessionId, "tool");
@@ -0,0 +1,15 @@
1
+ import type Database from "better-sqlite3";
2
+
3
+ export interface LegacyMigration {
4
+ id: number;
5
+ key?: undefined;
6
+ name: string;
7
+ up: (db: Database.Database) => void;
8
+ }
9
+ export interface KeyedMigration {
10
+ key: string;
11
+ id?: undefined;
12
+ name?: undefined;
13
+ up: (db: Database.Database) => void;
14
+ }
15
+ export type Migration = LegacyMigration | KeyedMigration;
@@ -0,0 +1,11 @@
1
+ import type { Migration } from "../migration-types";
2
+
3
+ export default {
4
+ id: 1,
5
+ name: "add_group_path_to_sessions",
6
+ up: (db) => {
7
+ db.exec(
8
+ `ALTER TABLE sessions ADD COLUMN group_path TEXT NOT NULL DEFAULT 'sessions'`
9
+ );
10
+ },
11
+ } satisfies Migration;
@@ -0,0 +1,11 @@
1
+ import type { Migration } from "../migration-types";
2
+
3
+ export default {
4
+ id: 2,
5
+ name: "add_agent_type_to_sessions",
6
+ up: (db) => {
7
+ db.exec(
8
+ `ALTER TABLE sessions ADD COLUMN agent_type TEXT NOT NULL DEFAULT 'claude'`
9
+ );
10
+ },
11
+ } satisfies Migration;
@@ -0,0 +1,12 @@
1
+ import type { Migration } from "../migration-types";
2
+
3
+ export default {
4
+ id: 3,
5
+ name: "add_worktree_columns_to_sessions",
6
+ up: (db) => {
7
+ db.exec(`ALTER TABLE sessions ADD COLUMN worktree_path TEXT`);
8
+ db.exec(`ALTER TABLE sessions ADD COLUMN branch_name TEXT`);
9
+ db.exec(`ALTER TABLE sessions ADD COLUMN base_branch TEXT`);
10
+ db.exec(`ALTER TABLE sessions ADD COLUMN dev_server_port INTEGER`);
11
+ },
12
+ } satisfies Migration;
@@ -0,0 +1,11 @@
1
+ import type { Migration } from "../migration-types";
2
+
3
+ export default {
4
+ id: 4,
5
+ name: "add_pr_tracking_to_sessions",
6
+ up: (db) => {
7
+ db.exec(`ALTER TABLE sessions ADD COLUMN pr_url TEXT`);
8
+ db.exec(`ALTER TABLE sessions ADD COLUMN pr_number INTEGER`);
9
+ db.exec(`ALTER TABLE sessions ADD COLUMN pr_status TEXT`);
10
+ },
11
+ } satisfies Migration;
@@ -0,0 +1,11 @@
1
+ import type { Migration } from "../migration-types";
2
+
3
+ export default {
4
+ id: 5,
5
+ name: "add_group_path_index",
6
+ up: (db) => {
7
+ db.exec(
8
+ `CREATE INDEX IF NOT EXISTS idx_sessions_group ON sessions(group_path)`
9
+ );
10
+ },
11
+ } satisfies Migration;
@@ -0,0 +1,16 @@
1
+ import type { Migration } from "../migration-types";
2
+
3
+ export default {
4
+ id: 6,
5
+ name: "add_orchestration_columns_to_sessions",
6
+ up: (db) => {
7
+ db.exec(
8
+ `ALTER TABLE sessions ADD COLUMN conductor_session_id TEXT REFERENCES sessions(id)`
9
+ );
10
+ db.exec(`ALTER TABLE sessions ADD COLUMN worker_task TEXT`);
11
+ db.exec(`ALTER TABLE sessions ADD COLUMN worker_status TEXT`);
12
+ db.exec(
13
+ `CREATE INDEX IF NOT EXISTS idx_sessions_conductor ON sessions(conductor_session_id)`
14
+ );
15
+ },
16
+ } satisfies Migration;
@@ -0,0 +1,11 @@
1
+ import type { Migration } from "../migration-types";
2
+
3
+ export default {
4
+ id: 7,
5
+ name: "add_auto_approve_to_sessions",
6
+ up: (db) => {
7
+ db.exec(
8
+ `ALTER TABLE sessions ADD COLUMN auto_approve INTEGER NOT NULL DEFAULT 0`
9
+ );
10
+ },
11
+ } satisfies Migration;
@@ -0,0 +1,19 @@
1
+ import type { Migration } from "../migration-types";
2
+
3
+ export default {
4
+ id: 8,
5
+ name: "add_dev_server_columns",
6
+ up: (db) => {
7
+ db.exec(
8
+ `ALTER TABLE dev_servers ADD COLUMN type TEXT NOT NULL DEFAULT 'node'`
9
+ );
10
+ db.exec(`ALTER TABLE dev_servers ADD COLUMN name TEXT NOT NULL DEFAULT ''`);
11
+ db.exec(
12
+ `ALTER TABLE dev_servers ADD COLUMN command TEXT NOT NULL DEFAULT ''`
13
+ );
14
+ db.exec(`ALTER TABLE dev_servers ADD COLUMN pid INTEGER`);
15
+ db.exec(
16
+ `ALTER TABLE dev_servers ADD COLUMN working_directory TEXT NOT NULL DEFAULT ''`
17
+ );
18
+ },
19
+ } satisfies Migration;
@@ -0,0 +1,17 @@
1
+ import type { Migration } from "../migration-types";
2
+
3
+ export default {
4
+ id: 9,
5
+ name: "add_project_id_to_sessions",
6
+ up: (db) => {
7
+ db.exec(
8
+ `ALTER TABLE sessions ADD COLUMN project_id TEXT REFERENCES projects(id)`
9
+ );
10
+ db.exec(
11
+ `UPDATE sessions SET project_id = 'uncategorized' WHERE project_id IS NULL`
12
+ );
13
+ db.exec(
14
+ `CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id)`
15
+ );
16
+ },
17
+ } satisfies Migration;
@@ -0,0 +1,36 @@
1
+ import type { Migration } from "../migration-types";
2
+
3
+ export default {
4
+ id: 10,
5
+ name: "add_project_id_to_dev_servers",
6
+ up: (db) => {
7
+ // Check if column exists first
8
+ const cols = db.prepare(`PRAGMA table_info(dev_servers)`).all() as {
9
+ name: string;
10
+ }[];
11
+ if (cols.some((c) => c.name === "project_id")) return;
12
+
13
+ db.exec(
14
+ `ALTER TABLE dev_servers ADD COLUMN project_id TEXT REFERENCES projects(id)`
15
+ );
16
+ // Migrate from session_id if it exists
17
+ const hasSessionId = cols.some((c) => c.name === "session_id");
18
+ if (hasSessionId) {
19
+ db.exec(`
20
+ UPDATE dev_servers
21
+ SET project_id = (
22
+ SELECT COALESCE(s.project_id, 'uncategorized')
23
+ FROM sessions s
24
+ WHERE s.id = dev_servers.session_id
25
+ )
26
+ WHERE project_id IS NULL
27
+ `);
28
+ }
29
+ db.exec(
30
+ `UPDATE dev_servers SET project_id = 'uncategorized' WHERE project_id IS NULL`
31
+ );
32
+ db.exec(
33
+ `CREATE INDEX IF NOT EXISTS idx_dev_servers_project ON dev_servers(project_id)`
34
+ );
35
+ },
36
+ } satisfies Migration;
@@ -0,0 +1,13 @@
1
+ import type { Migration } from "../migration-types";
2
+
3
+ export default {
4
+ id: 11,
5
+ name: "add_tmux_name_to_sessions",
6
+ up: (db) => {
7
+ db.exec(`ALTER TABLE sessions ADD COLUMN tmux_name TEXT`);
8
+ // Backfill existing sessions with computed tmux name
9
+ db.exec(
10
+ `UPDATE sessions SET tmux_name = agent_type || '-' || id WHERE tmux_name IS NULL`
11
+ );
12
+ },
13
+ } satisfies Migration;
@@ -0,0 +1,9 @@
1
+ import type { Migration } from "../migration-types";
2
+
3
+ export default {
4
+ id: 12,
5
+ name: "add_initial_prompt_to_projects",
6
+ up: (db) => {
7
+ db.exec(`ALTER TABLE projects ADD COLUMN initial_prompt TEXT`);
8
+ },
9
+ } satisfies Migration;
@@ -0,0 +1,22 @@
1
+ import type { Migration } from "../migration-types";
2
+
3
+ export default {
4
+ id: 13,
5
+ name: "add_project_repositories_table",
6
+ up: (db) => {
7
+ db.exec(`
8
+ CREATE TABLE IF NOT EXISTS project_repositories (
9
+ id TEXT PRIMARY KEY,
10
+ project_id TEXT NOT NULL,
11
+ name TEXT NOT NULL,
12
+ path TEXT NOT NULL,
13
+ is_primary INTEGER NOT NULL DEFAULT 0,
14
+ sort_order INTEGER NOT NULL DEFAULT 0,
15
+ FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
16
+ )
17
+ `);
18
+ db.exec(
19
+ `CREATE INDEX IF NOT EXISTS idx_project_repositories_project ON project_repositories(project_id)`
20
+ );
21
+ },
22
+ } satisfies Migration;
@@ -0,0 +1,23 @@
1
+ import type { Migration } from "../migration-types";
2
+
3
+ export default {
4
+ id: 14,
5
+ name: "add_hosts",
6
+ up: (db) => {
7
+ db.exec(`
8
+ CREATE TABLE IF NOT EXISTS hosts (
9
+ id TEXT PRIMARY KEY,
10
+ name TEXT NOT NULL,
11
+ ssh_target TEXT NOT NULL,
12
+ sort_order INTEGER NOT NULL DEFAULT 0,
13
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
14
+ )
15
+ `);
16
+ db.exec(
17
+ `ALTER TABLE projects ADD COLUMN host_id TEXT NOT NULL DEFAULT 'local'`
18
+ );
19
+ db.exec(
20
+ `ALTER TABLE sessions ADD COLUMN host_id TEXT NOT NULL DEFAULT 'local'`
21
+ );
22
+ },
23
+ } satisfies Migration;
@@ -0,0 +1,18 @@
1
+ import type { Migration } from "../migration-types";
2
+
3
+ export default {
4
+ id: 15,
5
+ name: "add_workspaces",
6
+ up: (db) => {
7
+ db.exec(`
8
+ CREATE TABLE IF NOT EXISTS workspaces (
9
+ id TEXT PRIMARY KEY,
10
+ name TEXT NOT NULL,
11
+ sort_order INTEGER NOT NULL DEFAULT 0,
12
+ collapsed INTEGER NOT NULL DEFAULT 0,
13
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
14
+ )
15
+ `);
16
+ db.exec(`ALTER TABLE projects ADD COLUMN workspace_id TEXT`);
17
+ },
18
+ } satisfies Migration;
@@ -0,0 +1,13 @@
1
+ import type { Migration } from "../migration-types";
2
+
3
+ export default {
4
+ id: 16,
5
+ name: "add_task_columns_to_sessions",
6
+ up: (db) => {
7
+ db.exec(`ALTER TABLE sessions ADD COLUMN task_prompt TEXT`);
8
+ db.exec(`ALTER TABLE sessions ADD COLUMN task_status TEXT`);
9
+ db.exec(
10
+ `CREATE INDEX IF NOT EXISTS idx_sessions_task_status ON sessions(task_status)`
11
+ );
12
+ },
13
+ } satisfies Migration;
@@ -0,0 +1,24 @@
1
+ import type { Migration } from "../migration-types";
2
+
3
+ export default {
4
+ id: 17,
5
+ name: "add_bus_messages",
6
+ up: (db) => {
7
+ db.exec(`
8
+ CREATE TABLE IF NOT EXISTS bus_messages (
9
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
10
+ from_id TEXT,
11
+ from_name TEXT NOT NULL,
12
+ to_id TEXT NOT NULL,
13
+ to_name TEXT NOT NULL,
14
+ body TEXT NOT NULL,
15
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
16
+ delivered_at TEXT,
17
+ read_at TEXT
18
+ )
19
+ `);
20
+ db.exec(
21
+ `CREATE INDEX IF NOT EXISTS idx_bus_to ON bus_messages(to_id, read_at)`
22
+ );
23
+ },
24
+ } satisfies Migration;