conductor-remote 1.116.0 → 1.118.0

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 (200) hide show
  1. package/README.md +10 -8
  2. package/dist/assets/{PierrePatch-BPp5Cf-p.js → PierrePatch-D4YbKD8Q.js} +1 -1
  3. package/dist/assets/index-BRzsSW8g.css +1 -0
  4. package/dist/assets/index-BaS6qXUW.js +65 -0
  5. package/dist/index.html +2 -2
  6. package/dist/push-sw.js +2 -2
  7. package/dist/sw.js +1 -1
  8. package/dist-node/scripts/nosleep-setup.js +2 -2
  9. package/dist-node/scripts/nosleep.js +3 -3
  10. package/dist-node/scripts/service/commands.js +32 -0
  11. package/dist-node/scripts/service/configuration.js +205 -0
  12. package/dist-node/scripts/service/environment.js +59 -0
  13. package/dist-node/scripts/service/flags.js +45 -0
  14. package/dist-node/scripts/service/installation.js +91 -0
  15. package/dist-node/scripts/service/launch-agent.js +134 -0
  16. package/dist-node/scripts/service/network.js +244 -0
  17. package/dist-node/scripts/service/presentation.js +101 -0
  18. package/dist-node/scripts/service/voice.js +113 -0
  19. package/dist-node/scripts/service.js +5 -980
  20. package/dist-node/src/{agent-config.js → agents/agent-config.js} +1 -1
  21. package/dist-node/src/{model-cache.js → agents/model-cache.js} +1 -1
  22. package/dist-node/src/{roles.js → agents/roles.js} +3 -3
  23. package/dist-node/src/db.js +1 -1
  24. package/dist-node/src/{parked.js → delivery/parked.js} +1 -1
  25. package/dist-node/src/{dev-server.js → dev-server/controller.js} +8 -291
  26. package/dist-node/src/dev-server/ports.js +63 -0
  27. package/dist-node/src/dev-server/processes.js +73 -0
  28. package/dist-node/src/dev-server/proxy.js +158 -0
  29. package/dist-node/src/{run-activity.js → dev-server/run-activity.js} +2 -2
  30. package/dist-node/src/dev-server/types.js +1 -0
  31. package/dist-node/src/{file-preview.js → files/file-preview.js} +1 -1
  32. package/dist-node/src/{change-stats.js → git/change-stats.js} +1 -1
  33. package/dist-node/src/{git.js → git/diff.js} +1 -1
  34. package/dist-node/src/{autoupdate.js → host/autoupdate.js} +1 -1
  35. package/dist-node/src/{funnel-watchdog.js → host/funnel-watchdog.js} +4 -4
  36. package/dist-node/src/{logbuf.js → host/logbuf.js} +1 -1
  37. package/dist-node/src/{nosleep-helper.js → host/nosleep-helper.js} +1 -1
  38. package/dist-node/src/{tailscale.js → host/tailscale.js} +1 -1
  39. package/dist-node/src/http/router-types.js +2 -0
  40. package/dist-node/src/http/router.js +78 -0
  41. package/dist-node/src/http/routes/create-workspace.js +90 -0
  42. package/dist-node/src/http/routes/files.js +53 -0
  43. package/dist-node/src/http/routes/prompts.js +363 -0
  44. package/dist-node/src/http/routes/sessions.js +279 -0
  45. package/dist-node/src/http/routes/state.js +186 -0
  46. package/dist-node/src/http/routes/system.js +220 -0
  47. package/dist-node/src/http/routes/voice.js +117 -0
  48. package/dist-node/src/http/routes/workflows.js +248 -0
  49. package/dist-node/src/http/routes/workspaces.js +267 -0
  50. package/dist-node/src/http/services/base.js +252 -0
  51. package/dist-node/src/http/services/delegations.js +301 -0
  52. package/dist-node/src/http/services/delivery.js +433 -0
  53. package/dist-node/src/http/services/files.js +199 -0
  54. package/dist-node/src/http/services/mcp.js +94 -0
  55. package/dist-node/src/http/services/responses.js +120 -0
  56. package/dist-node/src/http/services/voice.js +147 -0
  57. package/dist-node/src/http/services/workflow-probes.js +215 -0
  58. package/dist-node/src/http/services/workflow-state.js +195 -0
  59. package/dist-node/src/http/services/workflow.js +366 -0
  60. package/dist-node/src/http/services.js +35 -0
  61. package/dist-node/src/mcp/arguments.js +14 -0
  62. package/dist-node/src/mcp/dispatcher.js +56 -0
  63. package/dist-node/src/mcp/formatters.js +110 -0
  64. package/dist-node/src/mcp/protocol.js +7 -0
  65. package/dist-node/src/mcp/registry.js +66 -0
  66. package/dist-node/src/mcp/tools/agent-options.js +122 -0
  67. package/dist-node/src/mcp/tools/chat-actions.js +194 -0
  68. package/dist-node/src/mcp/tools/chats.js +194 -0
  69. package/dist-node/src/mcp/tools/delegation.js +135 -0
  70. package/dist-node/src/mcp/tools/system.js +135 -0
  71. package/dist-node/src/mcp/tools/voice.js +136 -0
  72. package/dist-node/src/mcp/tools/workspaces.js +259 -0
  73. package/dist-node/src/mcp/types.js +1 -0
  74. package/dist-node/src/mcp.js +6 -4
  75. package/dist-node/src/{notify.js → notifications/notify.js} +5 -5
  76. package/dist-node/src/{webpush.js → notifications/webpush.js} +1 -1
  77. package/dist-node/src/orchestration/delegation/codec.js +201 -0
  78. package/dist-node/src/{delegation-intake.js → orchestration/delegation/intake.js} +2 -2
  79. package/dist-node/src/{delegation-prompt.js → orchestration/delegation/prompt.js} +1 -1
  80. package/dist-node/src/orchestration/delegation/queue.js +255 -0
  81. package/dist-node/src/orchestration/delegation/store.js +111 -0
  82. package/dist-node/src/orchestration/delegation/types.js +2 -0
  83. package/dist-node/src/orchestration/persistence/capabilities.js +109 -0
  84. package/dist-node/src/orchestration/persistence/codecs.js +148 -0
  85. package/dist-node/src/orchestration/persistence/connection.js +205 -0
  86. package/dist-node/src/orchestration/persistence/db.js +101 -0
  87. package/dist-node/src/orchestration/persistence/effect-recovery.js +227 -0
  88. package/dist-node/src/orchestration/persistence/effect-transitions.js +157 -0
  89. package/dist-node/src/orchestration/persistence/effects.js +109 -0
  90. package/dist-node/src/orchestration/persistence/errors.js +39 -0
  91. package/dist-node/src/orchestration/persistence/events.js +29 -0
  92. package/dist-node/src/orchestration/persistence/idempotency.js +82 -0
  93. package/dist-node/src/orchestration/persistence/job-attempts.js +114 -0
  94. package/dist-node/src/orchestration/persistence/jobs.js +159 -0
  95. package/dist-node/src/orchestration/persistence/projections.js +190 -0
  96. package/dist-node/src/orchestration/persistence/quarantine.js +46 -0
  97. package/dist-node/src/orchestration/persistence/records.js +115 -0
  98. package/dist-node/src/orchestration/persistence/relays.js +78 -0
  99. package/dist-node/src/orchestration/persistence/runs.js +156 -0
  100. package/dist-node/src/{orchestration-schema.js → orchestration/persistence/schema.js} +1 -1
  101. package/dist-node/src/orchestration/persistence/types.js +1 -0
  102. package/dist-node/src/orchestration/persistence/ui-lease.js +197 -0
  103. package/dist-node/src/orchestration/persistence/values.js +114 -0
  104. package/dist-node/src/orchestration/workflow/commands.js +309 -0
  105. package/dist-node/src/orchestration/workflow/coordinator.js +28 -0
  106. package/dist-node/src/orchestration/workflow/effect-recovery.js +125 -0
  107. package/dist-node/src/orchestration/workflow/effects.js +307 -0
  108. package/dist-node/src/orchestration/workflow/errors.js +13 -0
  109. package/dist-node/src/orchestration/workflow/helpers.js +154 -0
  110. package/dist-node/src/orchestration/workflow/job-dispatch.js +312 -0
  111. package/dist-node/src/orchestration/workflow/job-results.js +280 -0
  112. package/dist-node/src/{workflow.js → orchestration/workflow/prompts.js} +2 -2
  113. package/dist-node/src/orchestration/workflow/root.js +293 -0
  114. package/dist-node/src/orchestration/workflow/start.js +99 -0
  115. package/dist-node/src/orchestration/workflow/state.js +105 -0
  116. package/dist-node/src/orchestration/workflow/types.js +1 -0
  117. package/dist-node/src/orchestration/workflow/wake.js +164 -0
  118. package/dist-node/src/prefs.js +1 -1
  119. package/dist-node/src/{background-tasks.js → reads/background-tasks.js} +1 -1
  120. package/dist-node/src/reads/messages.js +362 -0
  121. package/dist-node/src/reads/repository.js +87 -0
  122. package/dist-node/src/reads/sessions.js +165 -0
  123. package/dist-node/src/reads/types.js +1 -0
  124. package/dist-node/src/reads/workspaces.js +230 -0
  125. package/dist-node/src/reads/worktrees.js +45 -0
  126. package/dist-node/src/routes.js +5 -5
  127. package/dist-node/src/{search.js → search/coordinator.js} +4 -4
  128. package/dist-node/src/{search-worker.js → search/worker.js} +3 -3
  129. package/dist-node/src/server.js +14 -3930
  130. package/dist-node/src/settings.js +1 -1
  131. package/dist-node/src/shared.js +130 -9
  132. package/dist-node/src/{transcript.js → transcript/parser.js} +4 -124
  133. package/dist-node/src/{tool-usage-worker.js → usage/tool-usage-worker.js} +1 -1
  134. package/dist-node/src/{tool-usage.js → usage/tool-usage.js} +1 -1
  135. package/dist-node/src/voice/brief.js +22 -50
  136. package/dist-node/src/voice/context.js +1 -1
  137. package/dist-node/src/voice/dates.js +38 -0
  138. package/dist-node/src/voice/history.js +14 -5
  139. package/dist-node/src/voice/prompt.js +10 -9
  140. package/dist-node/src/voice/recall.js +115 -0
  141. package/dist-node/src/{speech.js → voice/speech.js} +1 -1
  142. package/dist-node/src/voice/tools.js +92 -3
  143. package/dist-node/src/voice/webrtc.js +1 -1
  144. package/dist-node/src/wire.js +2 -2
  145. package/dist-node/src/writes/actuator.js +124 -0
  146. package/dist-node/src/writes/agent-options.js +151 -0
  147. package/dist-node/src/writes/applescript/agent-options.applescript +334 -0
  148. package/dist-node/src/writes/applescript/chat-targeting.applescript +149 -0
  149. package/dist-node/src/writes/applescript/composer.applescript +195 -0
  150. package/dist-node/src/writes/applescript/hotspot.applescript +155 -0
  151. package/dist-node/src/writes/applescript/lifecycle.applescript +266 -0
  152. package/dist-node/src/writes/applescript/run-tasks.applescript +208 -0
  153. package/dist-node/src/writes/applescript/source.js +52 -0
  154. package/dist-node/src/writes/applescript/window.applescript +436 -0
  155. package/dist-node/src/writes/applescript/workspace-status.applescript +177 -0
  156. package/dist-node/src/writes/applescript/workspace-targeting.applescript +359 -0
  157. package/dist-node/src/writes/chats.js +160 -0
  158. package/dist-node/src/writes/guards.js +77 -0
  159. package/dist-node/src/writes/runner.js +67 -0
  160. package/dist-node/src/{sidecar.js → writes/sidecar.js} +1 -1
  161. package/dist-node/src/writes/system.js +81 -0
  162. package/dist-node/src/writes/targeting.js +121 -0
  163. package/dist-node/src/writes/types.js +1 -0
  164. package/dist-node/src/writes/ui-lock.js +229 -0
  165. package/dist-node/src/writes/workspaces.js +233 -0
  166. package/docs/voice-setup.md +34 -12
  167. package/package.json +2 -2
  168. package/dist/assets/index-DNLdIfZ2.css +0 -1
  169. package/dist/assets/index-QJR7tn2I.js +0 -61
  170. package/dist-node/src/conductor.applescript +0 -2279
  171. package/dist-node/src/delegations.js +0 -557
  172. package/dist-node/src/mcp-tools.js +0 -1319
  173. package/dist-node/src/orchestration-db.js +0 -2186
  174. package/dist-node/src/reads.js +0 -787
  175. package/dist-node/src/workflow-coordinator.js +0 -2119
  176. package/dist-node/src/writes.js +0 -1223
  177. /package/dist-node/src/{conductor-settings.js → agents/conductor-settings.js} +0 -0
  178. /package/dist-node/src/{firstprompt.js → delivery/firstprompt.js} +0 -0
  179. /package/dist-node/src/{sendonce.js → delivery/sendonce.js} +0 -0
  180. /package/dist-node/src/{preview-urls.js → dev-server/preview-urls.js} +0 -0
  181. /package/dist-node/src/{run-configs.js → dev-server/run-configs.js} +0 -0
  182. /package/dist-node/src/{attachments.js → files/attachments.js} +0 -0
  183. /package/dist-node/src/{icons.js → files/icons.js} +0 -0
  184. /package/dist-node/src/{staged-attachments.js → files/staged-attachments.js} +0 -0
  185. /package/dist-node/src/{fork-workspace.js → git/fork-workspace.js} +0 -0
  186. /package/dist-node/src/{merge.js → git/merge.js} +0 -0
  187. /package/dist-node/src/{pr.js → git/pr.js} +0 -0
  188. /package/dist-node/src/{nosleep.js → host/nosleep.js} +0 -0
  189. /package/dist-node/src/{relay-processes.js → host/relay-processes.js} +0 -0
  190. /package/dist-node/src/{ui-lease-watchdog.js → host/ui-lease-watchdog.js} +0 -0
  191. /package/dist-node/src/{wifi.js → host/wifi.js} +0 -0
  192. /package/dist-node/src/{orchestration-schema.generated.js → orchestration/persistence/schema.generated.js} +0 -0
  193. /package/dist-node/src/{workflow-effect-runner.js → orchestration/workflow/effect-runner.js} +0 -0
  194. /package/dist-node/src/{workflow-http.js → orchestration/workflow/http.js} +0 -0
  195. /package/dist-node/src/{workflow-machine.js → orchestration/workflow/machine.js} +0 -0
  196. /package/dist-node/src/{session-poller.js → reads/session-poller.js} +0 -0
  197. /package/dist-node/src/{context-breakdown.js → transcript/context-breakdown.js} +0 -0
  198. /package/dist-node/src/{chat-cursor.js → transcript/cursor.js} +0 -0
  199. /package/dist-node/src/{plan-usage.js → usage/plan-usage.js} +0 -0
  200. /package/dist-node/src/{tool-usage-service.js → usage/tool-usage-service.js} +0 -0
