@enderfga/claw-orchestrator 4.1.2 → 4.3.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 +11 -7
- package/dist/src/consensus.d.ts +9 -0
- package/dist/src/consensus.js +11 -3
- package/dist/src/consensus.js.map +1 -1
- package/dist/src/council.js +9 -2
- package/dist/src/council.js.map +1 -1
- package/dist/src/fanout.d.ts +81 -0
- package/dist/src/fanout.js +146 -0
- package/dist/src/fanout.js.map +1 -0
- package/dist/src/index.js +205 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/persistent-codex-app-session.d.ts +34 -0
- package/dist/src/persistent-codex-app-session.js +112 -3
- package/dist/src/persistent-codex-app-session.js.map +1 -1
- package/dist/src/persistent-codex-session.d.ts +8 -0
- package/dist/src/persistent-codex-session.js +39 -2
- package/dist/src/persistent-codex-session.js.map +1 -1
- package/dist/src/persistent-session.d.ts +8 -0
- package/dist/src/persistent-session.js +34 -4
- package/dist/src/persistent-session.js.map +1 -1
- package/dist/src/session-manager.d.ts +59 -0
- package/dist/src/session-manager.js +172 -20
- package/dist/src/session-manager.js.map +1 -1
- package/dist/src/types.d.ts +18 -1
- package/openclaw.plugin.json +10 -0
- package/package.json +2 -2
- package/skills/SKILL.md +9 -3
- package/skills/references/claude-cli-tracking.md +3 -1
- package/skills/references/multi-engine.md +8 -3
- package/skills/references/sessions.md +18 -0
- package/skills/references/tools.md +63 -1
|
@@ -17,6 +17,7 @@ interface PersistedSession {
|
|
|
17
17
|
import { type Logger } from './logger.js';
|
|
18
18
|
import { type SessionConfig, type SessionInfo, type SendResult, type PluginConfig, type EffortLevel, type EngineType, type AgentInfo, type SkillInfo, type RuleInfo, type StreamEvent, type ISession, type CouncilConfig, type CouncilSession, type CouncilReviewResult, type CouncilAcceptResult, type CouncilRejectResult, type InboxMessage, type UltraplanResult, type UltrareviewResult } from './types.js';
|
|
19
19
|
import { Council } from './council.js';
|
|
20
|
+
import { type FanoutConfig, type FanoutSession } from './fanout.js';
|
|
20
21
|
import { AutoloopRunner } from './autoloop/runner.js';
|
|
21
22
|
import { ClaudeAgentDispatcher } from './autoloop/dispatcher.js';
|
|
22
23
|
import type { AutoloopState } from './autoloop/types.js';
|
|
@@ -255,6 +256,40 @@ export declare class SessionManager {
|
|
|
255
256
|
ok: true;
|
|
256
257
|
goal: unknown;
|
|
257
258
|
};
|
|
259
|
+
private _getCodexAppSession;
|
|
260
|
+
codexInterrupt(name: string): Promise<{
|
|
261
|
+
ok: true;
|
|
262
|
+
interrupted: boolean;
|
|
263
|
+
}>;
|
|
264
|
+
codexSteer(name: string, text: string): Promise<{
|
|
265
|
+
ok: true;
|
|
266
|
+
steered: boolean;
|
|
267
|
+
turnId?: string;
|
|
268
|
+
text?: string;
|
|
269
|
+
}>;
|
|
270
|
+
codexForkThread(name: string): Promise<{
|
|
271
|
+
ok: true;
|
|
272
|
+
threadId: string;
|
|
273
|
+
}>;
|
|
274
|
+
codexRollback(name: string, numTurns: number): Promise<{
|
|
275
|
+
ok: true;
|
|
276
|
+
numTurns: number;
|
|
277
|
+
}>;
|
|
278
|
+
codexModels(name: string): Promise<{
|
|
279
|
+
ok: true;
|
|
280
|
+
models: unknown[];
|
|
281
|
+
}>;
|
|
282
|
+
codexThreads(name: string, opts?: {
|
|
283
|
+
cwd?: string;
|
|
284
|
+
searchTerm?: string;
|
|
285
|
+
archived?: boolean;
|
|
286
|
+
cursor?: string;
|
|
287
|
+
limit?: number;
|
|
288
|
+
}): Promise<{
|
|
289
|
+
ok: true;
|
|
290
|
+
data: unknown[];
|
|
291
|
+
nextCursor: string | null;
|
|
292
|
+
}>;
|
|
258
293
|
private _assertClaudeSession;
|
|
259
294
|
/** Send `/goal <objective>` to a claude session. Sets a completion condition that
|
|
260
295
|
* Claude Code pursues across turns, evaluating after each turn via Haiku. */
|
|
@@ -272,6 +307,18 @@ export declare class SessionManager {
|
|
|
272
307
|
stdout: string;
|
|
273
308
|
stderr: string;
|
|
274
309
|
}>;
|
|
310
|
+
/**
|
|
311
|
+
* Wraps `claude agents --json` — lists Claude Code background agent sessions
|
|
312
|
+
* (state/model/title/progress). One-shot spawn; not tied to a managed session.
|
|
313
|
+
* `all` adds `--all` (include completed); `cwd` scopes to a directory.
|
|
314
|
+
*/
|
|
315
|
+
claudeAgentsList(opts?: {
|
|
316
|
+
all?: boolean;
|
|
317
|
+
cwd?: string;
|
|
318
|
+
}): Promise<{
|
|
319
|
+
ok: true;
|
|
320
|
+
agents: unknown[];
|
|
321
|
+
}>;
|
|
275
322
|
private _codexBin;
|
|
276
323
|
/**
|
|
277
324
|
* Parse Codex `--json` JSONL output from a stdout buffer.
|
|
@@ -402,6 +449,17 @@ export declare class SessionManager {
|
|
|
402
449
|
councilReview(id: string): Promise<CouncilReviewResult>;
|
|
403
450
|
councilAccept(id: string): Promise<CouncilAcceptResult>;
|
|
404
451
|
councilReject(id: string, feedback: string): Promise<CouncilRejectResult>;
|
|
452
|
+
private fanouts;
|
|
453
|
+
private fanoutCleanupTimers;
|
|
454
|
+
/**
|
|
455
|
+
* Start a fan-out: run the task across N engine/model agents in parallel and
|
|
456
|
+
* collect their answers (optional synthesis). Runs in the background; poll
|
|
457
|
+
* with fanoutStatus. Distinct from council — no rounds, votes, or worktrees.
|
|
458
|
+
*/
|
|
459
|
+
fanoutStart(config: FanoutConfig): FanoutSession;
|
|
460
|
+
fanoutStatus(id: string): FanoutSession;
|
|
461
|
+
fanoutAbort(id: string): void;
|
|
462
|
+
private _scheduleFanoutCleanup;
|
|
405
463
|
private get _sessionLookup();
|
|
406
464
|
sessionSendTo(from: string, to: string, message: string, summary?: string): Promise<{
|
|
407
465
|
delivered: boolean;
|
|
@@ -424,6 +482,7 @@ export declare class SessionManager {
|
|
|
424
482
|
maxDurationMinutes?: number;
|
|
425
483
|
model?: string;
|
|
426
484
|
focus?: string;
|
|
485
|
+
engines?: EngineType[];
|
|
427
486
|
}): UltrareviewResult;
|
|
428
487
|
ultrareviewStatus(id: string): UltrareviewResult | undefined;
|
|
429
488
|
private autoloops;
|
|
@@ -115,6 +115,7 @@ import { PersistentCustomSession } from './persistent-custom-session.js';
|
|
|
115
115
|
import { overrideModelPricing, } from './types.js';
|
|
116
116
|
import { resolveAlias, isClaudeModel } from './models.js';
|
|
117
117
|
import { Council } from './council.js';
|
|
118
|
+
import { Fanout } from './fanout.js';
|
|
118
119
|
import { AutoloopRunner } from './autoloop/runner.js';
|
|
119
120
|
import { ClaudeAgentDispatcher } from './autoloop/dispatcher.js';
|
|
120
121
|
import { DEFAULT_PUSH_POLICY } from './autoloop/types.js';
|
|
@@ -935,6 +936,47 @@ export class SessionManager {
|
|
|
935
936
|
}
|
|
936
937
|
return { ok: true, goal: session.goal ?? null };
|
|
937
938
|
}
|
|
939
|
+
// ─── Codex app-server v2 RPCs (codex-app engine only, Codex 0.137) ────────
|
|
940
|
+
//
|
|
941
|
+
// turn/interrupt, turn/steer, thread/fork, thread/rollback, model/list — the
|
|
942
|
+
// high-value app-server surface beyond /goal. Each requires a `codex-app`
|
|
943
|
+
// session; the discriminator is the presence of the `interrupt` method.
|
|
944
|
+
_getCodexAppSession(name, feature) {
|
|
945
|
+
const managed = this.sessions.get(name);
|
|
946
|
+
if (!managed)
|
|
947
|
+
throw new Error(`Session not found: ${name}`);
|
|
948
|
+
const session = managed.session;
|
|
949
|
+
if (typeof session.interrupt !== 'function') {
|
|
950
|
+
const engine = managed.config.engine || 'claude';
|
|
951
|
+
throw new Error(`Session "${name}" uses engine "${engine}" which does not support ${feature}. ` +
|
|
952
|
+
`Start a session with engine: "codex-app".`);
|
|
953
|
+
}
|
|
954
|
+
return session;
|
|
955
|
+
}
|
|
956
|
+
async codexInterrupt(name) {
|
|
957
|
+
const r = await this._getCodexAppSession(name, 'turn/interrupt').interrupt();
|
|
958
|
+
return { ok: true, ...r };
|
|
959
|
+
}
|
|
960
|
+
async codexSteer(name, text) {
|
|
961
|
+
const r = await this._getCodexAppSession(name, 'turn/steer').steer(text);
|
|
962
|
+
return { ok: true, ...r };
|
|
963
|
+
}
|
|
964
|
+
async codexForkThread(name) {
|
|
965
|
+
const r = await this._getCodexAppSession(name, 'thread/fork').forkThread();
|
|
966
|
+
return { ok: true, ...r };
|
|
967
|
+
}
|
|
968
|
+
async codexRollback(name, numTurns) {
|
|
969
|
+
await this._getCodexAppSession(name, 'thread/rollback').rollback(numTurns);
|
|
970
|
+
return { ok: true, numTurns };
|
|
971
|
+
}
|
|
972
|
+
async codexModels(name) {
|
|
973
|
+
const models = await this._getCodexAppSession(name, 'model/list').listModels();
|
|
974
|
+
return { ok: true, models };
|
|
975
|
+
}
|
|
976
|
+
async codexThreads(name, opts = {}) {
|
|
977
|
+
const r = await this._getCodexAppSession(name, 'thread/list').listThreads(opts);
|
|
978
|
+
return { ok: true, ...r };
|
|
979
|
+
}
|
|
938
980
|
// ─── Claude /goal helpers (CLI 2.1.139, claude engine only) ────────
|
|
939
981
|
//
|
|
940
982
|
// Claude Code's /goal slash command works in non-interactive stream-json
|
|
@@ -984,6 +1026,34 @@ export class SessionManager {
|
|
|
984
1026
|
});
|
|
985
1027
|
return { stdout, stderr };
|
|
986
1028
|
}
|
|
1029
|
+
/**
|
|
1030
|
+
* Wraps `claude agents --json` — lists Claude Code background agent sessions
|
|
1031
|
+
* (state/model/title/progress). One-shot spawn; not tied to a managed session.
|
|
1032
|
+
* `all` adds `--all` (include completed); `cwd` scopes to a directory.
|
|
1033
|
+
*/
|
|
1034
|
+
async claudeAgentsList(opts = {}) {
|
|
1035
|
+
const args = ['agents', '--json'];
|
|
1036
|
+
if (opts.all)
|
|
1037
|
+
args.push('--all');
|
|
1038
|
+
if (opts.cwd)
|
|
1039
|
+
args.push('--cwd', path.resolve(opts.cwd));
|
|
1040
|
+
const { stdout } = await execFileAsync(this.pluginConfig.claudeBin, args, {
|
|
1041
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
1042
|
+
timeout: 30_000,
|
|
1043
|
+
});
|
|
1044
|
+
let agents = [];
|
|
1045
|
+
const trimmed = stdout.trim();
|
|
1046
|
+
if (trimmed) {
|
|
1047
|
+
try {
|
|
1048
|
+
const parsed = JSON.parse(trimmed);
|
|
1049
|
+
agents = Array.isArray(parsed) ? parsed : (parsed.agents ?? []);
|
|
1050
|
+
}
|
|
1051
|
+
catch {
|
|
1052
|
+
throw new Error(`claude agents --json returned non-JSON output: ${trimmed.slice(0, 200)}`);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
return { ok: true, agents };
|
|
1056
|
+
}
|
|
987
1057
|
// ─── Codex one-shot wrappers ──────────────────────────────────────────
|
|
988
1058
|
_codexBin() {
|
|
989
1059
|
return process.env.CODEX_BIN || 'codex';
|
|
@@ -1600,6 +1670,55 @@ export class SessionManager {
|
|
|
1600
1670
|
this._scheduleCouncilCleanup(id); // reset TTL — council may be restarted
|
|
1601
1671
|
return result;
|
|
1602
1672
|
}
|
|
1673
|
+
// ─── Fan-out (parallel multi-engine task, no consensus) ────────────────
|
|
1674
|
+
fanouts = new Map();
|
|
1675
|
+
fanoutCleanupTimers = new Map();
|
|
1676
|
+
/**
|
|
1677
|
+
* Start a fan-out: run the task across N engine/model agents in parallel and
|
|
1678
|
+
* collect their answers (optional synthesis). Runs in the background; poll
|
|
1679
|
+
* with fanoutStatus. Distinct from council — no rounds, votes, or worktrees.
|
|
1680
|
+
*/
|
|
1681
|
+
fanoutStart(config) {
|
|
1682
|
+
if (!config.agents?.length)
|
|
1683
|
+
throw new Error('fanoutStart: at least one agent is required');
|
|
1684
|
+
const names = config.agents.map((a) => a.name);
|
|
1685
|
+
if (new Set(names).size !== names.length) {
|
|
1686
|
+
throw new Error('fanoutStart: agent names must be unique (they form session names)');
|
|
1687
|
+
}
|
|
1688
|
+
const fanout = new Fanout(config, this, this.logger);
|
|
1689
|
+
const session = fanout.init();
|
|
1690
|
+
this.fanouts.set(session.id, fanout);
|
|
1691
|
+
fanout
|
|
1692
|
+
.run()
|
|
1693
|
+
.catch((err) => this.logger.error(`Fanout ${session.id} failed:`, err))
|
|
1694
|
+
.finally(() => this._scheduleFanoutCleanup(session.id));
|
|
1695
|
+
return session;
|
|
1696
|
+
}
|
|
1697
|
+
fanoutStatus(id) {
|
|
1698
|
+
const fanout = this.fanouts.get(id);
|
|
1699
|
+
if (!fanout)
|
|
1700
|
+
throw new Error(`Fanout '${id}' not found`);
|
|
1701
|
+
return fanout.getSession();
|
|
1702
|
+
}
|
|
1703
|
+
fanoutAbort(id) {
|
|
1704
|
+
const fanout = this.fanouts.get(id);
|
|
1705
|
+
if (!fanout)
|
|
1706
|
+
throw new Error(`Fanout '${id}' not found`);
|
|
1707
|
+
fanout.abort();
|
|
1708
|
+
this._scheduleFanoutCleanup(id);
|
|
1709
|
+
}
|
|
1710
|
+
_scheduleFanoutCleanup(id) {
|
|
1711
|
+
const existing = this.fanoutCleanupTimers.get(id);
|
|
1712
|
+
if (existing)
|
|
1713
|
+
clearTimeout(existing);
|
|
1714
|
+
const timer = setTimeout(() => {
|
|
1715
|
+
this.fanouts.delete(id);
|
|
1716
|
+
this.fanoutCleanupTimers.delete(id);
|
|
1717
|
+
}, RESULT_TTL_MS);
|
|
1718
|
+
if (typeof timer.unref === 'function')
|
|
1719
|
+
timer.unref();
|
|
1720
|
+
this.fanoutCleanupTimers.set(id, timer);
|
|
1721
|
+
}
|
|
1603
1722
|
// ─── Inbox (cross-session messaging) — delegated to InboxManager ────
|
|
1604
1723
|
get _sessionLookup() {
|
|
1605
1724
|
return {
|
|
@@ -1805,40 +1924,73 @@ export class SessionManager {
|
|
|
1805
1924
|
persona: 'You are a memory reviewer. Focus on: memory leaks, circular references, large object retention, stream handling.',
|
|
1806
1925
|
},
|
|
1807
1926
|
];
|
|
1808
|
-
const agents = reviewAngles.slice(0, agentCount).map((a) => ({
|
|
1809
|
-
...a,
|
|
1810
|
-
model: opts?.model,
|
|
1811
|
-
}));
|
|
1812
1927
|
const maxMinutes = Math.min(25, Math.max(5, opts?.maxDurationMinutes || 10));
|
|
1813
1928
|
const focus = opts?.focus || 'Find bugs, security issues, and code quality problems';
|
|
1814
|
-
const
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1929
|
+
const reviewInstruction = `# Code Review Task\n\nReview the codebase in this project. ${focus}.\n\n` +
|
|
1930
|
+
`Examine the code from your specialty angle and report bugs found with file paths and line numbers.`;
|
|
1931
|
+
// Cross-engine review: round-robin the requested engines across reviewers
|
|
1932
|
+
// (default claude-only — unchanged behavior). Each reviewer's persona is
|
|
1933
|
+
// its prompt; per-agent failures are isolated by the fan-out runner.
|
|
1934
|
+
const engines = opts?.engines?.length ? opts.engines : ['claude'];
|
|
1935
|
+
const agents = reviewAngles.slice(0, agentCount).map((a, i) => ({
|
|
1936
|
+
name: a.name,
|
|
1937
|
+
engine: engines[i % engines.length],
|
|
1938
|
+
model: opts?.model,
|
|
1939
|
+
prompt: `${a.persona}\n\n${reviewInstruction}`,
|
|
1940
|
+
// Review is read-only: keep reviewers out of edit mode so they analyse and
|
|
1941
|
+
// report without modifying the very code they review. (Unlike council,
|
|
1942
|
+
// fan-out shares the project dir — there is no worktree to sandbox edits.
|
|
1943
|
+
// `plan` constrains the claude engine; non-claude reviewers, which are
|
|
1944
|
+
// opt-in via `engines`, run under their engine's default sandbox.)
|
|
1945
|
+
permissionMode: 'plan',
|
|
1946
|
+
}));
|
|
1947
|
+
let fanoutSession;
|
|
1948
|
+
try {
|
|
1949
|
+
fanoutSession = this.fanoutStart({
|
|
1950
|
+
task: reviewInstruction,
|
|
1951
|
+
projectDir: cwd,
|
|
1952
|
+
agents,
|
|
1953
|
+
synthesize: true,
|
|
1954
|
+
agentTimeoutMs: maxMinutes * 60 * 1000,
|
|
1955
|
+
maxTurnsPerAgent: 20,
|
|
1956
|
+
});
|
|
1957
|
+
}
|
|
1958
|
+
catch (err) {
|
|
1959
|
+
// Fan-out failed to even start (e.g. validation) — surface it on the
|
|
1960
|
+
// stored result instead of leaving it frozen at 'running'.
|
|
1961
|
+
result.status = 'error';
|
|
1962
|
+
result.error = err.message;
|
|
1963
|
+
result.endTime = new Date().toISOString();
|
|
1964
|
+
setTimeout(() => this.ultrareviews.delete(id), RESULT_TTL_MS);
|
|
1965
|
+
return result;
|
|
1966
|
+
}
|
|
1967
|
+
// `councilId` is kept for the UltrareviewResult contract; it now holds the
|
|
1968
|
+
// fan-out id (an opaque run id used only by ultrareview_status).
|
|
1969
|
+
result.councilId = fanoutSession.id;
|
|
1970
|
+
// Poll the fan-out for completion (store ref for shutdown cleanup).
|
|
1825
1971
|
const pollInterval = setInterval(() => {
|
|
1826
1972
|
try {
|
|
1827
|
-
const status = this.
|
|
1973
|
+
const status = this.fanoutStatus(fanoutSession.id);
|
|
1828
1974
|
if (!status || status.status === 'running')
|
|
1829
1975
|
return;
|
|
1830
1976
|
clearInterval(pollInterval);
|
|
1831
1977
|
this.ultrareviewPollers.delete(id);
|
|
1832
1978
|
result.status = status.status === 'error' ? 'error' : 'completed';
|
|
1833
1979
|
result.endTime = new Date().toISOString();
|
|
1834
|
-
//
|
|
1835
|
-
if (status.
|
|
1836
|
-
result.findings = status.
|
|
1980
|
+
// Prefer the synthesis pass; fall back to joining successful results.
|
|
1981
|
+
if (status.synthesis) {
|
|
1982
|
+
result.findings = status.synthesis;
|
|
1983
|
+
}
|
|
1984
|
+
else if (status.results.length > 0) {
|
|
1985
|
+
result.findings = status.results
|
|
1986
|
+
.filter((r) => r.ok)
|
|
1987
|
+
.map((r) => `## ${r.agent}\n\n${r.output}`)
|
|
1988
|
+
.join('\n\n---\n\n');
|
|
1837
1989
|
}
|
|
1838
1990
|
setTimeout(() => this.ultrareviews.delete(id), RESULT_TTL_MS);
|
|
1839
1991
|
}
|
|
1840
1992
|
catch {
|
|
1841
|
-
//
|
|
1993
|
+
// Fan-out may have been cleaned up; stop polling.
|
|
1842
1994
|
clearInterval(pollInterval);
|
|
1843
1995
|
this.ultrareviewPollers.delete(id);
|
|
1844
1996
|
}
|