@saadnvd1/agent-os 0.3.637 → 0.3.644

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 (211) hide show
  1. package/app/api/bus/branches/route.ts +25 -0
  2. package/app/api/bus/resurrect/route.test.ts +3 -6
  3. package/app/api/sessions/[id]/pr/route.ts +3 -3
  4. package/app/api/sessions/[id]/setup/route.ts +3 -3
  5. package/app/api/tasks/[id]/approve-plan/route.ts +3 -4
  6. package/app/api/tasks/[id]/arm/route.ts +6 -5
  7. package/app/api/tasks/[id]/chat-facts/route.ts +2 -2
  8. package/app/api/tasks/[id]/chat-tail/route.ts +2 -2
  9. package/app/api/tasks/[id]/checks/route.ts +1 -1
  10. package/app/api/tasks/[id]/diff/route.ts +1 -1
  11. package/app/api/tasks/[id]/done/route.ts +2 -2
  12. package/app/api/tasks/[id]/drop/route.ts +6 -2
  13. package/app/api/tasks/[id]/export/route.ts +3 -5
  14. package/app/api/tasks/[id]/merge/route.ts +3 -3
  15. package/app/api/tasks/[id]/message/route.ts +2 -2
  16. package/app/api/tasks/[id]/model/route.ts +5 -1
  17. package/app/api/tasks/[id]/move/route.ts +5 -1
  18. package/app/api/tasks/[id]/moved/route.ts +6 -8
  19. package/app/api/tasks/[id]/plan/route.ts +4 -6
  20. package/app/api/tasks/[id]/plan-edits/route.ts +6 -5
  21. package/app/api/tasks/[id]/priority/route.ts +4 -1
  22. package/app/api/tasks/[id]/resume/route.ts +3 -4
  23. package/app/api/tasks/[id]/stop/route.ts +2 -2
  24. package/app/api/tasks/route.ts +1 -1
  25. package/app/api/worktree-sweep/route.test.ts +40 -0
  26. package/app/api/worktree-sweep/route.ts +31 -0
  27. package/app/api/worktree-sweep/run/route.ts +26 -0
  28. package/components/Health/HealthPanel.tsx +4 -1
  29. package/components/Health/SweepRuns.tsx +125 -0
  30. package/components/Merge/GlobalMergeSection.tsx +2 -0
  31. package/components/Merge/ProjectMergeSection.tsx +2 -0
  32. package/components/Merge/WorktreeSweepSection.tsx +125 -0
  33. package/components/Settings/NoApprovalsSection.tsx +1 -1
  34. package/components/Settings/SettingsDialog.tsx +1 -1
  35. package/data/worktree-sweep/index.ts +56 -0
  36. package/lib/approvals/enable.ts +1 -1
  37. package/lib/away/report.ts +5 -5
  38. package/lib/away/run.ts +2 -2
  39. package/lib/bookmarks/store.ts +3 -3
  40. package/lib/bus/held-reply.ts +2 -2
  41. package/lib/bus/index.ts +3 -3
  42. package/lib/chat/compact-context.ts +1 -1
  43. package/lib/chat/hold.ts +3 -3
  44. package/lib/chat/refresh.ts +1 -1
  45. package/lib/chat/registry.ts +3 -3
  46. package/lib/chat/runner.ts +1 -1
  47. package/lib/chat/worker/main.ts +1 -1
  48. package/lib/db/backup.ts +49 -0
  49. package/lib/db/index.ts +5 -1
  50. package/lib/db/lumifyhub.ts +8 -9
  51. package/lib/db/machine-scope.ts +12 -0
  52. package/lib/db/migrations/20261015T1733_gate_loop_guard.ts +40 -0
  53. package/lib/db/migrations/20261015T1800_split_sessions_tables.ts +172 -0
  54. package/lib/db/migrations/20261015T1830_drop_moved_session_columns.ts +173 -0
  55. package/lib/db/migrations/20261015T1900_worktree_sweep.ts +30 -0
  56. package/lib/db/migrations/README.md +11 -0
  57. package/lib/db/queries.ts +16 -31
  58. package/lib/db/session-parts/columns.ts +186 -0
  59. package/lib/db/session-parts/fill.ts +33 -0
  60. package/lib/db/session-parts/index.ts +10 -0
  61. package/lib/db/session-parts/pauses.ts +11 -0
  62. package/lib/db/session-parts/placement.ts +11 -0
  63. package/lib/db/session-parts/plans.ts +31 -0
  64. package/lib/db/session-parts/prs.ts +31 -0
  65. package/lib/db/session-parts/rows.ts +69 -0
  66. package/lib/db/session-parts/tasks.ts +27 -0
  67. package/lib/db/session-parts/view.ts +50 -0
  68. package/lib/db/session-parts/write.ts +61 -0
  69. package/lib/done/archive.ts +2 -2
  70. package/lib/done/auto-archive.ts +1 -1
  71. package/lib/done/bulk.ts +1 -1
  72. package/lib/done/index.ts +8 -14
  73. package/lib/done/remote.ts +9 -20
  74. package/lib/health/evidence.ts +1 -1
  75. package/lib/health/facts.ts +1 -1
  76. package/lib/health/pieces.ts +1 -1
  77. package/lib/hosts/peer-actions.ts +2 -2
  78. package/lib/hosts/peer-mirror.ts +2 -2
  79. package/lib/hosts/peer-sync.ts +69 -32
  80. package/lib/learning-cards/input.ts +1 -1
  81. package/lib/learning-cards/store.ts +1 -1
  82. package/lib/models/plan-step.ts +10 -24
  83. package/lib/models/resume.ts +5 -7
  84. package/lib/notify/index.ts +1 -1
  85. package/lib/notify/push/events.ts +1 -1
  86. package/lib/notify/push/index.ts +1 -1
  87. package/lib/orchestrator/act.ts +1 -1
  88. package/lib/orchestrator/armed.test-helpers.ts +228 -0
  89. package/lib/orchestrator/armed.ts +18 -16
  90. package/lib/orchestrator/ask-approvals.ts +1 -1
  91. package/lib/orchestrator/ask-heads.ts +1 -1
  92. package/lib/orchestrator/ask-settle.ts +1 -1
  93. package/lib/orchestrator/ask-verdict.ts +111 -0
  94. package/lib/orchestrator/asks.ts +26 -0
  95. package/lib/orchestrator/brakes.ts +1 -1
  96. package/lib/orchestrator/brief.ts +2 -1
  97. package/lib/orchestrator/digest.ts +2 -2
  98. package/lib/orchestrator/escalate.ts +25 -12
  99. package/lib/orchestrator/external-gates.ts +1 -0
  100. package/lib/orchestrator/external-pr.ts +2 -2
  101. package/lib/orchestrator/facts.ts +4 -28
  102. package/lib/orchestrator/gates.ts +94 -25
  103. package/lib/orchestrator/held-asks.ts +8 -11
  104. package/lib/orchestrator/held-review.ts +8 -4
  105. package/lib/orchestrator/home.ts +2 -2
  106. package/lib/orchestrator/loop-guard.ts +145 -0
  107. package/lib/orchestrator/merge-approvals.ts +2 -2
  108. package/lib/orchestrator/overview.ts +1 -1
  109. package/lib/orchestrator/pr-wake.ts +3 -5
  110. package/lib/orchestrator/questions.ts +1 -1
  111. package/lib/orchestrator/quick-store.ts +1 -1
  112. package/lib/orchestrator/quick.ts +11 -8
  113. package/lib/orchestrator/read.ts +1 -1
  114. package/lib/orchestrator/rebase.ts +8 -11
  115. package/lib/orchestrator/reconcile.ts +2 -2
  116. package/lib/orchestrator/review-prompt.ts +2 -0
  117. package/lib/orchestrator/review.ts +14 -4
  118. package/lib/orchestrator/signoff.test-helpers.ts +322 -0
  119. package/lib/orchestrator/signoff.ts +49 -11
  120. package/lib/orchestrator/targets.ts +1 -1
  121. package/lib/orchestrator/task-state.ts +4 -2
  122. package/lib/orchestrator/testing.ts +11 -14
  123. package/lib/orchestrator/watcher.ts +4 -0
  124. package/lib/plans/features.ts +1 -1
  125. package/lib/plans/release.ts +1 -1
  126. package/lib/ports.ts +1 -1
  127. package/lib/recipes/test-fixtures.ts +2 -3
  128. package/lib/resurrect/find.ts +1 -1
  129. package/lib/resurrect/index.ts +44 -20
  130. package/lib/resurrect/swept.ts +61 -0
  131. package/lib/schedules/store.ts +1 -1
  132. package/lib/session-audit/sources.ts +3 -3
  133. package/lib/session-logs/index.ts +1 -1
  134. package/lib/session-titles.ts +1 -1
  135. package/lib/sessions/setup-progress.ts +1 -1
  136. package/lib/sessions/stop.ts +1 -1
  137. package/lib/sessions/worktree-setup.ts +2 -1
  138. package/lib/simulator/index.ts +1 -1
  139. package/lib/simulator/task-devices.ts +1 -1
  140. package/lib/stacks/board.ts +1 -1
  141. package/lib/stacks/land.ts +1 -1
  142. package/lib/stacks/restack.ts +1 -1
  143. package/lib/stacks/start.ts +2 -2
  144. package/lib/stacks/testing.ts +17 -16
  145. package/lib/stacks/tick.ts +1 -1
  146. package/lib/stacks/view.ts +1 -1
  147. package/lib/status/incremental.ts +1 -1
  148. package/lib/tasks/contract/actions.ts +35 -0
  149. package/lib/tasks/contract/authorize.ts +82 -0
  150. package/lib/tasks/contract/client.ts +296 -0
  151. package/lib/tasks/{held-remote.ts → contract/held-calls.ts} +35 -66
  152. package/lib/tasks/contract/index.ts +28 -0
  153. package/lib/tasks/{remote-plan-path.ts → contract/plan-path.ts} +2 -2
  154. package/lib/tasks/{remote-plan.ts → contract/plan.ts} +76 -110
  155. package/lib/tasks/contract/routes.ts +26 -0
  156. package/lib/tasks/contract/start.ts +122 -0
  157. package/lib/tasks/contract/sync.ts +126 -0
  158. package/lib/tasks/contract/types.ts +41 -0
  159. package/lib/tasks/{remote-views.ts → contract/views.ts} +11 -18
  160. package/lib/tasks/finish.ts +30 -23
  161. package/lib/tasks/held-replies.ts +7 -13
  162. package/lib/tasks/held.ts +14 -18
  163. package/lib/tasks/import.ts +19 -15
  164. package/lib/tasks/index.ts +34 -22
  165. package/lib/tasks/launch-gate.ts +1 -1
  166. package/lib/tasks/move-carry.ts +15 -7
  167. package/lib/tasks/move-flow.ts +29 -36
  168. package/lib/tasks/move-guard.ts +1 -1
  169. package/lib/tasks/move-recover.ts +6 -25
  170. package/lib/tasks/move-testing.ts +16 -7
  171. package/lib/tasks/move.ts +23 -23
  172. package/lib/tasks/moved-away.ts +1 -1
  173. package/lib/tasks/pause-hold.ts +13 -5
  174. package/lib/tasks/pause.ts +19 -23
  175. package/lib/tasks/plan-view.ts +1 -1
  176. package/lib/tasks/pr-activity.ts +11 -2
  177. package/lib/tasks/prune-branches.ts +193 -0
  178. package/lib/tasks/prune-scope.ts +22 -0
  179. package/lib/tasks/queue-placement.ts +1 -1
  180. package/lib/tasks/queue-rows.ts +4 -4
  181. package/lib/tasks/queue-start.ts +5 -3
  182. package/lib/tasks/queue-tick.ts +2 -2
  183. package/lib/tasks/queue.ts +14 -6
  184. package/lib/tasks/remote-branch.ts +170 -0
  185. package/lib/tasks/session.ts +7 -4
  186. package/lib/tasks/start.ts +4 -4
  187. package/lib/tasks/starter.ts +8 -11
  188. package/lib/tasks/testing.ts +13 -7
  189. package/lib/usage/calls.ts +1 -1
  190. package/lib/usage/compare.ts +1 -1
  191. package/lib/usage/quality.ts +1 -1
  192. package/lib/usage/turns.ts +1 -1
  193. package/lib/voice/vocab.ts +1 -1
  194. package/lib/worktree-sweep/commits.ts +65 -0
  195. package/lib/worktree-sweep/findOrphans.ts +139 -0
  196. package/lib/worktree-sweep/findSettled.ts +112 -0
  197. package/lib/worktree-sweep/index.ts +10 -0
  198. package/lib/worktree-sweep/judge.ts +31 -0
  199. package/lib/worktree-sweep/panes.ts +18 -0
  200. package/lib/worktree-sweep/runSweep.ts +243 -0
  201. package/lib/worktree-sweep/settings.ts +83 -0
  202. package/lib/worktree-sweep/start.ts +64 -0
  203. package/lib/worktree-sweep/state.ts +36 -0
  204. package/lib/worktree-sweep/store.ts +96 -0
  205. package/lib/worktree-sweep/types.ts +53 -0
  206. package/package.json +1 -1
  207. package/scripts/backfill-session-logs.ts +1 -1
  208. package/scripts/screenshots/seed.ts +19 -18
  209. package/server.ts +3 -0
  210. package/lib/tasks/held-routes.ts +0 -33
  211. package/lib/tasks/remote.ts +0 -255