@@ -0,0 +1,230 @@
1
+ import { describeRepoIcon, resolveRepoIcon } from "../files/icons.js";
2
+ import { resolveWorktree } from "./worktrees.js";
3
+ /**
4
+ * Neutralize LIKE's own wildcards in a user's search word. `queryTokens` already
5
+ * strips everything but letters, digits and `_` — and `_` is LIKE's single-character
6
+ * wildcard, so "add_set" would quietly also match "addaset" without this.
7
+ */
8
+ function escapeLike(s) {
9
+ return s.replace(/[\\%_]/g, m => `\\${m}`);
10
+ }
11
+ /** Shared row → `SearchWorkspace` shape for the two search reads below. */
12
+ function toSearchWorkspace(r) {
13
+ return {
14
+ id: r.id,
15
+ workspace_name: r.workspace_name,
16
+ pr_title: r.pr_title,
17
+ branch: r.branch,
18
+ directory_name: r.directory_name,
19
+ state: r.state,
20
+ updated_at: r.updated_at,
21
+ repo_name: r.repo_name,
22
+ icon: describeRepoIcon({ icon: r.repo_icon, repoRoot: r.repo_root, remoteUrl: r.remote_url }),
23
+ archived: r.state === 'archived'
24
+ };
25
+ }
26
+ export class WorkspacesReads {
27
+ db;
28
+ workspacesRoot;
29
+ constructor(db, workspacesRoot) {
30
+ this.db = db;
31
+ this.workspacesRoot = workspacesRoot;
32
+ }
33
+ /**
34
+ * Unread chats, grouped by workspace. `workspaces.unread` is a **dead column** —
35
+ * Conductor still declares it (and its migration still says "when 1, the workspace
36
+ * should be marked visually as unread") but writes 0 on every row: 0 of 1691 are
37
+ * non-zero on this Mac, which is why the sidebar's bold/badge never once fired.
38
+ * The live flag is per session, and it *is* a flag — `unread_count` is only ever
39
+ * 0 or 1 — so what's worth counting is how many chats have news, not the column.
40
+ */
41
+ unreadSessions() {
42
+ const rows = this.db.query(`SELECT workspace_id, id, updated_at
43
+ FROM sessions
44
+ WHERE COALESCE(unread_count, 0) > 0 AND COALESCE(is_hidden, 0) = 0`);
45
+ const byWorkspace = new Map();
46
+ for (const r of rows) {
47
+ if (!r.workspace_id)
48
+ continue;
49
+ const list = byWorkspace.get(r.workspace_id);
50
+ if (list)
51
+ list.push({ id: r.id, at: r.updated_at });
52
+ else
53
+ byWorkspace.set(r.workspace_id, [{ id: r.id, at: r.updated_at }]);
54
+ }
55
+ return byWorkspace;
56
+ }
57
+ listWorkspaces() {
58
+ const rows = this.db.query(`SELECT w.id, w.directory_name, w.workspace_name, w.branch, w.pr_title, w.derived_status, w.manual_status,
59
+ w.state, w.created_at, w.updated_at, w.pinned_at, w.active_session_id, w.intended_target_branch,
60
+ r.name AS repo_name, r.root_path AS repo_root, r.icon AS repo_icon,
61
+ r.remote_url AS remote_url, r.default_branch AS default_branch,
62
+ s.status AS session_status, s.title AS session_title, s.model AS model,
63
+ s.agent_type AS agent_type
64
+ FROM workspaces w
65
+ LEFT JOIN repos r ON r.id = w.repository_id
66
+ LEFT JOIN sessions s ON s.id = w.active_session_id
67
+ WHERE w.state IN ('ready', 'setting_up')
68
+ ORDER BY (w.pinned_at IS NULL), w.updated_at DESC`);
69
+ const unread = this.unreadSessions();
70
+ return rows.map(r => ({
71
+ ...r,
72
+ unread_sessions: unread.get(r.id) ?? [],
73
+ worktree: resolveWorktree(this.workspacesRoot, r.repo_name, r.directory_name, r.branch, r.repo_root),
74
+ baseBranch: r.intended_target_branch || r.default_branch || 'main',
75
+ icon: describeRepoIcon({ icon: r.repo_icon, repoRoot: r.repo_root, remoteUrl: r.remote_url })
76
+ }));
77
+ }
78
+ getWorkspace(id) {
79
+ return this.listWorkspaces().find(w => w.id === id) ?? null;
80
+ }
81
+ /**
82
+ * The repos Conductor knows about, most recently used first. `root_path` is
83
+ * what a `conductor://…&path=` deep link needs to pick the target repo — with
84
+ * no path Conductor silently falls back to the *first* repo.
85
+ *
86
+ * "Used" is the newest workspace in the repo, never `repos.updated_at`: that
87
+ * column moves when the repo's *settings* change, so the repo worked in every
88
+ * day here reads as last touched six weeks ago. Repos with no workspace at all
89
+ * keep Conductor's own sidebar order, behind every repo that has one.
90
+ *
91
+ * The `replace()`s are load-bearing: `created_at` is always `YYYY-MM-DD HH:MM:SS`
92
+ * while `updated_at` is written both that way (306 rows) and as ISO with a `Z`
93
+ * (1,819), and 'T' sorts after ' ', so an untouched string compare can rank an
94
+ * earlier ISO row above a later plain one on the same day.
95
+ */
96
+ listRepos() {
97
+ const rows = this.db.query(`SELECT r.name, r.root_path, r.default_branch, r.icon, r.remote_url
98
+ FROM repos r
99
+ LEFT JOIN workspaces w ON w.repository_id = r.id
100
+ WHERE COALESCE(r.hidden, 0) = 0
101
+ GROUP BY r.id
102
+ ORDER BY (MAX(MAX(REPLACE(REPLACE(w.updated_at, 'T', ' '), 'Z', ''), w.created_at)) IS NULL),
103
+ MAX(MAX(REPLACE(REPLACE(w.updated_at, 'T', ' '), 'Z', ''), w.created_at)) DESC,
104
+ (r.display_order IS NULL), r.display_order, r.name`);
105
+ return rows.map(r => ({
106
+ name: r.name,
107
+ root_path: r.root_path,
108
+ default_branch: r.default_branch,
109
+ icon: describeRepoIcon({ icon: r.icon, repoRoot: r.root_path, remoteUrl: r.remote_url })
110
+ }));
111
+ }
112
+ /**
113
+ * Map chat ids to the workspace that owns them, archived included.
114
+ *
115
+ * `listWorkspaces` filters to `state IN ('ready','setting_up')`, which is right for
116
+ * the sidebar and wrong for search: the whole point of searching the transcript is
117
+ * to reach work you finished and put away. One query for the whole hit set, because
118
+ * a search fans out over up to 300 chunks and a per-chat lookup would be 300 of them.
119
+ */
120
+ searchTargets(sessionIds) {
121
+ const out = new Map();
122
+ if (!sessionIds.length)
123
+ return out;
124
+ const holes = sessionIds.map(() => '?').join(',');
125
+ const rows = this.db.query(`SELECT s.id AS session_id, s.title AS session_title,
126
+ w.id, w.workspace_name, w.pr_title, w.branch, w.directory_name, w.state, w.updated_at,
127
+ r.name AS repo_name, r.icon AS repo_icon, r.root_path AS repo_root, r.remote_url AS remote_url
128
+ FROM sessions s
129
+ JOIN workspaces w ON w.id = s.workspace_id
130
+ LEFT JOIN repos r ON r.id = w.repository_id
131
+ WHERE s.id IN (${holes})`, sessionIds);
132
+ for (const r of rows)
133
+ out.set(r.session_id, { sessionTitle: r.session_title, workspace: toSearchWorkspace(r) });
134
+ return out;
135
+ }
136
+ /**
137
+ * Every chat in the requested search scope. The index only carries chat ids, so
138
+ * repo and archive filters have to be resolved here before ranking. Names, not repo
139
+ * ids, because names are what the phone's picker and `list_repos` hold.
140
+ */
141
+ searchSessionIds(repos, includeArchived) {
142
+ if (repos && !repos.length)
143
+ return [];
144
+ const scope = [
145
+ ...(repos ? [`r.name IN (${repos.map(() => '?').join(',')})`] : []),
146
+ ...(!includeArchived ? ["w.state IS NOT 'archived'"] : [])
147
+ ];
148
+ return this.db
149
+ .query(`SELECT s.id FROM sessions s
150
+ JOIN workspaces w ON w.id = s.workspace_id
151
+ LEFT JOIN repos r ON r.id = w.repository_id
152
+ ${scope.length ? ` WHERE ${scope.join(' AND ')}` : ''}`, repos ?? [])
153
+ .map(r => r.id);
154
+ }
155
+ /**
156
+ * Workspaces whose own identity matches every token — name, PR title, branch,
157
+ * worktree codename or repo. Archived are included by default, for the same reason
158
+ * as `searchTargets`; `includeArchived` lets the phone narrow the search to current
159
+ * work. `repos` narrows to those repos by name; an empty list matches nothing.
160
+ *
161
+ * This is the half of search the transcript index cannot do. A workspace named for
162
+ * the thing you are looking for may never have said those words in its chat, and
163
+ * one whose chat is empty has no chunks at all.
164
+ */
165
+ findWorkspacesByName(tokens, limit = 20, repos, includeArchived = true) {
166
+ if (!tokens.length)
167
+ return [];
168
+ if (repos && !repos.length)
169
+ return [];
170
+ const fields = ['w.workspace_name', 'w.pr_title', 'w.branch', 'w.directory_name', 'r.name'];
171
+ // AND across tokens, OR across fields: "auk lamp" should find the lamp workspace in
172
+ // the auk repo, where no single column holds both words.
173
+ const byToken = tokens.map(() => `(${fields.map(f => `${f} LIKE ? ESCAPE '\\'`).join(' OR ')})`);
174
+ const scope = [
175
+ ...(repos ? [`r.name IN (${repos.map(() => '?').join(',')})`] : []),
176
+ ...(!includeArchived ? ["w.state IS NOT 'archived'"] : [])
177
+ ];
178
+ const where = [...byToken, ...scope].join(' AND ');
179
+ const params = [...tokens.flatMap(t => fields.map(() => `%${escapeLike(t)}%`)), ...(repos ?? [])];
180
+ const rows = this.db.query(`SELECT w.id, w.workspace_name, w.pr_title, w.branch, w.directory_name, w.state, w.updated_at,
181
+ r.name AS repo_name, r.icon AS repo_icon, r.root_path AS repo_root, r.remote_url AS remote_url
182
+ FROM workspaces w
183
+ LEFT JOIN repos r ON r.id = w.repository_id
184
+ WHERE ${where}
185
+ ORDER BY (w.state = 'archived'), w.updated_at DESC
186
+ LIMIT ?`, [...params, limit]);
187
+ return rows.map(toSearchWorkspace);
188
+ }
189
+ /**
190
+ * One workspace by id, whatever state it is in — the read behind opening an archived
191
+ * chat (`GET /api/workspaces/:id`).
192
+ *
193
+ * `getWorkspace` above is the *live* one: it resolves a worktree and a base branch,
194
+ * and returns null for the 1,846 archived workspaces here, which is right for every
195
+ * write (there is nothing to focus and nothing to diff) and wrong for reading. The
196
+ * transcript survives archiving — Conductor deletes the worktree, not the chat — so
197
+ * this returns the same `SearchWorkspace` a search result carries, with no worktree
198
+ * and no git, and `listSessions`/`getMessages` do the rest by id.
199
+ */
200
+ getAnyWorkspace(id) {
201
+ const rows = this.db.query(`SELECT w.id, w.workspace_name, w.pr_title, w.branch, w.directory_name, w.state, w.updated_at,
202
+ r.name AS repo_name, r.icon AS repo_icon, r.root_path AS repo_root, r.remote_url AS remote_url
203
+ FROM workspaces w
204
+ LEFT JOIN repos r ON r.id = w.repository_id
205
+ WHERE w.id = ?
206
+ LIMIT 1`, [id]);
207
+ return rows[0] ? toSearchWorkspace(rows[0]) : null;
208
+ }
209
+ /** Resolve a repo's icon by its name (the sidebar avatar) — null if the repo or icon is unknown. */
210
+ resolveRepoIcon(repoName) {
211
+ const rows = this.db.query('SELECT root_path FROM repos WHERE name = ? LIMIT 1', [
212
+ repoName
213
+ ]);
214
+ const root = rows[0]?.root_path;
215
+ return root ? resolveRepoIcon(root) : null;
216
+ }
217
+ /**
218
+ * Which workspace a chat belongs to.
219
+ *
220
+ * Every other route resolves this by matching `active_session_id`, which only ever
221
+ * finds the tab that is currently on screen. That is fine for a phone, where the
222
+ * chat you are looking at is the chat you are sending to. It is wrong for anything
223
+ * addressing a chat by id — a background tab has a workspace too, and `sessions`
224
+ * has carried `workspace_id` all along.
225
+ */
226
+ sessionWorkspaceId(sessionId) {
227
+ const rows = this.db.query(`SELECT workspace_id FROM sessions WHERE id = ? LIMIT 1`, [sessionId]);
228
+ return rows[0]?.workspace_id ?? null;
229
+ }
230
+ }
@@ -0,0 +1,45 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ const worktreeCache = new Map();
5
+ /**
6
+ * Resolve a workspace's worktree path. Conductor lays worktrees out as
7
+ * `<workspacesRoot>/<repoName>/<directoryName>`, but we verify against
8
+ * `git worktree list` (matched by branch) so a layout change can't silently
9
+ * point us at the wrong tree.
10
+ */
11
+ export function resolveWorktree(workspacesRoot, repoName, directoryName, branch, repoRoot) {
12
+ if (repoName && directoryName) {
13
+ const guess = path.join(workspacesRoot, repoName, directoryName);
14
+ if (fs.existsSync(path.join(guess, '.git')))
15
+ return guess;
16
+ }
17
+ if (!(repoRoot && branch))
18
+ return null;
19
+ const cacheKey = repoRoot;
20
+ let listing = worktreeCache.get(cacheKey);
21
+ if (listing === undefined) {
22
+ try {
23
+ listing = execFileSync('git', ['-C', repoRoot, 'worktree', 'list', '--porcelain'], {
24
+ encoding: 'utf8',
25
+ timeout: 5000
26
+ });
27
+ }
28
+ catch {
29
+ listing = null;
30
+ }
31
+ worktreeCache.set(cacheKey, listing);
32
+ }
33
+ if (!listing)
34
+ return null;
35
+ // Porcelain: blocks of "worktree <path>" / "branch refs/heads/<name>"
36
+ const blocks = listing.split('\n\n');
37
+ for (const block of blocks) {
38
+ if (block.includes(`refs/heads/${branch}`)) {
39
+ const m = block.match(/^worktree (.+)$/m);
40
+ if (m)
41
+ return m[1];
42
+ }
43
+ }
44
+ return null;
45
+ }
@@ -2,7 +2,7 @@
2
2
  * Every `/api` path, declared once, for the three callers that must agree on it.
