@triflux/remote 10.0.1 → 10.25.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/hub/pipe.mjs +239 -121
- package/hub/public/dashboard.html +68 -7
- package/hub/server.mjs +2181 -701
- package/hub/store-adapter.mjs +101 -37
- package/hub/store.mjs +356 -153
- package/hub/team/agent-map.json +26 -7
- package/hub/team/ansi.mjs +245 -146
- package/hub/team/backend.mjs +84 -17
- package/hub/team/build-worker-prompt.mjs +42 -0
- package/hub/team/check-mcp-hub.mjs +49 -0
- package/hub/team/claude-daemon-control.mjs +219 -0
- package/hub/team/claude-native-bridge.mjs +784 -0
- package/hub/team/claude-session-projection.mjs +65 -0
- package/hub/team/cli/commands/attach.mjs +57 -37
- package/hub/team/cli/commands/control.mjs +29 -8
- package/hub/team/cli/commands/debug.mjs +95 -74
- package/hub/team/cli/commands/focus.mjs +80 -53
- package/hub/team/cli/commands/interrupt.mjs +18 -6
- package/hub/team/cli/commands/kill.mjs +16 -5
- package/hub/team/cli/commands/list.mjs +31 -24
- package/hub/team/cli/commands/send.mjs +19 -6
- package/hub/team/cli/commands/start/index.mjs +154 -33
- package/hub/team/cli/commands/start/parse-args.mjs +89 -12
- package/hub/team/cli/commands/start/start-headless.mjs +127 -37
- package/hub/team/cli/commands/start/start-in-process.mjs +50 -40
- package/hub/team/cli/commands/start/start-mux.mjs +122 -73
- package/hub/team/cli/commands/start/start-wt.mjs +86 -69
- package/hub/team/cli/commands/status.mjs +43 -14
- package/hub/team/cli/commands/stop.mjs +11 -4
- package/hub/team/cli/commands/task.mjs +8 -3
- package/hub/team/cli/commands/tasks.mjs +13 -13
- package/hub/team/cli/index.mjs +2 -2
- package/hub/team/cli/manifest.mjs +38 -8
- package/hub/team/cli/render.mjs +52 -30
- package/hub/team/cli/services/attach-fallback.mjs +74 -54
- package/hub/team/cli/services/hub-client.mjs +45 -17
- package/hub/team/cli/services/member-selector.mjs +37 -30
- package/hub/team/cli/services/native-control.mjs +144 -116
- package/hub/team/cli/services/runtime-mode.mjs +55 -1
- package/hub/team/cli/services/state-store.mjs +66 -9
- package/hub/team/cli/services/task-model.mjs +40 -30
- package/hub/team/codex-review.mjs +521 -0
- package/hub/team/conductor-mesh-bridge.mjs +25 -24
- package/hub/team/conductor-registry.mjs +121 -0
- package/hub/team/conductor.mjs +1379 -671
- package/hub/team/consensus-meta.mjs +176 -0
- package/hub/team/dashboard-anchor.mjs +4 -5
- package/hub/team/dashboard-layout.mjs +3 -1
- package/hub/team/dashboard-open.mjs +21 -119
- package/hub/team/dashboard.mjs +76 -28
- package/hub/team/event-log.mjs +18 -10
- package/hub/team/execution-mode.mjs +241 -0
- package/hub/team/extract-completion-payload.mjs +110 -0
- package/hub/team/git-preflight.mjs +286 -0
- package/hub/team/handoff.mjs +49 -18
- package/hub/team/headless-bridge-session.mjs +17 -0
- package/hub/team/headless.mjs +2065 -1156
- package/hub/team/health-probe.mjs +232 -69
- package/hub/team/launcher-template.mjs +23 -13
- package/hub/team/lead-control.mjs +91 -4
- package/hub/team/mcp-selector.mjs +145 -0
- package/hub/team/native-supervisor.mjs +90 -26
- package/hub/team/native.mjs +74 -35
- package/hub/team/nativeProxy.mjs +184 -113
- package/hub/team/notify.mjs +147 -78
- package/hub/team/orchestrator.mjs +173 -161
- package/hub/team/pane.mjs +43 -15
- package/hub/team/process-cleanup.mjs +69 -25
- package/hub/team/psmux.mjs +1067 -264
- package/hub/team/recovery-store.mjs +149 -0
- package/hub/team/remote-probe.mjs +69 -53
- package/hub/team/remote-session.mjs +131 -59
- package/hub/team/remote-watcher.mjs +62 -34
- package/hub/team/retry-state-machine.mjs +267 -0
- package/hub/team/routing.mjs +51 -25
- package/hub/team/runtime-strategy.mjs +200 -0
- package/hub/team/schemas/worker-completion.json +39 -0
- package/hub/team/sentinel-capture.mjs +127 -0
- package/hub/team/session-sync.mjs +153 -6
- package/hub/team/session.mjs +605 -611
- package/hub/team/shared.mjs +13 -13
- package/hub/team/staleState.mjs +72 -30
- package/hub/team/swarm-cli.mjs +390 -0
- package/hub/team/swarm-hypervisor.mjs +1526 -198
- package/hub/team/swarm-intent.mjs +216 -0
- package/hub/team/swarm-locks.mjs +140 -25
- package/hub/team/swarm-planner.mjs +121 -21
- package/hub/team/swarm-preflight.mjs +317 -0
- package/hub/team/swarm-reconciler.mjs +128 -23
- package/hub/team/synapse-cli.mjs +458 -0
- package/hub/team/synapse-http.mjs +60 -0
- package/hub/team/synapse-registry.mjs +329 -0
- package/hub/team/terminal-opener.mjs +216 -0
- package/hub/team/tui-core.mjs +288 -0
- package/hub/team/tui-lite.mjs +234 -146
- package/hub/team/tui-remote-adapter.mjs +21 -37
- package/hub/team/tui-synapse.mjs +223 -0
- package/hub/team/tui-viewer.mjs +104 -45
- package/hub/team/tui-widgets.mjs +262 -0
- package/hub/team/tui.mjs +936 -449
- package/hub/team/worker-completion-validator.mjs +84 -0
- package/hub/team/worker-sandbox.mjs +93 -0
- package/hub/team/worker-signal.mjs +263 -0
- package/hub/team/worker-signal.types.d.ts +52 -0
- package/hub/team/worktree-lifecycle.mjs +518 -64
- package/hub/team/wt-manager.mjs +297 -26
- package/hub/tools.mjs +474 -252
- package/hub/tray.mjs +411 -368
- package/hub/workers/claude-worker.mjs +162 -118
- package/hub/workers/codex-app-server-worker.mjs +1015 -0
- package/hub/workers/codex-mcp.mjs +724 -507
- package/hub/workers/delegator-mcp.mjs +663 -337
- package/hub/workers/factory.mjs +100 -21
- package/hub/workers/gemini-worker.mjs +179 -86
- package/hub/workers/interface.mjs +23 -2
- package/hub/workers/lib/jsonrpc-stdio.mjs +463 -0
- package/hub/workers/worker-utils.mjs +22 -16
- package/package.json +4 -3
- package/scripts/lib/agent-json.mjs +27 -0
- package/scripts/lib/async.mjs +174 -0
- package/scripts/lib/claudemd-scanner.mjs +223 -0
- package/scripts/lib/cli-agy.mjs +62 -0
- package/scripts/lib/cli-claude.mjs +78 -0
- package/scripts/lib/cli-codex.mjs +199 -0
- package/scripts/lib/cli-gemini.mjs +67 -0
- package/scripts/lib/codex-recovery.sh +50 -0
- package/scripts/lib/context.mjs +67 -0
- package/scripts/lib/cross-review-utils.mjs +59 -0
- package/scripts/lib/doctor-env-checks.mjs +121 -0
- package/scripts/lib/dynamic-route-cli.mjs +107 -0
- package/scripts/lib/env-probe.mjs +269 -0
- package/scripts/lib/env.mjs +14 -0
- package/scripts/lib/gemini-profiles.mjs +119 -0
- package/scripts/lib/handoff.mjs +187 -0
- package/scripts/lib/hook-utils.mjs +16 -0
- package/scripts/lib/hub.mjs +48 -0
- package/scripts/lib/keyword-rules.mjs +195 -0
- package/scripts/lib/logger.mjs +105 -0
- package/scripts/lib/mcp-filter.mjs +883 -0
- package/scripts/lib/mcp-gateway-servers.mjs +70 -0
- package/scripts/lib/mcp-guard-engine.mjs +2120 -0
- package/scripts/lib/mcp-health.mjs +773 -0
- package/scripts/lib/mcp-manifest.mjs +89 -0
- package/scripts/lib/mcp-server-catalog.mjs +355 -0
- package/scripts/lib/pid.mjs +63 -0
- package/scripts/lib/process-utils.mjs +26 -0
- package/scripts/lib/psmux-info.mjs +179 -0
- package/scripts/lib/quota.mjs +47 -0
- package/scripts/lib/remote-spawn-transfer.mjs +226 -0
- package/scripts/lib/skill-state.mjs +220 -0
- package/scripts/lib/skill-template.mjs +375 -0
- package/scripts/lib/team.mjs +75 -0
- package/scripts/lib/timeout.mjs +66 -0
- package/scripts/lib/tmp.mjs +26 -0
- package/scripts/lib/toml.mjs +113 -0
package/hub/workers/factory.mjs
CHANGED
|
@@ -1,21 +1,100 @@
|
|
|
1
|
-
// hub/workers/factory.mjs — Worker 생성 팩토리
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
1
|
+
// hub/workers/factory.mjs — Worker 생성 팩토리
|
|
2
|
+
//
|
|
3
|
+
// Supported worker types:
|
|
4
|
+
// - 'gemini' → GeminiWorker
|
|
5
|
+
// - 'claude' → ClaudeWorker
|
|
6
|
+
// - 'codex' → CodexMcpWorker (default) or CodexAppServerWorker when
|
|
7
|
+
// opts.transport === 'app-server'
|
|
8
|
+
// - 'codex-app-server' → CodexAppServerWorker (explicit alias)
|
|
9
|
+
// - 'delegator' → DelegatorMcpWorker
|
|
10
|
+
//
|
|
11
|
+
// For the codex app-server transport the factory injects a default
|
|
12
|
+
// `publishCallback` that forwards each envelope to `/bridge/publish` via the
|
|
13
|
+
// existing `requestJson` helper. Callers can override by passing their own
|
|
14
|
+
// `publishCallback` or swap the transport with `requestJsonFn`.
|
|
15
|
+
|
|
16
|
+
import { ClaudeWorker } from "./claude-worker.mjs";
|
|
17
|
+
import { CodexAppServerWorker } from "./codex-app-server-worker.mjs";
|
|
18
|
+
import { GeminiWorker } from "./gemini-worker.mjs";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Build a best-effort publishCallback that posts the envelope to
|
|
22
|
+
* `/bridge/publish`. Failures are swallowed so publish pressure never crashes
|
|
23
|
+
* a running worker turn.
|
|
24
|
+
* @param {(path: string, opts?: object) => Promise<unknown>} [requestJsonFn]
|
|
25
|
+
* @returns {(publishMessage: object) => Promise<void>}
|
|
26
|
+
*/
|
|
27
|
+
function defaultPublishCallback(requestJsonFn = null) {
|
|
28
|
+
return async (publishMessage) => {
|
|
29
|
+
try {
|
|
30
|
+
const publishRequestJson =
|
|
31
|
+
requestJsonFn || (await import("@triflux/core/hub/bridge.mjs")).requestJson;
|
|
32
|
+
await publishRequestJson("/bridge/publish", { body: publishMessage });
|
|
33
|
+
} catch {
|
|
34
|
+
// best-effort; publish failures must not crash the worker
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Construct a CodexAppServerWorker with a default publishCallback wired to
|
|
41
|
+
* `requestJson('/bridge/publish', ...)` unless the caller supplied one.
|
|
42
|
+
*
|
|
43
|
+
* Issue #95 P1 #4 validation: the app-server transport does not yet implement
|
|
44
|
+
* the server-initiated approval / `tool/requestUserInput` round-trip. Any
|
|
45
|
+
* `approvalPolicy !== 'never'` would cause a codex turn to hang waiting for
|
|
46
|
+
* an approval response the worker cannot produce. Reject such configs at
|
|
47
|
+
* factory time with a clear, actionable message.
|
|
48
|
+
*
|
|
49
|
+
* @param {object} [opts]
|
|
50
|
+
*/
|
|
51
|
+
async function createCodexWorker(opts = {}) {
|
|
52
|
+
const { transport, requestJsonFn, publishCallback, ...rest } = opts;
|
|
53
|
+
|
|
54
|
+
if (transport === "app-server") {
|
|
55
|
+
const policy = rest.approvalPolicy;
|
|
56
|
+
if (policy !== undefined && policy !== null && policy !== "never") {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`codex app-server transport currently requires approvalPolicy='never' (got '${policy}'). ` +
|
|
59
|
+
"The server-initiated approval / tool/requestUserInput round-trip is not yet implemented. " +
|
|
60
|
+
"Set approvalPolicy='never' or use transport='mcp'. " +
|
|
61
|
+
"Tracked in follow-up issue.",
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
return new CodexAppServerWorker({
|
|
65
|
+
...rest,
|
|
66
|
+
publishCallback:
|
|
67
|
+
typeof publishCallback === "function"
|
|
68
|
+
? publishCallback
|
|
69
|
+
: defaultPublishCallback(requestJsonFn),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Default (and transport === 'mcp'): CodexMcpWorker with zero new deps.
|
|
74
|
+
const { CodexMcpWorker } = await import("./codex-mcp.mjs");
|
|
75
|
+
return new CodexMcpWorker(rest);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @param {'gemini'|'claude'|'codex'|'codex-app-server'|'delegator'} type
|
|
80
|
+
* @param {object} [opts]
|
|
81
|
+
* @returns {Promise<import('./interface.mjs').IWorker>}
|
|
82
|
+
*/
|
|
83
|
+
export async function createWorker(type, opts = {}) {
|
|
84
|
+
switch (type) {
|
|
85
|
+
case "gemini":
|
|
86
|
+
return new GeminiWorker(opts);
|
|
87
|
+
case "claude":
|
|
88
|
+
return new ClaudeWorker(opts);
|
|
89
|
+
case "codex":
|
|
90
|
+
return createCodexWorker(opts);
|
|
91
|
+
case "codex-app-server":
|
|
92
|
+
return createCodexWorker({ ...opts, transport: "app-server" });
|
|
93
|
+
case "delegator": {
|
|
94
|
+
const { DelegatorMcpWorker } = await import("./delegator-mcp.mjs");
|
|
95
|
+
return new DelegatorMcpWorker(opts);
|
|
96
|
+
}
|
|
97
|
+
default:
|
|
98
|
+
throw new Error(`Unknown worker type: ${type}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -1,20 +1,68 @@
|
|
|
1
1
|
// hub/workers/gemini-worker.mjs — Gemini headless subprocess 래퍼
|
|
2
2
|
// ADR-006: --output-format stream-json 기반 단발 실행 워커.
|
|
3
3
|
|
|
4
|
-
import { spawn } from
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { existsSync } from "node:fs";
|
|
6
|
+
import { extname } from "node:path";
|
|
7
|
+
import readline from "node:readline";
|
|
7
8
|
|
|
8
|
-
import { extractText, terminateChild, withRetry } from
|
|
9
|
+
import { extractText, terminateChild, withRetry } from "./worker-utils.mjs";
|
|
9
10
|
|
|
10
11
|
const DEFAULT_TIMEOUT_MS = 15 * 60 * 1000;
|
|
11
12
|
const DEFAULT_KILL_GRACE_MS = 1000;
|
|
12
13
|
|
|
13
14
|
function toStringList(value) {
|
|
14
15
|
if (!Array.isArray(value)) return [];
|
|
15
|
-
return value
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
return value.map((item) => String(item ?? "").trim()).filter(Boolean);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Windows에서 cmd.exe에 전달할 인자를 안전하게 quoting한다.
|
|
21
|
+
* 빈 문자열은 ""로, 특수문자 포함 시 큰따옴표로 감싼다.
|
|
22
|
+
*/
|
|
23
|
+
function quoteWindowsCmdArg(value) {
|
|
24
|
+
const raw = String(value ?? "").replace(/[\r\n]/g, " ");
|
|
25
|
+
if (raw.length === 0) return '""';
|
|
26
|
+
if (!/[\s"()^&|<>%!]/.test(raw)) return raw;
|
|
27
|
+
const escaped = raw
|
|
28
|
+
.replace(/%/g, "%%")
|
|
29
|
+
.replace(/(\\*)"/g, '$1$1\\"')
|
|
30
|
+
.replace(/(\\+)$/g, "$1$1");
|
|
31
|
+
return `"${escaped}"`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Windows npm shim(.cmd) spawn 문제를 해결한다.
|
|
36
|
+
* - command -v가 확장자 없는 경로를 반환하면 .exe → .cmd → .bat 순으로 탐색
|
|
37
|
+
* - .cmd/.bat는 CVE-2024-27980 이후 shell:false에서 실행 불가하므로 cmd.exe /d /s /c 경유
|
|
38
|
+
* - .exe는 직접 실행
|
|
39
|
+
* - non-Windows는 그대로 통과
|
|
40
|
+
*/
|
|
41
|
+
function buildSpawnSpec(command, args) {
|
|
42
|
+
if (process.platform !== "win32") {
|
|
43
|
+
return { command, args, shell: false };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let resolved = command;
|
|
47
|
+
if (!extname(resolved)) {
|
|
48
|
+
for (const ext of [".exe", ".cmd", ".bat"]) {
|
|
49
|
+
if (existsSync(resolved + ext)) {
|
|
50
|
+
resolved = resolved + ext;
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (/\.(cmd|bat)$/i.test(resolved)) {
|
|
57
|
+
const line = [resolved, ...args].map(quoteWindowsCmdArg).join(" ");
|
|
58
|
+
return {
|
|
59
|
+
command: "cmd.exe",
|
|
60
|
+
args: ["/d", "/s", "/v:off", "/c", line],
|
|
61
|
+
shell: false,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return { command: resolved, args, shell: false };
|
|
18
66
|
}
|
|
19
67
|
|
|
20
68
|
function safeJsonParse(line) {
|
|
@@ -36,25 +84,25 @@ function buildGeminiArgs(options) {
|
|
|
36
84
|
const args = [];
|
|
37
85
|
|
|
38
86
|
if (options.model) {
|
|
39
|
-
args.push(
|
|
87
|
+
args.push("--model", options.model);
|
|
40
88
|
}
|
|
41
89
|
|
|
42
90
|
if (options.approvalMode) {
|
|
43
|
-
args.push(
|
|
91
|
+
args.push("--approval-mode", options.approvalMode);
|
|
44
92
|
} else if (options.yolo !== false) {
|
|
45
|
-
args.push(
|
|
93
|
+
args.push("--yolo");
|
|
46
94
|
}
|
|
47
95
|
|
|
48
96
|
const allowedMcpServers = toStringList(options.allowedMcpServerNames);
|
|
49
97
|
if (allowedMcpServers.length) {
|
|
50
|
-
args.push(
|
|
98
|
+
args.push("--allowed-mcp-server-names", ...allowedMcpServers);
|
|
51
99
|
}
|
|
52
100
|
|
|
53
101
|
const extraArgs = toStringList(options.extraArgs);
|
|
54
102
|
if (extraArgs.length) args.push(...extraArgs);
|
|
55
103
|
|
|
56
|
-
args.push(
|
|
57
|
-
args.push(
|
|
104
|
+
args.push("--prompt", options.promptArgument ?? "");
|
|
105
|
+
args.push("--output-format", "stream-json");
|
|
58
106
|
|
|
59
107
|
return args;
|
|
60
108
|
}
|
|
@@ -66,49 +114,59 @@ function createWorkerError(message, details = {}) {
|
|
|
66
114
|
}
|
|
67
115
|
|
|
68
116
|
function normalizeRetryOptions(retryOptions) {
|
|
69
|
-
if (!retryOptions || typeof retryOptions !==
|
|
117
|
+
if (!retryOptions || typeof retryOptions !== "object") {
|
|
70
118
|
return Object.freeze({});
|
|
71
119
|
}
|
|
72
120
|
return Object.freeze({ ...retryOptions });
|
|
73
121
|
}
|
|
74
122
|
|
|
75
123
|
function isGeminiRetryable(error) {
|
|
76
|
-
return
|
|
77
|
-
|
|
78
|
-
|
|
124
|
+
return (
|
|
125
|
+
error?.code === "WORKER_EXIT" &&
|
|
126
|
+
error?.result?.exitCode !== 0 &&
|
|
127
|
+
error?.result?.exitCode !== 2
|
|
128
|
+
);
|
|
79
129
|
}
|
|
80
130
|
|
|
81
131
|
function detectGeminiCategory(error) {
|
|
82
|
-
const combined =
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
132
|
+
const combined =
|
|
133
|
+
`${error?.message || ""}\n${error?.stderr || ""}`.toLowerCase();
|
|
134
|
+
|
|
135
|
+
if (
|
|
136
|
+
/(unauthorized|forbidden|auth|login|token|credential|apikey|api key)/.test(
|
|
137
|
+
combined,
|
|
138
|
+
)
|
|
139
|
+
) {
|
|
140
|
+
return "auth";
|
|
86
141
|
}
|
|
87
|
-
if (
|
|
88
|
-
|
|
142
|
+
if (
|
|
143
|
+
error?.result?.exitCode === 2 ||
|
|
144
|
+
/expected stream-json|unknown option|invalid option|config/.test(combined)
|
|
145
|
+
) {
|
|
146
|
+
return "config";
|
|
89
147
|
}
|
|
90
|
-
if (error?.code ===
|
|
91
|
-
return
|
|
148
|
+
if (error?.code === "WORKER_EVENT_ERROR") {
|
|
149
|
+
return "input";
|
|
92
150
|
}
|
|
93
151
|
|
|
94
|
-
return
|
|
152
|
+
return "transient";
|
|
95
153
|
}
|
|
96
154
|
|
|
97
155
|
function buildGeminiErrorInfo(error, attempts) {
|
|
98
156
|
const category = detectGeminiCategory(error);
|
|
99
157
|
const retryable = isGeminiRetryable(error);
|
|
100
|
-
let recovery =
|
|
101
|
-
|
|
102
|
-
if (category ===
|
|
103
|
-
recovery =
|
|
104
|
-
} else if (category ===
|
|
105
|
-
recovery =
|
|
106
|
-
} else if (category ===
|
|
107
|
-
recovery =
|
|
158
|
+
let recovery = "Retry the Gemini worker after correcting the reported issue.";
|
|
159
|
+
|
|
160
|
+
if (category === "auth") {
|
|
161
|
+
recovery = "Refresh the Gemini authentication state and retry.";
|
|
162
|
+
} else if (category === "config") {
|
|
163
|
+
recovery = "Check the Gemini CLI flags and worker configuration.";
|
|
164
|
+
} else if (category === "input") {
|
|
165
|
+
recovery = "Check the Gemini request payload and streamed event format.";
|
|
108
166
|
}
|
|
109
167
|
|
|
110
168
|
return Object.freeze({
|
|
111
|
-
code: error?.code ||
|
|
169
|
+
code: error?.code || "GEMINI_EXECUTION_ERROR",
|
|
112
170
|
retryable,
|
|
113
171
|
attempts,
|
|
114
172
|
category,
|
|
@@ -120,10 +178,10 @@ function buildGeminiErrorInfo(error, attempts) {
|
|
|
120
178
|
* Gemini stream-json 래퍼
|
|
121
179
|
*/
|
|
122
180
|
export class GeminiWorker {
|
|
123
|
-
type =
|
|
181
|
+
type = "gemini";
|
|
124
182
|
|
|
125
183
|
constructor(options = {}) {
|
|
126
|
-
this.command = options.command ||
|
|
184
|
+
this.command = options.command || "gemini";
|
|
127
185
|
this.commandArgs = toStringList(options.commandArgs || options.args);
|
|
128
186
|
this.cwd = options.cwd || process.cwd();
|
|
129
187
|
this.env = { ...process.env, ...(options.env || {}) };
|
|
@@ -132,19 +190,26 @@ export class GeminiWorker {
|
|
|
132
190
|
this.yolo = options.yolo !== false;
|
|
133
191
|
this.allowedMcpServerNames = toStringList(options.allowedMcpServerNames);
|
|
134
192
|
this.extraArgs = toStringList(options.extraArgs);
|
|
135
|
-
this.timeoutMs =
|
|
136
|
-
|
|
193
|
+
this.timeoutMs =
|
|
194
|
+
Number(options.timeoutMs) > 0
|
|
195
|
+
? Number(options.timeoutMs)
|
|
196
|
+
: DEFAULT_TIMEOUT_MS;
|
|
197
|
+
this.killGraceMs =
|
|
198
|
+
Number(options.killGraceMs) > 0
|
|
199
|
+
? Number(options.killGraceMs)
|
|
200
|
+
: DEFAULT_KILL_GRACE_MS;
|
|
137
201
|
this.retryOptions = normalizeRetryOptions(options.retryOptions);
|
|
138
|
-
this.onEvent =
|
|
202
|
+
this.onEvent =
|
|
203
|
+
typeof options.onEvent === "function" ? options.onEvent : null;
|
|
139
204
|
|
|
140
|
-
this.state =
|
|
205
|
+
this.state = "idle";
|
|
141
206
|
this.child = null;
|
|
142
207
|
this.lastRun = null;
|
|
143
208
|
}
|
|
144
209
|
|
|
145
210
|
getStatus() {
|
|
146
211
|
return {
|
|
147
|
-
type:
|
|
212
|
+
type: "gemini",
|
|
148
213
|
state: this.state,
|
|
149
214
|
pid: this.child?.pid || null,
|
|
150
215
|
last_run_at_ms: this.lastRun?.finishedAtMs || null,
|
|
@@ -153,42 +218,47 @@ export class GeminiWorker {
|
|
|
153
218
|
}
|
|
154
219
|
|
|
155
220
|
async start() {
|
|
156
|
-
if (this.state ===
|
|
157
|
-
this.state =
|
|
221
|
+
if (this.state === "stopped") {
|
|
222
|
+
this.state = "idle";
|
|
158
223
|
}
|
|
159
224
|
return this.getStatus();
|
|
160
225
|
}
|
|
161
226
|
|
|
162
227
|
async stop() {
|
|
163
228
|
if (!this.child) {
|
|
164
|
-
this.state =
|
|
229
|
+
this.state = "stopped";
|
|
165
230
|
return this.getStatus();
|
|
166
231
|
}
|
|
167
232
|
const child = this.child;
|
|
168
233
|
terminateChild(child, this.killGraceMs);
|
|
169
234
|
await new Promise((resolve) => {
|
|
170
|
-
child.once(
|
|
235
|
+
child.once("close", resolve);
|
|
171
236
|
setTimeout(resolve, this.killGraceMs + 50).unref?.();
|
|
172
237
|
});
|
|
173
238
|
this.child = null;
|
|
174
|
-
this.state =
|
|
239
|
+
this.state = "stopped";
|
|
175
240
|
return this.getStatus();
|
|
176
241
|
}
|
|
177
242
|
|
|
178
243
|
async restart() {
|
|
179
244
|
await this.stop();
|
|
180
|
-
this.state =
|
|
245
|
+
this.state = "idle";
|
|
181
246
|
return this.getStatus();
|
|
182
247
|
}
|
|
183
248
|
|
|
184
249
|
async run(prompt, options = {}) {
|
|
185
250
|
if (this.child) {
|
|
186
|
-
throw createWorkerError(
|
|
251
|
+
throw createWorkerError("GeminiWorker is already running", {
|
|
252
|
+
code: "WORKER_BUSY",
|
|
253
|
+
});
|
|
187
254
|
}
|
|
188
255
|
|
|
189
256
|
await this.start();
|
|
190
257
|
|
|
191
|
-
const timeoutMs =
|
|
258
|
+
const timeoutMs =
|
|
259
|
+
Number(options.timeoutMs) > 0
|
|
260
|
+
? Number(options.timeoutMs)
|
|
261
|
+
: this.timeoutMs;
|
|
192
262
|
const startedAtMs = Date.now();
|
|
193
263
|
const args = [
|
|
194
264
|
...this.commandArgs,
|
|
@@ -196,22 +266,28 @@ export class GeminiWorker {
|
|
|
196
266
|
model: options.model || this.model,
|
|
197
267
|
approvalMode: options.approvalMode || this.approvalMode,
|
|
198
268
|
yolo: options.yolo ?? this.yolo,
|
|
199
|
-
allowedMcpServerNames:
|
|
269
|
+
allowedMcpServerNames:
|
|
270
|
+
options.allowedMcpServerNames || this.allowedMcpServerNames,
|
|
200
271
|
extraArgs: options.extraArgs || this.extraArgs,
|
|
201
|
-
promptArgument: options.promptArgument ??
|
|
272
|
+
promptArgument: options.promptArgument ?? "",
|
|
202
273
|
}),
|
|
203
274
|
];
|
|
204
275
|
|
|
205
|
-
const
|
|
276
|
+
const {
|
|
277
|
+
command: spawnCmd,
|
|
278
|
+
args: spawnArgs,
|
|
279
|
+
shell,
|
|
280
|
+
} = buildSpawnSpec(this.command, args);
|
|
281
|
+
const child = spawn(spawnCmd, spawnArgs, {
|
|
206
282
|
cwd: options.cwd || this.cwd,
|
|
207
283
|
env: { ...this.env, ...(options.env || {}) },
|
|
208
|
-
stdio: [
|
|
284
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
209
285
|
windowsHide: true,
|
|
210
|
-
shell
|
|
286
|
+
shell,
|
|
211
287
|
});
|
|
212
288
|
|
|
213
289
|
this.child = child;
|
|
214
|
-
this.state =
|
|
290
|
+
this.state = "running";
|
|
215
291
|
|
|
216
292
|
const events = [];
|
|
217
293
|
const stdoutLines = [];
|
|
@@ -230,7 +306,7 @@ export class GeminiWorker {
|
|
|
230
306
|
crlfDelay: Infinity,
|
|
231
307
|
});
|
|
232
308
|
|
|
233
|
-
stdoutReader.on(
|
|
309
|
+
stdoutReader.on("line", (line) => {
|
|
234
310
|
if (!line) return;
|
|
235
311
|
const event = safeJsonParse(line);
|
|
236
312
|
if (!event) {
|
|
@@ -239,20 +315,22 @@ export class GeminiWorker {
|
|
|
239
315
|
}
|
|
240
316
|
|
|
241
317
|
events.push(event);
|
|
242
|
-
if (event.type ===
|
|
318
|
+
if (event.type === "error") lastErrorEvent = event;
|
|
243
319
|
if (this.onEvent) {
|
|
244
|
-
try {
|
|
320
|
+
try {
|
|
321
|
+
this.onEvent(event);
|
|
322
|
+
} catch {}
|
|
245
323
|
}
|
|
246
324
|
});
|
|
247
325
|
|
|
248
|
-
stderrReader.on(
|
|
326
|
+
stderrReader.on("line", (line) => {
|
|
249
327
|
if (!line) return;
|
|
250
328
|
stderrLines.push(line);
|
|
251
329
|
});
|
|
252
330
|
|
|
253
331
|
const closePromise = new Promise((resolve, reject) => {
|
|
254
|
-
child.once(
|
|
255
|
-
child.once(
|
|
332
|
+
child.once("error", reject);
|
|
333
|
+
child.once("close", (code, signal) => {
|
|
256
334
|
exitCode = code;
|
|
257
335
|
exitSignal = signal;
|
|
258
336
|
resolve();
|
|
@@ -265,8 +343,8 @@ export class GeminiWorker {
|
|
|
265
343
|
}, timeoutMs);
|
|
266
344
|
timeout.unref?.();
|
|
267
345
|
|
|
268
|
-
child.stdin.on(
|
|
269
|
-
child.stdin.end(String(prompt ??
|
|
346
|
+
child.stdin.on("error", () => {});
|
|
347
|
+
child.stdin.end(String(prompt ?? ""));
|
|
270
348
|
|
|
271
349
|
try {
|
|
272
350
|
await closePromise;
|
|
@@ -277,32 +355,37 @@ export class GeminiWorker {
|
|
|
277
355
|
if (this.child === child) {
|
|
278
356
|
this.child = null;
|
|
279
357
|
}
|
|
280
|
-
this.state =
|
|
358
|
+
this.state = "idle";
|
|
281
359
|
}
|
|
282
360
|
|
|
283
|
-
const resultEvent = findLastEvent(
|
|
361
|
+
const resultEvent = findLastEvent(
|
|
362
|
+
events,
|
|
363
|
+
(event) => event?.type === "result",
|
|
364
|
+
);
|
|
284
365
|
const response = [
|
|
285
366
|
extractText(resultEvent),
|
|
286
367
|
...events
|
|
287
|
-
.filter(
|
|
368
|
+
.filter(
|
|
369
|
+
(event) => event?.type === "message" || event?.type === "assistant",
|
|
370
|
+
)
|
|
288
371
|
.map((event) => extractText(event))
|
|
289
372
|
.filter(Boolean),
|
|
290
373
|
...stdoutLines,
|
|
291
374
|
]
|
|
292
375
|
.filter(Boolean)
|
|
293
|
-
.join(
|
|
376
|
+
.join("\n")
|
|
294
377
|
.trim();
|
|
295
378
|
|
|
296
379
|
const result = {
|
|
297
|
-
type:
|
|
380
|
+
type: "gemini",
|
|
298
381
|
command: this.command,
|
|
299
382
|
args,
|
|
300
383
|
response,
|
|
301
384
|
events,
|
|
302
385
|
resultEvent,
|
|
303
386
|
usage: resultEvent?.usage || null,
|
|
304
|
-
stdout: stdoutLines.join(
|
|
305
|
-
stderr: stderrLines.join(
|
|
387
|
+
stdout: stdoutLines.join("\n").trim(),
|
|
388
|
+
stderr: stderrLines.join("\n").trim(),
|
|
306
389
|
exitCode,
|
|
307
390
|
exitSignal,
|
|
308
391
|
timedOut,
|
|
@@ -314,23 +397,27 @@ export class GeminiWorker {
|
|
|
314
397
|
|
|
315
398
|
if (timedOut) {
|
|
316
399
|
throw createWorkerError(`Gemini worker timed out after ${timeoutMs}ms`, {
|
|
317
|
-
code:
|
|
400
|
+
code: "ETIMEDOUT",
|
|
318
401
|
result,
|
|
319
402
|
stderr: result.stderr,
|
|
320
403
|
});
|
|
321
404
|
}
|
|
322
405
|
|
|
323
406
|
if (exitCode !== 0) {
|
|
324
|
-
|
|
325
|
-
|
|
407
|
+
// Build a descriptive message when stderr is empty to aid debugging
|
|
408
|
+
const errMsg = result.stderr
|
|
409
|
+
? `Gemini worker exited with code ${exitCode}`
|
|
410
|
+
: `Gemini worker exited with code ${exitCode} (stderr empty, signal=${exitSignal ?? "none"}, events=${events.length}, stdout=${result.stdout.length}B)`;
|
|
411
|
+
throw createWorkerError(errMsg, {
|
|
412
|
+
code: "WORKER_EXIT",
|
|
326
413
|
result,
|
|
327
414
|
stderr: result.stderr,
|
|
328
415
|
});
|
|
329
416
|
}
|
|
330
417
|
|
|
331
418
|
if (lastErrorEvent) {
|
|
332
|
-
throw createWorkerError(
|
|
333
|
-
code:
|
|
419
|
+
throw createWorkerError("Gemini worker emitted an error event", {
|
|
420
|
+
code: "WORKER_EVENT_ERROR",
|
|
334
421
|
result,
|
|
335
422
|
stderr: result.stderr,
|
|
336
423
|
});
|
|
@@ -340,20 +427,23 @@ export class GeminiWorker {
|
|
|
340
427
|
}
|
|
341
428
|
|
|
342
429
|
isReady() {
|
|
343
|
-
return this.state !==
|
|
430
|
+
return this.state !== "stopped";
|
|
344
431
|
}
|
|
345
432
|
|
|
346
433
|
async execute(prompt, options = {}) {
|
|
347
434
|
let attempts = 0;
|
|
348
435
|
|
|
349
436
|
try {
|
|
350
|
-
const result = await withRetry(
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
437
|
+
const result = await withRetry(
|
|
438
|
+
async () => {
|
|
439
|
+
attempts += 1;
|
|
440
|
+
return this.run(prompt, options);
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
...this.retryOptions,
|
|
444
|
+
shouldRetry: (error) => isGeminiRetryable(error),
|
|
445
|
+
},
|
|
446
|
+
);
|
|
357
447
|
|
|
358
448
|
return {
|
|
359
449
|
output: result.response,
|
|
@@ -363,8 +453,8 @@ export class GeminiWorker {
|
|
|
363
453
|
};
|
|
364
454
|
} catch (error) {
|
|
365
455
|
return {
|
|
366
|
-
output: error.stderr || error.message ||
|
|
367
|
-
exitCode: error.code ===
|
|
456
|
+
output: error.stderr || error.message || "Gemini worker failed",
|
|
457
|
+
exitCode: error.code === "ETIMEDOUT" ? 124 : 1,
|
|
368
458
|
sessionKey: options.sessionKey || null,
|
|
369
459
|
error: buildGeminiErrorInfo(error, attempts || 1),
|
|
370
460
|
raw: error.result || null,
|
|
@@ -372,3 +462,6 @@ export class GeminiWorker {
|
|
|
372
462
|
}
|
|
373
463
|
}
|
|
374
464
|
}
|
|
465
|
+
|
|
466
|
+
/** @visibleForTesting — 테스트 전용 export. 외부 소비 금지. */
|
|
467
|
+
export { buildSpawnSpec, quoteWindowsCmdArg };
|
|
@@ -16,6 +16,21 @@
|
|
|
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
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Codex 워커 생성 옵션 (팩토리 레벨)
|
|
24
|
+
* @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
|
+
* @property {string} [command] - codex 실행 바이너리 경로
|
|
29
|
+
* @property {string[]} [args] - codex CLI 인자 배열
|
|
30
|
+
* @property {string} [cwd] - 워커 작업 디렉터리
|
|
31
|
+
* @property {Record<string, string>} [env] - 추가 환경 변수
|
|
32
|
+
* @property {{ name: string, version: string }} [clientInfo] - MCP/JSON-RPC clientInfo
|
|
33
|
+
* @property {number} [bootstrapTimeoutMs] - 핸드셰이크 타임아웃(ms)
|
|
19
34
|
*/
|
|
20
35
|
|
|
21
36
|
/**
|
|
@@ -46,7 +61,13 @@
|
|
|
46
61
|
* @property {() => Promise<void>} start
|
|
47
62
|
* @property {() => Promise<void>} stop
|
|
48
63
|
* @property {() => boolean} isReady
|
|
49
|
-
* @property {
|
|
64
|
+
* @property {'codex'|'codex-app-server'|'gemini'|'claude'|'delegator'} type
|
|
50
65
|
*/
|
|
51
66
|
|
|
52
|
-
export const WORKER_TYPES = Object.freeze([
|
|
67
|
+
export const WORKER_TYPES = Object.freeze([
|
|
68
|
+
"codex",
|
|
69
|
+
"codex-app-server",
|
|
70
|
+
"gemini",
|
|
71
|
+
"claude",
|
|
72
|
+
"delegator",
|
|
73
|
+
]);
|