@@ -0,0 +1,296 @@
1
+ /**
2
+ * The one client for a task running on a linked machine: every call the Mac
3
+ * makes to that machine's task API, with its path, body, timeout and retry in
4
+ * one place. Callers hold a HostLink and say what they want; none of them
5
+ * builds a task URL.
6
+ */
7
+
8
+ import { MERGE_METHODS, type MergeMethod } from "../merge-methods";
9
+ import type { Session } from "../../db";
10
+ import {
11
+ HostApiError,
12
+ cleanRemoteBlock,
13
+ cleanRemoteText,
14
+ hostApi,
15
+ type HostLink,
16
+ } from "../../hosts/remote-api";
17
+ import type { OrchestratedBundle } from "../move-bundle";
18
+ import type { TaskBundle } from "../move-bundle";
19
+ import {
20
+ HELD_REPLIES,
21
+ IMPORT,
22
+ LIST,
23
+ TIMEOUT,
24
+ arrivedPath,
25
+ taskPath,
26
+ } from "./routes";
27
+ import type { ChatFacts, HostTasks } from "./types";
28
+
29
+ export const unknownOutcome = (err: unknown) =>
30
+ err instanceof HostApiError && !err.refused;
31
+
32
+ /** POST a call that's safe to repeat; once more if we can't tell it landed. */
33
+ export async function postIdempotent<T>(
34
+ link: HostLink,
35
+ path: string,
36
+ body: unknown,
37
+ timeout: number
38
+ ): Promise<T> {
39
+ try {
40
+ return await hostApi<T>(link, path, { body, timeout });
41
+ } catch (err) {
42
+ if (!unknownOutcome(err)) throw err;
43
+ return hostApi<T>(link, path, { body, timeout });
44
+ }
45
+ }
46
+
47
+ /** POST, repeated when the outcome is unknown (the calls are idempotent). */
48
+ export async function tell(
49
+ link: HostLink,
50
+ path: string,
51
+ body: object = {}
52
+ ): Promise<void> {
53
+ for (let i = 0; ; i++) {
54
+ try {
55
+ await hostApi(link, path, { body });
56
+ return;
57
+ } catch (err) {
58
+ if (!unknownOutcome(err) || i === 2) throw err;
59
+ }
60
+ }
61
+ }
62
+
63
+ const cache = new Map<string, { at: number; list: Promise<HostTasks> }>();
64
+
65
+ /** The machine's task list; the last answer within 3 s unless `fresh`. */
66
+ export function list(link: HostLink, fresh = false): Promise<HostTasks> {
67
+ const hit = cache.get(link.hostId);
68
+ if (!fresh && hit && Date.now() - hit.at < 3000) return hit.list;
69
+ const next = hostApi<HostTasks>(link, LIST, { timeout: TIMEOUT.list });
70
+ cache.set(link.hostId, { at: Date.now(), list: next });
71
+ next.catch(() => cache.delete(link.hostId));
72
+ return next;
73
+ }
74
+
75
+ export const forget = (hostId: string) => cache.delete(hostId);
76
+
77
+ export async function view(
78
+ link: HostLink,
79
+ id: string,
80
+ fresh = false
81
+ ): Promise<HostTasks["tasks"][number] | undefined> {
82
+ const { tasks } = await list(link, fresh);
83
+ return (Array.isArray(tasks) ? tasks : []).find((t) => t?.id === id);
84
+ }
85
+
86
+ /**
87
+ * A machine on an older AgentOS would take a chat task as a terminal one,
88
+ * or an orchestrator's task as a plain one that anyone there could merge, or
89
+ * refuse to hand one over.
90
+ */
91
+ export async function requireCapability(
92
+ link: HostLink,
93
+ capability: string,
94
+ what: string,
95
+ verb = "move"
96
+ ): Promise<void> {
97
+ const { capabilities = [] } = await list(link, true).catch((err) => {
98
+ throw new Error(
99
+ `Can't ask ${link.hostName} whether it takes ${what}: ${(err as Error).message}`
100
+ );
101
+ });
102
+ if (!capabilities.includes(capability))
103
+ throw new Error(
104
+ `${link.hostName}'s AgentOS can't ${verb} ${what} yet; update it first`
105
+ );
106
+ }
107
+
108
+ export const start = (link: HostLink, body: object) =>
109
+ postIdempotent<{ session: Session }>(link, LIST, body, TIMEOUT.start);
110
+
111
+ export async function plan(link: HostLink, id: string): Promise<string | null> {
112
+ const { text } = await hostApi<{ text: string | null }>(
113
+ link,
114
+ taskPath(id, "plan"),
115
+ { timeout: TIMEOUT.plan }
116
+ );
117
+ return text;
118
+ }
119
+
120
+ export const approvePlan = (link: HostLink, id: string, message?: string) =>
121
+ hostApi<{ model?: string }>(link, taskPath(id, "approve-plan"), {
122
+ body: message ? { message } : {},
123
+ timeout: TIMEOUT.approvePlan,
124
+ });
125
+
126
+ export async function hold(
127
+ link: HostLink,
128
+ id: string,
129
+ orchestrated: OrchestratedBundle
130
+ ): Promise<void> {
131
+ await hostApi(link, taskPath(id, "hold"), {
132
+ body: { orchestrated },
133
+ timeout: TIMEOUT.hold,
134
+ });
135
+ }
136
+
137
+ export async function message(
138
+ link: HostLink,
139
+ id: string,
140
+ body: { id?: string; line: string; from: string }
141
+ ): Promise<"queued" | "delivered"> {
142
+ const { state } = await hostApi<{ state?: unknown }>(
143
+ link,
144
+ taskPath(id, "message"),
145
+ { body, timeout: TIMEOUT.message }
146
+ );
147
+ return state === "queued" ? "queued" : "delivered";
148
+ }
149
+
150
+ export async function chatTail(
151
+ link: HostLink,
152
+ id: string,
153
+ lines: number
154
+ ): Promise<string | null> {
155
+ const { text } = await hostApi<{ text?: unknown }>(
156
+ link,
157
+ `${taskPath(id, "chat-tail")}?lines=${lines}`,
158
+ { timeout: TIMEOUT.chat }
159
+ );
160
+ // Whole lines, as a local read has them (read.ts caps and trims them).
161
+ return typeof text === "string" ? cleanRemoteBlock(text, 16000) : null;
162
+ }
163
+
164
+ export async function chatFacts(
165
+ link: HostLink,
166
+ id: string
167
+ ): Promise<ChatFacts | null> {
168
+ const res = await hostApi<{ blocked?: unknown; turn?: unknown }>(
169
+ link,
170
+ taskPath(id, "chat-facts"),
171
+ { timeout: TIMEOUT.chat }
172
+ );
173
+ if (res.blocked !== null && typeof res.blocked !== "string") return null;
174
+ if (res.turn !== null && typeof res.turn !== "string") return null;
175
+ return {
176
+ blocked: res.blocked ? cleanRemoteBlock(res.blocked, 2000) : null,
177
+ turn: res.turn,
178
+ };
179
+ }
180
+
181
+ export async function stop(link: HostLink, id: string): Promise<string> {
182
+ const { text } = await hostApi<{ text?: unknown }>(
183
+ link,
184
+ taskPath(id, "stop"),
185
+ {
186
+ body: {},
187
+ timeout: TIMEOUT.stop,
188
+ }
189
+ );
190
+ return cleanRemoteText(text);
191
+ }
192
+
193
+ export async function done(link: HostLink, id: string): Promise<void> {
194
+ await hostApi(link, taskPath(id, "done"), {
195
+ body: {},
196
+ timeout: TIMEOUT.done,
197
+ });
198
+ }
199
+
200
+ /** A merge returns the method that machine merged with; null when it doesn't say. */
201
+ export async function merge(
202
+ link: HostLink,
203
+ id: string,
204
+ pin?: { head: string; base?: string | null },
205
+ label = "it"
206
+ ): Promise<{ head: string | null; method: MergeMethod | null }> {
207
+ // An AgentOS that predates pinned merges would ignore the head and merge
208
+ // whatever the PR is at: refuse before asking it.
209
+ if (pin)
210
+ await requireCapability(link, "pinned-merge", "a merge to a commit", "pin");
211
+ const res = await hostApi<{ head?: string | null; method?: unknown }>(
212
+ link,
213
+ taskPath(id, "merge"),
214
+ {
215
+ // base: what the gates here were judged against; the machine that
216
+ // merges compares the PR with it, not with a base only it recorded.
217
+ body: {
218
+ ...(pin && { head: pin.head }),
219
+ ...(pin?.base && { base: pin.base }),
220
+ },
221
+ timeout: TIMEOUT.merge,
222
+ }
223
+ ).catch(async (err) => {
224
+ if (!unknownOutcome(err)) throw err;
225
+ // A proxy or a dropped connection after the merge ran answers 502 with
226
+ // nothing from AgentOS: ask what state the task is in before failing.
227
+ const merged = await view(link, id, true)
228
+ .then((t) => t?.state === "merged")
229
+ .catch(() => false);
230
+ if (!merged)
231
+ throw new Error(
232
+ `${(err as Error).message}: couldn't confirm whether ${link.hostName} merged ${label}; check its PR before signing off again`
233
+ );
234
+ return { head: pin?.head ?? null, method: undefined };
235
+ });
236
+ forget(link.hostId);
237
+ return {
238
+ head: res.head ?? null,
239
+ method: MERGE_METHODS.find((m) => m === res.method) ?? null,
240
+ };
241
+ }
242
+
243
+ export async function drop(link: HostLink, id: string): Promise<void> {
244
+ await hostApi(link, taskPath(id, "drop"), {
245
+ body: {},
246
+ timeout: TIMEOUT.merge,
247
+ });
248
+ forget(link.hostId);
249
+ }
250
+
251
+ export const resume = (link: HostLink, id: string, body: object = {}) =>
252
+ tell(link, taskPath(id, "resume"), body);
253
+
254
+ export const confirmMoved = (link: HostLink, id: string, to: string) =>
255
+ tell(link, taskPath(id, "moved"), { to });
256
+
257
+ export async function exportOut(
258
+ link: HostLink,
259
+ id: string,
260
+ to: string
261
+ ): Promise<TaskBundle> {
262
+ const { bundle } = await postIdempotent<{ bundle: TaskBundle }>(
263
+ link,
264
+ taskPath(id, "export"),
265
+ { to },
266
+ TIMEOUT.exportOut
267
+ );
268
+ return bundle;
269
+ }
270
+
271
+ export const importBundle = (link: HostLink, bundle: TaskBundle) =>
272
+ postIdempotent<{ session: Session }>(
273
+ link,
274
+ IMPORT,
275
+ bundle,
276
+ TIMEOUT.importBundle
277
+ );
278
+
279
+ export async function arrived(
280
+ link: HostLink,
281
+ from: string
282
+ ): Promise<Session | null> {
283
+ const { session } = await hostApi<{ session: Session | null }>(
284
+ link,
285
+ arrivedPath(from)
286
+ );
287
+ return session;
288
+ }
289
+
290
+ export const replies = (link: HostLink) =>
291
+ hostApi<{ messages?: unknown }>(link, HELD_REPLIES, {
292
+ timeout: TIMEOUT.replies,
293
+ });
294
+
295
+ export const ackReplies = (link: HostLink, ids: number[]) =>
296
+ hostApi(link, HELD_REPLIES, { body: { ids }, timeout: TIMEOUT.replies });
@@ -1,25 +1,22 @@
1
1
  /**
2
- * An orchestrator's calls on a task it moved to a linked machine, which that
3
- * machine's AgentOS answers for the device that sent the task (held.ts). The
4
- * orchestrator polls and calls; the other machine never reaches back.
2
+ * An orchestrator's calls on a task it put on a linked machine, which that
3
+ * machine's AgentOS answers for the device that sent the task (held.ts). A
4
+ * machine that doesn't hold the task yet is asked to, once, and the call
5
+ * repeated. The orchestrator polls and calls; the other machine never reaches
6
+ * back.
5
7
  */
