@femtomc/mu-server 26.2.90 → 26.2.91
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/README.md +23 -10
- package/dist/api/control_plane.js +64 -5
- package/dist/config.d.ts +3 -3
- package/dist/config.js +20 -15
- package/dist/control_plane.d.ts +20 -5
- package/dist/control_plane.js +303 -245
- package/dist/control_plane_adapter_registry.d.ts +1 -0
- package/dist/control_plane_adapter_registry.js +2 -0
- package/dist/control_plane_bootstrap_helpers.js +0 -1
- package/dist/control_plane_contract.d.ts +0 -35
- package/dist/control_plane_contract.js +1 -1
- package/dist/control_plane_telegram_generation.js +1 -0
- package/dist/control_plane_wake_delivery.d.ts +2 -1
- package/dist/control_plane_wake_delivery.js +3 -1
- package/dist/index.d.ts +1 -4
- package/dist/index.js +0 -2
- package/dist/server.js +2 -41
- package/dist/{server_program_orchestration.d.ts → server_program_coordination.d.ts} +1 -1
- package/dist/{server_program_orchestration.js → server_program_coordination.js} +1 -1
- package/package.json +4 -4
- package/dist/api/runs.d.ts +0 -2
- package/dist/api/runs.js +0 -124
- package/dist/control_plane_run_outbox.d.ts +0 -7
- package/dist/control_plane_run_outbox.js +0 -52
- package/dist/control_plane_run_queue_coordinator.d.ts +0 -42
- package/dist/control_plane_run_queue_coordinator.js +0 -266
- package/dist/orchestration_queue.d.ts +0 -44
- package/dist/orchestration_queue.js +0 -111
- package/dist/run_queue.d.ts +0 -95
- package/dist/run_queue.js +0 -816
- package/dist/run_supervisor.d.ts +0 -108
- package/dist/run_supervisor.js +0 -460
package/dist/run_supervisor.d.ts
DELETED
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import type { CommandRecord } from "@femtomc/mu-control-plane";
|
|
2
|
-
export type ControlPlaneRunMode = "run_start" | "run_resume";
|
|
3
|
-
export type ControlPlaneRunStatus = "running" | "completed" | "failed" | "cancelled";
|
|
4
|
-
export type ControlPlaneRunSnapshot = {
|
|
5
|
-
job_id: string;
|
|
6
|
-
mode: ControlPlaneRunMode;
|
|
7
|
-
status: ControlPlaneRunStatus;
|
|
8
|
-
prompt: string | null;
|
|
9
|
-
root_issue_id: string | null;
|
|
10
|
-
max_steps: number;
|
|
11
|
-
command_id: string | null;
|
|
12
|
-
source: "command" | "api";
|
|
13
|
-
started_at_ms: number;
|
|
14
|
-
updated_at_ms: number;
|
|
15
|
-
finished_at_ms: number | null;
|
|
16
|
-
exit_code: number | null;
|
|
17
|
-
pid: number | null;
|
|
18
|
-
last_progress: string | null;
|
|
19
|
-
queue_id?: string;
|
|
20
|
-
queue_state?: "queued" | "active" | "waiting_review" | "refining" | "done" | "failed" | "cancelled";
|
|
21
|
-
};
|
|
22
|
-
export type ControlPlaneRunTrace = {
|
|
23
|
-
run: ControlPlaneRunSnapshot;
|
|
24
|
-
stdout: string[];
|
|
25
|
-
stderr: string[];
|
|
26
|
-
log_hints: string[];
|
|
27
|
-
trace_files: string[];
|
|
28
|
-
};
|
|
29
|
-
export type ControlPlaneRunInterruptResult = {
|
|
30
|
-
ok: boolean;
|
|
31
|
-
reason: "not_found" | "not_running" | "missing_target" | null;
|
|
32
|
-
run: ControlPlaneRunSnapshot | null;
|
|
33
|
-
};
|
|
34
|
-
export type ControlPlaneRunEventKind = "run_started" | "run_root_discovered" | "run_progress" | "run_interrupt_requested" | "run_completed" | "run_failed" | "run_cancelled";
|
|
35
|
-
export type ControlPlaneRunEvent = {
|
|
36
|
-
seq: number;
|
|
37
|
-
ts_ms: number;
|
|
38
|
-
kind: ControlPlaneRunEventKind;
|
|
39
|
-
message: string;
|
|
40
|
-
run: ControlPlaneRunSnapshot;
|
|
41
|
-
command: CommandRecord | null;
|
|
42
|
-
};
|
|
43
|
-
export type ControlPlaneRunProcess = {
|
|
44
|
-
pid: number;
|
|
45
|
-
stdout: ReadableStream<Uint8Array> | null;
|
|
46
|
-
stderr: ReadableStream<Uint8Array> | null;
|
|
47
|
-
exited: Promise<number>;
|
|
48
|
-
kill(signal?: number | string): void;
|
|
49
|
-
};
|
|
50
|
-
export type ControlPlaneRunSupervisorOpts = {
|
|
51
|
-
repoRoot: string;
|
|
52
|
-
nowMs?: () => number;
|
|
53
|
-
spawnProcess?: (opts: {
|
|
54
|
-
argv: string[];
|
|
55
|
-
cwd: string;
|
|
56
|
-
}) => ControlPlaneRunProcess;
|
|
57
|
-
maxStoredLines?: number;
|
|
58
|
-
maxHistory?: number;
|
|
59
|
-
onEvent?: (event: ControlPlaneRunEvent) => void | Promise<void>;
|
|
60
|
-
};
|
|
61
|
-
/**
|
|
62
|
-
* Process execution boundary for orchestration runs.
|
|
63
|
-
*
|
|
64
|
-
* Contract with the durable queue/reconcile layer:
|
|
65
|
-
* - this supervisor executes already-selected work; it does not decide inter-root scheduling policy
|
|
66
|
-
* - sequential/parallel root policy is enforced by queue leasing before launch
|
|
67
|
-
* - queue-first launch is the supported execution path
|
|
68
|
-
*/
|
|
69
|
-
export declare class ControlPlaneRunSupervisor {
|
|
70
|
-
#private;
|
|
71
|
-
constructor(opts: ControlPlaneRunSupervisorOpts);
|
|
72
|
-
/**
|
|
73
|
-
* Queue-launch entrypoint for run-start intent.
|
|
74
|
-
* Expected call path: enqueue/activate first, then invoke after lease acquisition.
|
|
75
|
-
*/
|
|
76
|
-
launchStart(opts: {
|
|
77
|
-
prompt: string;
|
|
78
|
-
maxSteps?: number;
|
|
79
|
-
command?: CommandRecord | null;
|
|
80
|
-
commandId?: string | null;
|
|
81
|
-
source?: "command" | "api";
|
|
82
|
-
}): Promise<ControlPlaneRunSnapshot>;
|
|
83
|
-
/**
|
|
84
|
-
* Queue-launch entrypoint for run-resume intent.
|
|
85
|
-
* Queue-first reconcile remains the canonical execution path.
|
|
86
|
-
*/
|
|
87
|
-
launchResume(opts: {
|
|
88
|
-
rootIssueId: string;
|
|
89
|
-
maxSteps?: number;
|
|
90
|
-
command?: CommandRecord | null;
|
|
91
|
-
commandId?: string | null;
|
|
92
|
-
source?: "command" | "api";
|
|
93
|
-
}): Promise<ControlPlaneRunSnapshot>;
|
|
94
|
-
list(opts?: {
|
|
95
|
-
status?: ControlPlaneRunStatus;
|
|
96
|
-
limit?: number;
|
|
97
|
-
}): ControlPlaneRunSnapshot[];
|
|
98
|
-
get(idOrRoot: string): ControlPlaneRunSnapshot | null;
|
|
99
|
-
trace(idOrRoot: string, opts?: {
|
|
100
|
-
limit?: number;
|
|
101
|
-
}): Promise<ControlPlaneRunTrace | null>;
|
|
102
|
-
interrupt(opts: {
|
|
103
|
-
jobId?: string | null;
|
|
104
|
-
rootIssueId?: string | null;
|
|
105
|
-
}): ControlPlaneRunInterruptResult;
|
|
106
|
-
startFromCommand(command: CommandRecord): Promise<ControlPlaneRunSnapshot | null>;
|
|
107
|
-
stop(): Promise<void>;
|
|
108
|
-
}
|
package/dist/run_supervisor.js
DELETED
|
@@ -1,460 +0,0 @@
|
|
|
1
|
-
import { readdir } from "node:fs/promises";
|
|
2
|
-
import { join, relative } from "node:path";
|
|
3
|
-
import { getStorePaths } from "@femtomc/mu-core/node";
|
|
4
|
-
const DEFAULT_MAX_STEPS = 20;
|
|
5
|
-
const ROOT_RE = /\bRoot:\s*(mu-[a-z0-9][a-z0-9-]*)\b/i;
|
|
6
|
-
const STEP_RE = /^(Step|Done)\s+\d+\/\d+\s+/;
|
|
7
|
-
const LOG_HINT_RE = /\blogs:\s+(\S+)/i;
|
|
8
|
-
function defaultNowMs() {
|
|
9
|
-
return Date.now();
|
|
10
|
-
}
|
|
11
|
-
function defaultSpawnProcess(opts) {
|
|
12
|
-
const proc = Bun.spawn({
|
|
13
|
-
cmd: opts.argv,
|
|
14
|
-
cwd: opts.cwd,
|
|
15
|
-
stdin: "ignore",
|
|
16
|
-
stdout: "pipe",
|
|
17
|
-
stderr: "pipe",
|
|
18
|
-
env: Bun.env,
|
|
19
|
-
});
|
|
20
|
-
return {
|
|
21
|
-
pid: proc.pid,
|
|
22
|
-
stdout: proc.stdout,
|
|
23
|
-
stderr: proc.stderr,
|
|
24
|
-
exited: proc.exited,
|
|
25
|
-
kill(signal) {
|
|
26
|
-
proc.kill(signal);
|
|
27
|
-
},
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
function toPositiveInt(value, fallback) {
|
|
31
|
-
if (typeof value === "number" && Number.isFinite(value)) {
|
|
32
|
-
return Math.max(1, Math.trunc(value));
|
|
33
|
-
}
|
|
34
|
-
if (typeof value === "string" && /^\d+$/.test(value.trim())) {
|
|
35
|
-
return Math.max(1, Number.parseInt(value, 10));
|
|
36
|
-
}
|
|
37
|
-
return fallback;
|
|
38
|
-
}
|
|
39
|
-
function normalizeIssueId(value) {
|
|
40
|
-
if (!value)
|
|
41
|
-
return null;
|
|
42
|
-
const trimmed = value.trim();
|
|
43
|
-
if (!/^mu-[a-z0-9][a-z0-9-]*$/i.test(trimmed)) {
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
return trimmed.toLowerCase();
|
|
47
|
-
}
|
|
48
|
-
function pushBounded(lines, line, maxLines) {
|
|
49
|
-
lines.push(line);
|
|
50
|
-
if (lines.length <= maxLines) {
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
lines.splice(0, lines.length - maxLines);
|
|
54
|
-
}
|
|
55
|
-
async function consumeStreamLines(stream, onLine) {
|
|
56
|
-
if (!stream) {
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
const reader = stream.getReader();
|
|
60
|
-
const decoder = new TextDecoder();
|
|
61
|
-
let pending = "";
|
|
62
|
-
try {
|
|
63
|
-
while (true) {
|
|
64
|
-
const { done, value } = await reader.read();
|
|
65
|
-
if (done) {
|
|
66
|
-
break;
|
|
67
|
-
}
|
|
68
|
-
pending += decoder.decode(value, { stream: true });
|
|
69
|
-
while (true) {
|
|
70
|
-
const newline = pending.indexOf("\n");
|
|
71
|
-
if (newline < 0) {
|
|
72
|
-
break;
|
|
73
|
-
}
|
|
74
|
-
const line = pending.slice(0, newline).replace(/\r$/, "");
|
|
75
|
-
pending = pending.slice(newline + 1);
|
|
76
|
-
onLine(line);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
pending += decoder.decode();
|
|
80
|
-
const finalLine = pending.replace(/\r$/, "").trimEnd();
|
|
81
|
-
if (finalLine.length > 0) {
|
|
82
|
-
onLine(finalLine);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
finally {
|
|
86
|
-
reader.releaseLock();
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
function describeRun(snapshot) {
|
|
90
|
-
const root = snapshot.root_issue_id ?? snapshot.job_id;
|
|
91
|
-
return `${snapshot.mode} ${root}`;
|
|
92
|
-
}
|
|
93
|
-
/**
|
|
94
|
-
* Process execution boundary for orchestration runs.
|
|
95
|
-
*
|
|
96
|
-
* Contract with the durable queue/reconcile layer:
|
|
97
|
-
* - this supervisor executes already-selected work; it does not decide inter-root scheduling policy
|
|
98
|
-
* - sequential/parallel root policy is enforced by queue leasing before launch
|
|
99
|
-
* - queue-first launch is the supported execution path
|
|
100
|
-
*/
|
|
101
|
-
export class ControlPlaneRunSupervisor {
|
|
102
|
-
#repoRoot;
|
|
103
|
-
#nowMs;
|
|
104
|
-
#spawnProcess;
|
|
105
|
-
#maxStoredLines;
|
|
106
|
-
#maxHistory;
|
|
107
|
-
#onEvent;
|
|
108
|
-
#jobsById = new Map();
|
|
109
|
-
#jobIdByRootIssueId = new Map();
|
|
110
|
-
#seq = 0;
|
|
111
|
-
#jobCounter = 0;
|
|
112
|
-
constructor(opts) {
|
|
113
|
-
this.#repoRoot = opts.repoRoot;
|
|
114
|
-
this.#nowMs = opts.nowMs ?? defaultNowMs;
|
|
115
|
-
this.#spawnProcess = opts.spawnProcess ?? defaultSpawnProcess;
|
|
116
|
-
this.#maxStoredLines = Math.max(50, Math.trunc(opts.maxStoredLines ?? 1_000));
|
|
117
|
-
this.#maxHistory = Math.max(20, Math.trunc(opts.maxHistory ?? 200));
|
|
118
|
-
this.#onEvent = opts.onEvent ?? null;
|
|
119
|
-
}
|
|
120
|
-
#nextJobId() {
|
|
121
|
-
this.#jobCounter += 1;
|
|
122
|
-
return `run-job-${this.#jobCounter.toString(36)}-${crypto.randomUUID().slice(0, 8)}`;
|
|
123
|
-
}
|
|
124
|
-
#snapshot(job) {
|
|
125
|
-
return { ...job.snapshot };
|
|
126
|
-
}
|
|
127
|
-
#allJobsSorted() {
|
|
128
|
-
return [...this.#jobsById.values()].sort((a, b) => {
|
|
129
|
-
if (a.snapshot.started_at_ms !== b.snapshot.started_at_ms) {
|
|
130
|
-
return b.snapshot.started_at_ms - a.snapshot.started_at_ms;
|
|
131
|
-
}
|
|
132
|
-
return a.snapshot.job_id.localeCompare(b.snapshot.job_id);
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
#pruneHistory() {
|
|
136
|
-
const jobs = this.#allJobsSorted();
|
|
137
|
-
let kept = 0;
|
|
138
|
-
for (const job of jobs) {
|
|
139
|
-
if (job.snapshot.status === "running") {
|
|
140
|
-
kept += 1;
|
|
141
|
-
continue;
|
|
142
|
-
}
|
|
143
|
-
kept += 1;
|
|
144
|
-
if (kept <= this.#maxHistory) {
|
|
145
|
-
continue;
|
|
146
|
-
}
|
|
147
|
-
this.#jobsById.delete(job.snapshot.job_id);
|
|
148
|
-
if (job.snapshot.root_issue_id) {
|
|
149
|
-
const current = this.#jobIdByRootIssueId.get(job.snapshot.root_issue_id);
|
|
150
|
-
if (current === job.snapshot.job_id) {
|
|
151
|
-
this.#jobIdByRootIssueId.delete(job.snapshot.root_issue_id);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
#emit(kind, job, message) {
|
|
157
|
-
if (!this.#onEvent) {
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
const event = {
|
|
161
|
-
seq: ++this.#seq,
|
|
162
|
-
ts_ms: Math.trunc(this.#nowMs()),
|
|
163
|
-
kind,
|
|
164
|
-
message,
|
|
165
|
-
run: this.#snapshot(job),
|
|
166
|
-
command: job.command,
|
|
167
|
-
};
|
|
168
|
-
void Promise.resolve(this.#onEvent(event)).catch(() => {
|
|
169
|
-
// Swallow notifier errors to avoid destabilizing run supervision.
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
#touch(job) {
|
|
173
|
-
job.snapshot.updated_at_ms = Math.trunc(this.#nowMs());
|
|
174
|
-
}
|
|
175
|
-
#markRootIssueId(job, rootIssueId) {
|
|
176
|
-
const normalized = normalizeIssueId(rootIssueId);
|
|
177
|
-
if (!normalized) {
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
if (job.snapshot.root_issue_id === normalized) {
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
job.snapshot.root_issue_id = normalized;
|
|
184
|
-
this.#jobIdByRootIssueId.set(normalized, job.snapshot.job_id);
|
|
185
|
-
this.#touch(job);
|
|
186
|
-
this.#emit("run_root_discovered", job, `🧩 Run root identified: ${normalized}`);
|
|
187
|
-
}
|
|
188
|
-
#handleLine(job, stream, line) {
|
|
189
|
-
if (stream === "stdout") {
|
|
190
|
-
pushBounded(job.stdout_lines, line, this.#maxStoredLines);
|
|
191
|
-
}
|
|
192
|
-
else {
|
|
193
|
-
pushBounded(job.stderr_lines, line, this.#maxStoredLines);
|
|
194
|
-
}
|
|
195
|
-
const rootMatch = ROOT_RE.exec(line);
|
|
196
|
-
if (rootMatch?.[1]) {
|
|
197
|
-
this.#markRootIssueId(job, rootMatch[1]);
|
|
198
|
-
}
|
|
199
|
-
const logHintMatch = LOG_HINT_RE.exec(line);
|
|
200
|
-
if (logHintMatch?.[1]) {
|
|
201
|
-
job.log_hints.add(logHintMatch[1]);
|
|
202
|
-
this.#touch(job);
|
|
203
|
-
}
|
|
204
|
-
if (STEP_RE.test(line)) {
|
|
205
|
-
job.snapshot.last_progress = line.trim();
|
|
206
|
-
this.#touch(job);
|
|
207
|
-
this.#emit("run_progress", job, `📈 ${line.trim()}`);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
/**
|
|
211
|
-
* Executes one queue-activated run job.
|
|
212
|
-
*
|
|
213
|
-
* Queue contract expectation: caller has already persisted/activated the queue item and associated
|
|
214
|
-
* this launch with exactly one root slot under the active inter-root policy.
|
|
215
|
-
*/
|
|
216
|
-
async #launch(opts) {
|
|
217
|
-
const nowMs = Math.trunc(this.#nowMs());
|
|
218
|
-
const process = this.#spawnProcess({ argv: opts.argv, cwd: this.#repoRoot });
|
|
219
|
-
const snapshot = {
|
|
220
|
-
job_id: this.#nextJobId(),
|
|
221
|
-
mode: opts.mode,
|
|
222
|
-
status: "running",
|
|
223
|
-
prompt: opts.prompt,
|
|
224
|
-
root_issue_id: opts.rootIssueId,
|
|
225
|
-
max_steps: opts.maxSteps,
|
|
226
|
-
command_id: opts.command?.command_id ?? opts.commandId ?? null,
|
|
227
|
-
source: opts.source,
|
|
228
|
-
started_at_ms: nowMs,
|
|
229
|
-
updated_at_ms: nowMs,
|
|
230
|
-
finished_at_ms: null,
|
|
231
|
-
exit_code: null,
|
|
232
|
-
pid: process.pid,
|
|
233
|
-
last_progress: null,
|
|
234
|
-
};
|
|
235
|
-
const job = {
|
|
236
|
-
snapshot,
|
|
237
|
-
command: opts.command,
|
|
238
|
-
process,
|
|
239
|
-
stdout_lines: [],
|
|
240
|
-
stderr_lines: [],
|
|
241
|
-
log_hints: new Set(),
|
|
242
|
-
interrupt_requested: false,
|
|
243
|
-
hard_kill_timer: null,
|
|
244
|
-
};
|
|
245
|
-
this.#jobsById.set(snapshot.job_id, job);
|
|
246
|
-
if (snapshot.root_issue_id) {
|
|
247
|
-
this.#jobIdByRootIssueId.set(snapshot.root_issue_id, snapshot.job_id);
|
|
248
|
-
}
|
|
249
|
-
this.#emit("run_started", job, `🚀 Started ${describeRun(snapshot)} (job ${snapshot.job_id}, pid ${snapshot.pid ?? "?"})`);
|
|
250
|
-
void (async () => {
|
|
251
|
-
const stdoutTask = consumeStreamLines(process.stdout, (line) => this.#handleLine(job, "stdout", line));
|
|
252
|
-
const stderrTask = consumeStreamLines(process.stderr, (line) => this.#handleLine(job, "stderr", line));
|
|
253
|
-
const exitCode = await process.exited.catch(() => -1);
|
|
254
|
-
await Promise.allSettled([stdoutTask, stderrTask]);
|
|
255
|
-
if (job.hard_kill_timer) {
|
|
256
|
-
clearTimeout(job.hard_kill_timer);
|
|
257
|
-
job.hard_kill_timer = null;
|
|
258
|
-
}
|
|
259
|
-
job.snapshot.exit_code = exitCode;
|
|
260
|
-
job.snapshot.finished_at_ms = Math.trunc(this.#nowMs());
|
|
261
|
-
job.snapshot.updated_at_ms = job.snapshot.finished_at_ms;
|
|
262
|
-
if (job.interrupt_requested) {
|
|
263
|
-
job.snapshot.status = "cancelled";
|
|
264
|
-
this.#emit("run_cancelled", job, `🛑 ${describeRun(job.snapshot)} interrupted (exit ${exitCode}).`);
|
|
265
|
-
}
|
|
266
|
-
else if (exitCode === 0) {
|
|
267
|
-
job.snapshot.status = "completed";
|
|
268
|
-
this.#emit("run_completed", job, `✅ ${describeRun(job.snapshot)} completed successfully.`);
|
|
269
|
-
}
|
|
270
|
-
else {
|
|
271
|
-
job.snapshot.status = "failed";
|
|
272
|
-
this.#emit("run_failed", job, `❌ ${describeRun(job.snapshot)} failed (exit ${exitCode}).`);
|
|
273
|
-
}
|
|
274
|
-
this.#pruneHistory();
|
|
275
|
-
})();
|
|
276
|
-
return this.#snapshot(job);
|
|
277
|
-
}
|
|
278
|
-
/**
|
|
279
|
-
* Queue-launch entrypoint for run-start intent.
|
|
280
|
-
* Expected call path: enqueue/activate first, then invoke after lease acquisition.
|
|
281
|
-
*/
|
|
282
|
-
async launchStart(opts) {
|
|
283
|
-
const prompt = opts.prompt.trim();
|
|
284
|
-
if (prompt.length === 0) {
|
|
285
|
-
throw new Error("run_start_prompt_required");
|
|
286
|
-
}
|
|
287
|
-
const maxSteps = toPositiveInt(opts.maxSteps, DEFAULT_MAX_STEPS);
|
|
288
|
-
const argv = ["mu", "_run-direct", prompt, "--max-steps", String(maxSteps), "--raw-stream"];
|
|
289
|
-
return await this.#launch({
|
|
290
|
-
mode: "run_start",
|
|
291
|
-
prompt,
|
|
292
|
-
rootIssueId: null,
|
|
293
|
-
maxSteps,
|
|
294
|
-
argv,
|
|
295
|
-
command: opts.command ?? null,
|
|
296
|
-
commandId: opts.commandId ?? null,
|
|
297
|
-
source: opts.source ?? "api",
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
/**
|
|
301
|
-
* Queue-launch entrypoint for run-resume intent.
|
|
302
|
-
* Queue-first reconcile remains the canonical execution path.
|
|
303
|
-
*/
|
|
304
|
-
async launchResume(opts) {
|
|
305
|
-
const rootIssueId = normalizeIssueId(opts.rootIssueId);
|
|
306
|
-
if (!rootIssueId) {
|
|
307
|
-
throw new Error("run_resume_invalid_root_issue_id");
|
|
308
|
-
}
|
|
309
|
-
const maxSteps = toPositiveInt(opts.maxSteps, DEFAULT_MAX_STEPS);
|
|
310
|
-
const argv = ["mu", "resume", rootIssueId, "--max-steps", String(maxSteps), "--raw-stream"];
|
|
311
|
-
return await this.#launch({
|
|
312
|
-
mode: "run_resume",
|
|
313
|
-
prompt: null,
|
|
314
|
-
rootIssueId,
|
|
315
|
-
maxSteps,
|
|
316
|
-
argv,
|
|
317
|
-
command: opts.command ?? null,
|
|
318
|
-
commandId: opts.commandId ?? null,
|
|
319
|
-
source: opts.source ?? "api",
|
|
320
|
-
});
|
|
321
|
-
}
|
|
322
|
-
list(opts = {}) {
|
|
323
|
-
const limit = Math.max(1, Math.min(500, Math.trunc(opts.limit ?? 100)));
|
|
324
|
-
const filtered = this.#allJobsSorted().filter((job) => opts.status ? job.snapshot.status === opts.status : true);
|
|
325
|
-
return filtered.slice(0, limit).map((job) => this.#snapshot(job));
|
|
326
|
-
}
|
|
327
|
-
#resolveJob(idOrRoot) {
|
|
328
|
-
const trimmed = idOrRoot.trim();
|
|
329
|
-
if (trimmed.length === 0) {
|
|
330
|
-
return null;
|
|
331
|
-
}
|
|
332
|
-
const byId = this.#jobsById.get(trimmed);
|
|
333
|
-
if (byId) {
|
|
334
|
-
return byId;
|
|
335
|
-
}
|
|
336
|
-
const root = normalizeIssueId(trimmed);
|
|
337
|
-
if (!root) {
|
|
338
|
-
return null;
|
|
339
|
-
}
|
|
340
|
-
const jobId = this.#jobIdByRootIssueId.get(root);
|
|
341
|
-
if (!jobId) {
|
|
342
|
-
return null;
|
|
343
|
-
}
|
|
344
|
-
return this.#jobsById.get(jobId) ?? null;
|
|
345
|
-
}
|
|
346
|
-
get(idOrRoot) {
|
|
347
|
-
const job = this.#resolveJob(idOrRoot);
|
|
348
|
-
return job ? this.#snapshot(job) : null;
|
|
349
|
-
}
|
|
350
|
-
async trace(idOrRoot, opts = {}) {
|
|
351
|
-
const job = this.#resolveJob(idOrRoot);
|
|
352
|
-
if (!job) {
|
|
353
|
-
return null;
|
|
354
|
-
}
|
|
355
|
-
const limit = Math.max(1, Math.min(2_000, Math.trunc(opts.limit ?? 200)));
|
|
356
|
-
const rootIssueId = job.snapshot.root_issue_id;
|
|
357
|
-
const traceFiles = [];
|
|
358
|
-
if (rootIssueId) {
|
|
359
|
-
const rootLogsDir = join(getStorePaths(this.#repoRoot).logsDir, rootIssueId);
|
|
360
|
-
try {
|
|
361
|
-
const entries = await readdir(rootLogsDir, { withFileTypes: true });
|
|
362
|
-
for (const entry of entries) {
|
|
363
|
-
if (!entry.isFile())
|
|
364
|
-
continue;
|
|
365
|
-
if (!entry.name.endsWith(".jsonl"))
|
|
366
|
-
continue;
|
|
367
|
-
traceFiles.push(relative(this.#repoRoot, join(rootLogsDir, entry.name)).replaceAll("\\", "/"));
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
catch {
|
|
371
|
-
// best effort only
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
return {
|
|
375
|
-
run: this.#snapshot(job),
|
|
376
|
-
stdout: job.stdout_lines.slice(-limit),
|
|
377
|
-
stderr: job.stderr_lines.slice(-limit),
|
|
378
|
-
log_hints: [...job.log_hints],
|
|
379
|
-
trace_files: traceFiles.sort((a, b) => a.localeCompare(b)),
|
|
380
|
-
};
|
|
381
|
-
}
|
|
382
|
-
interrupt(opts) {
|
|
383
|
-
const target = opts.jobId?.trim() || opts.rootIssueId?.trim() || "";
|
|
384
|
-
if (target.length === 0) {
|
|
385
|
-
return { ok: false, reason: "missing_target", run: null };
|
|
386
|
-
}
|
|
387
|
-
const job = this.#resolveJob(target);
|
|
388
|
-
if (!job) {
|
|
389
|
-
return { ok: false, reason: "not_found", run: null };
|
|
390
|
-
}
|
|
391
|
-
if (job.snapshot.status !== "running") {
|
|
392
|
-
return { ok: false, reason: "not_running", run: this.#snapshot(job) };
|
|
393
|
-
}
|
|
394
|
-
job.interrupt_requested = true;
|
|
395
|
-
this.#touch(job);
|
|
396
|
-
try {
|
|
397
|
-
job.process.kill("SIGINT");
|
|
398
|
-
}
|
|
399
|
-
catch {
|
|
400
|
-
// best effort
|
|
401
|
-
}
|
|
402
|
-
job.hard_kill_timer = setTimeout(() => {
|
|
403
|
-
if (job.snapshot.status !== "running") {
|
|
404
|
-
return;
|
|
405
|
-
}
|
|
406
|
-
try {
|
|
407
|
-
job.process.kill("SIGKILL");
|
|
408
|
-
}
|
|
409
|
-
catch {
|
|
410
|
-
// best effort
|
|
411
|
-
}
|
|
412
|
-
}, 5_000);
|
|
413
|
-
const root = job.snapshot.root_issue_id ?? job.snapshot.job_id;
|
|
414
|
-
this.#emit("run_interrupt_requested", job, `⚠️ Interrupt requested for ${root}.`);
|
|
415
|
-
return { ok: true, reason: null, run: this.#snapshot(job) };
|
|
416
|
-
}
|
|
417
|
-
async startFromCommand(command) {
|
|
418
|
-
switch (command.target_type) {
|
|
419
|
-
case "run start": {
|
|
420
|
-
const prompt = command.command_args.join(" ").trim();
|
|
421
|
-
if (prompt.length === 0) {
|
|
422
|
-
throw new Error("run_start_prompt_required");
|
|
423
|
-
}
|
|
424
|
-
return await this.launchStart({ prompt, command, source: "command" });
|
|
425
|
-
}
|
|
426
|
-
case "run resume": {
|
|
427
|
-
const fallbackRoot = normalizeIssueId(command.target_id);
|
|
428
|
-
const explicitRoot = normalizeIssueId(command.command_args[0] ?? "") ?? fallbackRoot;
|
|
429
|
-
if (!explicitRoot) {
|
|
430
|
-
throw new Error("run_resume_invalid_root_issue_id");
|
|
431
|
-
}
|
|
432
|
-
const maxSteps = toPositiveInt(command.command_args[1], DEFAULT_MAX_STEPS);
|
|
433
|
-
return await this.launchResume({
|
|
434
|
-
rootIssueId: explicitRoot,
|
|
435
|
-
maxSteps,
|
|
436
|
-
command,
|
|
437
|
-
source: "command",
|
|
438
|
-
});
|
|
439
|
-
}
|
|
440
|
-
default:
|
|
441
|
-
return null;
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
async stop() {
|
|
445
|
-
for (const job of this.#jobsById.values()) {
|
|
446
|
-
if (job.hard_kill_timer) {
|
|
447
|
-
clearTimeout(job.hard_kill_timer);
|
|
448
|
-
job.hard_kill_timer = null;
|
|
449
|
-
}
|
|
450
|
-
if (job.snapshot.status === "running") {
|
|
451
|
-
try {
|
|
452
|
-
job.process.kill("SIGTERM");
|
|
453
|
-
}
|
|
454
|
-
catch {
|
|
455
|
-
// best effort
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
}
|