3
3
  *
4
4
  * The relay matches these, the phone builds them (`web/src/lib/api.ts`) and the MCP
5
- * tools build them too (`src/mcp-tools.ts`). Before this each path was spelled three
5
+ * tools build them too (`src/mcp/tools/`). Before this each path was spelled three
6
6
  * times: a regex here, a template literal there, another template literal in the third
7
7
  * place. `src/wire.ts` had already made the *shapes* impossible to disagree about, and
8
8
  * this is the other half — a renamed path used to typecheck cleanly in all three files
@@ -85,7 +85,7 @@ export const routes = {
85
85
  /**
86
86
  * Quit Conductor and start it again. Not a workspace route: it is about the app,
87
87
  * and its whole reason to exist is a Conductor that looks healthy from every other
88
- * route while nothing behind it runs (src/writes.ts ▸ restartConductorApp).
88
+ * route while nothing behind it runs (src/writes/system.ts ▸ restartConductorApp).
89
89
  */
90
90
  restartConductor: flat('POST', '/api/conductor/restart'),
91
91
  settings: flat('GET', '/api/settings'),
@@ -142,7 +142,7 @@ export const routes = {
142
142
  devServer: param('GET', '/api/workspaces/:workspaceId/dev-server'),
143
143
  startDevServer: param('POST', '/api/workspaces/:workspaceId/dev-server'),
144
144
  stopDevServer: param('DELETE', '/api/workspaces/:workspaceId/dev-server'),
145
- /** Dismiss a first prompt the relay never managed to deliver (src/firstprompt.ts). */
145
+ /** Dismiss a first prompt the relay never managed to deliver (src/delivery/firstprompt.ts). */
146
146
  dismissFirstPrompt: param('DELETE', '/api/workspaces/:workspaceId/prompt'),
147
147
  // ── chats ──
148
148
  messages: param('GET', '/api/sessions/:sessionId/messages'),
@@ -160,11 +160,11 @@ export const routes = {
160
160
  sendPrompt: param('POST', '/api/sessions/:sessionId/prompt'),
161
161
  /** Write a phone-selected file into Conductor's attachment layout for this chat's workspace. */
162
162
  uploadAttachment: param('POST', '/api/sessions/:sessionId/attachments'),
163
- /** Copy a chat into a fresh tab beside it, as a Conductor attachment (src/attachments.ts). */
163
+ /** Copy a chat into a fresh tab beside it, as a Conductor attachment (src/files/attachments.ts). */
164
164
  splitChat: param('POST', '/api/sessions/:sessionId/split'),
165
165
  /** Lightweight tracked delegation from a chat outside an active Workflow. */
166
166
  delegateTask: param('POST', '/api/sessions/:sessionId/delegate'),
167
- /** Dismiss a prompt parked behind the lock screen (src/parked.ts). */
167
+ /** Dismiss a prompt parked behind the lock screen (src/delivery/parked.ts). */
168
168
  dismissParkedPrompt: param('DELETE', '/api/sessions/:sessionId/prompt')
169
169
  };
170
170
  // ── matching, for the relay ─────────────────────────────────────────────────────
@@ -1,5 +1,5 @@
1
1
  import { Worker } from 'node:worker_threads';
2
- import { chatCursor } from "./chat-cursor.js";
2
+ import { chatCursor } from "../transcript/cursor.js";
3
3
  /**
4
4
  * Full-text search over the chat history, in a sidecar DB the relay owns.
5
5
  *
@@ -48,7 +48,7 @@ const CHUNK_LIMIT = 300;
48
48
  * phone, and no transcript contains them, so the client needs no parser. The client
49
49
  * must not render them literally.
50
50
  */
51
- export { HIT_CLOSE, HIT_OPEN } from "./shared.js";
51
+ export { HIT_CLOSE, HIT_OPEN } from "../shared.js";
52
52
  /**
53
53
  * Turn a phone query into an FTS5 MATCH expression.
54
54
  *
@@ -114,7 +114,7 @@ export function matchQuery(raw) {
114
114
  return required.length ? `${required.join(' AND ')} AND ${looseExpr}` : looseExpr;
115
115
  }
116
116
  /** The tokens `matchQuery` will search for — what a caller matches names against. */
117
- export { queryTokens } from "./shared.js";
117
+ export { queryTokens } from "../shared.js";
118
118
  /**
119
119
  * Main-thread facade for the disposable full-text index.
120
120
  *
@@ -142,7 +142,7 @@ export class SearchIndex {
142
142
  return;
143
143
  this.stopping = false;
144
144
  this.indexStatus = { chunks: 0, ready: false, progress: 0 };
145
- const module = import.meta.url.endsWith('.ts') ? './search-worker.ts' : './search-worker.js';
145
+ const module = import.meta.url.endsWith('.ts') ? './worker.ts' : './worker.js';
146
146
  try {
147
147
  const worker = new Worker(new URL(module, import.meta.url), {
148
148
  workerData: { sourceDbPath: this.sourceDbPath, file: this.file },
@@ -2,9 +2,9 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { DatabaseSync } from 'node:sqlite';
4
4
  import { parentPort, workerData } from 'node:worker_threads';
5
- import { ConductorDb } from "./db.js";
6
- import { HIT_CLOSE, HIT_OPEN, matchQuery } from "./search.js";
7
- import { parseMessage } from "./transcript.js";
5
+ import { ConductorDb } from "../db.js";
6
+ import { parseMessage } from "../transcript/parser.js";
7
+ import { HIT_CLOSE, HIT_OPEN, matchQuery } from "./coordinator.js";
8
8
  /** Bump when extraction or an invariant such as one-chunk-per-source changes stored rows. */
9
9
  // v3 rebuilds every chunk through WorkflowSecretScrubber so a capability indexed by
10
10
  // an older relay cannot survive after the private-envelope boundary is introduced.