@kasenri/dsh-orbit 0.5.1
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/LICENSE +21 -0
- package/README.md +220 -0
- package/cordis.patch.yml +9 -0
- package/lib/activation.js +110 -0
- package/lib/client.js +625 -0
- package/lib/decisions.js +167 -0
- package/lib/dsh-host.js +364 -0
- package/lib/evidence.js +149 -0
- package/lib/guard.js +157 -0
- package/lib/host.js +1 -0
- package/lib/index.js +138 -0
- package/lib/kernel.js +355 -0
- package/lib/pipeline-guard.js +75 -0
- package/lib/routes.js +52 -0
- package/lib/sanitize.js +39 -0
- package/lib/service.js +104 -0
- package/lib/settlement.js +33 -0
- package/lib/state-store.js +128 -0
- package/lib/supervisor.js +792 -0
- package/lib/tool.js +82 -0
- package/lib/types.js +19 -0
- package/package.json +74 -0
|
@@ -0,0 +1,792 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { buildEvidenceBundle, formatEvidenceBundle } from "./evidence.js";
|
|
4
|
+
import { assertCommanderDecision, assertStrategyDecision, assertTimeoutDecision, assertWatchdogDecision, assertGuardWatchdogDecision, COMMANDER_FINAL_EVALUATE_SCHEMA, COMMANDER_PLAN_SCHEMA, COMMANDER_STEP_EVALUATE_SCHEMA, COMMANDER_STRATEGY_SCHEMA, WATCHDOG_GUARD_SCHEMA, WATCHDOG_RUNTIME_SCHEMA, WATCHDOG_STRATEGY_SCHEMA, WATCHDOG_TIMEOUT_SCHEMA, } from "./decisions.js";
|
|
5
|
+
import { applyCommanderNeedsUser, applyCorrectionStep, applyExecutorCapabilityUnavailable, applyExecutorInterrupted, applyExecutorResume, applyExecutorSuccess, applyFinalAppend, applyFinalSuccess, applyPlan, applyStepPass, baseStepIdOf, beginStep, clearExecutorChild, clearGuardRecovery, correctionBlockCode, correctionDepthOf, createInitialState, enterBudgetExhausted, enterNeedsUser, markStrategyChallengeUsed, normalizePlan, openWatchdogAttempt, recordGuardRecovery, recordPlanFailure, recordWatchdogDecision, restoreEvaluationState, resumeFromNeedsUser, stopRun, } from "./kernel.js";
|
|
6
|
+
import { truncateSafe } from "./sanitize.js";
|
|
7
|
+
import { OrbitStateStore } from "./state-store.js";
|
|
8
|
+
import { COMMANDER_EXTENSION_MS, COMMANDER_HARD_CEILING_MS, COMMANDER_SOFT_DEADLINE_MS, DEFAULT_CAPABILITIES, EXECUTOR_TIMEOUT_MS, GUARD_ESCALATION_THRESHOLD, GUARD_FIRST_INSTRUCTION, GUARD_NEEDS_USER_INSTRUCTION, GUARD_RECOVERY_CAP, GUARD_REPEAT_INSTRUCTION, GUARD_RETRY_INSTRUCTION, MAX_EXECUTOR_INTERRUPT_RETRIES, MAX_WATCHDOG_CALLS_PER_STEP, WATCHDOG_TIMEOUT_MS, } from "./types.js";
|
|
9
|
+
const COMMANDER_PLAN_PROMPT = (goal, constraints) => `You are the Orbit Commander. Produce the smallest set of 2-5 logical engineering steps for this goal.
|
|
10
|
+
Rules: ordinary engineering steps must omit capabilities. Add capability "browser" only when the step must drive a real web page, and "web-api-recon" when it must analyze captured network/API traffic. Keep it minimal.
|
|
11
|
+
Submit your final plan through the structured result protocol.
|
|
12
|
+
Goal: ${goal}
|
|
13
|
+
Hard constraints: ${constraints.join('; ') || 'none'}`;
|
|
14
|
+
const COMMANDER_STEP_PROMPT = (goal, step, evidence, state) => `You are the Orbit Commander doing STEP_EVALUATE. Verify the real project state; do not trust the Executor claim alone. Treat verified execution evidence as authoritative.
|
|
15
|
+
Allowed decisions ONLY: PASS_CURRENT_STEP | CORRECT_CURRENT_STEP | NEEDS_USER.
|
|
16
|
+
- CORRECT_CURRENT_STEP requires a concrete next step goal (optional capabilities).
|
|
17
|
+
Submit your final judgment through the structured result protocol.
|
|
18
|
+
Original goal: ${goal}
|
|
19
|
+
Current step ${step.id}: ${step.goal}
|
|
20
|
+
Iteration counters: loop ${state.loop.used}/${state.loop.max}
|
|
21
|
+
Executor claim:
|
|
22
|
+
${evidence}`;
|
|
23
|
+
const COMMANDER_FINAL_PROMPT = (goal, plan, evidence, state) => `You are the Orbit Commander doing FINAL_EVALUATE. All planned steps are done. Decide whether the original goal is truly satisfied against the real project state.
|
|
24
|
+
Allowed decisions ONLY: SUCCESS | APPEND | NEEDS_USER.
|
|
25
|
+
- APPEND requires next steps or a next step goal; appends are bounded by the remaining loop budget.
|
|
26
|
+
Submit your final judgment through the structured result protocol.
|
|
27
|
+
Original goal: ${goal}
|
|
28
|
+
Plan summary: ${plan.summary}
|
|
29
|
+
Steps: ${plan.steps.map((step) => `${step.id}:${step.goal}[${step.status}]`).join('; ')}
|
|
30
|
+
Loop: ${state.loop.used}/${state.loop.max}
|
|
31
|
+
Executor claim:
|
|
32
|
+
${evidence}`;
|
|
33
|
+
const COMMANDER_STRATEGY_PROMPT = (goal, base, challenge, state) => `You are the Orbit Commander reconsidering strategy after a repeated correction on ${base} (STRATEGY_RECONSIDER).
|
|
34
|
+
Allowed decisions ONLY: KEEP_APPROACH | REPLACE_CURRENT_STEP | NEEDS_USER.
|
|
35
|
+
- REPLACE_CURRENT_STEP requires a replacement goal.
|
|
36
|
+
Submit your final judgment through the structured result protocol.
|
|
37
|
+
Goal: ${goal}
|
|
38
|
+
Loop: ${state.loop.used}/${state.loop.max}
|
|
39
|
+
Watchdog challenge: ${challenge}`;
|
|
40
|
+
const WATCHDOG_RUNTIME_PROMPT = (step, reason, telemetry) => `You are the Orbit Smart Watchdog doing RUNTIME_DIAGNOSE. Diagnose only the current runtime anomaly. Do not review code quality.
|
|
41
|
+
Allowed decisions ONLY: RESUME_CHILD | RESTART_STEP | NEEDS_USER | RUNTIME_BUG.
|
|
42
|
+
Submit your final judgment through the structured result protocol.
|
|
43
|
+
Failed step ${step.id}: ${step.goal}
|
|
44
|
+
Runtime anomaly: ${reason}
|
|
45
|
+
Telemetry: ${JSON.stringify(telemetry ?? {})}`;
|
|
46
|
+
const WATCHDOG_STRATEGY_PROMPT = (step, reason, state) => `You are the Orbit Smart Watchdog doing STRATEGY_CHALLENGE. Ask: is the current approach tunnel vision? Is this blocker truly required? Is there a simpler route?
|
|
47
|
+
Submit one focused challenge question through the structured result protocol.
|
|
48
|
+
Step ${step.id}: ${step.goal}
|
|
49
|
+
Repeated correction: ${reason}
|
|
50
|
+
Loop: ${state.loop.used}/${state.loop.max}`;
|
|
51
|
+
const WATCHDOG_GUARD_PROMPT = (code, count, stepId) => `You are the Orbit Smart Watchdog doing GUARD_ESCALATION. A safety guard blocked a tool ${count} times.
|
|
52
|
+
Allowed decisions ONLY: RETRY_DIFFERENTLY | NEEDS_USER.
|
|
53
|
+
Submit your final judgment through the structured result protocol.
|
|
54
|
+
Guard code: ${code}
|
|
55
|
+
Step: ${stepId}`;
|
|
56
|
+
const WATCHDOG_TIMEOUT_PROMPT = (mode, elapsed, extensions, telemetry) => `You are the Orbit Smart Watchdog doing COMMANDER_TIMEOUT_REVIEW. The Commander has run ${elapsed}ms with ${extensions} extension(s).
|
|
57
|
+
Allowed decisions ONLY: EXTEND | INTERRUPT | NEEDS_USER.
|
|
58
|
+
Submit your final judgment through the structured result protocol.
|
|
59
|
+
Mode: ${mode}
|
|
60
|
+
Telemetry: ${JSON.stringify(telemetry ?? {})}`;
|
|
61
|
+
export class OrbitSupervisor {
|
|
62
|
+
store;
|
|
63
|
+
host;
|
|
64
|
+
config;
|
|
65
|
+
/** Evidence for the step that just settled; never persisted into state.json. */
|
|
66
|
+
stepEvidence;
|
|
67
|
+
constructor(store, host, config) {
|
|
68
|
+
this.store = store;
|
|
69
|
+
this.host = host;
|
|
70
|
+
this.config = config;
|
|
71
|
+
}
|
|
72
|
+
now() {
|
|
73
|
+
return this.host.now();
|
|
74
|
+
}
|
|
75
|
+
createState(input) {
|
|
76
|
+
return createInitialState({
|
|
77
|
+
runId: typeof input.run_id === 'string' && input.run_id.length > 0 ? input.run_id : randomUUID(),
|
|
78
|
+
now: this.now(),
|
|
79
|
+
goal: (input.goal ?? '').trim(),
|
|
80
|
+
...(input.preset !== undefined ? { preset: input.preset } : {}),
|
|
81
|
+
routes: this.config.resolveRoutes?.() ?? this.config.defaultRoutes,
|
|
82
|
+
...(input.approved_loop_count !== undefined ? { approvedLoopCount: input.approved_loop_count } : {}),
|
|
83
|
+
...(input.max_loops !== undefined ? { maxLoops: input.max_loops } : {}),
|
|
84
|
+
...(input.user_hard_constraints ? { userHardConstraints: input.user_hard_constraints } : {}),
|
|
85
|
+
githubAllowed: input.github_allowed === true,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
async bootstrap(input, signal) {
|
|
89
|
+
const requestedGoal = (input.goal ?? '').trim();
|
|
90
|
+
let state = this.store.readState();
|
|
91
|
+
const raw = this.store.readRawState();
|
|
92
|
+
const legacy = raw !== null && raw['schema_version'] !== 2;
|
|
93
|
+
if (legacy && requestedGoal) {
|
|
94
|
+
state = this.store.writeState(this.createState(input));
|
|
95
|
+
}
|
|
96
|
+
else if (!state) {
|
|
97
|
+
if (!requestedGoal)
|
|
98
|
+
return { ok: false, action: 'run', message: 'ORBIT_GOAL_REQUIRED: provide a goal to start a run.' };
|
|
99
|
+
state = this.store.writeState(this.createState(input));
|
|
100
|
+
}
|
|
101
|
+
else if (input.run_id && input.run_id !== state.run_id && !legacy) {
|
|
102
|
+
return { ok: false, action: 'run', message: `ORBIT_RUN_NOT_FOUND: ${input.run_id}` };
|
|
103
|
+
}
|
|
104
|
+
if (['SUCCESS', 'STOPPED', 'BUDGET_EXHAUSTED'].includes(state.phase) && requestedGoal) {
|
|
105
|
+
state = this.store.writeState(this.createState(input));
|
|
106
|
+
}
|
|
107
|
+
if (!legacy && requestedGoal && state.goal && state.goal !== requestedGoal) {
|
|
108
|
+
return {
|
|
109
|
+
ok: false,
|
|
110
|
+
action: 'run',
|
|
111
|
+
run_id: state.run_id,
|
|
112
|
+
message: 'ORBIT_ACTIVE_RUN_EXISTS: current Lite run owns this project; resume it or stop it before starting a different goal.',
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
if (state.phase === 'NEEDS_USER' && requestedGoal) {
|
|
116
|
+
resumeFromNeedsUser(state);
|
|
117
|
+
this.store.writeState(state);
|
|
118
|
+
}
|
|
119
|
+
return this.run(state, signal);
|
|
120
|
+
}
|
|
121
|
+
async run(state, signal) {
|
|
122
|
+
if (signal?.aborted)
|
|
123
|
+
return this.result(state, false, 'ORBIT_ABORTED');
|
|
124
|
+
const projectDir = join(this.store.stateDir, '..');
|
|
125
|
+
const competitors = await this.host.otherMutationDrivers(projectDir);
|
|
126
|
+
if (competitors.length > 0) {
|
|
127
|
+
return {
|
|
128
|
+
ok: false,
|
|
129
|
+
action: 'run',
|
|
130
|
+
run_id: state.run_id,
|
|
131
|
+
phase: state.phase,
|
|
132
|
+
status: state.status,
|
|
133
|
+
message: `ORBIT_MUTATION_DRIVER_CONFLICT: ${competitors.join(', ')} already owns mutation in this workspace.`,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
if (state.phase === 'NEEDS_USER')
|
|
137
|
+
return this.result(state, true, 'ORBIT_AWAITING_USER');
|
|
138
|
+
if (state.phase === 'SUCCESS')
|
|
139
|
+
return this.result(state, true);
|
|
140
|
+
if (state.phase === 'STOPPED')
|
|
141
|
+
return this.result(state, true);
|
|
142
|
+
if (state.phase === 'BUDGET_EXHAUSTED')
|
|
143
|
+
return this.result(state, true);
|
|
144
|
+
if (state.plan.steps.length === 0 && state.phase === 'PLAN') {
|
|
145
|
+
const planOutcome = await this.makePlan(state, signal);
|
|
146
|
+
if (planOutcome)
|
|
147
|
+
return planOutcome;
|
|
148
|
+
}
|
|
149
|
+
if (state.phase === 'EVALUATE' && state.child?.status === 'completed' && state.current_step) {
|
|
150
|
+
const step = state.plan.steps.find((candidate) => candidate.id === state.current_step?.id);
|
|
151
|
+
if (!step) {
|
|
152
|
+
enterNeedsUser(state, 'EVALUATE_STEP_MISSING');
|
|
153
|
+
this.store.writeState(state);
|
|
154
|
+
return this.result(state, false);
|
|
155
|
+
}
|
|
156
|
+
const outcome = await this.commanderEvaluate(state, step, false, signal);
|
|
157
|
+
const applied = await this.applyCommanderOutcome(state, outcome, step, false, signal);
|
|
158
|
+
if (applied)
|
|
159
|
+
return applied;
|
|
160
|
+
return this.run(state, signal);
|
|
161
|
+
}
|
|
162
|
+
const step = state.plan.steps.find((candidate) => candidate.status === 'running') ??
|
|
163
|
+
state.plan.steps.find((candidate) => candidate.status === 'pending');
|
|
164
|
+
if (!step) {
|
|
165
|
+
const outcome = await this.commanderEvaluate(state, undefined, true, signal);
|
|
166
|
+
const applied = await this.applyCommanderOutcome(state, outcome, undefined, true, signal);
|
|
167
|
+
if (applied)
|
|
168
|
+
return applied;
|
|
169
|
+
return this.run(state, signal);
|
|
170
|
+
}
|
|
171
|
+
if (state.loop.used >= state.loop.max) {
|
|
172
|
+
enterBudgetExhausted(state, 'LOOP_BUDGET_EXHAUSTED');
|
|
173
|
+
this.store.writeState(state);
|
|
174
|
+
return this.result(state, false);
|
|
175
|
+
}
|
|
176
|
+
beginStep(state, step);
|
|
177
|
+
this.store.writeState(state);
|
|
178
|
+
const executed = await this.executeStep(state, step, signal);
|
|
179
|
+
if (executed.done)
|
|
180
|
+
return this.result(state, executed.ok, executed.message);
|
|
181
|
+
return this.run(state, signal);
|
|
182
|
+
}
|
|
183
|
+
// ── tool scoping ───────────────────────────────────────────────────────────
|
|
184
|
+
/**
|
|
185
|
+
* Build an allow-filter from configured names, keeping only tools that are
|
|
186
|
+
* actually registered (an unknown name makes `tools.restrict()` fail).
|
|
187
|
+
*/
|
|
188
|
+
toolAllow(names, label) {
|
|
189
|
+
const allowed = names.filter((name) => this.host.hasTool(name));
|
|
190
|
+
if (allowed.length === 0) {
|
|
191
|
+
throw new Error(`ORBIT_TOOL_FILTER_EMPTY: none of [${names.join(', ')}] are registered for ${label}`);
|
|
192
|
+
}
|
|
193
|
+
return { allow: allowed };
|
|
194
|
+
}
|
|
195
|
+
// ── commander supervised path ──────────────────────────────────────────────
|
|
196
|
+
async makePlan(state, signal) {
|
|
197
|
+
const outcome = await this.runCommander(state, 'PLAN', COMMANDER_PLAN_PROMPT(state.goal, state.user_hard_constraints), COMMANDER_PLAN_SCHEMA, signal);
|
|
198
|
+
if (outcome.kind === 'needs_user')
|
|
199
|
+
return this.setNeedsUser(state, outcome.reason);
|
|
200
|
+
if (outcome.kind === 'interrupted') {
|
|
201
|
+
recordPlanFailure(state, truncateSafe(outcome.reason, 500));
|
|
202
|
+
this.store.writeState(state);
|
|
203
|
+
return this.result(state, false, outcome.reason);
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
const plan = normalizePlan(outcome.structured);
|
|
207
|
+
applyPlan(state, plan);
|
|
208
|
+
this.store.writeState(state);
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
const reason = truncateSafe(error instanceof Error ? error.message : String(error), 500);
|
|
213
|
+
recordPlanFailure(state, reason);
|
|
214
|
+
this.store.writeState(state);
|
|
215
|
+
return this.result(state, false, reason);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
async commanderEvaluate(state, step, final, signal) {
|
|
219
|
+
const mode = final ? 'FINAL_EVALUATE' : 'STEP_EVALUATE';
|
|
220
|
+
const evidence = this.evidenceFor(state, step) ?? state.commander?.summary ?? state.last_error ?? 'no evidence recorded';
|
|
221
|
+
const prompt = final
|
|
222
|
+
? COMMANDER_FINAL_PROMPT(state.goal, state.plan, evidence, state)
|
|
223
|
+
: COMMANDER_STEP_PROMPT(state.goal, step, evidence, state);
|
|
224
|
+
const outcome = await this.runCommander(state, mode, prompt, final ? COMMANDER_FINAL_EVALUATE_SCHEMA : COMMANDER_STEP_EVALUATE_SCHEMA, signal);
|
|
225
|
+
if (outcome.kind === 'needs_user')
|
|
226
|
+
return { kind: 'needs_user', reason: outcome.reason };
|
|
227
|
+
if (outcome.kind === 'interrupted')
|
|
228
|
+
return { kind: 'interrupted', reason: outcome.reason };
|
|
229
|
+
try {
|
|
230
|
+
const decision = assertCommanderDecision(outcome.structured, mode);
|
|
231
|
+
return { kind: 'decision', decision };
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
// Invalid/temporary output is recoverable; it must not become NEEDS_USER.
|
|
235
|
+
return { kind: 'interrupted', reason: truncateSafe(error instanceof Error ? error.message : String(error), 500) };
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Format the settled step's evidence bundle. Only the step that just finished
|
|
240
|
+
* qualifies; after a cold resume the bundle is gone and the caller falls back
|
|
241
|
+
* to the durable state summary.
|
|
242
|
+
*/
|
|
243
|
+
evidenceFor(state, step) {
|
|
244
|
+
const stepId = step?.id ?? state.current_step?.id;
|
|
245
|
+
if (!this.stepEvidence || this.stepEvidence.stepId !== stepId)
|
|
246
|
+
return undefined;
|
|
247
|
+
return formatEvidenceBundle(this.stepEvidence.bundle);
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* The single adaptive Commander runner for PLAN, STEP_EVALUATE, FINAL_EVALUATE
|
|
251
|
+
* and STRATEGY_RECONSIDER. 360s/600s soft reviews, 840s deterministic ceiling;
|
|
252
|
+
* EXTEND always keeps the same child. Decisions arrive as DSH-native
|
|
253
|
+
* structured results; a requested schema without a capture is a failure.
|
|
254
|
+
*/
|
|
255
|
+
async runCommander(state, mode, prompt, outputSchema, signal) {
|
|
256
|
+
const startedAt = this.now();
|
|
257
|
+
let handle;
|
|
258
|
+
try {
|
|
259
|
+
handle = await this.host.startRole({
|
|
260
|
+
role: 'commander',
|
|
261
|
+
label: `commander-${mode.toLowerCase()}`,
|
|
262
|
+
prompt,
|
|
263
|
+
route: state.routes.commander,
|
|
264
|
+
toolFilter: this.toolAllow(this.config.commanderReadOnlyTools, `commander ${mode}`),
|
|
265
|
+
outputSchema,
|
|
266
|
+
...(signal ? { signal } : {}),
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
return { kind: 'interrupted', reason: truncateSafe(error instanceof Error ? error.message : String(error), 500) };
|
|
271
|
+
}
|
|
272
|
+
try {
|
|
273
|
+
for (let extensions = 0;;) {
|
|
274
|
+
const deadline = extensions === 0
|
|
275
|
+
? COMMANDER_SOFT_DEADLINE_MS
|
|
276
|
+
: extensions === 1
|
|
277
|
+
? COMMANDER_SOFT_DEADLINE_MS + COMMANDER_EXTENSION_MS
|
|
278
|
+
: COMMANDER_HARD_CEILING_MS;
|
|
279
|
+
const remaining = Math.max(0, deadline - (this.now() - startedAt));
|
|
280
|
+
const raced = await this.raceWithSleep(handle.result, remaining, signal);
|
|
281
|
+
if (raced.kind === 'work') {
|
|
282
|
+
if (raced.value.interrupted)
|
|
283
|
+
return { kind: 'interrupted', reason: raced.value.reason ?? `${mode}_INTERRUPTED` };
|
|
284
|
+
if (raced.value.structured === undefined) {
|
|
285
|
+
return { kind: 'interrupted', reason: `${mode}_STRUCTURED_OUTPUT_MISSING` };
|
|
286
|
+
}
|
|
287
|
+
return { kind: 'output', output: raced.value.output, structured: raced.value.structured };
|
|
288
|
+
}
|
|
289
|
+
if (raced.kind === 'aborted' || signal?.aborted) {
|
|
290
|
+
await this.cancelHandle(handle, 'ORBIT_ABORTED');
|
|
291
|
+
return { kind: 'interrupted', reason: 'ORBIT_ABORTED' };
|
|
292
|
+
}
|
|
293
|
+
if (extensions >= 2) {
|
|
294
|
+
await this.cancelHandle(handle, 'COMMANDER_HARD_TIMEOUT');
|
|
295
|
+
return { kind: 'interrupted', reason: 'COMMANDER_HARD_TIMEOUT' };
|
|
296
|
+
}
|
|
297
|
+
const review = await this.commanderTimeoutReview(state, mode, this.now() - startedAt, extensions, handle, signal);
|
|
298
|
+
if (review.decision === 'EXTEND') {
|
|
299
|
+
extensions += 1;
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
await this.cancelHandle(handle, review.decision === 'NEEDS_USER' ? 'COMMANDER_NEEDS_USER' : 'COMMANDER_TIMEOUT_INTERRUPTED');
|
|
303
|
+
if (review.decision === 'NEEDS_USER')
|
|
304
|
+
return { kind: 'needs_user', reason: review.reason ?? 'COMMANDER_TIMEOUT_NEEDS_USER' };
|
|
305
|
+
return { kind: 'interrupted', reason: 'COMMANDER_TIMEOUT_INTERRUPTED' };
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
finally {
|
|
309
|
+
await this.disposeHandle(handle);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
async commanderTimeoutReview(state, mode, elapsed, extensions, commanderHandle, signal) {
|
|
313
|
+
// Telemetry must describe the *current* Commander child, never a past Executor.
|
|
314
|
+
const telemetry = await commanderHandle.runtimeSnapshot?.();
|
|
315
|
+
const result = await this.runAuxRole(state, {
|
|
316
|
+
role: 'watchdog',
|
|
317
|
+
label: 'watchdog-commander-timeout',
|
|
318
|
+
prompt: WATCHDOG_TIMEOUT_PROMPT(mode, elapsed, extensions, telemetry),
|
|
319
|
+
outputSchema: WATCHDOG_TIMEOUT_SCHEMA,
|
|
320
|
+
...(signal ? { signal } : {}),
|
|
321
|
+
});
|
|
322
|
+
if (!result || result.interrupted) {
|
|
323
|
+
return { decision: extensions === 0 ? 'EXTEND' : 'INTERRUPT', reason: 'Smart Watchdog unavailable' };
|
|
324
|
+
}
|
|
325
|
+
try {
|
|
326
|
+
return assertTimeoutDecision(result.structured);
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
return { decision: extensions === 0 ? 'EXTEND' : 'INTERRUPT', reason: 'Smart Watchdog invalid output' };
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
// ── executor runtime ───────────────────────────────────────────────────────
|
|
333
|
+
async executeStep(state, step, signal) {
|
|
334
|
+
const capabilities = step.capabilities ?? [];
|
|
335
|
+
if (capabilities.includes('browser') && !this.host.hasTool(this.config.browserTools[0] ?? 'agent_browser')) {
|
|
336
|
+
applyExecutorCapabilityUnavailable(state, step.id);
|
|
337
|
+
this.store.writeState(state);
|
|
338
|
+
return { done: false, ok: false };
|
|
339
|
+
}
|
|
340
|
+
let toolFilter;
|
|
341
|
+
try {
|
|
342
|
+
const capabilityTools = capabilities.includes('browser') ? [...this.config.browserTools] : [];
|
|
343
|
+
toolFilter = this.toolAllow([...this.config.executorTools, ...capabilityTools], `executor ${step.id}`);
|
|
344
|
+
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
const reason = truncateSafe(error instanceof Error ? error.message : String(error), 500);
|
|
347
|
+
enterNeedsUser(state, reason);
|
|
348
|
+
this.store.writeState(state);
|
|
349
|
+
return { done: true, ok: false, message: reason };
|
|
350
|
+
}
|
|
351
|
+
let handle;
|
|
352
|
+
try {
|
|
353
|
+
handle = await this.host.startRole({
|
|
354
|
+
role: 'executor',
|
|
355
|
+
label: `executor-${step.id}`,
|
|
356
|
+
prompt: this.executorPrompt(state, step),
|
|
357
|
+
route: state.routes.executor,
|
|
358
|
+
toolFilter,
|
|
359
|
+
capabilities,
|
|
360
|
+
...(signal ? { signal } : {}),
|
|
361
|
+
...(state.child?.id && state.child.status === 'interrupted' ? { resumeOf: state.child.id } : {}),
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
catch (error) {
|
|
365
|
+
const reason = truncateSafe(error instanceof Error ? error.message : String(error), 500);
|
|
366
|
+
enterNeedsUser(state, reason);
|
|
367
|
+
this.store.writeState(state);
|
|
368
|
+
return { done: true, ok: false, message: reason };
|
|
369
|
+
}
|
|
370
|
+
const result = await this.awaitExecutor(handle, signal);
|
|
371
|
+
if (result.interrupted) {
|
|
372
|
+
const retries = applyExecutorInterrupted(state, {
|
|
373
|
+
...(result.childId ? { childId: result.childId } : {}),
|
|
374
|
+
lastError: truncateSafe(result.reason ?? 'EXECUTOR_INTERRUPTED', 500),
|
|
375
|
+
});
|
|
376
|
+
this.store.writeState(state);
|
|
377
|
+
if (signal?.aborted)
|
|
378
|
+
return { done: true, ok: false, message: 'ORBIT_ABORTED' };
|
|
379
|
+
if (result.reason === 'USER_HARD_SCOPE_VIOLATION') {
|
|
380
|
+
enterNeedsUser(state);
|
|
381
|
+
this.store.writeState(state);
|
|
382
|
+
return { done: true, ok: false, message: 'USER_HARD_SCOPE_VIOLATION' };
|
|
383
|
+
}
|
|
384
|
+
const recovery = await this.runtimeWatchdog(state, step, result, retries, signal);
|
|
385
|
+
if (recovery === 'needs_user') {
|
|
386
|
+
enterNeedsUser(state);
|
|
387
|
+
this.store.writeState(state);
|
|
388
|
+
return { done: true, ok: false, message: state.last_error ?? undefined };
|
|
389
|
+
}
|
|
390
|
+
if (recovery === 'resume' && result.childId) {
|
|
391
|
+
applyExecutorResume(state, result.childId);
|
|
392
|
+
this.store.writeState(state);
|
|
393
|
+
return { done: false, ok: false };
|
|
394
|
+
}
|
|
395
|
+
if (recovery === 'restart') {
|
|
396
|
+
await this.cancelHandle(handle, 'ORBIT_RESTART_STEP');
|
|
397
|
+
await this.disposeHandle(handle);
|
|
398
|
+
clearExecutorChild(state);
|
|
399
|
+
this.store.writeState(state);
|
|
400
|
+
return { done: false, ok: false };
|
|
401
|
+
}
|
|
402
|
+
if (retries >= MAX_EXECUTOR_INTERRUPT_RETRIES) {
|
|
403
|
+
enterNeedsUser(state, `EXECUTOR_INTERRUPTED: ${state.last_error ?? ''}`);
|
|
404
|
+
this.store.writeState(state);
|
|
405
|
+
return { done: true, ok: false, message: state.last_error ?? undefined };
|
|
406
|
+
}
|
|
407
|
+
return { done: false, ok: false };
|
|
408
|
+
}
|
|
409
|
+
applyExecutorSuccess(state, {
|
|
410
|
+
...(result.childId ? { childId: result.childId } : {}),
|
|
411
|
+
summary: truncateSafe(result.output, 2000),
|
|
412
|
+
changedFiles: result.changedFiles ?? this.host.changedFiles(join(this.store.stateDir, '..')),
|
|
413
|
+
testSummary: result.testSummary ?? [],
|
|
414
|
+
});
|
|
415
|
+
this.stepEvidence = {
|
|
416
|
+
stepId: step.id,
|
|
417
|
+
bundle: buildEvidenceBundle({
|
|
418
|
+
settlement: result.settlement,
|
|
419
|
+
executorOutput: result.output,
|
|
420
|
+
changedFiles: state.changed_files,
|
|
421
|
+
tools: result.toolEvidence,
|
|
422
|
+
telemetry: result.telemetry,
|
|
423
|
+
}),
|
|
424
|
+
};
|
|
425
|
+
this.store.writeState(state);
|
|
426
|
+
await this.disposeHandle(handle);
|
|
427
|
+
return { done: false, ok: false };
|
|
428
|
+
}
|
|
429
|
+
/** Deterministic executor runtime timeout; a timeout does not destroy the child. */
|
|
430
|
+
async awaitExecutor(handle, signal) {
|
|
431
|
+
const timeoutMs = this.config.executorTimeoutMs ?? EXECUTOR_TIMEOUT_MS;
|
|
432
|
+
const raced = await this.raceWithSleep(handle.result, timeoutMs, signal);
|
|
433
|
+
if (raced.kind === 'work')
|
|
434
|
+
return raced.value;
|
|
435
|
+
const telemetry = await handle.runtimeSnapshot?.();
|
|
436
|
+
return {
|
|
437
|
+
...(handle.childId ? { childId: handle.childId } : {}),
|
|
438
|
+
output: '',
|
|
439
|
+
interrupted: true,
|
|
440
|
+
reason: raced.kind === 'aborted' ? 'ORBIT_ABORTED' : 'EXECUTOR_TIMEOUT',
|
|
441
|
+
...(telemetry ? { telemetry } : {}),
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
executorPrompt(state, step) {
|
|
445
|
+
const lines = [
|
|
446
|
+
'You are the Orbit Executor. Implement exactly the current step with real tools and real verification.',
|
|
447
|
+
`Current step ${step.id}: ${step.goal}`,
|
|
448
|
+
`Working directory: ${join(this.store.stateDir, '..')}`,
|
|
449
|
+
`Hard constraints: ${state.user_hard_constraints.join('; ') || 'none'}`,
|
|
450
|
+
];
|
|
451
|
+
if ((step.capabilities ?? []).length > 0)
|
|
452
|
+
lines.push(`Capabilities: ${(step.capabilities ?? []).join(', ')}`);
|
|
453
|
+
lines.push('Return a compact evidence summary: what changed, commands/tests run, and residual risks.');
|
|
454
|
+
return lines.join('\n');
|
|
455
|
+
}
|
|
456
|
+
// ── decision application ───────────────────────────────────────────────────
|
|
457
|
+
async applyCommanderOutcome(state, outcome, step, final, signal) {
|
|
458
|
+
clearGuardRecovery(state);
|
|
459
|
+
if (outcome.kind === 'interrupted') {
|
|
460
|
+
// Recoverable: the run stops and a later resume retries the same phase.
|
|
461
|
+
state.last_error = truncateSafe(outcome.reason, 500);
|
|
462
|
+
this.store.writeState(state);
|
|
463
|
+
return this.result(state, false, outcome.reason);
|
|
464
|
+
}
|
|
465
|
+
if (outcome.kind === 'needs_user')
|
|
466
|
+
return this.setNeedsUser(state, outcome.reason);
|
|
467
|
+
const decision = outcome.decision;
|
|
468
|
+
if (decision.decision === 'NEEDS_USER') {
|
|
469
|
+
applyCommanderNeedsUser(state, decision.reason ?? 'COMMANDER_NEEDS_USER');
|
|
470
|
+
this.store.writeState(state);
|
|
471
|
+
return this.result(state, false);
|
|
472
|
+
}
|
|
473
|
+
if (decision.decision === 'SUCCESS') {
|
|
474
|
+
applyFinalSuccess(state, decision.summary);
|
|
475
|
+
this.store.writeState(state);
|
|
476
|
+
return this.result(state, true);
|
|
477
|
+
}
|
|
478
|
+
if (decision.decision === 'PASS_CURRENT_STEP') {
|
|
479
|
+
applyStepPass(state, step, decision.summary);
|
|
480
|
+
this.store.writeState(state);
|
|
481
|
+
return undefined;
|
|
482
|
+
}
|
|
483
|
+
if (decision.decision === 'CORRECT_CURRENT_STEP') {
|
|
484
|
+
return this.applyCorrection(state, step, decision, signal);
|
|
485
|
+
}
|
|
486
|
+
if (decision.decision === 'APPEND') {
|
|
487
|
+
const append = applyFinalAppend(state, decision);
|
|
488
|
+
if (append === 'invalid') {
|
|
489
|
+
return this.setNeedsUser(state, 'COMMANDER_EVALUATION_OUTPUT_INVALID: append needs next_steps or next_step_goal');
|
|
490
|
+
}
|
|
491
|
+
this.store.writeState(state);
|
|
492
|
+
if (append === 'budget_exhausted')
|
|
493
|
+
return this.result(state, false);
|
|
494
|
+
return undefined;
|
|
495
|
+
}
|
|
496
|
+
// FINAL_EVALUATE must not return PASS/CORRECT; assertCommanderDecision already rejects it.
|
|
497
|
+
return this.setNeedsUser(state, 'COMMANDER_FINAL_DECISION_INVALID');
|
|
498
|
+
}
|
|
499
|
+
setNeedsUser(state, reason) {
|
|
500
|
+
enterNeedsUser(state, truncateSafe(reason, 500));
|
|
501
|
+
this.store.writeState(state);
|
|
502
|
+
return this.result(state, false);
|
|
503
|
+
}
|
|
504
|
+
async applyCorrection(state, step, decision, signal) {
|
|
505
|
+
if (!step || !decision.next_step_goal) {
|
|
506
|
+
return this.setNeedsUser(state, 'COMMANDER_EVALUATION_OUTPUT_INVALID: correction needs next_step_goal');
|
|
507
|
+
}
|
|
508
|
+
const base = baseStepIdOf(step.id);
|
|
509
|
+
const correctionDepth = correctionDepthOf(step.id);
|
|
510
|
+
const blocked = correctionBlockCode(state, step);
|
|
511
|
+
if (blocked)
|
|
512
|
+
return this.setNeedsUser(state, blocked);
|
|
513
|
+
let nextGoal = decision.next_step_goal;
|
|
514
|
+
if (correctionDepth === 1 && state.strategy_challenge?.base_step_id !== base) {
|
|
515
|
+
const reconsider = await this.strategyChallenge(state, base, step, decision.reason ?? 'repeated correction', signal);
|
|
516
|
+
if (reconsider.kind === 'needs_user')
|
|
517
|
+
return this.setNeedsUser(state, reconsider.reason);
|
|
518
|
+
if (reconsider.kind === 'interrupted') {
|
|
519
|
+
// Temporary failure: keep the run resumable and do NOT consume the challenge.
|
|
520
|
+
restoreEvaluationState(state, truncateSafe(reconsider.reason, 500));
|
|
521
|
+
this.store.writeState(state);
|
|
522
|
+
return this.result(state, false, reconsider.reason);
|
|
523
|
+
}
|
|
524
|
+
if (reconsider.kind === 'replace')
|
|
525
|
+
nextGoal = reconsider.replacementGoal;
|
|
526
|
+
}
|
|
527
|
+
applyCorrectionStep(state, step, {
|
|
528
|
+
nextGoal,
|
|
529
|
+
capabilities: decision.next_step_capabilities,
|
|
530
|
+
});
|
|
531
|
+
this.store.writeState(state);
|
|
532
|
+
return undefined;
|
|
533
|
+
}
|
|
534
|
+
async strategyChallenge(state, base, step, reason, signal) {
|
|
535
|
+
const challengeResult = await this.runAuxRole(state, {
|
|
536
|
+
role: 'watchdog',
|
|
537
|
+
label: 'watchdog-strategy',
|
|
538
|
+
prompt: WATCHDOG_STRATEGY_PROMPT(step, reason, state),
|
|
539
|
+
outputSchema: WATCHDOG_STRATEGY_SCHEMA,
|
|
540
|
+
...(signal ? { signal } : {}),
|
|
541
|
+
});
|
|
542
|
+
if (!challengeResult || challengeResult.interrupted) {
|
|
543
|
+
return { kind: 'interrupted', reason: 'SMART_WATCHDOG_UNAVAILABLE' };
|
|
544
|
+
}
|
|
545
|
+
if (challengeResult.structured === undefined) {
|
|
546
|
+
return { kind: 'interrupted', reason: 'SMART_WATCHDOG_STRATEGY_STRUCTURED_OUTPUT_MISSING' };
|
|
547
|
+
}
|
|
548
|
+
const challengeValue = challengeResult.structured;
|
|
549
|
+
const challenge = typeof challengeValue.question === 'string' ? challengeValue.question.trim() : '';
|
|
550
|
+
if (!challenge)
|
|
551
|
+
return { kind: 'interrupted', reason: 'SMART_WATCHDOG_STRATEGY_OUTPUT_INVALID: question is required' };
|
|
552
|
+
const outcome = await this.runCommander(state, 'STRATEGY_RECONSIDER', COMMANDER_STRATEGY_PROMPT(state.goal, base, challenge, state), COMMANDER_STRATEGY_SCHEMA, signal);
|
|
553
|
+
if (outcome.kind === 'needs_user')
|
|
554
|
+
return { kind: 'needs_user', reason: outcome.reason };
|
|
555
|
+
if (outcome.kind === 'interrupted')
|
|
556
|
+
return { kind: 'interrupted', reason: outcome.reason };
|
|
557
|
+
try {
|
|
558
|
+
const decision = assertStrategyDecision(outcome.structured);
|
|
559
|
+
if (decision.decision === 'NEEDS_USER')
|
|
560
|
+
return { kind: 'needs_user', reason: decision.reason ?? 'COMMANDER_STRATEGY_NEEDS_USER' };
|
|
561
|
+
markStrategyChallengeUsed(state, base);
|
|
562
|
+
if (decision.decision === 'REPLACE_CURRENT_STEP')
|
|
563
|
+
return { kind: 'replace', replacementGoal: decision.replacement_goal };
|
|
564
|
+
return { kind: 'keep' };
|
|
565
|
+
}
|
|
566
|
+
catch (error) {
|
|
567
|
+
return { kind: 'interrupted', reason: truncateSafe(error instanceof Error ? error.message : String(error), 500) };
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
async runtimeWatchdog(state, step, result, retries, signal) {
|
|
571
|
+
const capReached = openWatchdogAttempt(state, step.id, {
|
|
572
|
+
atCap: state.last_error?.slice(0, 500),
|
|
573
|
+
attempt: truncateSafe(result.reason ?? state.last_error ?? '', 500),
|
|
574
|
+
});
|
|
575
|
+
if (capReached) {
|
|
576
|
+
this.store.writeState(state);
|
|
577
|
+
return 'needs_user';
|
|
578
|
+
}
|
|
579
|
+
this.store.writeState(state);
|
|
580
|
+
const watchdogResult = await this.runAuxRole(state, {
|
|
581
|
+
role: 'watchdog',
|
|
582
|
+
label: 'watchdog-runtime',
|
|
583
|
+
prompt: WATCHDOG_RUNTIME_PROMPT(step, result.reason ?? 'runtime failure', result.telemetry),
|
|
584
|
+
outputSchema: WATCHDOG_RUNTIME_SCHEMA,
|
|
585
|
+
...(signal ? { signal } : {}),
|
|
586
|
+
});
|
|
587
|
+
if (!watchdogResult || watchdogResult.interrupted) {
|
|
588
|
+
recordWatchdogDecision(state, 'UNAVAILABLE');
|
|
589
|
+
this.store.writeState(state);
|
|
590
|
+
return 'fallback';
|
|
591
|
+
}
|
|
592
|
+
let diagnosis;
|
|
593
|
+
try {
|
|
594
|
+
diagnosis = assertWatchdogDecision(watchdogResult.structured);
|
|
595
|
+
}
|
|
596
|
+
catch {
|
|
597
|
+
recordWatchdogDecision(state, 'UNAVAILABLE');
|
|
598
|
+
this.store.writeState(state);
|
|
599
|
+
return 'fallback';
|
|
600
|
+
}
|
|
601
|
+
recordWatchdogDecision(state, diagnosis.decision);
|
|
602
|
+
this.store.writeState(state);
|
|
603
|
+
if (diagnosis.decision === 'NEEDS_USER' || diagnosis.decision === 'RUNTIME_BUG')
|
|
604
|
+
return 'needs_user';
|
|
605
|
+
if (diagnosis.decision === 'RESUME_CHILD') {
|
|
606
|
+
return result.childId && retries < MAX_EXECUTOR_INTERRUPT_RETRIES ? 'resume' : 'fallback';
|
|
607
|
+
}
|
|
608
|
+
return state.current_step?.id === step.id && !signal?.aborted && retries < MAX_EXECUTOR_INTERRUPT_RETRIES ? 'restart' : 'fallback';
|
|
609
|
+
}
|
|
610
|
+
// ── guards ─────────────────────────────────────────────────────────────────
|
|
611
|
+
async recordGuardBlock(code, reason) {
|
|
612
|
+
const state = this.store.readState();
|
|
613
|
+
if (!state)
|
|
614
|
+
return { disposition: 'block_continue', code, count: 0, watchdog_calls: 0, instruction: GUARD_FIRST_INSTRUCTION };
|
|
615
|
+
const stepId = state.current_step?.id ?? state.plan.steps.find((candidate) => candidate.status === 'running')?.id ?? '';
|
|
616
|
+
const count = recordGuardRecovery(state, stepId, code);
|
|
617
|
+
this.store.writeState(state);
|
|
618
|
+
if (count < GUARD_ESCALATION_THRESHOLD) {
|
|
619
|
+
return {
|
|
620
|
+
disposition: 'block_continue',
|
|
621
|
+
code,
|
|
622
|
+
count,
|
|
623
|
+
watchdog_calls: 0,
|
|
624
|
+
instruction: count >= 2 ? GUARD_REPEAT_INSTRUCTION : GUARD_FIRST_INSTRUCTION,
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
if (count > GUARD_RECOVERY_CAP)
|
|
628
|
+
return this.guardNeedsUser(state, code, reason, count);
|
|
629
|
+
const watchdog = await this.guardEscalation(state, code, count);
|
|
630
|
+
if (watchdog.decision === 'NEEDS_USER')
|
|
631
|
+
return this.guardNeedsUser(state, code, reason, count, watchdog.instruction);
|
|
632
|
+
return { disposition: 'block_continue', code, count, watchdog_calls: 1, instruction: watchdog.instruction ?? GUARD_RETRY_INSTRUCTION };
|
|
633
|
+
}
|
|
634
|
+
async guardEscalation(state, code, count) {
|
|
635
|
+
const result = await this.runAuxRole(state, {
|
|
636
|
+
role: 'watchdog',
|
|
637
|
+
label: 'watchdog-guard',
|
|
638
|
+
prompt: WATCHDOG_GUARD_PROMPT(code, count, state.current_step?.id ?? ''),
|
|
639
|
+
outputSchema: WATCHDOG_GUARD_SCHEMA,
|
|
640
|
+
});
|
|
641
|
+
if (!result || result.interrupted)
|
|
642
|
+
return this.guardWatchdogFallback(count);
|
|
643
|
+
try {
|
|
644
|
+
return assertGuardWatchdogDecision(result.structured);
|
|
645
|
+
}
|
|
646
|
+
catch {
|
|
647
|
+
return this.guardWatchdogFallback(count);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
guardWatchdogFallback(count) {
|
|
651
|
+
return count >= GUARD_RECOVERY_CAP
|
|
652
|
+
? { decision: 'NEEDS_USER', instruction: GUARD_NEEDS_USER_INSTRUCTION }
|
|
653
|
+
: { decision: 'RETRY_DIFFERENTLY', instruction: GUARD_RETRY_INSTRUCTION };
|
|
654
|
+
}
|
|
655
|
+
guardNeedsUser(state, code, reason, count, instruction) {
|
|
656
|
+
enterNeedsUser(state, `ORBIT_GUARD_ESCALATION: ${reason.slice(0, 300)}`);
|
|
657
|
+
this.store.writeState(state);
|
|
658
|
+
return { disposition: 'block_needs_user', code, count, watchdog_calls: 0, instruction: instruction ?? GUARD_NEEDS_USER_INSTRUCTION };
|
|
659
|
+
}
|
|
660
|
+
// ── role lifecycle helpers ─────────────────────────────────────────────────
|
|
661
|
+
/**
|
|
662
|
+
* One-shot auxiliary role (watchdogs and similar). Always releases the handle,
|
|
663
|
+
* even on interruption or timeout.
|
|
664
|
+
*/
|
|
665
|
+
async runAuxRole(state, request) {
|
|
666
|
+
const names = request.role === 'watchdog' ? this.config.watchdogTools : this.config.commanderReadOnlyTools;
|
|
667
|
+
let handle;
|
|
668
|
+
try {
|
|
669
|
+
handle = await this.host.startRole({
|
|
670
|
+
role: request.role,
|
|
671
|
+
label: request.label,
|
|
672
|
+
prompt: request.prompt,
|
|
673
|
+
route: state.routes[request.role],
|
|
674
|
+
toolFilter: this.toolAllow(names, request.role),
|
|
675
|
+
...(request.outputSchema ? { outputSchema: request.outputSchema } : {}),
|
|
676
|
+
...(request.signal ? { signal: request.signal } : {}),
|
|
677
|
+
});
|
|
678
|
+
const timeoutMs = request.timeoutMs ?? this.config.watchdogTimeoutMs ?? WATCHDOG_TIMEOUT_MS;
|
|
679
|
+
const raced = await this.raceWithSleep(handle.result, timeoutMs, request.signal);
|
|
680
|
+
if (raced.kind === 'work')
|
|
681
|
+
return raced.value;
|
|
682
|
+
await this.cancelHandle(handle, raced.kind === 'aborted' ? 'ORBIT_ABORTED' : 'WATCHDOG_TIMEOUT');
|
|
683
|
+
return {
|
|
684
|
+
...(handle.childId ? { childId: handle.childId } : {}),
|
|
685
|
+
output: '',
|
|
686
|
+
interrupted: true,
|
|
687
|
+
reason: raced.kind === 'aborted' ? 'ORBIT_ABORTED' : 'WATCHDOG_TIMEOUT',
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
catch {
|
|
691
|
+
return undefined;
|
|
692
|
+
}
|
|
693
|
+
finally {
|
|
694
|
+
if (handle)
|
|
695
|
+
await this.disposeHandle(handle);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
async cancelHandle(handle, reason) {
|
|
699
|
+
try {
|
|
700
|
+
if (handle.cancel)
|
|
701
|
+
await handle.cancel(reason);
|
|
702
|
+
else
|
|
703
|
+
await this.host.interruptRole(handle, reason);
|
|
704
|
+
}
|
|
705
|
+
catch {
|
|
706
|
+
// cancellation is best-effort; dispose still runs
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
async disposeHandle(handle) {
|
|
710
|
+
try {
|
|
711
|
+
if (handle.dispose)
|
|
712
|
+
await handle.dispose();
|
|
713
|
+
else
|
|
714
|
+
await this.host.releaseRole(handle);
|
|
715
|
+
}
|
|
716
|
+
catch {
|
|
717
|
+
// idempotent cleanup
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
/** Race a work promise against a sleeping deadline, aborting the timer cleanly. */
|
|
721
|
+
async raceWithSleep(work, timeoutMs, signal) {
|
|
722
|
+
const controller = new AbortController();
|
|
723
|
+
const onAbort = () => controller.abort();
|
|
724
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
725
|
+
const timeout = this.host
|
|
726
|
+
.sleep(timeoutMs, controller.signal)
|
|
727
|
+
.then(() => ({ kind: 'timeout' }))
|
|
728
|
+
.catch(() => ({ kind: 'aborted' }));
|
|
729
|
+
try {
|
|
730
|
+
return await Promise.race([
|
|
731
|
+
work.then((value) => {
|
|
732
|
+
controller.abort();
|
|
733
|
+
return { kind: 'work', value };
|
|
734
|
+
}),
|
|
735
|
+
timeout,
|
|
736
|
+
]);
|
|
737
|
+
}
|
|
738
|
+
finally {
|
|
739
|
+
controller.abort();
|
|
740
|
+
signal?.removeEventListener('abort', onAbort);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
// ── lifecycle ──────────────────────────────────────────────────────────────
|
|
744
|
+
stop(action, runId) {
|
|
745
|
+
const state = this.store.readState();
|
|
746
|
+
if (!state)
|
|
747
|
+
return { ok: false, action, message: 'ORBIT_RUN_NOT_FOUND: no active run.' };
|
|
748
|
+
if (runId && runId !== state.run_id)
|
|
749
|
+
return { ok: false, action, message: `ORBIT_RUN_NOT_FOUND: ${runId}` };
|
|
750
|
+
stopRun(state);
|
|
751
|
+
this.store.writeState(state);
|
|
752
|
+
return this.result(state, true);
|
|
753
|
+
}
|
|
754
|
+
async status() {
|
|
755
|
+
const state = this.store.readState();
|
|
756
|
+
if (!state)
|
|
757
|
+
return { ok: false, action: 'status', message: 'ORBIT_RUN_NOT_FOUND: no run state.' };
|
|
758
|
+
return this.result(state, true);
|
|
759
|
+
}
|
|
760
|
+
result(state, ok, message) {
|
|
761
|
+
// Tool output must be lossless JSON: optional state fields are omitted, not
|
|
762
|
+
// emitted as `undefined`.
|
|
763
|
+
const data = {
|
|
764
|
+
goal: state.goal,
|
|
765
|
+
preset: state.preset,
|
|
766
|
+
loop: state.loop,
|
|
767
|
+
remaining_budget: state.remaining_budget,
|
|
768
|
+
current_step: state.current_step,
|
|
769
|
+
child: state.child,
|
|
770
|
+
plan: state.plan,
|
|
771
|
+
commander: state.commander,
|
|
772
|
+
smart_watchdog: state.smart_watchdog,
|
|
773
|
+
strategy_challenge: state.strategy_challenge,
|
|
774
|
+
guard_recovery: state.guard_recovery,
|
|
775
|
+
last_error: state.last_error,
|
|
776
|
+
changed_files: state.changed_files,
|
|
777
|
+
test_summary: state.test_summary,
|
|
778
|
+
driver_ownership: state.driver_ownership,
|
|
779
|
+
state_revision: state.state_revision,
|
|
780
|
+
};
|
|
781
|
+
return {
|
|
782
|
+
ok,
|
|
783
|
+
action: 'run',
|
|
784
|
+
run_id: state.run_id,
|
|
785
|
+
phase: state.phase,
|
|
786
|
+
status: state.status,
|
|
787
|
+
...(message ? { message } : {}),
|
|
788
|
+
data: Object.fromEntries(Object.entries(data).filter(([, value]) => value !== undefined)),
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
export { DEFAULT_CAPABILITIES };
|