@enderfga/claw-orchestrator 7.14.0 → 7.15.0
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 +160 -135
- package/configs/autoloop-reviewer-prompt.md +1 -1
- package/configs/engines/README.md +31 -11
- package/dist/bin/acp-server.js +7 -12
- package/dist/bin/acp-server.js.map +1 -1
- package/dist/bin/cli.js +71 -40
- package/dist/bin/cli.js.map +1 -1
- package/dist/bin/mcp-server.js +2 -54
- package/dist/bin/mcp-server.js.map +1 -1
- package/dist/src/acp-server.d.ts +17 -9
- package/dist/src/acp-server.js +149 -30
- package/dist/src/acp-server.js.map +1 -1
- package/dist/src/autoloop/agent-tools.d.ts +2 -0
- package/dist/src/autoloop/agent-tools.js +7 -0
- package/dist/src/autoloop/agent-tools.js.map +1 -1
- package/dist/src/autoloop/dispatcher.d.ts +6 -0
- package/dist/src/autoloop/dispatcher.js +23 -1
- package/dist/src/autoloop/dispatcher.js.map +1 -1
- package/dist/src/autoloop/planner-tools.js +2 -0
- package/dist/src/autoloop/planner-tools.js.map +1 -1
- package/dist/src/council.d.ts +8 -0
- package/dist/src/council.js +45 -12
- package/dist/src/council.js.map +1 -1
- package/dist/src/dashboard/index.html +28 -14
- package/dist/src/embedded-server.js +27 -8
- package/dist/src/embedded-server.js.map +1 -1
- package/dist/src/engine-spawn.js +3 -1
- package/dist/src/engine-spawn.js.map +1 -1
- package/dist/src/fanout.d.ts +13 -3
- package/dist/src/fanout.js +10 -0
- package/dist/src/fanout.js.map +1 -1
- package/dist/src/inbox-manager.d.ts +9 -0
- package/dist/src/inbox-manager.js +8 -2
- package/dist/src/inbox-manager.js.map +1 -1
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +117 -51
- package/dist/src/index.js.map +1 -1
- package/dist/src/kernel/conditions.d.ts +1 -1
- package/dist/src/kernel/conditions.js +1 -1
- package/dist/src/kernel/engine.d.ts +28 -8
- package/dist/src/kernel/engine.js +93 -19
- package/dist/src/kernel/engine.js.map +1 -1
- package/dist/src/kernel/nodes/agent.js +2 -0
- package/dist/src/kernel/nodes/agent.js.map +1 -1
- package/dist/src/kernel/nodes/council.js +9 -4
- package/dist/src/kernel/nodes/council.js.map +1 -1
- package/dist/src/kernel/nodes/fanout.js +5 -0
- package/dist/src/kernel/nodes/fanout.js.map +1 -1
- package/dist/src/kernel/nodes/goal.d.ts +8 -1
- package/dist/src/kernel/nodes/goal.js +40 -19
- package/dist/src/kernel/nodes/goal.js.map +1 -1
- package/dist/src/kernel/nodes/subflow.js +9 -1
- package/dist/src/kernel/nodes/subflow.js.map +1 -1
- package/dist/src/kernel/nodes/ultraapp.d.ts +1 -0
- package/dist/src/kernel/nodes/ultraapp.js +1 -0
- package/dist/src/kernel/nodes/ultraapp.js.map +1 -1
- package/dist/src/kernel/nodes/verifier.js +5 -1
- package/dist/src/kernel/nodes/verifier.js.map +1 -1
- package/dist/src/kernel/projections.d.ts +1 -0
- package/dist/src/kernel/projections.js +7 -1
- package/dist/src/kernel/projections.js.map +1 -1
- package/dist/src/kernel/templates/index.d.ts +6 -3
- package/dist/src/kernel/templates/index.js +8 -3
- package/dist/src/kernel/templates/index.js.map +1 -1
- package/dist/src/kernel/types.d.ts +30 -10
- package/dist/src/kernel/types.js.map +1 -1
- package/dist/src/logger.d.ts +6 -0
- package/dist/src/logger.js +14 -0
- package/dist/src/logger.js.map +1 -1
- package/dist/src/mcp-annotations.d.ts +13 -0
- package/dist/src/mcp-annotations.js +100 -0
- package/dist/src/mcp-annotations.js.map +1 -0
- package/dist/src/models.d.ts +7 -0
- package/dist/src/models.js +9 -0
- package/dist/src/models.js.map +1 -1
- package/dist/src/openai-compat.d.ts +11 -5
- package/dist/src/openai-compat.js +55 -54
- package/dist/src/openai-compat.js.map +1 -1
- package/dist/src/persistent-acp-session.d.ts +7 -0
- package/dist/src/persistent-acp-session.js +26 -2
- package/dist/src/persistent-acp-session.js.map +1 -1
- package/dist/src/persistent-codex-app-session.d.ts +2 -0
- package/dist/src/persistent-codex-app-session.js +18 -9
- package/dist/src/persistent-codex-app-session.js.map +1 -1
- package/dist/src/persistent-codex-session.d.ts +14 -3
- package/dist/src/persistent-codex-session.js +22 -19
- package/dist/src/persistent-codex-session.js.map +1 -1
- package/dist/src/persistent-custom-session.d.ts +9 -0
- package/dist/src/persistent-custom-session.js +41 -21
- package/dist/src/persistent-custom-session.js.map +1 -1
- package/dist/src/persistent-grok-session.js +1 -1
- package/dist/src/persistent-grok-session.js.map +1 -1
- package/dist/src/run-ledger.d.ts +3 -3
- package/dist/src/session-manager.d.ts +34 -1
- package/dist/src/session-manager.js +160 -30
- package/dist/src/session-manager.js.map +1 -1
- package/dist/src/types.d.ts +34 -11
- package/dist/src/types.js.map +1 -1
- package/dist/src/ultraapp/build.d.ts +5 -11
- package/dist/src/ultraapp/build.js +5 -11
- package/dist/src/ultraapp/build.js.map +1 -1
- package/dist/src/ultraapp/council-adapter.d.ts +5 -2
- package/dist/src/ultraapp/council-adapter.js +6 -6
- package/dist/src/ultraapp/council-adapter.js.map +1 -1
- package/dist/src/ultraapp/files.d.ts +7 -0
- package/dist/src/ultraapp/files.js +7 -1
- package/dist/src/ultraapp/files.js.map +1 -1
- package/dist/src/ultraapp/manager.d.ts +23 -3
- package/dist/src/ultraapp/manager.js +170 -72
- package/dist/src/ultraapp/manager.js.map +1 -1
- package/dist/src/ultraapp/narrator.js +1 -1
- package/dist/src/ultraapp/narrator.js.map +1 -1
- package/dist/src/ultraapp/versions.d.ts +27 -5
- package/dist/src/ultraapp/versions.js +58 -13
- package/dist/src/ultraapp/versions.js.map +1 -1
- package/openclaw.plugin.json +4 -13
- package/package.json +13 -6
- package/skills/SKILL.md +52 -24
- package/skills/references/acp.md +140 -61
- package/skills/references/autoloop.md +141 -35
- package/skills/references/claude-cli-tracking.md +8 -6
- package/skills/references/cli.md +122 -93
- package/skills/references/council.md +44 -22
- package/skills/references/dashboard.md +37 -21
- package/skills/references/getting-started.md +49 -15
- package/skills/references/inbox.md +13 -8
- package/skills/references/mcp.md +25 -17
- package/skills/references/multi-engine.md +161 -145
- package/skills/references/observability.md +90 -58
- package/skills/references/openai-compat.md +172 -82
- package/skills/references/sessions.md +101 -65
- package/skills/references/tools.md +209 -158
- package/skills/references/ultra.md +7 -7
- package/skills/references/ultraapp.md +101 -56
- package/skills/references/verification.md +112 -56
- package/skills/references/workflow.md +227 -93
- package/assets/banner.jpg +0 -0
|
@@ -115,7 +115,7 @@ import { detectRepoLang } from './kernel/repo.js';
|
|
|
115
115
|
import { RunKernel, runDir as kernelRunDir } from './kernel/engine.js';
|
|
116
116
|
import { registerDefaultExecutors } from './kernel/nodes/index.js';
|
|
117
117
|
import { autoloopStateFromRecord, makeAutoloopExecutor } from './kernel/nodes/autoloop.js';
|
|
118
|
-
import { isValidRunId, loadRun, readNodeOutput } from './kernel/store.js';
|
|
118
|
+
import { isValidRunId, loadRun, readNodeOutput, runExists } from './kernel/store.js';
|
|
119
119
|
import { LEGACY_NODE, joinFindings, toCouncilSession, toFanoutSession, toUltraplanResult, toUltrareviewResult, } from './kernel/projections.js';
|
|
120
120
|
import { legacyCouncilWorkflow, legacyFanoutWorkflow, legacyUltraplanWorkflow, splitAgentSecrets, } from './kernel/templates/index.js';
|
|
121
121
|
import { normalizeContract } from './verify/contract.js';
|
|
@@ -139,7 +139,7 @@ import { PersistentCustomSession } from './persistent-custom-session.js';
|
|
|
139
139
|
import { PersistentAcpSession } from './persistent-acp-session.js';
|
|
140
140
|
import { resolveCustomEngine } from './engine-presets.js';
|
|
141
141
|
import { EFFORT_LEVELS, ENGINE_TYPES, overrideModelPricing, ENGINE_BINARY_NAMES, } from './types.js';
|
|
142
|
-
import { resolveAlias, isClaudeModel, lookupModel } from './models.js';
|
|
142
|
+
import { resolveAlias, isClaudeModel, lookupModel, resolveEngineAndModel } from './models.js';
|
|
143
143
|
import { isAgyConversationId } from './agy-conversation.js';
|
|
144
144
|
import { Council } from './council.js';
|
|
145
145
|
import { AutoloopRunner } from './autoloop/runner.js';
|
|
@@ -217,6 +217,15 @@ function readStoredAutoloopResumeContext(workspace, runId, originalSendTimeoutMs
|
|
|
217
217
|
}
|
|
218
218
|
return { effectiveSendTimeoutMs, pendingDispatch };
|
|
219
219
|
}
|
|
220
|
+
/** A process's command line as `ps` prints it, or undefined when it cannot be read. */
|
|
221
|
+
function processCommand(pid) {
|
|
222
|
+
try {
|
|
223
|
+
return execFileSync('ps', ['-p', String(pid), '-o', 'command='], { encoding: 'utf8', timeout: 3_000 }).trim();
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
return undefined;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
220
229
|
function validateSendTimeoutIncrease(value, current) {
|
|
221
230
|
validateAutoloopTimeoutConfig({ sendTimeoutMs: value });
|
|
222
231
|
if (value <= current) {
|
|
@@ -319,8 +328,24 @@ function validateAutoloopRole(role, engine, customEngine) {
|
|
|
319
328
|
throw new Error(`${label} custom engine config is required`);
|
|
320
329
|
validateAutoloopCustomEngine(role, customEngine);
|
|
321
330
|
}
|
|
331
|
+
// An Autoloop role is configured by engine name alone and has no field for the
|
|
332
|
+
// agent an `acp` engine starts, so the session could only fail later.
|
|
333
|
+
if (resolved === 'acp')
|
|
334
|
+
throw new Error(`${label} engine 'acp' is not supported by Autoloop`);
|
|
322
335
|
return resolved;
|
|
323
336
|
}
|
|
337
|
+
/**
|
|
338
|
+
* The plugin's `defaultModel` for a session on `engine` that names no model, or
|
|
339
|
+
* undefined to let the engine run its own default. Engines do not share a model
|
|
340
|
+
* namespace, so the default goes only to an engine that runs it; the claude
|
|
341
|
+
* engine takes any model, proxying a non-Claude one to the gateway.
|
|
342
|
+
*/
|
|
343
|
+
function defaultModelFor(engine, defaultModel) {
|
|
344
|
+
if (!defaultModel || engine === 'claude')
|
|
345
|
+
return defaultModel;
|
|
346
|
+
const owner = resolveEngineAndModel(defaultModel).engine;
|
|
347
|
+
return owner === (engine === 'codex-app' ? 'codex' : engine) ? defaultModel : undefined;
|
|
348
|
+
}
|
|
324
349
|
function validateAutoloopEffort(role, effort) {
|
|
325
350
|
if (effort === undefined)
|
|
326
351
|
return;
|
|
@@ -789,6 +814,12 @@ export class SessionManager {
|
|
|
789
814
|
/** In-flight proxy startup, so concurrent callers share one server. */
|
|
790
815
|
_proxyStartPromise = null;
|
|
791
816
|
_activePids = new Map();
|
|
817
|
+
/**
|
|
818
|
+
* Each recorded pid's command line when it was recorded. An orphan is killed
|
|
819
|
+
* only if it is still that process: this is what lets cleanup recognise an
|
|
820
|
+
* ACP agent or a custom engine, whose binary is no fixed name.
|
|
821
|
+
*/
|
|
822
|
+
_pidCommands = new Map();
|
|
792
823
|
_circuitBreaker = new CircuitBreaker();
|
|
793
824
|
_inbox = new InboxManager();
|
|
794
825
|
/** cwd → detected language, so the manifest probe runs once per directory. */
|
|
@@ -929,6 +960,7 @@ export class SessionManager {
|
|
|
929
960
|
const persisted = skipPersist ? undefined : this.persistedSessions.get(name);
|
|
930
961
|
// Unified: only use resumeSessionId (claudeResumeId is an internal alias, not exposed)
|
|
931
962
|
const resumeId = config.resumeSessionId ?? persisted?.claudeSessionId;
|
|
963
|
+
const engine = config.engine || persisted?.engine || 'claude';
|
|
932
964
|
// ORDER IS LOAD-BEARING — do not "fix" it by moving `...config` up.
|
|
933
965
|
//
|
|
934
966
|
// Object spread copies own keys even when their value is `undefined`, so any
|
|
@@ -944,7 +976,7 @@ export class SessionManager {
|
|
|
944
976
|
cwd: config.cwd || persisted?.cwd || process.cwd(),
|
|
945
977
|
permissionMode: config.permissionMode || this.pluginConfig.defaultPermissionMode,
|
|
946
978
|
effort: config.effort || this.pluginConfig.defaultEffort,
|
|
947
|
-
model: config.model || persisted?.model || this.pluginConfig.defaultModel,
|
|
979
|
+
model: config.model || persisted?.model || defaultModelFor(engine, this.pluginConfig.defaultModel),
|
|
948
980
|
sandboxMode: config.sandboxMode ?? persisted?.sandboxMode,
|
|
949
981
|
autoCompactPercent: config.autoCompactPercent ?? persisted?.autoCompactPercent,
|
|
950
982
|
chrome: config.chrome ?? persisted?.chrome,
|
|
@@ -955,16 +987,15 @@ export class SessionManager {
|
|
|
955
987
|
if (fullConfig.model) {
|
|
956
988
|
fullConfig.resolvedModel = this._resolveModel(fullConfig.model, fullConfig.modelOverrides);
|
|
957
989
|
}
|
|
958
|
-
// Auto-inject proxy baseUrl for non-Claude models on the claude engine.
|
|
959
|
-
// Starts a local proxy server that converts Anthropic → OpenAI format
|
|
960
|
-
// and forwards to the OpenClaw gateway. Zero config required.
|
|
961
|
-
const engine = fullConfig.engine || persisted?.engine || 'claude';
|
|
962
990
|
// Write the resolved engine back so downstream consumers of the managed
|
|
963
991
|
// config (agy resume-id lookups, _persistSession's registry entry) see the
|
|
964
992
|
// real engine even when it came from the persisted registry.
|
|
965
993
|
fullConfig.engine = engine;
|
|
966
994
|
// Circuit breaker — reject early if engine is in backoff
|
|
967
995
|
this._circuitBreaker.check(engine);
|
|
996
|
+
// Auto-inject proxy baseUrl for non-Claude models on the claude engine.
|
|
997
|
+
// Starts a local proxy server that converts Anthropic → OpenAI format
|
|
998
|
+
// and forwards to the OpenClaw gateway. Zero config required.
|
|
968
999
|
if (engine === 'claude' && fullConfig.resolvedModel && !fullConfig.baseUrl) {
|
|
969
1000
|
if (!isClaudeModel(fullConfig.resolvedModel)) {
|
|
970
1001
|
const proxyPort = await this._ensureProxyServer();
|
|
@@ -987,6 +1018,9 @@ export class SessionManager {
|
|
|
987
1018
|
// Track child process PID for orphan cleanup
|
|
988
1019
|
if (session.pid) {
|
|
989
1020
|
this._activePids.set(name, session.pid);
|
|
1021
|
+
const command = processCommand(session.pid);
|
|
1022
|
+
if (command)
|
|
1023
|
+
this._pidCommands.set(session.pid, command);
|
|
990
1024
|
this._savePids();
|
|
991
1025
|
}
|
|
992
1026
|
const managed = {
|
|
@@ -1008,6 +1042,10 @@ export class SessionManager {
|
|
|
1008
1042
|
return this._toSessionInfo(name, managed);
|
|
1009
1043
|
}
|
|
1010
1044
|
async sendMessage(name, message, options = {}) {
|
|
1045
|
+
return this._sendTurn(name, message, options);
|
|
1046
|
+
}
|
|
1047
|
+
/** `onDispatch` fires once the turn has passed every check and is handed to the engine. */
|
|
1048
|
+
async _sendTurn(name, message, options, onDispatch) {
|
|
1011
1049
|
const managed = this._getSession(name);
|
|
1012
1050
|
// Per-session serialization. Two concurrent sendMessage() calls on the
|
|
1013
1051
|
// same session previously raced on PersistentClaudeSession._streamCallbacks
|
|
@@ -1103,7 +1141,14 @@ export class SessionManager {
|
|
|
1103
1141
|
// A session handed off from another engine carries that conversation in
|
|
1104
1142
|
// front of its first message; after that the engine holds it itself.
|
|
1105
1143
|
const outgoing = managed.pendingHandoff ? `${managed.pendingHandoff}\n\n${message}` : message;
|
|
1144
|
+
onDispatch?.();
|
|
1106
1145
|
const result = await managed.session.send(outgoing, sendOpts);
|
|
1146
|
+
const after = this._statsSnapshot(managed);
|
|
1147
|
+
const usage = {
|
|
1148
|
+
tokensIn: Math.max(0, after.tokensIn - ledgerBefore.tokensIn),
|
|
1149
|
+
tokensOut: Math.max(0, after.tokensOut - ledgerBefore.tokensOut),
|
|
1150
|
+
costUsd: Math.max(0, after.costUsd - ledgerBefore.costUsd),
|
|
1151
|
+
};
|
|
1107
1152
|
// Update the resume-capable session ID if available (skip disk persist
|
|
1108
1153
|
// for ephemeral sessions that were started with skipPersistence)
|
|
1109
1154
|
const resumableId = this._managedResumeId(managed);
|
|
@@ -1145,11 +1190,12 @@ export class SessionManager {
|
|
|
1145
1190
|
sessionId: this._managedResumeId(managed),
|
|
1146
1191
|
error: turnError,
|
|
1147
1192
|
events: [],
|
|
1193
|
+
usage,
|
|
1148
1194
|
...(permissionDenials.length ? { permissionDenials } : {}),
|
|
1149
1195
|
...(images.length ? { images } : {}),
|
|
1150
1196
|
};
|
|
1151
1197
|
}
|
|
1152
|
-
return { output: '', sessionId: this._managedResumeId(managed), events: [] };
|
|
1198
|
+
return { output: '', sessionId: this._managedResumeId(managed), events: [], usage };
|
|
1153
1199
|
}
|
|
1154
1200
|
catch (err) {
|
|
1155
1201
|
turnError = err.message;
|
|
@@ -1219,6 +1265,7 @@ export class SessionManager {
|
|
|
1219
1265
|
engine: opts.engine,
|
|
1220
1266
|
...(opts.model ? { model: opts.model } : {}),
|
|
1221
1267
|
...(opts.customEngine ? { customEngine: opts.customEngine } : {}),
|
|
1268
|
+
...(opts.acpAgent ? { acpAgent: opts.acpAgent } : {}),
|
|
1222
1269
|
});
|
|
1223
1270
|
const target = this._getSession(targetName);
|
|
1224
1271
|
// The new session's record starts as the source's, so a second handoff from
|
|
@@ -1534,6 +1581,11 @@ export class SessionManager {
|
|
|
1534
1581
|
if (!contract)
|
|
1535
1582
|
throw new Error('verifyRun requires a contract with at least one recognised check');
|
|
1536
1583
|
const runId = args.label || `verify-${Date.now().toString(36)}`;
|
|
1584
|
+
// The bundle goes under the run store, keyed by the label. A label naming a
|
|
1585
|
+
// workflow run would plant this evidence in that run's directory.
|
|
1586
|
+
if (runExists(runId)) {
|
|
1587
|
+
throw new Error(`verify_run label '${runId}' names an existing workflow run; choose another label`);
|
|
1588
|
+
}
|
|
1537
1589
|
const dir = kernelRunDir(runId);
|
|
1538
1590
|
const evidenceId = 'verify-01';
|
|
1539
1591
|
const { results, rounds } = await runContract(contract, {
|
|
@@ -1560,7 +1612,7 @@ export class SessionManager {
|
|
|
1560
1612
|
managed.session.stop();
|
|
1561
1613
|
this.sessions.delete(name);
|
|
1562
1614
|
// Remove PID tracking
|
|
1563
|
-
this.
|
|
1615
|
+
this._forgetPid(name);
|
|
1564
1616
|
this._savePids();
|
|
1565
1617
|
if (!opts.keepPersisted) {
|
|
1566
1618
|
// Explicit stop = user intent to end session — remove from disk too.
|
|
@@ -1569,6 +1621,9 @@ export class SessionManager {
|
|
|
1569
1621
|
// Claude conversation) pass keepPersisted: true.
|
|
1570
1622
|
this._forgetPersisted(name);
|
|
1571
1623
|
this._savePersisted();
|
|
1624
|
+
// Its unread mail was for this conversation; a later session that takes
|
|
1625
|
+
// the name is a different one and must not receive it.
|
|
1626
|
+
this._inbox.clear(name);
|
|
1572
1627
|
}
|
|
1573
1628
|
}
|
|
1574
1629
|
hasSession(name) {
|
|
@@ -1961,6 +2016,7 @@ export class SessionManager {
|
|
|
1961
2016
|
this.sessions.clear();
|
|
1962
2017
|
// Clear PID tracking
|
|
1963
2018
|
this._activePids.clear();
|
|
2019
|
+
this._pidCommands.clear();
|
|
1964
2020
|
this._savePids();
|
|
1965
2021
|
// Stop proxy server
|
|
1966
2022
|
if (this._proxyServer) {
|
|
@@ -2436,6 +2492,12 @@ export class SessionManager {
|
|
|
2436
2492
|
}
|
|
2437
2493
|
// ─── PID Tracking ──────────────────────────────────────────────────────
|
|
2438
2494
|
static PID_FILE = path.join(os.homedir(), '.openclaw', 'session-pids.json');
|
|
2495
|
+
_forgetPid(name) {
|
|
2496
|
+
const pid = this._activePids.get(name);
|
|
2497
|
+
if (pid !== undefined)
|
|
2498
|
+
this._pidCommands.delete(pid);
|
|
2499
|
+
this._activePids.delete(name);
|
|
2500
|
+
}
|
|
2439
2501
|
_savePids() {
|
|
2440
2502
|
try {
|
|
2441
2503
|
const dir = path.dirname(SessionManager.PID_FILE);
|
|
@@ -2479,11 +2541,13 @@ export class SessionManager {
|
|
|
2479
2541
|
pid: entry.pid,
|
|
2480
2542
|
ownerPid: entry.ownerPid,
|
|
2481
2543
|
since: entry.since ?? now,
|
|
2544
|
+
...(typeof entry.command === 'string' ? { command: entry.command } : {}),
|
|
2482
2545
|
};
|
|
2483
2546
|
}
|
|
2484
2547
|
// Add OUR current entries
|
|
2485
2548
|
for (const [name, pid] of this._activePids) {
|
|
2486
|
-
|
|
2549
|
+
const command = this._pidCommands.get(pid);
|
|
2550
|
+
merged[name] = { pid, ownerPid: process.pid, since: now, ...(command ? { command } : {}) };
|
|
2487
2551
|
}
|
|
2488
2552
|
fs.writeFileSync(SessionManager.PID_FILE, JSON.stringify(merged));
|
|
2489
2553
|
}
|
|
@@ -2495,7 +2559,7 @@ export class SessionManager {
|
|
|
2495
2559
|
* Verify that a PID belongs to a known coding CLI before killing it.
|
|
2496
2560
|
* Prevents killing unrelated processes if the OS recycled the PID.
|
|
2497
2561
|
*/
|
|
2498
|
-
_isKnownCliProcess(pid) {
|
|
2562
|
+
_isKnownCliProcess(pid, recordedCommand) {
|
|
2499
2563
|
// Match known CLI binaries by basename to avoid false positives
|
|
2500
2564
|
// (e.g., 'agent' must not match 'ssh-agent' or 'gpg-agent')
|
|
2501
2565
|
// Anchor each name to executable/path position ((?:^|[/\s])name(?:[\s/]|$))
|
|
@@ -2510,16 +2574,14 @@ export class SessionManager {
|
|
|
2510
2574
|
...ENGINE_BINARY_NAMES.map((bin) => new RegExp(`(?:^|[/\\s])${bin}(?:[\\s/]|$)`)),
|
|
2511
2575
|
/(?:^|\/)agent(?:[\s/]|$)/, // 'agent' only as executable/after a slash (not ssh-agent)
|
|
2512
2576
|
];
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
encoding: 'utf8',
|
|
2516
|
-
timeout: 3_000,
|
|
2517
|
-
}).trim();
|
|
2518
|
-
return knownPatterns.some((pattern) => pattern.test(cmd));
|
|
2519
|
-
}
|
|
2520
|
-
catch {
|
|
2577
|
+
const cmd = processCommand(pid);
|
|
2578
|
+
if (cmd === undefined)
|
|
2521
2579
|
return false; // ps failed — process likely dead or not accessible
|
|
2522
|
-
|
|
2580
|
+
// The same command line as when it was recorded: the process itself, not a
|
|
2581
|
+
// recycled pid — whatever binary it runs.
|
|
2582
|
+
if (recordedCommand && cmd === recordedCommand)
|
|
2583
|
+
return true;
|
|
2584
|
+
return knownPatterns.some((pattern) => pattern.test(cmd));
|
|
2523
2585
|
}
|
|
2524
2586
|
_cleanupOrphanedPids() {
|
|
2525
2587
|
try {
|
|
@@ -2530,6 +2592,7 @@ export class SessionManager {
|
|
|
2530
2592
|
// Resolve entry shape: legacy = number; current = { pid, ownerPid, since }.
|
|
2531
2593
|
let pid;
|
|
2532
2594
|
let ownerPid;
|
|
2595
|
+
let recordedCommand;
|
|
2533
2596
|
if (typeof raw === 'number') {
|
|
2534
2597
|
pid = raw;
|
|
2535
2598
|
ownerPid = null; // unknown owner — treat conservatively (skip kill)
|
|
@@ -2540,6 +2603,7 @@ export class SessionManager {
|
|
|
2540
2603
|
continue;
|
|
2541
2604
|
pid = e.pid;
|
|
2542
2605
|
ownerPid = typeof e.ownerPid === 'number' ? e.ownerPid : null;
|
|
2606
|
+
recordedCommand = typeof e.command === 'string' ? e.command : undefined;
|
|
2543
2607
|
}
|
|
2544
2608
|
else {
|
|
2545
2609
|
continue;
|
|
@@ -2572,7 +2636,7 @@ export class SessionManager {
|
|
|
2572
2636
|
try {
|
|
2573
2637
|
process.kill(pid, 0); // check if alive
|
|
2574
2638
|
// Alive — but verify it's actually a coding CLI, not a recycled PID
|
|
2575
|
-
if (!this._isKnownCliProcess(pid)) {
|
|
2639
|
+
if (!this._isKnownCliProcess(pid, recordedCommand)) {
|
|
2576
2640
|
this.logger.info(`PID ${pid} (session: ${name}) is alive but not a known CLI — skipping kill`);
|
|
2577
2641
|
continue;
|
|
2578
2642
|
}
|
|
@@ -2804,12 +2868,26 @@ export class SessionManager {
|
|
|
2804
2868
|
throw new Error(`Council '${id}' not found`);
|
|
2805
2869
|
throw new Error(`Council '${id}' is ${record.state} and not running in this process — its record is readable, but there is no live round to act on`);
|
|
2806
2870
|
}
|
|
2871
|
+
/**
|
|
2872
|
+
* Abort a council running in this process. One that is not — finished, or
|
|
2873
|
+
* running elsewhere — is left alone and the result says so: its worktrees are
|
|
2874
|
+
* either awaiting accept/reject or in use by the process that runs it.
|
|
2875
|
+
*/
|
|
2807
2876
|
councilAbort(id) {
|
|
2808
2877
|
// Cancel the run first so the kernel stops advancing, then abort the engine
|
|
2809
2878
|
// so the current round tears down its worktrees.
|
|
2810
|
-
if (
|
|
2879
|
+
if (this.kernel.cancel(id)) {
|
|
2880
|
+
this.kernel.handle(id, LEGACY_NODE)?.abort();
|
|
2881
|
+
return { aborted: true };
|
|
2882
|
+
}
|
|
2883
|
+
const record = loadRun(id);
|
|
2884
|
+
if (!record)
|
|
2811
2885
|
throw new Error(`Council '${id}' not found`);
|
|
2812
|
-
|
|
2886
|
+
return {
|
|
2887
|
+
aborted: false,
|
|
2888
|
+
state: record.state,
|
|
2889
|
+
reason: `Council '${id}' is ${record.state} and not running in this process — nothing was aborted`,
|
|
2890
|
+
};
|
|
2813
2891
|
}
|
|
2814
2892
|
councilInject(id, message) {
|
|
2815
2893
|
this._liveCouncil(id).injectMessage(message);
|
|
@@ -2818,10 +2896,34 @@ export class SessionManager {
|
|
|
2818
2896
|
return this._councilForPostProcessing(id).review();
|
|
2819
2897
|
}
|
|
2820
2898
|
async councilAccept(id) {
|
|
2821
|
-
|
|
2899
|
+
const result = await this._councilForPostProcessing(id).accept();
|
|
2900
|
+
this._recordCouncilDecision(id, 'accepted');
|
|
2901
|
+
return result;
|
|
2822
2902
|
}
|
|
2823
2903
|
async councilReject(id, feedback) {
|
|
2824
|
-
|
|
2904
|
+
const result = await this._councilForPostProcessing(id).reject(feedback);
|
|
2905
|
+
this._recordCouncilDecision(id, 'rejected');
|
|
2906
|
+
return result;
|
|
2907
|
+
}
|
|
2908
|
+
/**
|
|
2909
|
+
* Write the decision onto the run, which is where `council_status` reads it.
|
|
2910
|
+
* The `Council` that took it is usually rebuilt from the record and dropped,
|
|
2911
|
+
* so its own status would go nowhere.
|
|
2912
|
+
*/
|
|
2913
|
+
_recordCouncilDecision(id, status) {
|
|
2914
|
+
// A council live in this process carries the status on its own session.
|
|
2915
|
+
if (this.kernel.handle(id, LEGACY_NODE))
|
|
2916
|
+
return;
|
|
2917
|
+
let recorded = false;
|
|
2918
|
+
let why = 'the run has not finished';
|
|
2919
|
+
try {
|
|
2920
|
+
recorded = this.kernel.amendFinishedNode(id, LEGACY_NODE, { councilStatus: status }, `council ${status}`);
|
|
2921
|
+
}
|
|
2922
|
+
catch (err) {
|
|
2923
|
+
why = err.message;
|
|
2924
|
+
}
|
|
2925
|
+
if (!recorded)
|
|
2926
|
+
this.logger.warn(`[council] ${id}: ${status}, but the status could not be recorded (${why})`);
|
|
2825
2927
|
}
|
|
2826
2928
|
/**
|
|
2827
2929
|
* A `Council` for review / accept / reject.
|
|
@@ -2870,6 +2972,7 @@ export class SessionManager {
|
|
|
2870
2972
|
synthesize: config.synthesize,
|
|
2871
2973
|
synthesisEngine: config.synthesisEngine,
|
|
2872
2974
|
synthesisModel: config.synthesisModel,
|
|
2975
|
+
synthesisAcpAgent: config.synthesisAcpAgent,
|
|
2873
2976
|
synthesisPermissionMode: config.synthesisPermissionMode,
|
|
2874
2977
|
maxTurnsPerAgent: config.maxTurnsPerAgent,
|
|
2875
2978
|
maxBudgetUsd: config.maxBudgetUsd,
|
|
@@ -2895,8 +2998,30 @@ export class SessionManager {
|
|
|
2895
2998
|
getSession: (name) => this.sessions.get(name),
|
|
2896
2999
|
exists: (name) => this.sessions.has(name),
|
|
2897
3000
|
allNames: () => this.sessions.keys(),
|
|
3001
|
+
deliver: (name, text) => this._deliverInboxTurn(name, text),
|
|
2898
3002
|
};
|
|
2899
3003
|
}
|
|
3004
|
+
/**
|
|
3005
|
+
* An inbox delivery is a turn like any other — capped, written to the run
|
|
3006
|
+
* ledger and kept in the handoff record — that its sender does not wait on.
|
|
3007
|
+
* Resolves once the turn reaches the engine; a refusal before that (spend cap,
|
|
3008
|
+
* session stopped) rejects so the inbox queues the message instead. A failure
|
|
3009
|
+
* after it belongs to the turn and is recorded with it.
|
|
3010
|
+
*/
|
|
3011
|
+
_deliverInboxTurn(name, text) {
|
|
3012
|
+
return new Promise((resolve, reject) => {
|
|
3013
|
+
let dispatched = false;
|
|
3014
|
+
this._sendTurn(name, text, {}, () => {
|
|
3015
|
+
dispatched = true;
|
|
3016
|
+
resolve();
|
|
3017
|
+
}).then(() => resolve(), (err) => {
|
|
3018
|
+
if (!dispatched)
|
|
3019
|
+
reject(err);
|
|
3020
|
+
else
|
|
3021
|
+
this.logger.warn(`[SessionManager] inbox turn on '${name}' failed: ${err.message}`);
|
|
3022
|
+
});
|
|
3023
|
+
});
|
|
3024
|
+
}
|
|
2900
3025
|
async sessionSendTo(from, to, message, summary) {
|
|
2901
3026
|
return this._inbox.sendTo(from, to, message, this._sessionLookup, summary, (name, err) => {
|
|
2902
3027
|
this.logger.error(`Broadcast delivery to '${name}' failed:`, err.message);
|
|
@@ -2972,10 +3097,14 @@ export class SessionManager {
|
|
|
2972
3097
|
// Every reviewer runs read-only (see below). A custom reviewer has no config
|
|
2973
3098
|
// to start from, so it is turned away here instead of failing one reviewer
|
|
2974
3099
|
// at a time. (grok was turned away too until its read-only mode moved onto
|
|
2975
|
-
// grok's OS sandbox.)
|
|
3100
|
+
// grok's OS sandbox.) An ACP agent cannot be held read-only at all: its tools
|
|
3101
|
+
// run inside the agent, so the session would refuse to start.
|
|
2976
3102
|
if ((opts?.engines ?? []).includes('custom')) {
|
|
2977
3103
|
throw new Error('ultrareview cannot use custom: reviewers run read-only, which a custom engine cannot be configured for here');
|
|
2978
3104
|
}
|
|
3105
|
+
if ((opts?.engines ?? []).includes('acp')) {
|
|
3106
|
+
throw new Error('ultrareview cannot use acp: reviewers run read-only, which an ACP agent cannot be held to');
|
|
3107
|
+
}
|
|
2979
3108
|
const id = `ultrareview-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
2980
3109
|
const agentCount = Math.min(20, Math.max(1, opts?.agentCount || 5));
|
|
2981
3110
|
const result = {
|
|
@@ -3104,9 +3233,10 @@ export class SessionManager {
|
|
|
3104
3233
|
// Review is read-only: reviewers analyse and report without modifying the
|
|
3105
3234
|
// very code they review. Unlike council, fan-out shares the project dir —
|
|
3106
3235
|
// there is no worktree to contain an edit — so this has to hold on every
|
|
3107
|
-
// engine. `plan`
|
|
3108
|
-
// engine-agnostic setting, which codex,
|
|
3109
|
-
// read-only modes. Before it was set, a
|
|
3236
|
+
// engine. `plan` reaches only the engines with a plan mode (Claude, Grok);
|
|
3237
|
+
// `sandboxMode: 'read-only'` is the engine-agnostic setting, which codex,
|
|
3238
|
+
// agy and opencode map to their own read-only modes. Before it was set, a
|
|
3239
|
+
// codex reviewer ran workspace-write.
|
|
3110
3240
|
permissionMode: 'plan',
|
|
3111
3241
|
sandboxMode: 'read-only',
|
|
3112
3242
|
}));
|
|
@@ -3867,7 +3997,7 @@ export class SessionManager {
|
|
|
3867
3997
|
// owner. After an unclean exit `_cleanupOrphanedPids()` reads them back
|
|
3868
3998
|
// and probes each one; a PID the OS has since recycled to a
|
|
3869
3999
|
// coding-CLI-shaped process gets killed.
|
|
3870
|
-
this.
|
|
4000
|
+
this._forgetPid(name);
|
|
3871
4001
|
pidsChanged = true;
|
|
3872
4002
|
// NOTE: do NOT delete from persistedSessions — idle cleanup is
|
|
3873
4003
|
// in-memory only. Persisted entries survive for PERSIST_DISK_TTL_MS
|