@saadnvd1/agent-os 0.3.637 → 0.3.639
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/api/sessions/[id]/pr/route.ts +3 -3
- package/app/api/sessions/[id]/setup/route.ts +3 -3
- package/app/api/tasks/[id]/approve-plan/route.ts +3 -4
- package/app/api/tasks/[id]/arm/route.ts +6 -5
- package/app/api/tasks/[id]/chat-facts/route.ts +2 -2
- package/app/api/tasks/[id]/chat-tail/route.ts +2 -2
- package/app/api/tasks/[id]/checks/route.ts +1 -1
- package/app/api/tasks/[id]/diff/route.ts +1 -1
- package/app/api/tasks/[id]/done/route.ts +2 -2
- package/app/api/tasks/[id]/drop/route.ts +6 -2
- package/app/api/tasks/[id]/export/route.ts +3 -5
- package/app/api/tasks/[id]/merge/route.ts +3 -3
- package/app/api/tasks/[id]/message/route.ts +2 -2
- package/app/api/tasks/[id]/model/route.ts +5 -1
- package/app/api/tasks/[id]/move/route.ts +5 -1
- package/app/api/tasks/[id]/moved/route.ts +6 -8
- package/app/api/tasks/[id]/plan/route.ts +4 -6
- package/app/api/tasks/[id]/plan-edits/route.ts +6 -5
- package/app/api/tasks/[id]/priority/route.ts +4 -1
- package/app/api/tasks/[id]/resume/route.ts +3 -4
- package/app/api/tasks/[id]/stop/route.ts +2 -2
- package/app/api/tasks/route.ts +1 -1
- package/lib/approvals/enable.ts +1 -1
- package/lib/away/report.ts +4 -4
- package/lib/away/run.ts +2 -2
- package/lib/bookmarks/store.ts +3 -3
- package/lib/bus/held-reply.ts +2 -2
- package/lib/bus/index.ts +3 -3
- package/lib/chat/compact-context.ts +1 -1
- package/lib/chat/hold.ts +3 -3
- package/lib/chat/refresh.ts +1 -1
- package/lib/chat/registry.ts +3 -3
- package/lib/chat/runner.ts +1 -1
- package/lib/chat/worker/main.ts +1 -1
- package/lib/db/index.ts +5 -1
- package/lib/db/lumifyhub.ts +8 -9
- package/lib/db/machine-scope.ts +12 -0
- package/lib/db/migrations/20261015T1800_split_sessions_tables.ts +172 -0
- package/lib/db/migrations/README.md +7 -0
- package/lib/db/queries.ts +16 -31
- package/lib/db/session-parts/columns.ts +186 -0
- package/lib/db/session-parts/fill.ts +33 -0
- package/lib/db/session-parts/index.ts +9 -0
- package/lib/db/session-parts/pauses.ts +11 -0
- package/lib/db/session-parts/placement.ts +11 -0
- package/lib/db/session-parts/plans.ts +31 -0
- package/lib/db/session-parts/prs.ts +31 -0
- package/lib/db/session-parts/tasks.ts +27 -0
- package/lib/db/session-parts/view.ts +43 -0
- package/lib/db/session-parts/write.ts +69 -0
- package/lib/done/archive.ts +2 -2
- package/lib/done/auto-archive.ts +1 -1
- package/lib/done/bulk.ts +1 -1
- package/lib/done/index.ts +8 -14
- package/lib/health/evidence.ts +1 -1
- package/lib/health/facts.ts +1 -1
- package/lib/health/pieces.ts +1 -1
- package/lib/hosts/peer-actions.ts +2 -2
- package/lib/hosts/peer-mirror.ts +2 -2
- package/lib/hosts/peer-sync.ts +69 -32
- package/lib/learning-cards/input.ts +1 -1
- package/lib/learning-cards/store.ts +1 -1
- package/lib/models/plan-step.ts +10 -24
- package/lib/models/resume.ts +5 -7
- package/lib/notify/index.ts +1 -1
- package/lib/notify/push/events.ts +1 -1
- package/lib/notify/push/index.ts +1 -1
- package/lib/orchestrator/act.ts +1 -1
- package/lib/orchestrator/armed.ts +17 -15
- package/lib/orchestrator/ask-approvals.ts +1 -1
- package/lib/orchestrator/ask-heads.ts +1 -1
- package/lib/orchestrator/ask-settle.ts +1 -1
- package/lib/orchestrator/brakes.ts +1 -1
- package/lib/orchestrator/digest.ts +2 -2
- package/lib/orchestrator/external-pr.ts +2 -2
- package/lib/orchestrator/facts.ts +4 -28
- package/lib/orchestrator/held-asks.ts +3 -3
- package/lib/orchestrator/home.ts +2 -2
- package/lib/orchestrator/overview.ts +1 -1
- package/lib/orchestrator/pr-wake.ts +3 -5
- package/lib/orchestrator/questions.ts +1 -1
- package/lib/orchestrator/quick-store.ts +1 -1
- package/lib/orchestrator/quick.ts +10 -7
- package/lib/orchestrator/read.ts +1 -1
- package/lib/orchestrator/rebase.ts +8 -11
- package/lib/orchestrator/reconcile.ts +2 -2
- package/lib/orchestrator/signoff.ts +3 -1
- package/lib/orchestrator/targets.ts +1 -1
- package/lib/orchestrator/task-state.ts +4 -2
- package/lib/plans/features.ts +1 -1
- package/lib/plans/release.ts +1 -1
- package/lib/ports.ts +1 -1
- package/lib/resurrect/find.ts +1 -1
- package/lib/resurrect/index.ts +24 -18
- package/lib/schedules/store.ts +1 -1
- package/lib/session-audit/sources.ts +3 -3
- package/lib/session-logs/index.ts +1 -1
- package/lib/session-titles.ts +1 -1
- package/lib/sessions/setup-progress.ts +1 -1
- package/lib/sessions/stop.ts +1 -1
- package/lib/sessions/worktree-setup.ts +2 -1
- package/lib/simulator/index.ts +1 -1
- package/lib/simulator/task-devices.ts +1 -1
- package/lib/stacks/board.ts +1 -1
- package/lib/stacks/land.ts +1 -1
- package/lib/stacks/restack.ts +1 -1
- package/lib/stacks/start.ts +2 -2
- package/lib/stacks/tick.ts +1 -1
- package/lib/stacks/view.ts +1 -1
- package/lib/status/incremental.ts +1 -1
- package/lib/tasks/contract/actions.ts +35 -0
- package/lib/tasks/contract/authorize.ts +82 -0
- package/lib/tasks/contract/client.ts +296 -0
- package/lib/tasks/{held-remote.ts → contract/held-calls.ts} +35 -66
- package/lib/tasks/contract/index.ts +28 -0
- package/lib/tasks/{remote-plan-path.ts → contract/plan-path.ts} +2 -2
- package/lib/tasks/{remote-plan.ts → contract/plan.ts} +76 -110
- package/lib/tasks/contract/routes.ts +26 -0
- package/lib/tasks/contract/start.ts +122 -0
- package/lib/tasks/contract/sync.ts +126 -0
- package/lib/tasks/contract/types.ts +41 -0
- package/lib/tasks/{remote-views.ts → contract/views.ts} +11 -18
- package/lib/tasks/finish.ts +10 -9
- package/lib/tasks/held-replies.ts +7 -13
- package/lib/tasks/held.ts +14 -18
- package/lib/tasks/import.ts +19 -15
- package/lib/tasks/index.ts +34 -22
- package/lib/tasks/launch-gate.ts +1 -1
- package/lib/tasks/move-carry.ts +15 -7
- package/lib/tasks/move-flow.ts +29 -36
- package/lib/tasks/move-guard.ts +1 -1
- package/lib/tasks/move-recover.ts +6 -25
- package/lib/tasks/move.ts +23 -23
- package/lib/tasks/moved-away.ts +1 -1
- package/lib/tasks/pause-hold.ts +13 -5
- package/lib/tasks/pause.ts +19 -23
- package/lib/tasks/plan-view.ts +1 -1
- package/lib/tasks/queue-placement.ts +1 -1
- package/lib/tasks/queue-rows.ts +4 -4
- package/lib/tasks/queue-start.ts +5 -3
- package/lib/tasks/queue-tick.ts +2 -2
- package/lib/tasks/queue.ts +14 -6
- package/lib/tasks/session.ts +7 -4
- package/lib/tasks/start.ts +4 -4
- package/lib/tasks/starter.ts +8 -11
- package/lib/usage/calls.ts +1 -1
- package/lib/usage/compare.ts +1 -1
- package/lib/usage/quality.ts +1 -1
- package/lib/usage/turns.ts +1 -1
- package/lib/voice/vocab.ts +1 -1
- package/package.json +1 -1
- package/scripts/backfill-session-logs.ts +1 -1
- package/lib/tasks/held-routes.ts +0 -33
- 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
|
|
3
|
-
* machine's AgentOS answers for the device that sent the task (held.ts).
|
|
4
|
-
*
|
|
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 "
|
|
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 "
|
|
16
|
-
import type { Delivery } from "
|
|
17
|
-
import { wakeLine } from "
|
|
18
|
-
import { orchestratedStart } from "
|
|
19
|
-
import { orchestratorOf } from "
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
|
154
|
-
return { state
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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 "
|
|
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 (
|
|
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`);
|