6
8
 
7
- import { db, type Session } from "../db";
9
+ import { db, type Session } from "../../db";
8
10
  import {
9
11
  HostApiError,
10
- cleanRemoteBlock,
11
- cleanRemoteText,
12
- hostApi,
13
12
  hostLink,
14
13
  requireHostLink,
15
- } from "../hosts/remote-api";
16
- import type { Delivery } from "../bus/delivery";
17
- import { wakeLine } from "../bus/format";
18
- import { orchestratedStart } from "./held";
19
- import { orchestratorOf } from "./starter";
20
-
21
- const path = (id: string, leaf: string) =>
22
- `/api/tasks/${encodeURIComponent(id)}/${leaf}`;
14
+ } from "../../hosts/remote-api";
15
+ import type { Delivery } from "../../bus/delivery";
16
+ import { wakeLine } from "../../bus/format";
17
+ import { orchestratedStart } from "../held";
18
+ import { orchestratorOf } from "../starter";
19
+ import * as client from "./client";
23
20
 
24
21
  /**
25
22
  * A task its orchestrator put on a linked machine, whether by moving it or
@@ -54,16 +51,11 @@ async function adoptHeld(task: Held): Promise<boolean> {
54
51
  if (adopted.has(task.id)) return false;
55
52
  adopted.set(task.id, now);
56
53
  try {
57
- const link = requireHostLink(task.host_id);
58
- await hostApi(link, path(task.id, "hold"), {
59
- body: {
60
- orchestrated: orchestratedStart(
61
- task.started_by!,
62
- task.task_since ?? null
63
- ),
64
- },
65
- timeout: 20000,
66
- });
54
+ await client.hold(
55
+ requireHostLink(task.host_id),
56
+ task.id,
57
+ orchestratedStart(task.started_by!, task.task_since ?? null)
58
+ );
67
59
  } catch {
68
60
  return false;
69
61
  }
@@ -72,6 +64,8 @@ async function adoptHeld(task: Held): Promise<boolean> {
72
64
  return true;
73
65
  }
74
66
 
67
+ // compat: task-contract-1. A task the queue placed before machines held them
68
+ // is unheld there; drop this heal once every linked machine holds on start.
75
69
  /** The call, held first if that machine says it isn't, once. */
