@triflux/remote 10.51.0 → 10.52.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/cto/brief.mjs +0 -8
- package/cto/collect.mjs +1 -127
- package/cto/current.schema.json +2 -19
- package/cto/hygiene.mjs +2 -61
- package/cto/lake-root.mjs +0 -56
- package/cto/status.mjs +5 -139
- package/hub/index.mjs +0 -9
- package/hub/team/agents-row-attach.py +120 -0
- package/hub/team/agents-row.mjs +200 -0
- package/hub/team/claude-daemon-control.mjs +1 -2
- package/hub/team/claude-session-projection.mjs +5 -73
- package/hub/team/cli/commands/debug.mjs +1 -2
- package/hub/team/cli/commands/start/index.mjs +2 -1
- package/hub/team/cli/commands/start/start-mux.mjs +2 -4
- package/hub/team/cli/services/state-store.mjs +5 -20
- package/hub/team/headless.mjs +96 -543
- package/hub/team/psmux.mjs +3 -33
- package/hub/team/session-context.mjs +1 -1
- package/hub/team/tui-viewer.mjs +9 -56
- package/hub/team/tui.mjs +0 -37
- package/hub/team/uds-orchestrator.mjs +2 -580
- package/hub/workers/factory.mjs +6 -51
- package/hub/workers/interface.mjs +1 -7
- package/hub/workers/lib/jsonrpc-core.mjs +11 -0
- package/hub/workers/lib/jsonrpc-ws-uds.mjs +5 -6
- package/package.json +3 -8
- package/scripts/lib/agent-route-policy.mjs +0 -67
- package/scripts/lib/cli-claude.mjs +0 -12
- package/scripts/lib/env-probe.mjs +0 -70
- package/scripts/lib/legacy-mcp-cleanup.mjs +283 -0
- package/scripts/lib/mcp-guard-engine.mjs +40 -98
- package/hub/hub-lifecycle.mjs +0 -248
- package/hub/pipe.mjs +0 -825
- package/hub/promote-penalties.mjs +0 -138
- package/hub/public/dashboard.html +0 -416
- package/hub/schema.sql +0 -209
- package/hub/server.mjs +0 -3048
- package/hub/store-adapter.mjs +0 -182
- package/hub/store.mjs +0 -1736
- package/hub/team/cli/services/hub-client.mjs +0 -178
- package/hub/team/conductor-registry.mjs +0 -121
- package/hub/team/cto-auto-collect.mjs +0 -115
- package/hub/team/lead-control.mjs +0 -191
- package/hub/team/session-sync.mjs +0 -316
- package/hub/team/swarm-intent.mjs +0 -18
- package/hub/team/synapse-cli.mjs +0 -458
- package/hub/team/synapse-http.mjs +0 -311
- package/hub/team/synapse-registry.mjs +0 -618
- package/hub/team/tui-synapse.mjs +0 -223
- package/hub/team/worker-sandbox.mjs +0 -125
- package/hub/tools.mjs +0 -900
- package/hub/workers/codex-app-server-worker.mjs +0 -1067
- package/hub/workers/delegator-mcp.mjs +0 -1098
- package/hub/workers/lib/jsonrpc-stdio.mjs +0 -204
- package/scripts/lib/claudemd-scanner.mjs +0 -26
- package/scripts/lib/context.mjs +0 -51
- package/scripts/lib/handoff.mjs +0 -187
- package/scripts/lib/logger.mjs +0 -122
- package/scripts/lib/stealth-fetch.mjs +0 -176
|
@@ -1,26 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import { mkdtemp, realpath, rm, stat } from "node:fs/promises";
|
|
4
|
-
import net from "node:net";
|
|
5
|
-
import { tmpdir } from "node:os";
|
|
6
|
-
import { join as pathJoin, resolve as pathResolve } from "node:path";
|
|
7
|
-
|
|
8
|
-
import { execute as defaultExecuteCodex } from "@triflux/core/hub/codex-adapter.mjs";
|
|
1
|
+
import { realpath } from "node:fs/promises";
|
|
2
|
+
import { resolve as pathResolve } from "node:path";
|
|
9
3
|
import {
|
|
10
4
|
createActivityLifecycle,
|
|
11
5
|
isActivityLifecycleEnabled,
|
|
12
6
|
resolveHardCeilingMs,
|
|
13
7
|
} from "@triflux/core/hub/lib/worker-lifecycle.mjs";
|
|
14
8
|
import { JsonRpcWsUdsClient } from "../workers/lib/jsonrpc-ws-uds.mjs";
|
|
15
|
-
import {
|
|
16
|
-
buildClaudePromptDispatchPayload,
|
|
17
|
-
deriveClaudeDaemonPaths,
|
|
18
|
-
dispatchClaudeDaemonJob,
|
|
19
|
-
teardownClaudeDaemonJob,
|
|
20
|
-
} from "./claude-daemon-control.mjs";
|
|
21
9
|
import { contextGuard, readCodexContext } from "./session-context.mjs";
|
|
22
10
|
|
|
23
|
-
const DEFAULT_TIMEOUT_MS = 90_000;
|
|
24
11
|
const DEFAULT_CODEX_APP_SERVER_UDS_TIMEOUT_MS = 120_000;
|
|
25
12
|
const DEFAULT_CODEX_APP_SERVER_UDS_BOOTSTRAP_MS = 12_000;
|
|
26
13
|
|
|
@@ -456,568 +443,3 @@ export async function askCodexAppServerThread({
|
|
|
456
443
|
client.close();
|
|
457
444
|
}
|
|
458
445
|
}
|
|
459
|
-
|
|
460
|
-
function requireEndpoint(endpoint, name) {
|
|
461
|
-
if (!endpoint || typeof endpoint.ask !== "function") {
|
|
462
|
-
throw new Error(`${name} endpoint with ask(prompt) is required`);
|
|
463
|
-
}
|
|
464
|
-
return endpoint;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
function turn(endpoint, response, role) {
|
|
468
|
-
return {
|
|
469
|
-
role,
|
|
470
|
-
endpoint: response.endpoint || endpoint.name || role,
|
|
471
|
-
text: response.text || "",
|
|
472
|
-
done: response.done === true,
|
|
473
|
-
meta: response.meta || undefined,
|
|
474
|
-
};
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
function codexLeadPrompt(task) {
|
|
478
|
-
return `Lead as Codex. Create a concise plan for this task, then stop.\n\nTask:\n${task}`;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
function claudeLeadPrompt(task) {
|
|
482
|
-
return `Lead as Claude. Create a concise plan for this task, then stop.\n\nTask:\n${task}`;
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
function claudeReviewCodexPrompt(task, codexPlan) {
|
|
486
|
-
return `Review Codex's plan for this task. Return risks and one improvement.\n\nTask:\n${task}\n\nCodex plan:\n${codexPlan}`;
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
function codexReviewClaudePrompt(task, claudePlan) {
|
|
490
|
-
return `Review Claude's plan for this task. Return risks and one improvement.\n\nTask:\n${task}\n\nClaude plan:\n${claudePlan}`;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
function codexFinalPrompt(task, codexPlan, claudeReview) {
|
|
494
|
-
return `Finalize as Codex using Claude's review. Return the final orchestration decision.\n\nTask:\n${task}\n\nCodex plan:\n${codexPlan}\n\nClaude review:\n${claudeReview}`;
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
function claudeFinalPrompt(task, claudePlan, codexReview) {
|
|
498
|
-
return `Finalize as Claude using Codex's review. Return the final orchestration decision.\n\nTask:\n${task}\n\nClaude plan:\n${claudePlan}\n\nCodex review:\n${codexReview}`;
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
function peerPrompt(agentName, task) {
|
|
502
|
-
return `Act as a peer collaborator named ${agentName}. Independently answer this task in a concise way.\n\nTask:\n${task}`;
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
function peerSynthesisPrompt(task, claudeAnswer, codexAnswer) {
|
|
506
|
-
return `Synthesize peer answers from Claude and Codex. Return the agreed next action and conflicts.\n\nTask:\n${task}\n\nClaude answer:\n${claudeAnswer}\n\nCodex answer:\n${codexAnswer}`;
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
export async function runUdsOrchestration({ mode, task, claude, codex } = {}) {
|
|
510
|
-
const selectedMode = mode || "peer";
|
|
511
|
-
const claudeEndpoint = requireEndpoint(claude, "claude");
|
|
512
|
-
const codexEndpoint = requireEndpoint(codex, "codex");
|
|
513
|
-
if (!task || typeof task !== "string") throw new Error("task is required");
|
|
514
|
-
|
|
515
|
-
const turns = [];
|
|
516
|
-
if (selectedMode === "codex-led") {
|
|
517
|
-
const codexPlan = await codexEndpoint.ask(codexLeadPrompt(task), {
|
|
518
|
-
mode: selectedMode,
|
|
519
|
-
phase: "lead",
|
|
520
|
-
});
|
|
521
|
-
turns.push(turn(codexEndpoint, codexPlan, "lead"));
|
|
522
|
-
|
|
523
|
-
const claudeReview = await claudeEndpoint.ask(
|
|
524
|
-
claudeReviewCodexPrompt(task, codexPlan.text),
|
|
525
|
-
{ mode: selectedMode, phase: "review" },
|
|
526
|
-
);
|
|
527
|
-
turns.push(turn(claudeEndpoint, claudeReview, "review"));
|
|
528
|
-
|
|
529
|
-
const final = await codexEndpoint.ask(
|
|
530
|
-
codexFinalPrompt(task, codexPlan.text, claudeReview.text),
|
|
531
|
-
{ mode: selectedMode, phase: "final" },
|
|
532
|
-
);
|
|
533
|
-
turns.push(turn(codexEndpoint, final, "final"));
|
|
534
|
-
return { mode: selectedMode, task, turns, final: turns.at(-1) };
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
if (selectedMode === "claude-led") {
|
|
538
|
-
const claudePlan = await claudeEndpoint.ask(claudeLeadPrompt(task), {
|
|
539
|
-
mode: selectedMode,
|
|
540
|
-
phase: "lead",
|
|
541
|
-
});
|
|
542
|
-
turns.push(turn(claudeEndpoint, claudePlan, "lead"));
|
|
543
|
-
|
|
544
|
-
const codexReview = await codexEndpoint.ask(
|
|
545
|
-
codexReviewClaudePrompt(task, claudePlan.text),
|
|
546
|
-
{ mode: selectedMode, phase: "review" },
|
|
547
|
-
);
|
|
548
|
-
turns.push(turn(codexEndpoint, codexReview, "review"));
|
|
549
|
-
|
|
550
|
-
const final = await claudeEndpoint.ask(
|
|
551
|
-
claudeFinalPrompt(task, claudePlan.text, codexReview.text),
|
|
552
|
-
{ mode: selectedMode, phase: "final" },
|
|
553
|
-
);
|
|
554
|
-
turns.push(turn(claudeEndpoint, final, "final"));
|
|
555
|
-
return { mode: selectedMode, task, turns, final: turns.at(-1) };
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
if (selectedMode === "peer") {
|
|
559
|
-
const [claudePeer, codexPeer] = await Promise.all([
|
|
560
|
-
claudeEndpoint.ask(peerPrompt("Claude", task), {
|
|
561
|
-
mode: selectedMode,
|
|
562
|
-
phase: "peer",
|
|
563
|
-
}),
|
|
564
|
-
codexEndpoint.ask(peerPrompt("Codex", task), {
|
|
565
|
-
mode: selectedMode,
|
|
566
|
-
phase: "peer",
|
|
567
|
-
}),
|
|
568
|
-
]);
|
|
569
|
-
turns.push(turn(claudeEndpoint, claudePeer, "peer"));
|
|
570
|
-
turns.push(turn(codexEndpoint, codexPeer, "peer"));
|
|
571
|
-
|
|
572
|
-
const final = await claudeEndpoint.ask(
|
|
573
|
-
peerSynthesisPrompt(task, claudePeer.text, codexPeer.text),
|
|
574
|
-
{ mode: selectedMode, phase: "synthesis" },
|
|
575
|
-
);
|
|
576
|
-
turns.push(turn(claudeEndpoint, final, "synthesis"));
|
|
577
|
-
return { mode: selectedMode, task, turns, final: turns.at(-1) };
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
throw new Error(`Unsupported UDS orchestration mode: ${selectedMode}`);
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
function scanMessage(message, marker, state, onActivity = () => {}) {
|
|
584
|
-
const scan = (text) => {
|
|
585
|
-
// Only stream/snapshot content advances liveness. "state" heartbeats do
|
|
586
|
-
// not keep an otherwise silent turn alive forever.
|
|
587
|
-
onActivity();
|
|
588
|
-
state.streamBytes += Buffer.byteLength(text);
|
|
589
|
-
state.stream.push(text);
|
|
590
|
-
if (/⏺/.test(stripAnsi(text))) state.assistantStarted = true;
|
|
591
|
-
if (state.assistantStarted && text.includes(marker)) {
|
|
592
|
-
state.markerSeen = true;
|
|
593
|
-
}
|
|
594
|
-
};
|
|
595
|
-
|
|
596
|
-
state.messageCount += 1;
|
|
597
|
-
if (message.type === "snapshot") {
|
|
598
|
-
for (const line of message.streamTail || []) scan(line);
|
|
599
|
-
} else if (message.type === "stream" && typeof message.line === "string") {
|
|
600
|
-
scan(message.line);
|
|
601
|
-
} else if (message.type === "state") {
|
|
602
|
-
state.stateCount += 1;
|
|
603
|
-
} else if (message.type === "settled") {
|
|
604
|
-
state.settled = message.outcome || "settled";
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
export function subscribeClaudeUntilMarker(
|
|
609
|
-
controlSock,
|
|
610
|
-
short,
|
|
611
|
-
marker,
|
|
612
|
-
{
|
|
613
|
-
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
614
|
-
maxTurnMs = Math.min(resolveHardCeilingMs(), 15 * 60_000),
|
|
615
|
-
} = {},
|
|
616
|
-
) {
|
|
617
|
-
return new Promise((resolve) => {
|
|
618
|
-
const socket = net.connect(controlSock);
|
|
619
|
-
let data = "";
|
|
620
|
-
let finished = false;
|
|
621
|
-
const startedAt = Date.now();
|
|
622
|
-
const state = {
|
|
623
|
-
short,
|
|
624
|
-
marker,
|
|
625
|
-
markerSeen: false,
|
|
626
|
-
settled: null,
|
|
627
|
-
messageCount: 0,
|
|
628
|
-
stateCount: 0,
|
|
629
|
-
streamBytes: 0,
|
|
630
|
-
stream: [],
|
|
631
|
-
assistantStarted: false,
|
|
632
|
-
};
|
|
633
|
-
const activityEnabled = isActivityLifecycleEnabled();
|
|
634
|
-
const lifecycle = createActivityLifecycle({
|
|
635
|
-
enabled: activityEnabled,
|
|
636
|
-
interventionMs: timeoutMs,
|
|
637
|
-
hardCeilingMs: maxTurnMs,
|
|
638
|
-
});
|
|
639
|
-
let timer = null;
|
|
640
|
-
|
|
641
|
-
const armTimer = () => {
|
|
642
|
-
timer = setTimeout(async () => {
|
|
643
|
-
if (!activityEnabled) return finish({ timedOut: true });
|
|
644
|
-
const reason = await lifecycle.check();
|
|
645
|
-
if (reason) return finish({ timedOut: true, timeoutReason: reason });
|
|
646
|
-
armTimer();
|
|
647
|
-
}, timeoutMs);
|
|
648
|
-
};
|
|
649
|
-
armTimer();
|
|
650
|
-
|
|
651
|
-
function finish(extra = {}) {
|
|
652
|
-
if (finished) return;
|
|
653
|
-
finished = true;
|
|
654
|
-
clearTimeout(timer);
|
|
655
|
-
socket.destroy();
|
|
656
|
-
resolve({ ...state, elapsedMs: Date.now() - startedAt, ...extra });
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
socket.on("error", (error) => finish({ error: error.message }));
|
|
660
|
-
socket.on("connect", () => {
|
|
661
|
-
socket.write(
|
|
662
|
-
`${JSON.stringify({ proto: 1, op: "subscribe", short, tail: 80, marker })}\n`,
|
|
663
|
-
);
|
|
664
|
-
});
|
|
665
|
-
socket.on("data", (chunk) => {
|
|
666
|
-
data += chunk.toString("utf8");
|
|
667
|
-
while (data.includes("\n")) {
|
|
668
|
-
const index = data.indexOf("\n");
|
|
669
|
-
const line = data.slice(0, index).trim();
|
|
670
|
-
data = data.slice(index + 1);
|
|
671
|
-
if (!line) continue;
|
|
672
|
-
try {
|
|
673
|
-
scanMessage(JSON.parse(line), marker, state, () =>
|
|
674
|
-
lifecycle.observe(),
|
|
675
|
-
);
|
|
676
|
-
} catch (_error) {
|
|
677
|
-
state.messageCount += 1;
|
|
678
|
-
}
|
|
679
|
-
if (state.markerSeen || state.settled) finish();
|
|
680
|
-
}
|
|
681
|
-
});
|
|
682
|
-
socket.on("close", () => finish({ closed: true }));
|
|
683
|
-
});
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
export function stripAnsi(text) {
|
|
687
|
-
return String(text)
|
|
688
|
-
.replace(/\x1B\][^\x07]*(?:\x07|\x1B\\)/g, "")
|
|
689
|
-
.replace(/\x1B[78]/g, "")
|
|
690
|
-
.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "");
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
function isClaudeUdsNoiseLine(line) {
|
|
694
|
-
return (
|
|
695
|
-
/Claude Code|Opus|remote-control|Brewed|Baked|Cogitated|Crunched|▐|▛|▜|▝|▘|Max|context/.test(
|
|
696
|
-
line,
|
|
697
|
-
) ||
|
|
698
|
-
/^[╭╮╰╯│─┌┐└┘├┤┬┴┼\s]+$/.test(line) ||
|
|
699
|
-
/^\s*[❯✳✻]\s*/.test(line)
|
|
700
|
-
);
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
export function extractClaudeUdsText(text, marker) {
|
|
704
|
-
const cleaned = stripAnsi(String(text).replaceAll(marker, "")).replace(
|
|
705
|
-
/\r/g,
|
|
706
|
-
"\n",
|
|
707
|
-
);
|
|
708
|
-
const lines = cleaned
|
|
709
|
-
.split("\n")
|
|
710
|
-
.map((line) => line.trimEnd())
|
|
711
|
-
.filter((line) => line.trim().length > 0);
|
|
712
|
-
const assistantIndex = lines.findLastIndex((line) => /^\s*⏺\s*/.test(line));
|
|
713
|
-
if (assistantIndex >= 0) {
|
|
714
|
-
const useful = lines
|
|
715
|
-
.slice(assistantIndex)
|
|
716
|
-
.map((line, index) => (index === 0 ? line.replace(/^\s*⏺\s*/, "") : line))
|
|
717
|
-
.filter((line) => !isClaudeUdsNoiseLine(line));
|
|
718
|
-
return useful.join("\n").trim();
|
|
719
|
-
}
|
|
720
|
-
const useful = lines.filter((line) => !isClaudeUdsNoiseLine(line));
|
|
721
|
-
return useful.at(-1)?.trim() || cleaned.trim();
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
export function createClaudeUdsEndpoint({
|
|
725
|
-
controlSock,
|
|
726
|
-
daemonPaths = deriveClaudeDaemonPaths(),
|
|
727
|
-
cwd = process.cwd(),
|
|
728
|
-
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
729
|
-
maxTurnMs = Math.min(resolveHardCeilingMs(), 15 * 60_000),
|
|
730
|
-
markerFactory = () =>
|
|
731
|
-
`TFX_UDS_ORCH_DONE_${crypto.randomBytes(6).toString("hex")}`,
|
|
732
|
-
shortFactory,
|
|
733
|
-
sessionIdFactory,
|
|
734
|
-
dispatchDaemonJob = dispatchClaudeDaemonJob,
|
|
735
|
-
teardownDaemonJob = teardownClaudeDaemonJob,
|
|
736
|
-
} = {}) {
|
|
737
|
-
const sock = controlSock || daemonPaths.controlSock;
|
|
738
|
-
return {
|
|
739
|
-
name: "claude-uds",
|
|
740
|
-
async ask(prompt, meta = {}) {
|
|
741
|
-
const marker = markerFactory();
|
|
742
|
-
const wrappedPrompt = `${prompt}\n\nWhen finished, print this exact completion marker on its own line: ${marker}`;
|
|
743
|
-
const name =
|
|
744
|
-
meta.name ||
|
|
745
|
-
`tfx uds ${meta.mode || "orchestration"} ${meta.phase || "ask"}`;
|
|
746
|
-
const payload = buildClaudePromptDispatchPayload({
|
|
747
|
-
short: shortFactory?.(),
|
|
748
|
-
sessionId: sessionIdFactory?.(),
|
|
749
|
-
cwd,
|
|
750
|
-
prompt: wrappedPrompt,
|
|
751
|
-
name,
|
|
752
|
-
});
|
|
753
|
-
const dispatch = await dispatchDaemonJob({
|
|
754
|
-
paths: daemonPaths,
|
|
755
|
-
controlSock: sock,
|
|
756
|
-
payload,
|
|
757
|
-
agent: "claude",
|
|
758
|
-
name,
|
|
759
|
-
cwd,
|
|
760
|
-
dispatchTimeoutMs: 5000,
|
|
761
|
-
});
|
|
762
|
-
const subscription = await subscribeClaudeUntilMarker(
|
|
763
|
-
sock,
|
|
764
|
-
payload.short,
|
|
765
|
-
marker,
|
|
766
|
-
{ timeoutMs, maxTurnMs },
|
|
767
|
-
);
|
|
768
|
-
await teardownDaemonJob({
|
|
769
|
-
controlSock: sock,
|
|
770
|
-
paths: daemonPaths,
|
|
771
|
-
short: payload.short,
|
|
772
|
-
sessionProjectionPath: dispatch.sessionProjectionPath,
|
|
773
|
-
sessionId: payload.sessionId,
|
|
774
|
-
});
|
|
775
|
-
return {
|
|
776
|
-
endpoint: "claude-uds",
|
|
777
|
-
text: extractClaudeUdsText(subscription.stream.join(""), marker),
|
|
778
|
-
done: subscription.markerSeen === true,
|
|
779
|
-
dispatch,
|
|
780
|
-
subscription: {
|
|
781
|
-
markerSeen: subscription.markerSeen,
|
|
782
|
-
elapsedMs: subscription.elapsedMs,
|
|
783
|
-
messageCount: subscription.messageCount,
|
|
784
|
-
streamBytes: subscription.streamBytes,
|
|
785
|
-
timedOut: subscription.timedOut === true,
|
|
786
|
-
settled: subscription.settled,
|
|
787
|
-
},
|
|
788
|
-
cleanup: { ok: true },
|
|
789
|
-
meta: { short: payload.short, marker },
|
|
790
|
-
};
|
|
791
|
-
},
|
|
792
|
-
};
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
export function createCodexExecEndpoint({
|
|
796
|
-
executeCodex = defaultExecuteCodex,
|
|
797
|
-
workdir = process.cwd(),
|
|
798
|
-
timeout = 120_000,
|
|
799
|
-
profile,
|
|
800
|
-
} = {}) {
|
|
801
|
-
return {
|
|
802
|
-
name: "codex",
|
|
803
|
-
async ask(prompt) {
|
|
804
|
-
const result = await executeCodex({ prompt, workdir, timeout, profile });
|
|
805
|
-
if (result?.ok === false) {
|
|
806
|
-
throw new Error(
|
|
807
|
-
`Codex exec failed: ${result.stderr || result.error || "unknown error"}`,
|
|
808
|
-
);
|
|
809
|
-
}
|
|
810
|
-
return {
|
|
811
|
-
endpoint: "codex",
|
|
812
|
-
text: String(
|
|
813
|
-
result?.output ?? result?.stdout ?? result?.text ?? "",
|
|
814
|
-
).trim(),
|
|
815
|
-
done: true,
|
|
816
|
-
};
|
|
817
|
-
},
|
|
818
|
-
};
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
function extractCodexThreadId(result) {
|
|
822
|
-
if (!result || typeof result !== "object") return null;
|
|
823
|
-
if (typeof result.threadId === "string") return result.threadId;
|
|
824
|
-
if (result.thread && typeof result.thread.id === "string") {
|
|
825
|
-
return result.thread.id;
|
|
826
|
-
}
|
|
827
|
-
return null;
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
async function waitForSocketFile(sockPath, deadlineMs) {
|
|
831
|
-
const start = Date.now();
|
|
832
|
-
while (Date.now() - start < deadlineMs) {
|
|
833
|
-
try {
|
|
834
|
-
if ((await stat(sockPath)).isSocket()) return true;
|
|
835
|
-
} catch {
|
|
836
|
-
/* not bound yet */
|
|
837
|
-
}
|
|
838
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
839
|
-
}
|
|
840
|
-
return false;
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
/**
|
|
844
|
-
* SIGTERM a child, wait up to graceMs for it to actually exit, then SIGKILL if
|
|
845
|
-
* still alive. Liveness is `exitCode === null && signalCode === null` — NOT
|
|
846
|
-
* `child.killed`, which only means "a signal was sent" and would suppress the
|
|
847
|
-
* SIGKILL fallback when the child ignores SIGTERM.
|
|
848
|
-
* @param {import('node:child_process').ChildProcess} child
|
|
849
|
-
* @param {number} [graceMs]
|
|
850
|
-
*/
|
|
851
|
-
async function terminateChild(child, graceMs = 1000) {
|
|
852
|
-
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
853
|
-
const exited = new Promise((resolve) => {
|
|
854
|
-
child.once("exit", () => resolve("exited"));
|
|
855
|
-
});
|
|
856
|
-
try {
|
|
857
|
-
child.kill("SIGTERM");
|
|
858
|
-
} catch {}
|
|
859
|
-
const outcome = await Promise.race([
|
|
860
|
-
exited,
|
|
861
|
-
new Promise((resolve) => setTimeout(() => resolve("timeout"), graceMs)),
|
|
862
|
-
]);
|
|
863
|
-
if (
|
|
864
|
-
outcome !== "exited" &&
|
|
865
|
-
child.exitCode === null &&
|
|
866
|
-
child.signalCode === null
|
|
867
|
-
) {
|
|
868
|
-
try {
|
|
869
|
-
child.kill("SIGKILL");
|
|
870
|
-
} catch {}
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
/**
|
|
875
|
-
* Experimental Codex endpoint that drives a real `codex app-server` over its
|
|
876
|
-
* WebSocket-over-Unix-Domain-Socket control plane (the symmetric peer of the
|
|
877
|
-
* Claude daemon `control.sock` path). Same `ask(prompt) -> {endpoint,text,done}`
|
|
878
|
-
* contract as `createCodexExecEndpoint`, so it is a drop-in for
|
|
879
|
-
* `runUdsOrchestration`'s `codex` slot — but it is NOT the default.
|
|
880
|
-
*
|
|
881
|
-
* Two modes:
|
|
882
|
-
* - spawnServer (default): mkdtemp a private socket, spawn
|
|
883
|
-
* `codex app-server --listen unix://PATH`, attach, run one turn, tear down.
|
|
884
|
-
* - attach (socketPath + spawnServer=false): connect to an already-running
|
|
885
|
-
* daemon socket (e.g. $CODEX_HOME/app-server-control/app-server-control.sock).
|
|
886
|
-
*
|
|
887
|
-
* Wire transport proven against codex-cli 0.135.0 — see
|
|
888
|
-
* experiments/native-bridge-feasibility/codex-app-server-uds-smoke.mjs.
|
|
889
|
-
*
|
|
890
|
-
* @param {object} [options]
|
|
891
|
-
* @param {string|null} [options.socketPath] Existing daemon socket to attach to.
|
|
892
|
-
* @param {boolean} [options.spawnServer] Spawn a private app-server (default true).
|
|
893
|
-
* @param {string} [options.codexBin]
|
|
894
|
-
* @param {string} [options.cwd]
|
|
895
|
-
* @param {string} [options.model]
|
|
896
|
-
* @param {number} [options.timeoutMs] Turn timeout.
|
|
897
|
-
* @param {number} [options.bootstrapTimeoutMs] Connect + handshake timeout.
|
|
898
|
-
* @param {(opts: object) => JsonRpcWsUdsClient} [options.clientFactory]
|
|
899
|
-
* @param {(command: string, args: string[], options: object) => import('node:child_process').ChildProcess} [options.spawnFn]
|
|
900
|
-
*/
|
|
901
|
-
export function createCodexAppServerUdsEndpoint({
|
|
902
|
-
socketPath = null,
|
|
903
|
-
spawnServer = true,
|
|
904
|
-
codexBin = process.env.CODEX_BIN || "codex",
|
|
905
|
-
cwd = process.cwd(),
|
|
906
|
-
model,
|
|
907
|
-
timeoutMs = DEFAULT_CODEX_APP_SERVER_UDS_TIMEOUT_MS,
|
|
908
|
-
maxTurnMs = Math.min(resolveHardCeilingMs(), 15 * 60_000),
|
|
909
|
-
bootstrapTimeoutMs = DEFAULT_CODEX_APP_SERVER_UDS_BOOTSTRAP_MS,
|
|
910
|
-
clientFactory = (opts) => new JsonRpcWsUdsClient(opts),
|
|
911
|
-
spawnFn = spawn,
|
|
912
|
-
} = {}) {
|
|
913
|
-
return {
|
|
914
|
-
name: "codex-app-server-uds",
|
|
915
|
-
async ask(prompt, _meta = {}) {
|
|
916
|
-
if (typeof prompt !== "string" || !prompt.trim()) {
|
|
917
|
-
throw new Error(
|
|
918
|
-
"codex-app-server-uds endpoint requires a non-empty prompt",
|
|
919
|
-
);
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
let dir = null;
|
|
923
|
-
let child = null;
|
|
924
|
-
let client = null;
|
|
925
|
-
let resolvedSock = socketPath;
|
|
926
|
-
|
|
927
|
-
try {
|
|
928
|
-
if (spawnServer && !socketPath) {
|
|
929
|
-
dir = await mkdtemp(pathJoin(tmpdir(), "tfx-codex-appserver-uds-"));
|
|
930
|
-
resolvedSock = pathJoin(dir, "app-server.sock");
|
|
931
|
-
child = spawnFn(
|
|
932
|
-
codexBin,
|
|
933
|
-
["app-server", "--listen", `unix://${resolvedSock}`],
|
|
934
|
-
{ stdio: ["ignore", "pipe", "pipe"], cwd, env: process.env },
|
|
935
|
-
);
|
|
936
|
-
let stderr = "";
|
|
937
|
-
child.stderr?.on("data", (chunk) => {
|
|
938
|
-
stderr += String(chunk);
|
|
939
|
-
if (stderr.length > 8000) stderr = stderr.slice(-8000);
|
|
940
|
-
});
|
|
941
|
-
const earlyExit = new Promise((resolve) => {
|
|
942
|
-
child.once("error", (err) =>
|
|
943
|
-
resolve(`spawn error: ${err.message}`),
|
|
944
|
-
);
|
|
945
|
-
child.once("exit", (code, sig) =>
|
|
946
|
-
resolve(`exited early code=${code} sig=${sig || ""}`),
|
|
947
|
-
);
|
|
948
|
-
});
|
|
949
|
-
const ready = await Promise.race([
|
|
950
|
-
waitForSocketFile(resolvedSock, bootstrapTimeoutMs).then((ok) =>
|
|
951
|
-
ok ? "ready" : "no-socket",
|
|
952
|
-
),
|
|
953
|
-
earlyExit,
|
|
954
|
-
]);
|
|
955
|
-
if (ready !== "ready") {
|
|
956
|
-
throw new Error(
|
|
957
|
-
`codex app-server did not bind socket: ${ready}${
|
|
958
|
-
stderr ? ` — ${stderr.slice(-400)}` : ""
|
|
959
|
-
}`,
|
|
960
|
-
);
|
|
961
|
-
}
|
|
962
|
-
}
|
|
963
|
-
if (!resolvedSock) {
|
|
964
|
-
throw new Error(
|
|
965
|
-
"codex-app-server-uds endpoint needs socketPath or spawnServer=true",
|
|
966
|
-
);
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
client = codexClient(resolvedSock, bootstrapTimeoutMs, clientFactory);
|
|
970
|
-
await initializeCodexClient(client, bootstrapTimeoutMs);
|
|
971
|
-
|
|
972
|
-
const threadParams = {
|
|
973
|
-
sandbox: "read-only",
|
|
974
|
-
approvalPolicy: "never",
|
|
975
|
-
ephemeral: true,
|
|
976
|
-
};
|
|
977
|
-
if (typeof model === "string" && model) threadParams.model = model;
|
|
978
|
-
if (cwd) threadParams.cwd = cwd;
|
|
979
|
-
const threadStart = await client.request(
|
|
980
|
-
"thread/start",
|
|
981
|
-
threadParams,
|
|
982
|
-
bootstrapTimeoutMs,
|
|
983
|
-
);
|
|
984
|
-
const threadId = extractCodexThreadId(threadStart);
|
|
985
|
-
if (!threadId) {
|
|
986
|
-
throw new Error(
|
|
987
|
-
"codex app-server thread/start returned no thread id",
|
|
988
|
-
);
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
const settled = await runCodexAppServerTurn({
|
|
992
|
-
client,
|
|
993
|
-
threadId,
|
|
994
|
-
prompt,
|
|
995
|
-
timeoutMs,
|
|
996
|
-
maxTurnMs,
|
|
997
|
-
});
|
|
998
|
-
|
|
999
|
-
return {
|
|
1000
|
-
endpoint: "codex-app-server-uds",
|
|
1001
|
-
text: settled.response,
|
|
1002
|
-
done: settled.status === "completed",
|
|
1003
|
-
meta: {
|
|
1004
|
-
threadId,
|
|
1005
|
-
status: settled.status,
|
|
1006
|
-
retries: settled.retries,
|
|
1007
|
-
commentary: settled.commentary,
|
|
1008
|
-
socketPath: resolvedSock,
|
|
1009
|
-
spawned: Boolean(child),
|
|
1010
|
-
...(settled.message ? { error: settled.message } : {}),
|
|
1011
|
-
},
|
|
1012
|
-
};
|
|
1013
|
-
} finally {
|
|
1014
|
-
try {
|
|
1015
|
-
client?.close();
|
|
1016
|
-
} catch {}
|
|
1017
|
-
if (child) await terminateChild(child);
|
|
1018
|
-
if (dir)
|
|
1019
|
-
await rm(dir, { recursive: true, force: true }).catch(() => {});
|
|
1020
|
-
}
|
|
1021
|
-
},
|
|
1022
|
-
};
|
|
1023
|
-
}
|
package/hub/workers/factory.mjs
CHANGED
|
@@ -1,54 +1,13 @@
|
|
|
1
1
|
// hub/workers/factory.mjs — Worker 생성 팩토리
|
|
2
2
|
//
|
|
3
3
|
// Supported worker types:
|
|
4
|
-
// - 'claude'
|
|
5
|
-
// - 'codex'
|
|
6
|
-
// opts.transport === 'app-server'
|
|
7
|
-
// - 'codex-app-server' → CodexAppServerWorker (explicit alias)
|
|
8
|
-
// - 'delegator' → DelegatorMcpWorker
|
|
4
|
+
// - 'claude' → ClaudeWorker
|
|
5
|
+
// - 'codex' → CodexMcpWorker
|
|
9
6
|
//
|
|
10
7
|
import { ClaudeWorker } from "./claude-worker.mjs";
|
|
11
|
-
import { CodexAppServerWorker } from "./codex-app-server-worker.mjs";
|
|
12
8
|
|
|
13
9
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* Issue #95 P1 #4 validation: the app-server transport does not yet implement
|
|
17
|
-
* the server-initiated approval / `tool/requestUserInput` round-trip. Any
|
|
18
|
-
* `approvalPolicy !== 'never'` would cause a codex turn to hang waiting for
|
|
19
|
-
* an approval response the worker cannot produce. Reject such configs at
|
|
20
|
-
* factory time with a clear, actionable message.
|
|
21
|
-
*
|
|
22
|
-
* @param {object} [opts]
|
|
23
|
-
*/
|
|
24
|
-
async function createCodexWorker(opts = {}) {
|
|
25
|
-
const { transport, requestJsonFn, publishCallback, ...rest } = opts;
|
|
26
|
-
void requestJsonFn;
|
|
27
|
-
|
|
28
|
-
if (transport === "app-server") {
|
|
29
|
-
const policy = rest.approvalPolicy;
|
|
30
|
-
if (policy !== undefined && policy !== null && policy !== "never") {
|
|
31
|
-
throw new Error(
|
|
32
|
-
`codex app-server transport currently requires approvalPolicy='never' (got '${policy}'). ` +
|
|
33
|
-
"The server-initiated approval / tool/requestUserInput round-trip is not yet implemented. " +
|
|
34
|
-
"Set approvalPolicy='never' or use transport='mcp'. " +
|
|
35
|
-
"Tracked in follow-up issue.",
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
return new CodexAppServerWorker({
|
|
39
|
-
...rest,
|
|
40
|
-
publishCallback:
|
|
41
|
-
typeof publishCallback === "function" ? publishCallback : null,
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Default (and transport === 'mcp'): CodexMcpWorker with zero new deps.
|
|
46
|
-
const { CodexMcpWorker } = await import("./codex-mcp.mjs");
|
|
47
|
-
return new CodexMcpWorker(rest);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* @param {'claude'|'codex'|'codex-app-server'|'delegator'} type
|
|
10
|
+
* @param {'claude'|'codex'} type
|
|
52
11
|
* @param {object} [opts]
|
|
53
12
|
* @returns {Promise<import('./interface.mjs').IWorker>}
|
|
54
13
|
*/
|
|
@@ -56,13 +15,9 @@ export async function createWorker(type, opts = {}) {
|
|
|
56
15
|
switch (type) {
|
|
57
16
|
case "claude":
|
|
58
17
|
return new ClaudeWorker(opts);
|
|
59
|
-
case "codex":
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return createCodexWorker({ ...opts, transport: "app-server" });
|
|
63
|
-
case "delegator": {
|
|
64
|
-
const { DelegatorMcpWorker } = await import("./delegator-mcp.mjs");
|
|
65
|
-
return new DelegatorMcpWorker(opts);
|
|
18
|
+
case "codex": {
|
|
19
|
+
const { CodexMcpWorker } = await import("./codex-mcp.mjs");
|
|
20
|
+
return new CodexMcpWorker(opts);
|
|
66
21
|
}
|
|
67
22
|
default:
|
|
68
23
|
throw new Error(`Unknown worker type: ${type}`);
|
|
@@ -16,15 +16,11 @@
|
|
|
16
16
|
* @property {string} [developerInstructions] - 개발자 지침
|
|
17
17
|
* @property {string} [compactPrompt] - 컴팩션 프롬프트
|
|
18
18
|
* @property {number} [timeoutMs] - MCP 요청 타임아웃(ms)
|
|
19
|
-
* @property {'mcp'|'app-server'} [transport] - Codex 전송 계층 선택 (팩토리 전용)
|
|
20
19
|
*/
|
|
21
20
|
|
|
22
21
|
/**
|
|
23
22
|
* Codex 워커 생성 옵션 (팩토리 레벨)
|
|
24
23
|
* @typedef {object} CodexWorkerFactoryOptions
|
|
25
|
-
* @property {'mcp'|'app-server'} [transport] - 'mcp' (기본) 또는 'app-server'
|
|
26
|
-
* @property {(publishMessage: object) => void|Promise<void>} [publishCallback] - app-server 전용 publish 후크
|
|
27
|
-
* @property {(path: string, opts?: object) => Promise<unknown>} [requestJsonFn] - 테스트/리팩터용 requestJson 주입
|
|
28
24
|
* @property {string} [command] - codex 실행 바이너리 경로
|
|
29
25
|
* @property {string[]} [args] - codex CLI 인자 배열
|
|
30
26
|
* @property {string} [cwd] - 워커 작업 디렉터리
|
|
@@ -61,14 +57,12 @@
|
|
|
61
57
|
* @property {() => Promise<void>} start
|
|
62
58
|
* @property {() => Promise<void>} stop
|
|
63
59
|
* @property {() => boolean} isReady
|
|
64
|
-
* @property {'codex'|'
|
|
60
|
+
* @property {'codex'|'gemini'|'antigravity'|'claude'} type
|
|
65
61
|
*/
|
|
66
62
|
|
|
67
63
|
export const WORKER_TYPES = Object.freeze([
|
|
68
64
|
"codex",
|
|
69
|
-
"codex-app-server",
|
|
70
65
|
"gemini",
|
|
71
66
|
"antigravity",
|
|
72
67
|
"claude",
|
|
73
|
-
"delegator",
|
|
74
68
|
]);
|