@enderfga/claw-orchestrator 4.7.0 → 4.8.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/README.md +8 -9
- package/configs/autoloop-coder-prompt.md +4 -4
- package/configs/autoloop-planner-prompt.md +10 -10
- package/configs/autoloop-reviewer-prompt.md +3 -4
- package/dist/src/autoloop/dispatcher.d.ts +59 -10
- package/dist/src/autoloop/dispatcher.js +261 -40
- package/dist/src/autoloop/dispatcher.js.map +1 -1
- package/dist/src/autoloop/planner-tools.d.ts +4 -3
- package/dist/src/autoloop/planner-tools.js +35 -2
- package/dist/src/autoloop/planner-tools.js.map +1 -1
- package/dist/src/autoloop/types.d.ts +2 -0
- package/dist/src/autoloop/types.js.map +1 -1
- package/dist/src/dashboard/index.html +185 -94
- package/dist/src/embedded-server.js +67 -9
- package/dist/src/embedded-server.js.map +1 -1
- package/dist/src/index.js +84 -52
- package/dist/src/index.js.map +1 -1
- package/dist/src/persistent-agy-session.js +4 -1
- package/dist/src/persistent-agy-session.js.map +1 -1
- package/dist/src/persistent-codex-session.js +3 -0
- package/dist/src/persistent-codex-session.js.map +1 -1
- package/dist/src/persistent-cursor-session.d.ts +6 -0
- package/dist/src/persistent-cursor-session.js +98 -3
- package/dist/src/persistent-cursor-session.js.map +1 -1
- package/dist/src/persistent-custom-session.js +13 -0
- package/dist/src/persistent-custom-session.js.map +1 -1
- package/dist/src/persistent-gemini-session.d.ts +4 -0
- package/dist/src/persistent-gemini-session.js +55 -2
- package/dist/src/persistent-gemini-session.js.map +1 -1
- package/dist/src/persistent-opencode-session.d.ts +5 -5
- package/dist/src/persistent-opencode-session.js +58 -6
- package/dist/src/persistent-opencode-session.js.map +1 -1
- package/dist/src/persistent-session.js +6 -1
- package/dist/src/persistent-session.js.map +1 -1
- package/dist/src/session-manager.d.ts +44 -5
- package/dist/src/session-manager.js +190 -18
- package/dist/src/session-manager.js.map +1 -1
- package/dist/src/types.d.ts +3 -2
- package/package.json +1 -1
- package/skills/SKILL.md +18 -26
- package/skills/references/autoloop.md +49 -6
- package/skills/references/claude-cli-tracking.md +2 -1
- package/skills/references/cli.md +2 -2
- package/skills/references/council.md +1 -1
- package/skills/references/getting-started.md +4 -4
- package/skills/references/multi-engine.md +21 -34
- package/skills/references/openai-compat.md +1 -1
- package/skills/references/sessions.md +3 -3
- package/skills/references/tools.md +39 -20
|
@@ -113,7 +113,7 @@ import { PersistentCursorSession } from './persistent-cursor-session.js';
|
|
|
113
113
|
import { PersistentOpencodeSession } from './persistent-opencode-session.js';
|
|
114
114
|
import { PersistentAgySession } from './persistent-agy-session.js';
|
|
115
115
|
import { PersistentCustomSession } from './persistent-custom-session.js';
|
|
116
|
-
import { overrideModelPricing, } from './types.js';
|
|
116
|
+
import { ENGINE_TYPES, overrideModelPricing, } from './types.js';
|
|
117
117
|
import { resolveAlias, isClaudeModel } from './models.js';
|
|
118
118
|
import { isAgyConversationId } from './agy-conversation.js';
|
|
119
119
|
import { Council } from './council.js';
|
|
@@ -135,11 +135,86 @@ import { PERSIST_DISK_TTL_MS, DEBOUNCED_SAVE_MS, CLEANUP_INTERVAL_MS, TURN_TIMEO
|
|
|
135
135
|
// - Autoloop: registry at ~/.claw-orchestrator/autoloop-registry.jsonl
|
|
136
136
|
const DEFAULT_COUNCIL_LOG_DIR = path.join(os.homedir(), '.openclaw', 'council-logs');
|
|
137
137
|
const DEFAULT_AUTOLOOP_REGISTRY = path.join(os.homedir(), '.claw-orchestrator', 'autoloop-registry.jsonl');
|
|
138
|
+
function isStringRecord(value) {
|
|
139
|
+
return (typeof value === 'object' &&
|
|
140
|
+
value !== null &&
|
|
141
|
+
!Array.isArray(value) &&
|
|
142
|
+
Object.values(value).every((entry) => typeof entry === 'string'));
|
|
143
|
+
}
|
|
144
|
+
function validateAutoloopCustomEngine(role, config) {
|
|
145
|
+
const label = role[0].toUpperCase() + role.slice(1);
|
|
146
|
+
const raw = config;
|
|
147
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
148
|
+
throw new Error(`${label} custom engine config must be an object`);
|
|
149
|
+
}
|
|
150
|
+
if (typeof raw.name !== 'string' || !raw.name.trim()) {
|
|
151
|
+
throw new Error(`${label} custom engine config.name must be a non-empty string`);
|
|
152
|
+
}
|
|
153
|
+
if (typeof raw.bin !== 'string' || !raw.bin.trim()) {
|
|
154
|
+
throw new Error(`${label} custom engine config.bin must be a non-empty string`);
|
|
155
|
+
}
|
|
156
|
+
if (typeof raw.args !== 'object' || raw.args === null || Array.isArray(raw.args)) {
|
|
157
|
+
throw new Error(`${label} custom engine config.args must be an object`);
|
|
158
|
+
}
|
|
159
|
+
for (const [key, value] of Object.entries(raw.args)) {
|
|
160
|
+
if (value === undefined)
|
|
161
|
+
continue;
|
|
162
|
+
if (key === 'extra') {
|
|
163
|
+
if (!Array.isArray(value) || value.some((entry) => typeof entry !== 'string')) {
|
|
164
|
+
throw new Error(`${label} custom engine config.args.extra must be an array of strings`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
else if (typeof value !== 'string') {
|
|
168
|
+
throw new Error(`${label} custom engine config.args.${key} must be a string`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (raw.persistent !== undefined && typeof raw.persistent !== 'boolean') {
|
|
172
|
+
throw new Error(`${label} custom engine config.persistent must be a boolean`);
|
|
173
|
+
}
|
|
174
|
+
if (raw.env !== undefined && !isStringRecord(raw.env)) {
|
|
175
|
+
throw new Error(`${label} custom engine config.env must contain only string values`);
|
|
176
|
+
}
|
|
177
|
+
if (raw.permissionModes !== undefined && !isStringRecord(raw.permissionModes)) {
|
|
178
|
+
throw new Error(`${label} custom engine config.permissionModes must contain only string values`);
|
|
179
|
+
}
|
|
180
|
+
if (raw.sanitizePatterns !== undefined &&
|
|
181
|
+
(!Array.isArray(raw.sanitizePatterns) || raw.sanitizePatterns.some((entry) => typeof entry !== 'string'))) {
|
|
182
|
+
throw new Error(`${label} custom engine config.sanitizePatterns must be an array of strings`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function validateAutoloopRole(role, engine, customEngine) {
|
|
186
|
+
const resolved = engine ?? 'claude';
|
|
187
|
+
const label = role[0].toUpperCase() + role.slice(1);
|
|
188
|
+
if (!ENGINE_TYPES.includes(resolved)) {
|
|
189
|
+
throw new Error(`${label} engine '${String(resolved)}' is not supported`);
|
|
190
|
+
}
|
|
191
|
+
if (resolved === 'custom') {
|
|
192
|
+
if (!customEngine)
|
|
193
|
+
throw new Error(`${label} custom engine config is required`);
|
|
194
|
+
validateAutoloopCustomEngine(role, customEngine);
|
|
195
|
+
}
|
|
196
|
+
return resolved;
|
|
197
|
+
}
|
|
138
198
|
/** Append-only registry write. Safe under concurrent writers — append is atomic for short lines. */
|
|
139
199
|
export function appendAutoloopRegistry(file, entry) {
|
|
140
200
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
141
201
|
fs.appendFileSync(file, JSON.stringify(entry) + '\n');
|
|
142
202
|
}
|
|
203
|
+
/**
|
|
204
|
+
* Write the current row for a run, dropping any older rows for the same id.
|
|
205
|
+
*
|
|
206
|
+
* The registry is append-only and `listAutoloopsFromRegistry` dedups on read
|
|
207
|
+
* (newest wins), so correctness never depended on cleanup — but a run now emits
|
|
208
|
+
* a row at start, another on every successful `spawn_subagents`, and another on
|
|
209
|
+
* every resume, none of which were ever removed. The file grew monotonically and
|
|
210
|
+
* every list / resume parses all of it. Callers use this AFTER the operation
|
|
211
|
+
* succeeds, so a failed start still leaves the previous row intact.
|
|
212
|
+
*/
|
|
213
|
+
export function upsertAutoloopRegistry(file, entry) {
|
|
214
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
215
|
+
removeAutoloopFromRegistry(file, entry.run_id);
|
|
216
|
+
fs.appendFileSync(file, JSON.stringify(entry) + '\n');
|
|
217
|
+
}
|
|
143
218
|
/**
|
|
144
219
|
* Read the registry, dedup by run_id (newest entry wins), drop entries whose
|
|
145
220
|
* ledger_dir no longer exists on disk (cleanup of moved/deleted workspaces).
|
|
@@ -356,12 +431,23 @@ export class SessionManager {
|
|
|
356
431
|
const persisted = skipPersist ? undefined : this.persistedSessions.get(name);
|
|
357
432
|
// Unified: only use resumeSessionId (claudeResumeId is an internal alias, not exposed)
|
|
358
433
|
const resumeId = config.resumeSessionId ?? persisted?.claudeSessionId;
|
|
434
|
+
// ORDER IS LOAD-BEARING — do not "fix" it by moving `...config` up.
|
|
435
|
+
//
|
|
436
|
+
// Object spread copies own keys even when their value is `undefined`, so any
|
|
437
|
+
// key the caller sets EXPLICITLY (even to undefined) wins over the resolved
|
|
438
|
+
// fallbacks above it. That is deliberate: the autoloop dispatcher passes
|
|
439
|
+
// `model: undefined` for a non-Claude role to mean "use that engine's own
|
|
440
|
+
// default", which must NOT be replaced by the Claude-shaped global default;
|
|
441
|
+
// likewise `sandboxMode: undefined` means "no sandbox". Callers that simply
|
|
442
|
+
// omit a key (MCP session_start, HTTP /session/start, auto-resume by name)
|
|
443
|
+
// leave it absent, so the persisted/default fallback below still applies.
|
|
359
444
|
const fullConfig = {
|
|
360
445
|
name,
|
|
361
446
|
cwd: config.cwd || persisted?.cwd || process.cwd(),
|
|
362
447
|
permissionMode: config.permissionMode || this.pluginConfig.defaultPermissionMode,
|
|
363
448
|
effort: config.effort || this.pluginConfig.defaultEffort,
|
|
364
449
|
model: config.model || persisted?.model || this.pluginConfig.defaultModel,
|
|
450
|
+
sandboxMode: config.sandboxMode ?? persisted?.sandboxMode,
|
|
365
451
|
...config,
|
|
366
452
|
...(resumeId ? { resumeSessionId: resumeId } : {}),
|
|
367
453
|
};
|
|
@@ -1370,6 +1456,7 @@ export class SessionManager {
|
|
|
1370
1456
|
cwd: managed.cwd,
|
|
1371
1457
|
model: managed.config.resolvedModel || managed.config.model,
|
|
1372
1458
|
engine: managed.config.engine,
|
|
1459
|
+
sandboxMode: managed.config.sandboxMode,
|
|
1373
1460
|
originalCreated: existing?.originalCreated || managed.created,
|
|
1374
1461
|
lastResumed: new Date().toISOString(),
|
|
1375
1462
|
lastActivity: managed.lastActivity,
|
|
@@ -1595,20 +1682,25 @@ export class SessionManager {
|
|
|
1595
1682
|
this._storedResumeId(managed.config.engine, managed.claudeSessionId));
|
|
1596
1683
|
}
|
|
1597
1684
|
/**
|
|
1598
|
-
* Return only IDs that can actually resume the engine.
|
|
1599
|
-
*
|
|
1600
|
-
*
|
|
1685
|
+
* Return only IDs that can actually resume the engine. Agy and Codex expose
|
|
1686
|
+
* harvested conversation/thread IDs; their BaseOneShot sessionId values are
|
|
1687
|
+
* synthetic wrapper identifiers and must never be persisted for resume.
|
|
1601
1688
|
*/
|
|
1602
1689
|
_sessionResumeId(engine, session) {
|
|
1603
1690
|
if (engine === 'agy') {
|
|
1604
1691
|
const conversationId = session.conversationId;
|
|
1605
1692
|
return isAgyConversationId(conversationId) ? conversationId : undefined;
|
|
1606
1693
|
}
|
|
1694
|
+
if (engine === 'codex') {
|
|
1695
|
+
return session.threadId;
|
|
1696
|
+
}
|
|
1607
1697
|
return session.sessionId;
|
|
1608
1698
|
}
|
|
1609
1699
|
_storedResumeId(engine, id) {
|
|
1610
1700
|
if (engine === 'agy')
|
|
1611
1701
|
return isAgyConversationId(id) ? id : undefined;
|
|
1702
|
+
if (engine === 'codex')
|
|
1703
|
+
return id && !/^codex-\d+-/.test(id) ? id : undefined;
|
|
1612
1704
|
return id;
|
|
1613
1705
|
}
|
|
1614
1706
|
_listMdFiles(dir) {
|
|
@@ -2112,6 +2204,14 @@ export class SessionManager {
|
|
|
2112
2204
|
// concurrent autoloopStart recreating the same id (or autoloopChat using a
|
|
2113
2205
|
// dispatcher mid-shutdown) during the async delete window.
|
|
2114
2206
|
_deletingAutoloops = new Set();
|
|
2207
|
+
/**
|
|
2208
|
+
* Runs whose Planner is mid-startup. The delete fence was one-directional:
|
|
2209
|
+
* a start could not race a delete, but a delete COULD race a start — it would
|
|
2210
|
+
* resolve `true`, drop the registry row, and leave the still-starting Planner
|
|
2211
|
+
* session orphaned (no run to stop it, no entry to find it by). Deleting a run
|
|
2212
|
+
* that is still coming up is rejected instead.
|
|
2213
|
+
*/
|
|
2214
|
+
_startingAutoloops = new Set();
|
|
2115
2215
|
/**
|
|
2116
2216
|
* Start a v2 autoloop in chat mode. Creates the Planner persistent session,
|
|
2117
2217
|
* returns the run handle. Coder/Reviewer are NOT started until S3's
|
|
@@ -2124,6 +2224,15 @@ export class SessionManager {
|
|
|
2124
2224
|
if (this._deletingAutoloops.has(opts.runId)) {
|
|
2125
2225
|
throw new Error(`Autoloop with id '${opts.runId}' is being deleted`);
|
|
2126
2226
|
}
|
|
2227
|
+
const plannerEngine = validateAutoloopRole('planner', opts.plannerEngine, opts.plannerCustomEngine);
|
|
2228
|
+
const coderEngine = validateAutoloopRole('coder', opts.coderEngine, opts.coderCustomEngine);
|
|
2229
|
+
const reviewerEngine = validateAutoloopRole('reviewer', opts.reviewerEngine, opts.reviewerCustomEngine);
|
|
2230
|
+
for (const role of ['planner', 'coder', 'reviewer']) {
|
|
2231
|
+
const sessionName = `autoloop-${opts.runId}-${role}`;
|
|
2232
|
+
if (this.sessions.has(sessionName) || this._pendingSessions.has(sessionName)) {
|
|
2233
|
+
throw new Error(`Autoloop session name '${sessionName}' is already in use`);
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2127
2236
|
const ledgerDir = path.join(opts.workspace, 'tasks', opts.runId);
|
|
2128
2237
|
if (!fs.existsSync(ledgerDir)) {
|
|
2129
2238
|
fs.mkdirSync(ledgerDir, { recursive: true });
|
|
@@ -2139,7 +2248,15 @@ export class SessionManager {
|
|
|
2139
2248
|
runId: opts.runId,
|
|
2140
2249
|
workspace: opts.workspace,
|
|
2141
2250
|
plannerPromptPath: opts.plannerPromptPath,
|
|
2251
|
+
plannerEngine,
|
|
2142
2252
|
plannerModel: opts.plannerModel,
|
|
2253
|
+
plannerCustomEngine: opts.plannerCustomEngine,
|
|
2254
|
+
coderEngine,
|
|
2255
|
+
coderModel: opts.coderModel,
|
|
2256
|
+
coderCustomEngine: opts.coderCustomEngine,
|
|
2257
|
+
reviewerEngine,
|
|
2258
|
+
reviewerModel: opts.reviewerModel,
|
|
2259
|
+
reviewerCustomEngine: opts.reviewerCustomEngine,
|
|
2143
2260
|
sendTimeoutMs: opts.sendTimeoutMs,
|
|
2144
2261
|
logger: this.logger,
|
|
2145
2262
|
pushPolicyRef: pushPolicy,
|
|
@@ -2148,6 +2265,26 @@ export class SessionManager {
|
|
|
2148
2265
|
await dispatcherRef?.spawnSubagents(args);
|
|
2149
2266
|
runnerRef?.markSubagentsSpawned();
|
|
2150
2267
|
},
|
|
2268
|
+
onRoleSelectionChanged: async (selection) => {
|
|
2269
|
+
try {
|
|
2270
|
+
upsertAutoloopRegistry(DEFAULT_AUTOLOOP_REGISTRY, {
|
|
2271
|
+
run_id: runId,
|
|
2272
|
+
workspace: opts.workspace,
|
|
2273
|
+
ledger_dir: ledgerDir,
|
|
2274
|
+
started_at: runnerRef?.state.started_at ?? new Date().toISOString(),
|
|
2275
|
+
planner_session: dispatcherRef?.sessionNames.planner ?? `autoloop-${runId}-planner`,
|
|
2276
|
+
planner_engine: plannerEngine,
|
|
2277
|
+
planner_model: opts.plannerModel,
|
|
2278
|
+
coder_engine: selection.coder.engine,
|
|
2279
|
+
coder_model: selection.coder.model,
|
|
2280
|
+
reviewer_engine: selection.reviewer.engine,
|
|
2281
|
+
reviewer_model: selection.reviewer.model,
|
|
2282
|
+
});
|
|
2283
|
+
}
|
|
2284
|
+
catch (err) {
|
|
2285
|
+
this.logger.warn?.(`[autoloop/${runId}] registry update after spawn failed: ${err.message}`);
|
|
2286
|
+
}
|
|
2287
|
+
},
|
|
2151
2288
|
};
|
|
2152
2289
|
const dispatcher = new ClaudeAgentDispatcher(dispatcherConfig);
|
|
2153
2290
|
dispatcherRef = dispatcher;
|
|
@@ -2184,17 +2321,40 @@ export class SessionManager {
|
|
|
2184
2321
|
ledgerDir,
|
|
2185
2322
|
pushPolicy,
|
|
2186
2323
|
});
|
|
2187
|
-
|
|
2324
|
+
this._startingAutoloops.add(opts.runId);
|
|
2325
|
+
try {
|
|
2326
|
+
await runner.start();
|
|
2327
|
+
}
|
|
2328
|
+
catch (err) {
|
|
2329
|
+
this.autoloops.delete(opts.runId);
|
|
2330
|
+
try {
|
|
2331
|
+
await dispatcher.shutdown('start-failed', { purge: true });
|
|
2332
|
+
}
|
|
2333
|
+
catch (cleanupErr) {
|
|
2334
|
+
this.logger.warn?.(`[autoloop/${runId}] cleanup after failed start failed: ${cleanupErr.message}`);
|
|
2335
|
+
}
|
|
2336
|
+
runner.stop();
|
|
2337
|
+
throw err;
|
|
2338
|
+
}
|
|
2339
|
+
finally {
|
|
2340
|
+
this._startingAutoloops.delete(opts.runId);
|
|
2341
|
+
}
|
|
2188
2342
|
// Record into the cross-process registry so the dashboard / another
|
|
2189
2343
|
// SessionManager instance can list this run even after it ends. Best
|
|
2190
2344
|
// effort — registry failure should not block the run.
|
|
2191
2345
|
try {
|
|
2192
|
-
|
|
2346
|
+
upsertAutoloopRegistry(DEFAULT_AUTOLOOP_REGISTRY, {
|
|
2193
2347
|
run_id: opts.runId,
|
|
2194
2348
|
workspace: opts.workspace,
|
|
2195
2349
|
ledger_dir: ledgerDir,
|
|
2196
2350
|
started_at: runner.state.started_at,
|
|
2197
2351
|
planner_session: dispatcher.sessionNames.planner,
|
|
2352
|
+
planner_engine: plannerEngine,
|
|
2353
|
+
planner_model: opts.plannerModel,
|
|
2354
|
+
coder_engine: coderEngine,
|
|
2355
|
+
coder_model: opts.coderModel,
|
|
2356
|
+
reviewer_engine: reviewerEngine,
|
|
2357
|
+
reviewer_model: opts.reviewerModel,
|
|
2198
2358
|
});
|
|
2199
2359
|
}
|
|
2200
2360
|
catch (err) {
|
|
@@ -2316,27 +2476,33 @@ export class SessionManager {
|
|
|
2316
2476
|
* is still served via /autoloop/<id>/chat_history so the dashboard can
|
|
2317
2477
|
* replay the conversation visually.
|
|
2318
2478
|
*/
|
|
2319
|
-
async autoloopResume(runId) {
|
|
2479
|
+
async autoloopResume(runId, opts = {}) {
|
|
2320
2480
|
const existing = this.autoloops.get(runId);
|
|
2321
2481
|
if (existing)
|
|
2322
2482
|
return existing.runner.state;
|
|
2323
2483
|
const entry = listAutoloopsFromRegistry().find((e) => e.run_id === runId);
|
|
2324
2484
|
if (!entry)
|
|
2325
2485
|
throw new Error(`Autoloop run '${runId}' not found in registry`);
|
|
2326
|
-
//
|
|
2327
|
-
//
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
catch (err) {
|
|
2335
|
-
this.logger.warn?.(`[autoloop/${runId}] registry scrub during resume failed: ${err.message}`);
|
|
2336
|
-
}
|
|
2486
|
+
// Validate the full restart configuration before touching the registry.
|
|
2487
|
+
// Old rows omit these fields and intentionally recover the legacy Claude defaults.
|
|
2488
|
+
const plannerEngine = validateAutoloopRole('planner', entry.planner_engine, opts.plannerCustomEngine);
|
|
2489
|
+
const coderEngine = validateAutoloopRole('coder', entry.coder_engine, opts.coderCustomEngine);
|
|
2490
|
+
const reviewerEngine = validateAutoloopRole('reviewer', entry.reviewer_engine, opts.reviewerCustomEngine);
|
|
2491
|
+
// The registry is append-only and newest entry wins. Leave the prior row
|
|
2492
|
+
// untouched while starting so a transient failure cannot erase or restore
|
|
2493
|
+
// stale cross-process state. A successful start appends the replacement.
|
|
2337
2494
|
return (await this.autoloopStart({
|
|
2338
2495
|
runId: entry.run_id,
|
|
2339
2496
|
workspace: entry.workspace,
|
|
2497
|
+
plannerEngine,
|
|
2498
|
+
plannerModel: entry.planner_model,
|
|
2499
|
+
plannerCustomEngine: opts.plannerCustomEngine,
|
|
2500
|
+
coderEngine,
|
|
2501
|
+
coderModel: entry.coder_model,
|
|
2502
|
+
coderCustomEngine: opts.coderCustomEngine,
|
|
2503
|
+
reviewerEngine,
|
|
2504
|
+
reviewerModel: entry.reviewer_model,
|
|
2505
|
+
reviewerCustomEngine: opts.reviewerCustomEngine,
|
|
2340
2506
|
})).state;
|
|
2341
2507
|
}
|
|
2342
2508
|
/**
|
|
@@ -2349,6 +2515,12 @@ export class SessionManager {
|
|
|
2349
2515
|
* Returns true if anything was removed (in-memory entry OR registry row).
|
|
2350
2516
|
*/
|
|
2351
2517
|
async autoloopDelete(runId) {
|
|
2518
|
+
// Refuse to tear down a run that is still coming up: its Planner session is
|
|
2519
|
+
// mid-startSession, so deleting now would drop the registry row and orphan a
|
|
2520
|
+
// session that finishes starting a moment later.
|
|
2521
|
+
if (this._startingAutoloops.has(runId)) {
|
|
2522
|
+
throw new Error(`Autoloop with id '${runId}' is still starting`);
|
|
2523
|
+
}
|
|
2352
2524
|
// Fence the async teardown so a concurrent start/chat can't race on this id.
|
|
2353
2525
|
this._deletingAutoloops.add(runId);
|
|
2354
2526
|
try {
|