@enderfga/claw-orchestrator 7.14.0 → 7.16.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 +162 -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 +84 -46
- 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 +72 -9
- 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/claude-goal-session.d.ts +147 -0
- package/dist/src/claude-goal-session.js +355 -0
- package/dist/src/claude-goal-session.js.map +1 -0
- 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 +135 -53
- package/dist/src/index.js.map +1 -1
- package/dist/src/kernel/agent-step.d.ts +6 -0
- package/dist/src/kernel/agent-step.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 +19 -5
- package/dist/src/kernel/nodes/goal.js +81 -34
- 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 +16 -11
- package/dist/src/kernel/templates/index.js +14 -8
- package/dist/src/kernel/templates/index.js.map +1 -1
- package/dist/src/kernel/types.d.ts +37 -13
- 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/persistent-session.d.ts +9 -0
- package/dist/src/persistent-session.js +23 -0
- package/dist/src/persistent-session.js.map +1 -1
- package/dist/src/run-ledger.d.ts +3 -3
- package/dist/src/session-manager.d.ts +45 -1
- package/dist/src/session-manager.js +199 -34
- 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 +148 -38
- package/skills/references/claude-cli-tracking.md +9 -6
- package/skills/references/cli.md +139 -108
- 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 +217 -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 +275 -97
- 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';
|
|
@@ -131,6 +131,7 @@ import { cloneTranscript, DEFAULT_HANDOFF_CHARS, MIN_HANDOFF_CHARS, newTranscrip
|
|
|
131
131
|
import { PersistentGeminiSession } from './persistent-gemini-session.js';
|
|
132
132
|
import { PersistentCodexSession } from './persistent-codex-session.js';
|
|
133
133
|
import { PersistentCodexAppServerSession } from './persistent-codex-app-session.js';
|
|
134
|
+
import { ClaudeGoalSession } from './claude-goal-session.js';
|
|
134
135
|
import { PersistentCursorSession } from './persistent-cursor-session.js';
|
|
135
136
|
import { PersistentGrokSession } from './persistent-grok-session.js';
|
|
136
137
|
import { PersistentOpencodeSession } from './persistent-opencode-session.js';
|
|
@@ -139,7 +140,7 @@ import { PersistentCustomSession } from './persistent-custom-session.js';
|
|
|
139
140
|
import { PersistentAcpSession } from './persistent-acp-session.js';
|
|
140
141
|
import { resolveCustomEngine } from './engine-presets.js';
|
|
141
142
|
import { EFFORT_LEVELS, ENGINE_TYPES, overrideModelPricing, ENGINE_BINARY_NAMES, } from './types.js';
|
|
142
|
-
import { resolveAlias, isClaudeModel, lookupModel } from './models.js';
|
|
143
|
+
import { resolveAlias, isClaudeModel, lookupModel, resolveEngineAndModel } from './models.js';
|
|
143
144
|
import { isAgyConversationId } from './agy-conversation.js';
|
|
144
145
|
import { Council } from './council.js';
|
|
145
146
|
import { AutoloopRunner } from './autoloop/runner.js';
|
|
@@ -217,6 +218,15 @@ function readStoredAutoloopResumeContext(workspace, runId, originalSendTimeoutMs
|
|
|
217
218
|
}
|
|
218
219
|
return { effectiveSendTimeoutMs, pendingDispatch };
|
|
219
220
|
}
|
|
221
|
+
/** A process's command line as `ps` prints it, or undefined when it cannot be read. */
|
|
222
|
+
function processCommand(pid) {
|
|
223
|
+
try {
|
|
224
|
+
return execFileSync('ps', ['-p', String(pid), '-o', 'command='], { encoding: 'utf8', timeout: 3_000 }).trim();
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
220
230
|
function validateSendTimeoutIncrease(value, current) {
|
|
221
231
|
validateAutoloopTimeoutConfig({ sendTimeoutMs: value });
|
|
222
232
|
if (value <= current) {
|
|
@@ -319,8 +329,24 @@ function validateAutoloopRole(role, engine, customEngine) {
|
|
|
319
329
|
throw new Error(`${label} custom engine config is required`);
|
|
320
330
|
validateAutoloopCustomEngine(role, customEngine);
|
|
321
331
|
}
|
|
332
|
+
// An Autoloop role is configured by engine name alone and has no field for the
|
|
333
|
+
// agent an `acp` engine starts, so the session could only fail later.
|
|
334
|
+
if (resolved === 'acp')
|
|
335
|
+
throw new Error(`${label} engine 'acp' is not supported by Autoloop`);
|
|
322
336
|
return resolved;
|
|
323
337
|
}
|
|
338
|
+
/**
|
|
339
|
+
* The plugin's `defaultModel` for a session on `engine` that names no model, or
|
|
340
|
+
* undefined to let the engine run its own default. Engines do not share a model
|
|
341
|
+
* namespace, so the default goes only to an engine that runs it; the claude
|
|
342
|
+
* engine takes any model, proxying a non-Claude one to the gateway.
|
|
343
|
+
*/
|
|
344
|
+
function defaultModelFor(engine, defaultModel) {
|
|
345
|
+
if (!defaultModel || engine === 'claude')
|
|
346
|
+
return defaultModel;
|
|
347
|
+
const owner = resolveEngineAndModel(defaultModel).engine;
|
|
348
|
+
return owner === (engine === 'codex-app' ? 'codex' : engine) ? defaultModel : undefined;
|
|
349
|
+
}
|
|
324
350
|
function validateAutoloopEffort(role, effort) {
|
|
325
351
|
if (effort === undefined)
|
|
326
352
|
return;
|
|
@@ -772,6 +798,8 @@ function readClosedLedgerRestore(ledgerDir) {
|
|
|
772
798
|
}
|
|
773
799
|
export class SessionManager {
|
|
774
800
|
sessions = new Map();
|
|
801
|
+
/** Goal adapters over `claude` sessions, by session name; dropped when the session stops. */
|
|
802
|
+
_claudeGoals = new Map();
|
|
775
803
|
_pendingSessions = new Map();
|
|
776
804
|
/**
|
|
777
805
|
* Starts that passed the capacity check and are not in `sessions` yet. The
|
|
@@ -789,6 +817,12 @@ export class SessionManager {
|
|
|
789
817
|
/** In-flight proxy startup, so concurrent callers share one server. */
|
|
790
818
|
_proxyStartPromise = null;
|
|
791
819
|
_activePids = new Map();
|
|
820
|
+
/**
|
|
821
|
+
* Each recorded pid's command line when it was recorded. An orphan is killed
|
|
822
|
+
* only if it is still that process: this is what lets cleanup recognise an
|
|
823
|
+
* ACP agent or a custom engine, whose binary is no fixed name.
|
|
824
|
+
*/
|
|
825
|
+
_pidCommands = new Map();
|
|
792
826
|
_circuitBreaker = new CircuitBreaker();
|
|
793
827
|
_inbox = new InboxManager();
|
|
794
828
|
/** cwd → detected language, so the manifest probe runs once per directory. */
|
|
@@ -929,6 +963,7 @@ export class SessionManager {
|
|
|
929
963
|
const persisted = skipPersist ? undefined : this.persistedSessions.get(name);
|
|
930
964
|
// Unified: only use resumeSessionId (claudeResumeId is an internal alias, not exposed)
|
|
931
965
|
const resumeId = config.resumeSessionId ?? persisted?.claudeSessionId;
|
|
966
|
+
const engine = config.engine || persisted?.engine || 'claude';
|
|
932
967
|
// ORDER IS LOAD-BEARING — do not "fix" it by moving `...config` up.
|
|
933
968
|
//
|
|
934
969
|
// Object spread copies own keys even when their value is `undefined`, so any
|
|
@@ -944,7 +979,7 @@ export class SessionManager {
|
|
|
944
979
|
cwd: config.cwd || persisted?.cwd || process.cwd(),
|
|
945
980
|
permissionMode: config.permissionMode || this.pluginConfig.defaultPermissionMode,
|
|
946
981
|
effort: config.effort || this.pluginConfig.defaultEffort,
|
|
947
|
-
model: config.model || persisted?.model || this.pluginConfig.defaultModel,
|
|
982
|
+
model: config.model || persisted?.model || defaultModelFor(engine, this.pluginConfig.defaultModel),
|
|
948
983
|
sandboxMode: config.sandboxMode ?? persisted?.sandboxMode,
|
|
949
984
|
autoCompactPercent: config.autoCompactPercent ?? persisted?.autoCompactPercent,
|
|
950
985
|
chrome: config.chrome ?? persisted?.chrome,
|
|
@@ -955,16 +990,15 @@ export class SessionManager {
|
|
|
955
990
|
if (fullConfig.model) {
|
|
956
991
|
fullConfig.resolvedModel = this._resolveModel(fullConfig.model, fullConfig.modelOverrides);
|
|
957
992
|
}
|
|
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
993
|
// Write the resolved engine back so downstream consumers of the managed
|
|
963
994
|
// config (agy resume-id lookups, _persistSession's registry entry) see the
|
|
964
995
|
// real engine even when it came from the persisted registry.
|
|
965
996
|
fullConfig.engine = engine;
|
|
966
997
|
// Circuit breaker — reject early if engine is in backoff
|
|
967
998
|
this._circuitBreaker.check(engine);
|
|
999
|
+
// Auto-inject proxy baseUrl for non-Claude models on the claude engine.
|
|
1000
|
+
// Starts a local proxy server that converts Anthropic → OpenAI format
|
|
1001
|
+
// and forwards to the OpenClaw gateway. Zero config required.
|
|
968
1002
|
if (engine === 'claude' && fullConfig.resolvedModel && !fullConfig.baseUrl) {
|
|
969
1003
|
if (!isClaudeModel(fullConfig.resolvedModel)) {
|
|
970
1004
|
const proxyPort = await this._ensureProxyServer();
|
|
@@ -987,6 +1021,9 @@ export class SessionManager {
|
|
|
987
1021
|
// Track child process PID for orphan cleanup
|
|
988
1022
|
if (session.pid) {
|
|
989
1023
|
this._activePids.set(name, session.pid);
|
|
1024
|
+
const command = processCommand(session.pid);
|
|
1025
|
+
if (command)
|
|
1026
|
+
this._pidCommands.set(session.pid, command);
|
|
990
1027
|
this._savePids();
|
|
991
1028
|
}
|
|
992
1029
|
const managed = {
|
|
@@ -1008,6 +1045,10 @@ export class SessionManager {
|
|
|
1008
1045
|
return this._toSessionInfo(name, managed);
|
|
1009
1046
|
}
|
|
1010
1047
|
async sendMessage(name, message, options = {}) {
|
|
1048
|
+
return this._sendTurn(name, message, options);
|
|
1049
|
+
}
|
|
1050
|
+
/** `onDispatch` fires once the turn has passed every check and is handed to the engine. */
|
|
1051
|
+
async _sendTurn(name, message, options, onDispatch) {
|
|
1011
1052
|
const managed = this._getSession(name);
|
|
1012
1053
|
// Per-session serialization. Two concurrent sendMessage() calls on the
|
|
1013
1054
|
// same session previously raced on PersistentClaudeSession._streamCallbacks
|
|
@@ -1103,7 +1144,14 @@ export class SessionManager {
|
|
|
1103
1144
|
// A session handed off from another engine carries that conversation in
|
|
1104
1145
|
// front of its first message; after that the engine holds it itself.
|
|
1105
1146
|
const outgoing = managed.pendingHandoff ? `${managed.pendingHandoff}\n\n${message}` : message;
|
|
1147
|
+
onDispatch?.();
|
|
1106
1148
|
const result = await managed.session.send(outgoing, sendOpts);
|
|
1149
|
+
const after = this._statsSnapshot(managed);
|
|
1150
|
+
const usage = {
|
|
1151
|
+
tokensIn: Math.max(0, after.tokensIn - ledgerBefore.tokensIn),
|
|
1152
|
+
tokensOut: Math.max(0, after.tokensOut - ledgerBefore.tokensOut),
|
|
1153
|
+
costUsd: Math.max(0, after.costUsd - ledgerBefore.costUsd),
|
|
1154
|
+
};
|
|
1107
1155
|
// Update the resume-capable session ID if available (skip disk persist
|
|
1108
1156
|
// for ephemeral sessions that were started with skipPersistence)
|
|
1109
1157
|
const resumableId = this._managedResumeId(managed);
|
|
@@ -1145,11 +1193,12 @@ export class SessionManager {
|
|
|
1145
1193
|
sessionId: this._managedResumeId(managed),
|
|
1146
1194
|
error: turnError,
|
|
1147
1195
|
events: [],
|
|
1196
|
+
usage,
|
|
1148
1197
|
...(permissionDenials.length ? { permissionDenials } : {}),
|
|
1149
1198
|
...(images.length ? { images } : {}),
|
|
1150
1199
|
};
|
|
1151
1200
|
}
|
|
1152
|
-
return { output: '', sessionId: this._managedResumeId(managed), events: [] };
|
|
1201
|
+
return { output: '', sessionId: this._managedResumeId(managed), events: [], usage };
|
|
1153
1202
|
}
|
|
1154
1203
|
catch (err) {
|
|
1155
1204
|
turnError = err.message;
|
|
@@ -1219,6 +1268,7 @@ export class SessionManager {
|
|
|
1219
1268
|
engine: opts.engine,
|
|
1220
1269
|
...(opts.model ? { model: opts.model } : {}),
|
|
1221
1270
|
...(opts.customEngine ? { customEngine: opts.customEngine } : {}),
|
|
1271
|
+
...(opts.acpAgent ? { acpAgent: opts.acpAgent } : {}),
|
|
1222
1272
|
});
|
|
1223
1273
|
const target = this._getSession(targetName);
|
|
1224
1274
|
// The new session's record starts as the source's, so a second handoff from
|
|
@@ -1534,6 +1584,11 @@ export class SessionManager {
|
|
|
1534
1584
|
if (!contract)
|
|
1535
1585
|
throw new Error('verifyRun requires a contract with at least one recognised check');
|
|
1536
1586
|
const runId = args.label || `verify-${Date.now().toString(36)}`;
|
|
1587
|
+
// The bundle goes under the run store, keyed by the label. A label naming a
|
|
1588
|
+
// workflow run would plant this evidence in that run's directory.
|
|
1589
|
+
if (runExists(runId)) {
|
|
1590
|
+
throw new Error(`verify_run label '${runId}' names an existing workflow run; choose another label`);
|
|
1591
|
+
}
|
|
1537
1592
|
const dir = kernelRunDir(runId);
|
|
1538
1593
|
const evidenceId = 'verify-01';
|
|
1539
1594
|
const { results, rounds } = await runContract(contract, {
|
|
@@ -1558,9 +1613,9 @@ export class SessionManager {
|
|
|
1558
1613
|
async stopSession(name, opts = {}) {
|
|
1559
1614
|
const managed = this._getSession(name);
|
|
1560
1615
|
managed.session.stop();
|
|
1561
|
-
this.
|
|
1616
|
+
this._removeSession(name);
|
|
1562
1617
|
// Remove PID tracking
|
|
1563
|
-
this.
|
|
1618
|
+
this._forgetPid(name);
|
|
1564
1619
|
this._savePids();
|
|
1565
1620
|
if (!opts.keepPersisted) {
|
|
1566
1621
|
// Explicit stop = user intent to end session — remove from disk too.
|
|
@@ -1569,6 +1624,9 @@ export class SessionManager {
|
|
|
1569
1624
|
// Claude conversation) pass keepPersisted: true.
|
|
1570
1625
|
this._forgetPersisted(name);
|
|
1571
1626
|
this._savePersisted();
|
|
1627
|
+
// Its unread mail was for this conversation; a later session that takes
|
|
1628
|
+
// the name is a different one and must not receive it.
|
|
1629
|
+
this._inbox.clear(name);
|
|
1572
1630
|
}
|
|
1573
1631
|
}
|
|
1574
1632
|
hasSession(name) {
|
|
@@ -1655,7 +1713,7 @@ export class SessionManager {
|
|
|
1655
1713
|
}
|
|
1656
1714
|
const oldConfig = { ...managed.config };
|
|
1657
1715
|
managed.session.stop();
|
|
1658
|
-
this.
|
|
1716
|
+
this._removeSession(name);
|
|
1659
1717
|
try {
|
|
1660
1718
|
return await this.startSession({
|
|
1661
1719
|
...oldConfig,
|
|
@@ -1720,7 +1778,7 @@ export class SessionManager {
|
|
|
1720
1778
|
newDisallowed = newDisallowed.filter((t) => !removeSet.has(t));
|
|
1721
1779
|
}
|
|
1722
1780
|
managed.session.stop();
|
|
1723
|
-
this.
|
|
1781
|
+
this._removeSession(name);
|
|
1724
1782
|
try {
|
|
1725
1783
|
return await this.startSession({
|
|
1726
1784
|
...oldConfig,
|
|
@@ -1959,8 +2017,12 @@ export class SessionManager {
|
|
|
1959
2017
|
this.logger.info(`Stopped session: ${name}`);
|
|
1960
2018
|
}
|
|
1961
2019
|
this.sessions.clear();
|
|
2020
|
+
for (const adapter of this._claudeGoals.values())
|
|
2021
|
+
adapter.detach();
|
|
2022
|
+
this._claudeGoals.clear();
|
|
1962
2023
|
// Clear PID tracking
|
|
1963
2024
|
this._activePids.clear();
|
|
2025
|
+
this._pidCommands.clear();
|
|
1964
2026
|
this._savePids();
|
|
1965
2027
|
// Stop proxy server
|
|
1966
2028
|
if (this._proxyServer) {
|
|
@@ -1977,6 +2039,29 @@ export class SessionManager {
|
|
|
1977
2039
|
const session = this.sessions.get(name)?.session;
|
|
1978
2040
|
return session instanceof PersistentCodexAppServerSession ? session : undefined;
|
|
1979
2041
|
}
|
|
2042
|
+
/**
|
|
2043
|
+
* The goal surface of the session behind `name`: a `codex-app` session as it
|
|
2044
|
+
* is, a `claude` session through `ClaudeGoalSession` (one per session, made on
|
|
2045
|
+
* first use with what the node knows of the thread). Other engines have none.
|
|
2046
|
+
*/
|
|
2047
|
+
goalSession(name, open) {
|
|
2048
|
+
const session = this.sessions.get(name)?.session;
|
|
2049
|
+
if (session instanceof PersistentCodexAppServerSession)
|
|
2050
|
+
return session;
|
|
2051
|
+
if (!(session instanceof PersistentClaudeSession))
|
|
2052
|
+
return undefined;
|
|
2053
|
+
let adapter = this._claudeGoals.get(name);
|
|
2054
|
+
// A session replaced under the same name (model switch, tool update) gets a new adapter.
|
|
2055
|
+
if (adapter && adapter.host !== session) {
|
|
2056
|
+
adapter.detach();
|
|
2057
|
+
adapter = undefined;
|
|
2058
|
+
}
|
|
2059
|
+
if (!adapter) {
|
|
2060
|
+
adapter = new ClaudeGoalSession(session, open);
|
|
2061
|
+
this._claudeGoals.set(name, adapter);
|
|
2062
|
+
}
|
|
2063
|
+
return adapter;
|
|
2064
|
+
}
|
|
1980
2065
|
/**
|
|
1981
2066
|
* Act on a `codex-app` session's goal through the app-server's
|
|
1982
2067
|
* `thread/goal/*` RPCs. Used by the `codex_goal_*` tools; `get` asks the
|
|
@@ -2436,6 +2521,18 @@ export class SessionManager {
|
|
|
2436
2521
|
}
|
|
2437
2522
|
// ─── PID Tracking ──────────────────────────────────────────────────────
|
|
2438
2523
|
static PID_FILE = path.join(os.homedir(), '.openclaw', 'session-pids.json');
|
|
2524
|
+
/** Take a session out of memory, with the goal adapter listening to it. */
|
|
2525
|
+
_removeSession(name) {
|
|
2526
|
+
this._claudeGoals.get(name)?.detach();
|
|
2527
|
+
this._claudeGoals.delete(name);
|
|
2528
|
+
this.sessions.delete(name);
|
|
2529
|
+
}
|
|
2530
|
+
_forgetPid(name) {
|
|
2531
|
+
const pid = this._activePids.get(name);
|
|
2532
|
+
if (pid !== undefined)
|
|
2533
|
+
this._pidCommands.delete(pid);
|
|
2534
|
+
this._activePids.delete(name);
|
|
2535
|
+
}
|
|
2439
2536
|
_savePids() {
|
|
2440
2537
|
try {
|
|
2441
2538
|
const dir = path.dirname(SessionManager.PID_FILE);
|
|
@@ -2479,11 +2576,13 @@ export class SessionManager {
|
|
|
2479
2576
|
pid: entry.pid,
|
|
2480
2577
|
ownerPid: entry.ownerPid,
|
|
2481
2578
|
since: entry.since ?? now,
|
|
2579
|
+
...(typeof entry.command === 'string' ? { command: entry.command } : {}),
|
|
2482
2580
|
};
|
|
2483
2581
|
}
|
|
2484
2582
|
// Add OUR current entries
|
|
2485
2583
|
for (const [name, pid] of this._activePids) {
|
|
2486
|
-
|
|
2584
|
+
const command = this._pidCommands.get(pid);
|
|
2585
|
+
merged[name] = { pid, ownerPid: process.pid, since: now, ...(command ? { command } : {}) };
|
|
2487
2586
|
}
|
|
2488
2587
|
fs.writeFileSync(SessionManager.PID_FILE, JSON.stringify(merged));
|
|
2489
2588
|
}
|
|
@@ -2495,7 +2594,7 @@ export class SessionManager {
|
|
|
2495
2594
|
* Verify that a PID belongs to a known coding CLI before killing it.
|
|
2496
2595
|
* Prevents killing unrelated processes if the OS recycled the PID.
|
|
2497
2596
|
*/
|
|
2498
|
-
_isKnownCliProcess(pid) {
|
|
2597
|
+
_isKnownCliProcess(pid, recordedCommand) {
|
|
2499
2598
|
// Match known CLI binaries by basename to avoid false positives
|
|
2500
2599
|
// (e.g., 'agent' must not match 'ssh-agent' or 'gpg-agent')
|
|
2501
2600
|
// Anchor each name to executable/path position ((?:^|[/\s])name(?:[\s/]|$))
|
|
@@ -2510,16 +2609,14 @@ export class SessionManager {
|
|
|
2510
2609
|
...ENGINE_BINARY_NAMES.map((bin) => new RegExp(`(?:^|[/\\s])${bin}(?:[\\s/]|$)`)),
|
|
2511
2610
|
/(?:^|\/)agent(?:[\s/]|$)/, // 'agent' only as executable/after a slash (not ssh-agent)
|
|
2512
2611
|
];
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
encoding: 'utf8',
|
|
2516
|
-
timeout: 3_000,
|
|
2517
|
-
}).trim();
|
|
2518
|
-
return knownPatterns.some((pattern) => pattern.test(cmd));
|
|
2519
|
-
}
|
|
2520
|
-
catch {
|
|
2612
|
+
const cmd = processCommand(pid);
|
|
2613
|
+
if (cmd === undefined)
|
|
2521
2614
|
return false; // ps failed — process likely dead or not accessible
|
|
2522
|
-
|
|
2615
|
+
// The same command line as when it was recorded: the process itself, not a
|
|
2616
|
+
// recycled pid — whatever binary it runs.
|
|
2617
|
+
if (recordedCommand && cmd === recordedCommand)
|
|
2618
|
+
return true;
|
|
2619
|
+
return knownPatterns.some((pattern) => pattern.test(cmd));
|
|
2523
2620
|
}
|
|
2524
2621
|
_cleanupOrphanedPids() {
|
|
2525
2622
|
try {
|
|
@@ -2530,6 +2627,7 @@ export class SessionManager {
|
|
|
2530
2627
|
// Resolve entry shape: legacy = number; current = { pid, ownerPid, since }.
|
|
2531
2628
|
let pid;
|
|
2532
2629
|
let ownerPid;
|
|
2630
|
+
let recordedCommand;
|
|
2533
2631
|
if (typeof raw === 'number') {
|
|
2534
2632
|
pid = raw;
|
|
2535
2633
|
ownerPid = null; // unknown owner — treat conservatively (skip kill)
|
|
@@ -2540,6 +2638,7 @@ export class SessionManager {
|
|
|
2540
2638
|
continue;
|
|
2541
2639
|
pid = e.pid;
|
|
2542
2640
|
ownerPid = typeof e.ownerPid === 'number' ? e.ownerPid : null;
|
|
2641
|
+
recordedCommand = typeof e.command === 'string' ? e.command : undefined;
|
|
2543
2642
|
}
|
|
2544
2643
|
else {
|
|
2545
2644
|
continue;
|
|
@@ -2572,7 +2671,7 @@ export class SessionManager {
|
|
|
2572
2671
|
try {
|
|
2573
2672
|
process.kill(pid, 0); // check if alive
|
|
2574
2673
|
// Alive — but verify it's actually a coding CLI, not a recycled PID
|
|
2575
|
-
if (!this._isKnownCliProcess(pid)) {
|
|
2674
|
+
if (!this._isKnownCliProcess(pid, recordedCommand)) {
|
|
2576
2675
|
this.logger.info(`PID ${pid} (session: ${name}) is alive but not a known CLI — skipping kill`);
|
|
2577
2676
|
continue;
|
|
2578
2677
|
}
|
|
@@ -2804,12 +2903,26 @@ export class SessionManager {
|
|
|
2804
2903
|
throw new Error(`Council '${id}' not found`);
|
|
2805
2904
|
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
2905
|
}
|
|
2906
|
+
/**
|
|
2907
|
+
* Abort a council running in this process. One that is not — finished, or
|
|
2908
|
+
* running elsewhere — is left alone and the result says so: its worktrees are
|
|
2909
|
+
* either awaiting accept/reject or in use by the process that runs it.
|
|
2910
|
+
*/
|
|
2807
2911
|
councilAbort(id) {
|
|
2808
2912
|
// Cancel the run first so the kernel stops advancing, then abort the engine
|
|
2809
2913
|
// so the current round tears down its worktrees.
|
|
2810
|
-
if (
|
|
2914
|
+
if (this.kernel.cancel(id)) {
|
|
2915
|
+
this.kernel.handle(id, LEGACY_NODE)?.abort();
|
|
2916
|
+
return { aborted: true };
|
|
2917
|
+
}
|
|
2918
|
+
const record = loadRun(id);
|
|
2919
|
+
if (!record)
|
|
2811
2920
|
throw new Error(`Council '${id}' not found`);
|
|
2812
|
-
|
|
2921
|
+
return {
|
|
2922
|
+
aborted: false,
|
|
2923
|
+
state: record.state,
|
|
2924
|
+
reason: `Council '${id}' is ${record.state} and not running in this process — nothing was aborted`,
|
|
2925
|
+
};
|
|
2813
2926
|
}
|
|
2814
2927
|
councilInject(id, message) {
|
|
2815
2928
|
this._liveCouncil(id).injectMessage(message);
|
|
@@ -2818,10 +2931,34 @@ export class SessionManager {
|
|
|
2818
2931
|
return this._councilForPostProcessing(id).review();
|
|
2819
2932
|
}
|
|
2820
2933
|
async councilAccept(id) {
|
|
2821
|
-
|
|
2934
|
+
const result = await this._councilForPostProcessing(id).accept();
|
|
2935
|
+
this._recordCouncilDecision(id, 'accepted');
|
|
2936
|
+
return result;
|
|
2822
2937
|
}
|
|
2823
2938
|
async councilReject(id, feedback) {
|
|
2824
|
-
|
|
2939
|
+
const result = await this._councilForPostProcessing(id).reject(feedback);
|
|
2940
|
+
this._recordCouncilDecision(id, 'rejected');
|
|
2941
|
+
return result;
|
|
2942
|
+
}
|
|
2943
|
+
/**
|
|
2944
|
+
* Write the decision onto the run, which is where `council_status` reads it.
|
|
2945
|
+
* The `Council` that took it is usually rebuilt from the record and dropped,
|
|
2946
|
+
* so its own status would go nowhere.
|
|
2947
|
+
*/
|
|
2948
|
+
_recordCouncilDecision(id, status) {
|
|
2949
|
+
// A council live in this process carries the status on its own session.
|
|
2950
|
+
if (this.kernel.handle(id, LEGACY_NODE))
|
|
2951
|
+
return;
|
|
2952
|
+
let recorded = false;
|
|
2953
|
+
let why = 'the run has not finished';
|
|
2954
|
+
try {
|
|
2955
|
+
recorded = this.kernel.amendFinishedNode(id, LEGACY_NODE, { councilStatus: status }, `council ${status}`);
|
|
2956
|
+
}
|
|
2957
|
+
catch (err) {
|
|
2958
|
+
why = err.message;
|
|
2959
|
+
}
|
|
2960
|
+
if (!recorded)
|
|
2961
|
+
this.logger.warn(`[council] ${id}: ${status}, but the status could not be recorded (${why})`);
|
|
2825
2962
|
}
|
|
2826
2963
|
/**
|
|
2827
2964
|
* A `Council` for review / accept / reject.
|
|
@@ -2870,6 +3007,7 @@ export class SessionManager {
|
|
|
2870
3007
|
synthesize: config.synthesize,
|
|
2871
3008
|
synthesisEngine: config.synthesisEngine,
|
|
2872
3009
|
synthesisModel: config.synthesisModel,
|
|
3010
|
+
synthesisAcpAgent: config.synthesisAcpAgent,
|
|
2873
3011
|
synthesisPermissionMode: config.synthesisPermissionMode,
|
|
2874
3012
|
maxTurnsPerAgent: config.maxTurnsPerAgent,
|
|
2875
3013
|
maxBudgetUsd: config.maxBudgetUsd,
|
|
@@ -2895,8 +3033,30 @@ export class SessionManager {
|
|
|
2895
3033
|
getSession: (name) => this.sessions.get(name),
|
|
2896
3034
|
exists: (name) => this.sessions.has(name),
|
|
2897
3035
|
allNames: () => this.sessions.keys(),
|
|
3036
|
+
deliver: (name, text) => this._deliverInboxTurn(name, text),
|
|
2898
3037
|
};
|
|
2899
3038
|
}
|
|
3039
|
+
/**
|
|
3040
|
+
* An inbox delivery is a turn like any other — capped, written to the run
|
|
3041
|
+
* ledger and kept in the handoff record — that its sender does not wait on.
|
|
3042
|
+
* Resolves once the turn reaches the engine; a refusal before that (spend cap,
|
|
3043
|
+
* session stopped) rejects so the inbox queues the message instead. A failure
|
|
3044
|
+
* after it belongs to the turn and is recorded with it.
|
|
3045
|
+
*/
|
|
3046
|
+
_deliverInboxTurn(name, text) {
|
|
3047
|
+
return new Promise((resolve, reject) => {
|
|
3048
|
+
let dispatched = false;
|
|
3049
|
+
this._sendTurn(name, text, {}, () => {
|
|
3050
|
+
dispatched = true;
|
|
3051
|
+
resolve();
|
|
3052
|
+
}).then(() => resolve(), (err) => {
|
|
3053
|
+
if (!dispatched)
|
|
3054
|
+
reject(err);
|
|
3055
|
+
else
|
|
3056
|
+
this.logger.warn(`[SessionManager] inbox turn on '${name}' failed: ${err.message}`);
|
|
3057
|
+
});
|
|
3058
|
+
});
|
|
3059
|
+
}
|
|
2900
3060
|
async sessionSendTo(from, to, message, summary) {
|
|
2901
3061
|
return this._inbox.sendTo(from, to, message, this._sessionLookup, summary, (name, err) => {
|
|
2902
3062
|
this.logger.error(`Broadcast delivery to '${name}' failed:`, err.message);
|
|
@@ -2972,10 +3132,14 @@ export class SessionManager {
|
|
|
2972
3132
|
// Every reviewer runs read-only (see below). A custom reviewer has no config
|
|
2973
3133
|
// to start from, so it is turned away here instead of failing one reviewer
|
|
2974
3134
|
// at a time. (grok was turned away too until its read-only mode moved onto
|
|
2975
|
-
// grok's OS sandbox.)
|
|
3135
|
+
// grok's OS sandbox.) An ACP agent cannot be held read-only at all: its tools
|
|
3136
|
+
// run inside the agent, so the session would refuse to start.
|
|
2976
3137
|
if ((opts?.engines ?? []).includes('custom')) {
|
|
2977
3138
|
throw new Error('ultrareview cannot use custom: reviewers run read-only, which a custom engine cannot be configured for here');
|
|
2978
3139
|
}
|
|
3140
|
+
if ((opts?.engines ?? []).includes('acp')) {
|
|
3141
|
+
throw new Error('ultrareview cannot use acp: reviewers run read-only, which an ACP agent cannot be held to');
|
|
3142
|
+
}
|
|
2979
3143
|
const id = `ultrareview-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
2980
3144
|
const agentCount = Math.min(20, Math.max(1, opts?.agentCount || 5));
|
|
2981
3145
|
const result = {
|
|
@@ -3104,9 +3268,10 @@ export class SessionManager {
|
|
|
3104
3268
|
// Review is read-only: reviewers analyse and report without modifying the
|
|
3105
3269
|
// very code they review. Unlike council, fan-out shares the project dir —
|
|
3106
3270
|
// 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
|
|
3271
|
+
// engine. `plan` reaches only the engines with a plan mode (Claude, Grok);
|
|
3272
|
+
// `sandboxMode: 'read-only'` is the engine-agnostic setting, which codex,
|
|
3273
|
+
// agy and opencode map to their own read-only modes. Before it was set, a
|
|
3274
|
+
// codex reviewer ran workspace-write.
|
|
3110
3275
|
permissionMode: 'plan',
|
|
3111
3276
|
sandboxMode: 'read-only',
|
|
3112
3277
|
}));
|
|
@@ -3860,14 +4025,14 @@ export class SessionManager {
|
|
|
3860
4025
|
catch {
|
|
3861
4026
|
// Best-effort — session may already be dead; must not block TTL cleanup
|
|
3862
4027
|
}
|
|
3863
|
-
this.
|
|
4028
|
+
this._removeSession(name);
|
|
3864
4029
|
// The child is gone, so its PID must go too — `stopSession` does this
|
|
3865
4030
|
// and the TTL path did not, so `_activePids` only ever grew and the
|
|
3866
4031
|
// next `_savePids()` rewrote those dead PIDs to disk under the current
|
|
3867
4032
|
// owner. After an unclean exit `_cleanupOrphanedPids()` reads them back
|
|
3868
4033
|
// and probes each one; a PID the OS has since recycled to a
|
|
3869
4034
|
// coding-CLI-shaped process gets killed.
|
|
3870
|
-
this.
|
|
4035
|
+
this._forgetPid(name);
|
|
3871
4036
|
pidsChanged = true;
|
|
3872
4037
|
// NOTE: do NOT delete from persistedSessions — idle cleanup is
|
|
3873
4038
|
// in-memory only. Persisted entries survive for PERSIST_DISK_TTL_MS
|