@saadnvd1/agent-os 0.3.631 → 0.3.639
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +5 -1
- 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 -4
- 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 +35 -0
- 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 +5 -2
- package/app/globals.css +28 -0
- package/components/Chat/ChatPanel.tsx +2 -11
- package/components/Chat/EventTurns.tsx +1 -7
- package/components/Chat/Markdown.tsx +1 -0
- package/components/Chat/Messages.tsx +1 -1
- package/lib/approvals/enable.ts +9 -2
- 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/events.ts +4 -0
- package/lib/chat/group.ts +3 -1
- package/lib/chat/hold.ts +3 -3
- package/lib/chat/reading-order.ts +50 -0
- package/lib/chat/refresh.ts +1 -1
- package/lib/chat/registry.ts +3 -3
- package/lib/chat/runner.ts +1 -1
- package/lib/chat/turns.ts +22 -3
- package/lib/chat/worker/host.ts +3 -0
- 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/20261015T1600_plan_waits_for_caller.ts +19 -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/db/types.ts +6 -1
- 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/env-setup.ts +17 -4
- 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 +83 -46
- 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/contract/plan-path.ts +7 -0
- package/lib/tasks/contract/plan.ts +274 -0
- 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 +36 -17
- package/lib/tasks/hold-checks.ts +134 -0
- package/lib/tasks/holds.ts +281 -0
- package/lib/tasks/import.ts +26 -14
- package/lib/tasks/index.ts +56 -22
- package/lib/tasks/launch-gate.ts +1 -1
- package/lib/tasks/move-bundle.ts +17 -2
- package/lib/tasks/move-carry.ts +15 -7
- package/lib/tasks/move-flow.ts +29 -36
- package/lib/tasks/move-guard.ts +10 -2
- package/lib/tasks/move-recover.ts +6 -25
- package/lib/tasks/move.ts +28 -27
- package/lib/tasks/moved-away.ts +1 -1
- package/lib/tasks/pause-hold.ts +13 -5
- package/lib/tasks/pause.ts +21 -54
- package/lib/tasks/placement.ts +16 -16
- package/lib/tasks/plan-view.ts +11 -3
- package/lib/tasks/queue-candidates.ts +151 -0
- package/lib/tasks/queue-placement.ts +155 -0
- package/lib/tasks/queue-rows.ts +236 -0
- package/lib/tasks/queue-start.ts +213 -0
- package/lib/tasks/queue-tick.ts +148 -0
- package/lib/tasks/queue-types.ts +141 -0
- package/lib/tasks/queue-view.ts +112 -0
- package/lib/tasks/queue.ts +67 -1102
- package/lib/tasks/schedule.ts +44 -147
- 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/scripts/check +55 -7
- package/lib/tasks/auto-hold.ts +0 -17
- package/lib/tasks/held-routes.ts +0 -33
- package/lib/tasks/remote.ts +0 -238
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every reason a task waits, as one type and one ordered list. A hold is
|
|
3
|
+
* pure data: `holdsFor` turns a candidate and what we know of its machine
|
|
4
|
+
* into the holds that apply, always in HOLD_ORDER, and the first is the
|
|
5
|
+
* reason shown. The queue view, a queued row's note, a paused task's resume
|
|
6
|
+
* note and the start decision all read that list, so "why is this waiting"
|
|
7
|
+
* has one source. No database and no clock of its own: the checks that need
|
|
8
|
+
* either are in hold-checks.ts.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
DEFAULT_PRIORITY,
|
|
13
|
+
type Priority,
|
|
14
|
+
type WaitCode,
|
|
15
|
+
} from "./schedule-types";
|
|
16
|
+
import type { Candidate } from "./schedule";
|
|
17
|
+
|
|
18
|
+
// Out of red this long before automatic starts resume.
|
|
19
|
+
export const LOAD_SETTLE_MS = 120_000;
|
|
20
|
+
// On amber, only this priority and above start.
|
|
21
|
+
export const AMBER_MIN_PRIORITY: Priority = "blocking";
|
|
22
|
+
// Between automatic starts on one machine.
|
|
23
|
+
export const START_GAP_MS = 60_000;
|
|
24
|
+
// Between starts of tasks that boot a simulator.
|
|
25
|
+
export const HEAVY_START_GAP_MS = 5 * 60_000;
|
|
26
|
+
|
|
27
|
+
export const PRIORITY_POINTS: Record<Priority, number> = {
|
|
28
|
+
urgent: 400,
|
|
29
|
+
blocking: 300,
|
|
30
|
+
normal: 200,
|
|
31
|
+
background: 100,
|
|
32
|
+
};
|
|
33
|
+
const priorityRank = (p: Priority) => PRIORITY_POINTS[p] ?? 0;
|
|
34
|
+
|
|
35
|
+
export type MachineLevel = "green" | "amber" | "red";
|
|
36
|
+
|
|
37
|
+
// What the machine a candidate would start on looks like right now.
|
|
38
|
+
export interface MachineView {
|
|
39
|
+
// Shown in hold texts, e.g. "this machine".
|
|
40
|
+
label: string;
|
|
41
|
+
// False: no load holds at all (AGENTOS_LOAD=off).
|
|
42
|
+
enabled: boolean;
|
|
43
|
+
// No sample read yet since AgentOS started.
|
|
44
|
+
reading: boolean;
|
|
45
|
+
level: MachineLevel;
|
|
46
|
+
// Not red, and calm for LOAD_SETTLE_MS since it last was.
|
|
47
|
+
settled: boolean;
|
|
48
|
+
// Seconds of calm still needed, when not settled and not red.
|
|
49
|
+
settleLeftMs: number;
|
|
50
|
+
booted: number;
|
|
51
|
+
maxBooted: number;
|
|
52
|
+
// Heavy tasks started within HEAVY_START_GAP_MS that hold no lease yet.
|
|
53
|
+
heavyStarting: number;
|
|
54
|
+
lastAutoStartAt: number | null;
|
|
55
|
+
lastHeavyStartAt: number | null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type MachineCode =
|
|
59
|
+
| "needs"
|
|
60
|
+
| "load_red"
|
|
61
|
+
| "load_amber"
|
|
62
|
+
| "heavy"
|
|
63
|
+
| "spacing";
|
|
64
|
+
|
|
65
|
+
// A reason a task isn't starting. `code` is also what a queue row stores and
|
|
66
|
+
// the client reads. A hold that rests on a machine says which one, and when
|
|
67
|
+
// it knows, the time (ms) it lifts.
|
|
68
|
+
export type Hold =
|
|
69
|
+
| { code: "after"; text: string }
|
|
70
|
+
| { code: "after_any"; text: string }
|
|
71
|
+
| { code: "orch_paused"; text: string }
|
|
72
|
+
| { code: "window"; text: string }
|
|
73
|
+
| {
|
|
74
|
+
code: MachineCode;
|
|
75
|
+
text: string;
|
|
76
|
+
machine: string;
|
|
77
|
+
until?: number;
|
|
78
|
+
// Why it waits here instead of starting on a linked machine.
|
|
79
|
+
stay?: string;
|
|
80
|
+
}
|
|
81
|
+
| { code: "slot"; text: string };
|
|
82
|
+
|
|
83
|
+
// The order holds are reported in; the first that applies is the reason.
|
|
84
|
+
export const HOLD_ORDER: readonly WaitCode[] = [
|
|
85
|
+
"after",
|
|
86
|
+
"after_any",
|
|
87
|
+
"orch_paused",
|
|
88
|
+
"window",
|
|
89
|
+
"needs",
|
|
90
|
+
"load_red",
|
|
91
|
+
"load_amber",
|
|
92
|
+
"heavy",
|
|
93
|
+
"spacing",
|
|
94
|
+
"slot",
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
// Whether the hold lifts without anyone acting. A Pause is lifted by a
|
|
98
|
+
// person, and a task that needs a Mac never gets one by waiting.
|
|
99
|
+
const CLEARS_BY_ITSELF: Record<WaitCode, boolean> = {
|
|
100
|
+
after: true,
|
|
101
|
+
after_any: true,
|
|
102
|
+
orch_paused: false,
|
|
103
|
+
window: true,
|
|
104
|
+
needs: false,
|
|
105
|
+
load_red: true,
|
|
106
|
+
load_amber: true,
|
|
107
|
+
heavy: true,
|
|
108
|
+
spacing: true,
|
|
109
|
+
slot: true,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export const clearsByItself = (h: Hold): boolean => CLEARS_BY_ITSELF[h.code];
|
|
113
|
+
|
|
114
|
+
// The holds a linked machine could lift by taking the start.
|
|
115
|
+
const PLACEABLE = new Set<WaitCode>([
|
|
116
|
+
"load_red",
|
|
117
|
+
"load_amber",
|
|
118
|
+
"heavy",
|
|
119
|
+
"needs",
|
|
120
|
+
]);
|
|
121
|
+
|
|
122
|
+
export const placeable = (h: Pick<Hold, "code">): boolean =>
|
|
123
|
+
PLACEABLE.has(h.code);
|
|
124
|
+
|
|
125
|
+
// The text a person sees: the hold, and for a task that can't go elsewhere,
|
|
126
|
+
// why it stays here.
|
|
127
|
+
export function holdText(h: Hold): string {
|
|
128
|
+
return "stay" in h && h.stay ? `${h.text} (${h.stay})` : h.text;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// The same without the "Waiting: " lead, for a note that has its own.
|
|
132
|
+
export const holdReason = (h: Hold): string =>
|
|
133
|
+
holdText(h).replace(/^Waiting: /, "");
|
|
134
|
+
|
|
135
|
+
// What a row stores of its first hold: null code and text when none.
|
|
136
|
+
export function queueNote(holds: readonly Hold[] | undefined): {
|
|
137
|
+
code: WaitCode | null;
|
|
138
|
+
text: string | null;
|
|
139
|
+
} | null {
|
|
140
|
+
if (!holds) return null;
|
|
141
|
+
const head = holds[0];
|
|
142
|
+
return head
|
|
143
|
+
? { code: head.code, text: holdText(head) }
|
|
144
|
+
: { code: null, text: null };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// What a timed resume waiting for its turn says on its task.
|
|
148
|
+
export function resumeNote(
|
|
149
|
+
holds: readonly Hold[] | undefined,
|
|
150
|
+
slots: { running: number; limit: number | null }
|
|
151
|
+
): string | null {
|
|
152
|
+
if (!holds) return null;
|
|
153
|
+
const head = holds[0];
|
|
154
|
+
if (head && head.code !== "slot") return `held: ${holdReason(head)}`;
|
|
155
|
+
return slots.limit === null
|
|
156
|
+
? "resuming: waiting for its turn"
|
|
157
|
+
: `resuming: waiting for a slot, ${slots.running} of ${slots.limit} running`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const seconds = (ms: number) => Math.max(1, Math.ceil(ms / 1000));
|
|
161
|
+
|
|
162
|
+
export function slotText(rank: number, of: number, priority: Priority): string {
|
|
163
|
+
const word = priority === DEFAULT_PRIORITY ? "" : ` · ${priority}`;
|
|
164
|
+
return `#${rank} of ${of}${word}`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
type MachineAsk = Pick<
|
|
168
|
+
Candidate,
|
|
169
|
+
"heavy" | "needsUnmet" | "remote" | "priority"
|
|
170
|
+
> &
|
|
171
|
+
Partial<Pick<Candidate, "planOnly">>;
|
|
172
|
+
|
|
173
|
+
// The holds the machine puts on a start, in order. Empty: it may start here
|
|
174
|
+
// now. A machine with no reading yet says only that; one with load checks
|
|
175
|
+
// off says only what it lacks.
|
|
176
|
+
export function machineHolds(
|
|
177
|
+
c: MachineAsk,
|
|
178
|
+
m: MachineView,
|
|
179
|
+
now: number
|
|
180
|
+
): Hold[] {
|
|
181
|
+
if (c.remote || c.planOnly) return [];
|
|
182
|
+
const out: Hold[] = [];
|
|
183
|
+
const add = (code: MachineCode, text: string, until?: number): void => {
|
|
184
|
+
out.push({
|
|
185
|
+
code,
|
|
186
|
+
text: `Waiting: ${text}`,
|
|
187
|
+
machine: m.label,
|
|
188
|
+
...(until === undefined ? {} : { until }),
|
|
189
|
+
});
|
|
190
|
+
};
|
|
191
|
+
if (c.needsUnmet) add("needs", "needs a Mac");
|
|
192
|
+
if (!m.enabled) return out;
|
|
193
|
+
if (m.reading) {
|
|
194
|
+
add("load_red", "reading machine load");
|
|
195
|
+
return out;
|
|
196
|
+
}
|
|
197
|
+
if (m.level === "red") add("load_red", `${m.label} load red`);
|
|
198
|
+
else if (!m.settled)
|
|
199
|
+
add(
|
|
200
|
+
"load_red",
|
|
201
|
+
`${m.label} load was red; calm for ${seconds(m.settleLeftMs)}s more`,
|
|
202
|
+
now + m.settleLeftMs
|
|
203
|
+
);
|
|
204
|
+
else if (
|
|
205
|
+
m.level === "amber" &&
|
|
206
|
+
priorityRank(c.priority) < priorityRank(AMBER_MIN_PRIORITY)
|
|
207
|
+
)
|
|
208
|
+
add(
|
|
209
|
+
"load_amber",
|
|
210
|
+
`${m.label} load amber; ${c.priority} work starts on green`
|
|
211
|
+
);
|
|
212
|
+
if (c.heavy && m.level !== "red") {
|
|
213
|
+
if (m.level !== "green")
|
|
214
|
+
add("heavy", `${m.label} load amber; simulator work starts on green`);
|
|
215
|
+
else if (m.heavyStarting > 0)
|
|
216
|
+
add("heavy", `a simulator is booting on ${m.label}`);
|
|
217
|
+
else if (m.booted >= m.maxBooted)
|
|
218
|
+
add("heavy", `${m.booted} of ${m.maxBooted} simulators in use`);
|
|
219
|
+
else if (
|
|
220
|
+
m.lastHeavyStartAt !== null &&
|
|
221
|
+
now - m.lastHeavyStartAt < HEAVY_START_GAP_MS
|
|
222
|
+
)
|
|
223
|
+
add(
|
|
224
|
+
"heavy",
|
|
225
|
+
`a simulator task just started on ${m.label} (next in ${seconds(HEAVY_START_GAP_MS - (now - m.lastHeavyStartAt))}s)`,
|
|
226
|
+
m.lastHeavyStartAt + HEAVY_START_GAP_MS
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
if (m.lastAutoStartAt !== null && now - m.lastAutoStartAt < START_GAP_MS)
|
|
230
|
+
add(
|
|
231
|
+
"spacing",
|
|
232
|
+
`spacing starts (next in ${seconds(START_GAP_MS - (now - m.lastAutoStartAt))}s)`,
|
|
233
|
+
m.lastAutoStartAt + START_GAP_MS
|
|
234
|
+
);
|
|
235
|
+
return out;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// The first machine hold, or null: the one every placement check asks.
|
|
239
|
+
export const machineHold = (
|
|
240
|
+
c: MachineAsk,
|
|
241
|
+
m: MachineView,
|
|
242
|
+
now: number
|
|
243
|
+
): Hold | null => machineHolds(c, m, now)[0] ?? null;
|
|
244
|
+
|
|
245
|
+
export interface HoldContext {
|
|
246
|
+
machine: MachineView;
|
|
247
|
+
now: number;
|
|
248
|
+
// Finishes other waiters have already taken this look.
|
|
249
|
+
used: ReadonlySet<string>;
|
|
250
|
+
// Running tasks in its line, and the line's limit.
|
|
251
|
+
inFlight: number;
|
|
252
|
+
limit: number;
|
|
253
|
+
// Its place in the line's score order.
|
|
254
|
+
rank: number;
|
|
255
|
+
of: number;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Every hold on a candidate, in HOLD_ORDER. The first is why it waits; none:
|
|
259
|
+
// it starts.
|
|
260
|
+
export function holdsFor(c: Candidate, ctx: HoldContext): Hold[] {
|
|
261
|
+
const out: Hold[] = [];
|
|
262
|
+
if (c.before) out.push(c.before);
|
|
263
|
+
if (c.any && !c.any.released) {
|
|
264
|
+
const free = c.any.finished.some((id) => !ctx.used.has(id));
|
|
265
|
+
const spent =
|
|
266
|
+
c.any.ids.length > 0 && c.any.finished.length === c.any.ids.length;
|
|
267
|
+
if (!free && !spent)
|
|
268
|
+
out.push({
|
|
269
|
+
code: "after_any",
|
|
270
|
+
text: `Waiting: after any running task (${c.any.label})`,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
if (c.auto) out.push(...([] as Hold[]).concat(c.auto));
|
|
274
|
+
out.push(...machineHolds(c, ctx.machine, ctx.now));
|
|
275
|
+
if (!c.planOnly && ctx.inFlight >= ctx.limit)
|
|
276
|
+
out.push({
|
|
277
|
+
code: "slot",
|
|
278
|
+
text: slotText(ctx.rank, ctx.of, c.priority),
|
|
279
|
+
});
|
|
280
|
+
return out;
|
|
281
|
+
}
|
package/lib/tasks/import.ts
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
validateBundle,
|
|
39
39
|
type TaskBundle,
|
|
40
40
|
} from "./move-bundle";
|
|
41
|
+
import { setPlan, setTask } from "../db/session-parts";
|
|
41
42
|
|
|
42
43
|
// Imports in flight in this process, by move id.
|
|
43
44
|
const g = globalThis as unknown as { __agentosImporting?: Set<string> };
|
|
@@ -47,7 +48,7 @@ const importing = (g.__agentosImporting ??= new Set());
|
|
|
47
48
|
export const arrived = (moveId: string) =>
|
|
48
49
|
db
|
|
49
50
|
.prepare(
|
|
50
|
-
`SELECT * FROM
|
|
51
|
+
`SELECT * FROM session_rows WHERE moved_from = ? AND host_id = 'local'
|
|
51
52
|
AND task_status IN ('running', 'moving', 'moved', 'merged', 'done')`
|
|
52
53
|
)
|
|
53
54
|
.get(moveId) as Session | undefined;
|
|
@@ -56,7 +57,7 @@ export const arrived = (moveId: string) =>
|
|
|
56
57
|
function refuseBusyBranch(projectId: string, branch: string): void {
|
|
57
58
|
const busy = db
|
|
58
59
|
.prepare(
|
|
59
|
-
`SELECT id FROM
|
|
60
|
+
`SELECT id FROM session_rows WHERE project_id = ? AND branch_name = ?
|
|
60
61
|
AND task_status IN ('running', 'moving') AND host_id = 'local'`
|
|
61
62
|
)
|
|
62
63
|
.get(projectId, branch);
|
|
@@ -118,9 +119,10 @@ async function arrive(
|
|
|
118
119
|
};
|
|
119
120
|
stepProgress(bundle.moveId, "conversation");
|
|
120
121
|
// The plan is never in git: it comes in the bundle.
|
|
121
|
-
|
|
122
|
+
const planFile = bundle.planWait?.file ?? orch?.planFile ?? null;
|
|
123
|
+
if (planFile != null) {
|
|
122
124
|
preparePlanDir(worktreePath);
|
|
123
|
-
fs.writeFileSync(path.join(worktreePath, PLAN_FILE),
|
|
125
|
+
fs.writeFileSync(path.join(worktreePath, PLAN_FILE), planFile);
|
|
124
126
|
}
|
|
125
127
|
if (claude && paths) {
|
|
126
128
|
await writeTranscript(
|
|
@@ -165,22 +167,29 @@ async function arrive(
|
|
|
165
167
|
.run(worktreePath, bundle.branch, baseBranch, null, id);
|
|
166
168
|
// A chat's worker reads its brief from the row; a terminal's is passed.
|
|
167
169
|
db.prepare(
|
|
168
|
-
`UPDATE sessions SET
|
|
169
|
-
|
|
170
|
-
|
|
170
|
+
`UPDATE sessions SET name_source = 'user', claude_session_id = ? WHERE id = ?`
|
|
171
|
+
).run(claude?.sessionId ?? null, id);
|
|
172
|
+
setTask(db, id, {
|
|
173
|
+
task_prompt: bundle.prompt,
|
|
174
|
+
task_brief: brief,
|
|
175
|
+
task_status: "running",
|
|
176
|
+
moved_from: bundle.moveId,
|
|
177
|
+
});
|
|
171
178
|
if (bundle.chat) unpackChat(id, bundle.chat, paths);
|
|
179
|
+
if (bundle.planWait) setPlan(db, id, { plan_phase: "awaiting" });
|
|
172
180
|
if (orch) {
|
|
173
|
-
db.
|
|
174
|
-
|
|
175
|
-
).run(orch.planPhase, orch.since, id);
|
|
181
|
+
setPlan(db, id, { plan_phase: orch.planPhase });
|
|
182
|
+
setTask(db, id, { task_since: orch.since });
|
|
176
183
|
// Held for the orchestrator's device. The orchestrator's own state
|
|
177
184
|
// (started_by, its gates) never comes from the bundle: it stays on its
|
|
178
185
|
// machine, and a task coming back takes it from the mirror's record.
|
|
179
186
|
if (holder) {
|
|
180
187
|
holdFor(id, holder, orch);
|
|
181
|
-
db.prepare(
|
|
182
|
-
|
|
183
|
-
|
|
188
|
+
db.prepare(`UPDATE sessions SET model_provider = ? WHERE id = ?`).run(
|
|
189
|
+
orch.modelProvider,
|
|
190
|
+
id
|
|
191
|
+
);
|
|
192
|
+
setTask(db, id, { model_overrides: orch.modelOverrides });
|
|
184
193
|
}
|
|
185
194
|
}
|
|
186
195
|
carryTaskState(bundle.moveId, id);
|
|
@@ -205,7 +214,10 @@ async function arrive(
|
|
|
205
214
|
}
|
|
206
215
|
const note = arrivalNote(bundle, worktreePath, os.hostname());
|
|
207
216
|
const prompt = claude ? note : `${bundle.prompt}\n\n${note}`;
|
|
208
|
-
if (bundle.
|
|
217
|
+
if (bundle.planWait) {
|
|
218
|
+
// Waiting on its plan's approval: nothing runs until the message that
|
|
219
|
+
// implements it, which resumes the carried conversation.
|
|
220
|
+
} else if (bundle.chat) {
|
|
209
221
|
// Its worker starts on the carried conversation, takes the note
|
|
210
222
|
// first, then what was queued.
|
|
211
223
|
await sendChat(id, {
|
package/lib/tasks/index.ts
CHANGED
|
@@ -59,12 +59,13 @@ import { finishTaskStart } from "./start";
|
|
|
59
59
|
import { isRemoteHost } from "../hosts";
|
|
60
60
|
import { DEFAULT_START_VIEW, type StartView } from "../sessions/launch";
|
|
61
61
|
import { viewOf } from "./move-targets";
|
|
62
|
-
import { isMirror, startRemoteTask } from "./
|
|
63
|
-
import {
|
|
62
|
+
import { isMirror, startRemoteTask } from "./contract/start";
|
|
63
|
+
import { mirrorViews } from "./contract/views";
|
|
64
64
|
import { chatStateNow } from "../chat/runner";
|
|
65
65
|
import { saidSinceLastMessage } from "../chat/store";
|
|
66
66
|
|
|
67
67
|
import { lastActiveOf } from "./last-active";
|
|
68
|
+
import { setPlan, setTask } from "../db/session-parts";
|
|
68
69
|
export * from "./state";
|
|
69
70
|
export { codeReviewGate, codeReviewRefusal } from "./code-review";
|
|
70
71
|
export { signOffTask, dropTask, signingOff, mergeSettled } from "./finish";
|
|
@@ -173,6 +174,9 @@ export async function createTask(opts: {
|
|
|
173
174
|
held?: { device: string; orchestrated: OrchestratedBundle };
|
|
174
175
|
// The paired device that sent this keyed start: the only one that may hold it later.
|
|
175
176
|
startedByDevice?: string | null;
|
|
177
|
+
// Another machine placed this plan here and decides what follows it: the
|
|
178
|
+
// task plans, then waits for that machine (implies planOnly).
|
|
179
|
+
planWaits?: boolean;
|
|
176
180
|
}): Promise<Session> {
|
|
177
181
|
if (opts.id) {
|
|
178
182
|
const existing = queries.getSession(db).get(opts.id) as Session | undefined;
|
|
@@ -244,12 +248,27 @@ async function startTask(
|
|
|
244
248
|
throw new Error(`${agentLabel(agent)} tasks run on this machine for now`);
|
|
245
249
|
}
|
|
246
250
|
if (isRemoteHost(hostId)) {
|
|
247
|
-
if (opts.planOnly)
|
|
248
|
-
throw new Error("A plan-only task runs on this machine for now");
|
|
249
251
|
if (opts.base || opts.cardId)
|
|
250
252
|
throw new Error(
|
|
251
253
|
"Stacked and card tasks run on this machine only for now"
|
|
252
254
|
);
|
|
255
|
+
if (opts.planOnly && opts.plan === false)
|
|
256
|
+
throw new Error(
|
|
257
|
+
"planOnly implies plan; it can't be combined with plan false"
|
|
258
|
+
);
|
|
259
|
+
if (opts.planOnly && view !== "chat")
|
|
260
|
+
throw new Error("A plan-only task needs a chat, not a terminal");
|
|
261
|
+
// The plan step is this machine's to run and decide: it plans there,
|
|
262
|
+
// stops, and what follows is chosen here (contract/plan.ts).
|
|
263
|
+
const planned =
|
|
264
|
+
!!(opts.plan || opts.planOnly) &&
|
|
265
|
+
taskRouting(
|
|
266
|
+
project.workspace_id,
|
|
267
|
+
view,
|
|
268
|
+
opts.model,
|
|
269
|
+
opts.plan,
|
|
270
|
+
opts.planOnly
|
|
271
|
+
).plan;
|
|
253
272
|
const session = await startRemoteTask(hostId, project, {
|
|
254
273
|
id,
|
|
255
274
|
prompt,
|
|
@@ -258,8 +277,11 @@ async function startTask(
|
|
|
258
277
|
baseBranch: opts.baseBranch,
|
|
259
278
|
view,
|
|
260
279
|
startedBy: opts.startedBy,
|
|
280
|
+
plan: planned
|
|
281
|
+
? { only: !!opts.planOnly, reason: opts.planReason?.trim() || null }
|
|
282
|
+
: undefined,
|
|
261
283
|
});
|
|
262
|
-
// The mirror's started_by stays orchestrator-only: a held
|
|
284
|
+
// The mirror's started_by stays orchestrator-only: a held task on a linked machine is
|
|
263
285
|
// relayed, stopped and answered through it. A plain calling task is its
|
|
264
286
|
// parent and started_via only.
|
|
265
287
|
recordStart(session.id, {
|
|
@@ -345,29 +367,41 @@ async function startTask(
|
|
|
345
367
|
.run(wt.worktreePath, wt.branchName, baseBranch, null, id);
|
|
346
368
|
// Everything the launch needs is on the row, so a restart can resume it.
|
|
347
369
|
db.prepare(
|
|
348
|
-
`UPDATE sessions SET
|
|
349
|
-
model_provider = ?, model_overrides = ?, plan_phase = ?, plan_only = ?,
|
|
350
|
-
plan_decided = ?, plan_reason = ? WHERE id = ?`
|
|
370
|
+
`UPDATE sessions SET setup_status = 'running', name_source = ?, view = ?, model_provider = ? WHERE id = ?`
|
|
351
371
|
).run(
|
|
352
|
-
prompt,
|
|
353
|
-
buildTaskBrief({
|
|
354
|
-
branch: wt.branchName,
|
|
355
|
-
baseBranch,
|
|
356
|
-
stack: opts.base?.stack,
|
|
357
|
-
test: declaredTest(wt.worktreePath),
|
|
358
|
-
}),
|
|
359
372
|
naming.source,
|
|
360
373
|
view,
|
|
361
374
|
routing.first.provider.id === ANTHROPIC
|
|
362
375
|
? null
|
|
363
376
|
: routing.first.provider.id,
|
|
364
|
-
routing.override ? JSON.stringify(routing.override) : null,
|
|
365
|
-
routing.plan ? "planning" : null,
|
|
366
|
-
routing.planOnly ? 1 : null,
|
|
367
|
-
routing.planOnly ? 1 : opts.plan === undefined ? null : opts.plan ? 1 : 0,
|
|
368
|
-
opts.planReason?.trim() || null,
|
|
369
377
|
id
|
|
370
378
|
);
|
|
379
|
+
setTask(db, id, {
|
|
380
|
+
task_prompt: prompt,
|
|
381
|
+
task_brief: buildTaskBrief({
|
|
382
|
+
branch: wt.branchName,
|
|
383
|
+
baseBranch,
|
|
384
|
+
stack: opts.base?.stack,
|
|
385
|
+
test: declaredTest(wt.worktreePath),
|
|
386
|
+
}),
|
|
387
|
+
task_status: "running",
|
|
388
|
+
model_overrides: routing.override
|
|
389
|
+
? JSON.stringify(routing.override)
|
|
390
|
+
: null,
|
|
391
|
+
});
|
|
392
|
+
setPlan(db, id, {
|
|
393
|
+
plan_phase: routing.plan ? "planning" : null,
|
|
394
|
+
plan_only: routing.planOnly ? 1 : null,
|
|
395
|
+
plan_decided: routing.planOnly
|
|
396
|
+
? 1
|
|
397
|
+
: opts.plan === undefined
|
|
398
|
+
? null
|
|
399
|
+
: opts.plan
|
|
400
|
+
? 1
|
|
401
|
+
: 0,
|
|
402
|
+
plan_reason: opts.planReason?.trim() || null,
|
|
403
|
+
plan_waits_for_caller: routing.planOnly && opts.planWaits ? 1 : null,
|
|
404
|
+
});
|
|
371
405
|
recordStart(id, {
|
|
372
406
|
startedBy: opts.startedBy ?? null,
|
|
373
407
|
parentSessionId: opts.parentSessionId ?? null,
|
|
@@ -510,7 +544,7 @@ export async function taskView(session: Session): Promise<TaskView> {
|
|
|
510
544
|
export function movedTasks(): { id: string; movedTo: string | null }[] {
|
|
511
545
|
return db
|
|
512
546
|
.prepare(
|
|
513
|
-
`SELECT id, moved_to AS movedTo FROM
|
|
547
|
+
`SELECT id, moved_to AS movedTo FROM session_rows
|
|
514
548
|
WHERE task_status = 'moved' AND host_id = 'local'
|
|
515
549
|
ORDER BY updated_at DESC LIMIT 500`
|
|
516
550
|
)
|
|
@@ -522,7 +556,7 @@ export async function listTasks(): Promise<TaskView[]> {
|
|
|
522
556
|
const sessions = taskSessions(false);
|
|
523
557
|
const [local, remote] = await Promise.all([
|
|
524
558
|
Promise.all(sessions.filter((s) => !isMirror(s)).map(taskView)),
|
|
525
|
-
|
|
559
|
+
mirrorViews(sessions.filter(isMirror)),
|
|
526
560
|
]);
|
|
527
561
|
const costs = taskCosts(local.map((t) => t.id));
|
|
528
562
|
for (const t of local) {
|
package/lib/tasks/launch-gate.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { db } from "../db";
|
|
|
9
9
|
export function launchPending(sessionId: string): boolean {
|
|
10
10
|
const row = db
|
|
11
11
|
.prepare(
|
|
12
|
-
`SELECT 1 FROM
|
|
12
|
+
`SELECT 1 FROM session_rows WHERE id = ? AND task_status = 'running'
|
|
13
13
|
AND setup_status IN ('running', 'held')`
|
|
14
14
|
)
|
|
15
15
|
.get(sessionId);
|
package/lib/tasks/move-bundle.ts
CHANGED
|
@@ -57,6 +57,10 @@ export interface TaskBundle {
|
|
|
57
57
|
// What a task started by an orchestrator carries besides its work; absent
|
|
58
58
|
// for any other task, or from a machine that predates these moves.
|
|
59
59
|
orchestrated?: OrchestratedBundle;
|
|
60
|
+
// A plan written and waiting for the machine that placed it (it is moving
|
|
61
|
+
// there to be built): the plan file, which is never in git. The arriving
|
|
62
|
+
// machine runs no turn; the approval that follows starts one.
|
|
63
|
+
planWait?: { file: string | null };
|
|
60
64
|
}
|
|
61
65
|
|
|
62
66
|
export interface OrchestratedBundle {
|
|
@@ -70,7 +74,7 @@ export interface OrchestratedBundle {
|
|
|
70
74
|
// machine without that provider refuses the task rather than change it.
|
|
71
75
|
modelProvider: string | null;
|
|
72
76
|
modelOverrides: string | null;
|
|
73
|
-
planPhase: "planning" | "implementing" | null;
|
|
77
|
+
planPhase: "planning" | "awaiting" | "implementing" | null;
|
|
74
78
|
// .agentos/plan.md, which is never in git.
|
|
75
79
|
planFile: string | null;
|
|
76
80
|
// The orchestrator that holds the task (its id and name on the machine
|
|
@@ -153,7 +157,7 @@ function validateChat(c: ChatBundle): ChatBundle {
|
|
|
153
157
|
return { ...c, plan: !!c.plan };
|
|
154
158
|
}
|
|
155
159
|
|
|
156
|
-
const PHASES = ["planning", "implementing"];
|
|
160
|
+
const PHASES = ["planning", "awaiting", "implementing"];
|
|
157
161
|
|
|
158
162
|
export function validateOrchestrated(
|
|
159
163
|
o: OrchestratedBundle
|
|
@@ -196,6 +200,16 @@ export function validateOrchestrated(
|
|
|
196
200
|
};
|
|
197
201
|
}
|
|
198
202
|
|
|
203
|
+
function validatePlanWait(w: unknown): TaskBundle["planWait"] {
|
|
204
|
+
if (w === undefined || w === null) return undefined;
|
|
205
|
+
const file = (w as { file?: unknown }).file;
|
|
206
|
+
if (typeof w !== "object" || !(file === null || isStr(file)))
|
|
207
|
+
throw new Error("Bad plan");
|
|
208
|
+
if ((file?.length ?? 0) > MAX_PLAN_BYTES)
|
|
209
|
+
throw new Error("The plan is too large to move");
|
|
210
|
+
return { file };
|
|
211
|
+
}
|
|
212
|
+
|
|
199
213
|
export function validateBundle(b: TaskBundle): TaskBundle {
|
|
200
214
|
if (!b || typeof b !== "object") throw new Error("No task to import");
|
|
201
215
|
if (!isSessionId(b.moveId)) throw new Error("Bad move id");
|
|
@@ -228,6 +242,7 @@ export function validateBundle(b: TaskBundle): TaskBundle {
|
|
|
228
242
|
orchestrated: b.orchestrated
|
|
229
243
|
? validateOrchestrated(b.orchestrated)
|
|
230
244
|
: undefined,
|
|
245
|
+
planWait: validatePlanWait(b.planWait),
|
|
231
246
|
chat: b.chat ? validateChat(b.chat) : undefined,
|
|
232
247
|
name: b.name.slice(0, 200),
|
|
233
248
|
from: String(b.from || "another machine")
|
package/lib/tasks/move-carry.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import type Database from "better-sqlite3";
|
|
11
11
|
import { db } from "../db";
|
|
12
12
|
import { orchestratorOf } from "./starter";
|
|
13
|
+
import { fillMissing } from "../db/session-parts";
|
|
13
14
|
|
|
14
15
|
// table, column, and how the id sits in it: the whole value, `task:<id>`, or
|
|
15
16
|
// the tail of a `<condition>:<id>` key.
|
|
@@ -100,15 +101,22 @@ export function carryTaskState(
|
|
|
100
101
|
.run(fromId.length, toId, `%:${fromId}`);
|
|
101
102
|
}
|
|
102
103
|
const from = handle
|
|
103
|
-
.prepare(`SELECT * FROM
|
|
104
|
+
.prepare(`SELECT * FROM session_rows WHERE id = ?`)
|
|
104
105
|
.get(fromId) as Record<string, unknown> | undefined;
|
|
105
106
|
if (!from) return;
|
|
106
|
-
const
|
|
107
|
-
.map((c) => `${c} = COALESCE(${c}, @${c})`)
|
|
108
|
-
.join(", ");
|
|
109
|
-
handle.prepare(`UPDATE sessions SET ${sets} WHERE id = @id`).run({
|
|
107
|
+
const carried = {
|
|
110
108
|
...orchestrationOf(from),
|
|
111
109
|
task_since: (from.task_since as string | null) ?? from.created_at ?? null,
|
|
112
|
-
|
|
113
|
-
|
|
110
|
+
} as Record<string, string | number | null>;
|
|
111
|
+
handle
|
|
112
|
+
.prepare(
|
|
113
|
+
`UPDATE sessions SET started_by = COALESCE(started_by, @started_by),
|
|
114
|
+
model_provider = COALESCE(model_provider, @model_provider) WHERE id = @id`
|
|
115
|
+
)
|
|
116
|
+
.run({
|
|
117
|
+
started_by: carried.started_by,
|
|
118
|
+
model_provider: carried.model_provider,
|
|
119
|
+
id: toId,
|
|
120
|
+
});
|
|
121
|
+
fillMissing(handle, toId, carried);
|
|
114
122
|
}
|