76
70
  async function withHold<T>(task: Held, call: () => Promise<T>): Promise<T> {
77
71
  try {
@@ -142,16 +136,13 @@ export async function relayToHeldTask(
142
136
  try {
143
137
  const link = requireHostLink(to.host_id);
144
138
  const send = () =>
145
- hostApi<{ state?: unknown }>(link, path(to.id, "message"), {
146
- body: {
147
- id: messageId ? `bus-${messageId}` : undefined,
148
- line,
149
- from: fromName,
150
- },
151
- timeout: 20000,
139
+ client.message(link, to.id, {
140
+ id: messageId ? `bus-${messageId}` : undefined,
141
+ line,
142
+ from: fromName,
152
143
  });
153
- const { state } = await (heal ? withHold(to, send) : send());
154
- return { state: state === "queued" ? "queued" : "delivered" };
144
+ const state = await (heal ? withHold(to, send) : send());
145
+ return { state };
155
146
  } catch (error) {
156
147
  const why = error instanceof Error ? error.message : String(error);
157
148
  return { state: "failed", why: why.slice(0, 300) };
@@ -164,15 +155,9 @@ export async function heldChatTail(
164
155
  lines: number
165
156
  ): Promise<string | null> {
166
157
  const link = requireHostLink(task.host_id);
167
- const { text } = await withHold(task, () =>
168
- hostApi<{ text?: unknown }>(
169
- link,
170
- `${path(task.id, "chat-tail")}?lines=${lines}`,
171
- { timeout: 10000 }
172
- )
173
- ).catch(() => ({ text: undefined }));
174
- // Whole lines, as a local read has them (read.ts caps and trims them).
175
- return typeof text === "string" ? cleanRemoteBlock(text, 16000) : null;
158
+ return withHold(task, () => client.chatTail(link, task.id, lines)).catch(
159
+ () => null
160
+ );
176
161
  }
177
162
 
178
163
  /**
@@ -184,38 +169,22 @@ export async function heldChatFacts(
184
169
  task: Held
185
170
  ): Promise<{ blocked: string | null; turn: string | null } | null> {
186
171
  const link = requireHostLink(task.host_id);
187
- const res = await withHold(task, () =>
188
- hostApi<{ blocked?: unknown; turn?: unknown }>(
189
- link,
190
- path(task.id, "chat-facts"),
191
- { timeout: 10000 }
192
- )
193
- ).catch(() => null);
194
- if (!res || (res.blocked !== null && typeof res.blocked !== "string"))
195
- return null;
196
- if (res.turn !== null && typeof res.turn !== "string") return null;
197
- return {
198
- blocked: res.blocked ? cleanRemoteBlock(res.blocked, 2000) : null,
199
- turn: res.turn,
200
- };
172
+ return withHold(task, () => client.chatFacts(link, task.id)).catch(
173
+ () => null
174
+ );
201
175
  }
202
176
 
203
177
  /** Stops its agent there; its conversation and worktree are kept. */
204
178
  export async function stopHeldTask(task: Held): Promise<string> {
205
179
  const link = requireHostLink(task.host_id);
206
- const { text } = await withHold(task, () =>
207
- hostApi<{ text?: unknown }>(link, path(task.id, "stop"), {
208
- body: {},
209
- timeout: 30000,
210
- })
180
+ return (
181
+ (await withHold(task, () => client.stop(link, task.id))) ||
182
+ `Stopped ${task.name}.`
211
183
  );
212
- return cleanRemoteText(text) || `Stopped ${task.name}.`;
213
184
  }
214
185
 
215
186
  /** Done there: its agent stops and its worktree goes. Never a merge. */
216
187
  export async function doneHeldTask(task: Held): Promise<void> {
217
188
  const link = requireHostLink(task.host_id);
218
- await withHold(task, () =>
219
- hostApi(link, path(task.id, "done"), { body: {}, timeout: 120000 })
220
- );
189
+ await withHold(task, () => client.done(link, task.id));
221
190
  }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * How a task runs on a linked machine: one client for its API
3
+ * (client.ts), one authorization rule per route (authorize.ts), one loop that
4
+ * keeps mirrors current (sync.ts). Local code paths call these through the
5
+ * entry points they already had (signOff, dropTask, deliver, ...).
6
+ */
7
+
8
+ export * as taskClient from "./client";
9
+ export { TASK_CAPABILITIES, type HostTasks } from "./types";
10
+ export {
11
+ isMirror,
12
+ mirrorTask,
13
+ startRemoteTask,
14
+ setMirrorStatus,
15
+ } from "./start";
16
+ export { mergeThere, dropThere } from "./actions";
17
+ export { mirrorViews } from "./views";
18
+ export { syncMirrors } from "./sync";
19
+ export { taskRoute, refuse, callerRefusal, type Rule } from "./authorize";
20
+ export {
21
+ isHeldRemote,
22
+ relayToHeldTask,
23
+ heldChatTail,
24
+ heldChatFacts,
25
+ stopHeldTask,
26
+ doneHeldTask,
27
+ flushUndelivered,
28
+ } from "./held-calls";
@@ -1,7 +1,7 @@
1
1
  import path from "path";
2
- import { plansDir } from "../plans/features";
2
+ import { plansDir } from "../../plans/features";
3
3
 
4
4
  // A plan written on a linked machine, copied here so this machine's
5
- // orchestrator and plan view can read it (remote-plan.ts).
5
+ // orchestrator and plan view can read it (contract/plan.ts).
6
6
  export const remotePlanPath = (sessionId: string) =>
7
7
  path.join(plansDir(), "remote", `${sessionId}.md`);