@triflux/remote 10.50.2 → 10.51.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.
Files changed (97) hide show
  1. package/cto/brief.mjs +0 -12
  2. package/cto/collect.mjs +0 -73
  3. package/cto/current.schema.json +0 -4
  4. package/cto/status.mjs +2 -50
  5. package/hub/index.mjs +0 -8
  6. package/hub/team/agent-map.json +0 -1
  7. package/hub/team/ansi.mjs +1 -35
  8. package/hub/team/backend.mjs +9 -145
  9. package/hub/team/cli/commands/attach.mjs +1 -17
  10. package/hub/team/cli/commands/control.mjs +12 -38
  11. package/hub/team/cli/commands/debug.mjs +1 -37
  12. package/hub/team/cli/commands/focus.mjs +5 -31
  13. package/hub/team/cli/commands/interrupt.mjs +6 -25
  14. package/hub/team/cli/commands/kill.mjs +1 -30
  15. package/hub/team/cli/commands/list.mjs +0 -16
  16. package/hub/team/cli/commands/send.mjs +6 -26
  17. package/hub/team/cli/commands/start/index.mjs +41 -133
  18. package/hub/team/cli/commands/start/parse-args.mjs +15 -40
  19. package/hub/team/cli/commands/start/start-headless.mjs +138 -105
  20. package/hub/team/cli/commands/start/start-mux.mjs +0 -3
  21. package/hub/team/cli/commands/status.mjs +2 -57
  22. package/hub/team/cli/commands/stop.mjs +14 -19
  23. package/hub/team/cli/help.mjs +4 -5
  24. package/hub/team/cli/manifest.mjs +5 -1
  25. package/hub/team/cli/render.mjs +0 -12
  26. package/hub/team/cli/services/hub-client.mjs +2 -88
  27. package/hub/team/cli/services/runtime-mode.mjs +27 -98
  28. package/hub/team/handoff.mjs +3 -34
  29. package/hub/team/headless.mjs +13 -754
  30. package/hub/team/orchestrator.mjs +4 -30
  31. package/hub/team/pane.mjs +6 -28
  32. package/hub/team/psmux.mjs +0 -1
  33. package/hub/team/remote-session.mjs +1 -2
  34. package/hub/team/session.mjs +7 -91
  35. package/hub/team/swarm-intent.mjs +0 -198
  36. package/hub/team/tui-core.mjs +1 -1
  37. package/hub/team/tui-viewer.mjs +2 -2
  38. package/hub/team/tui.mjs +8 -269
  39. package/hub/team/wt-manager.mjs +2 -2
  40. package/hub/workers/factory.mjs +4 -37
  41. package/hub/workers/lib/jsonrpc-ws-uds.mjs +0 -2
  42. package/package.json +1 -1
  43. package/scripts/lib/agent-route-policy.mjs +162 -14
  44. package/scripts/lib/claudemd-scanner.mjs +2 -199
  45. package/scripts/lib/cli-claude.mjs +0 -2
  46. package/scripts/lib/context.mjs +3 -19
  47. package/scripts/lib/env-probe.mjs +5 -64
  48. package/scripts/lib/gemini-profiles.mjs +2 -69
  49. package/scripts/lib/machine-profile.mjs +3 -17
  50. package/scripts/lib/mcp-filter.mjs +109 -53
  51. package/scripts/lib/mcp-health.mjs +2 -2
  52. package/scripts/lib/mcp-server-catalog.mjs +2 -2
  53. package/scripts/lib/psmux-info.mjs +11 -59
  54. package/scripts/lib/skill-template.mjs +0 -245
  55. package/hub/team/build-worker-prompt.mjs +0 -92
  56. package/hub/team/check-mcp-hub.mjs +0 -49
  57. package/hub/team/claude-native-bridge.mjs +0 -987
  58. package/hub/team/cli/commands/start/start-in-process.mjs +0 -50
  59. package/hub/team/cli/commands/start/start-wt.mjs +0 -86
  60. package/hub/team/cli/services/native-control.mjs +0 -145
  61. package/hub/team/codex-review.mjs +0 -520
  62. package/hub/team/conductor-mesh-bridge.mjs +0 -122
  63. package/hub/team/conductor.mjs +0 -1529
  64. package/hub/team/consensus-meta.mjs +0 -176
  65. package/hub/team/cto-pull.mjs +0 -31
  66. package/hub/team/daemon-pty-tmux-bridge.mjs +0 -146
  67. package/hub/team/dashboard.mjs +0 -322
  68. package/hub/team/event-log.mjs +0 -84
  69. package/hub/team/execution-mode.mjs +0 -324
  70. package/hub/team/extract-completion-payload.mjs +0 -110
  71. package/hub/team/headless-bridge-session.mjs +0 -17
  72. package/hub/team/health-probe.mjs +0 -435
  73. package/hub/team/interactive-native-launcher.mjs +0 -119
  74. package/hub/team/interactive-native-worker.mjs +0 -80
  75. package/hub/team/interactive-tui-transport.mjs +0 -479
  76. package/hub/team/launcher-template.mjs +0 -144
  77. package/hub/team/mcp-selector.mjs +0 -145
  78. package/hub/team/native-supervisor.mjs +0 -460
  79. package/hub/team/native.mjs +0 -727
  80. package/hub/team/recovery-store.mjs +0 -149
  81. package/hub/team/remote-probe.mjs +0 -292
  82. package/hub/team/remote-watcher.mjs +0 -506
  83. package/hub/team/runtime-strategy.mjs +0 -200
  84. package/hub/team/schemas/worker-completion.json +0 -39
  85. package/hub/team/sentinel-capture.mjs +0 -127
  86. package/hub/team/swarm-cli.mjs +0 -390
  87. package/hub/team/swarm-hypervisor.mjs +0 -2206
  88. package/hub/team/swarm-locks.mjs +0 -335
  89. package/hub/team/swarm-planner.mjs +0 -371
  90. package/hub/team/swarm-preflight.mjs +0 -366
  91. package/hub/team/swarm-reconciler.mjs +0 -242
  92. package/hub/team/tui-remote-adapter.mjs +0 -377
  93. package/hub/team/worker-completion-validator.mjs +0 -79
  94. package/hub/team/worktree-lifecycle.mjs +0 -639
  95. package/hub/team/wt-templates.json +0 -43
  96. package/hub/workers/antigravity-route-worker.mjs +0 -179
  97. package/scripts/lib/dynamic-route-cli.mjs +0 -107
@@ -1,84 +0,0 @@
1
- // hub/team/event-log.mjs — JSONL 블랙박스 리코더
2
- // Conductor 세션 lifecycle 이벤트를 JSONL 파일에 기록한다.
3
- // 기존 hub/server.mjs의 batch-events.jsonl(MCP 이벤트)과 독립. 공존.
4
-
5
- import { createWriteStream, mkdirSync } from "node:fs";
6
- import { dirname } from "node:path";
7
-
8
- /**
9
- * JSONL event log 팩토리.
10
- * @param {string} filePath — 로그 파일 경로 (.jsonl)
11
- * @param {object} [opts]
12
- * @param {string} [opts.sessionId] — 모든 이벤트에 자동 삽입할 세션 ID
13
- * @returns {{ append, flush, close, filePath }}
14
- */
15
- export function createEventLog(filePath, opts = {}) {
16
- const { sessionId } = opts;
17
-
18
- mkdirSync(dirname(filePath), { recursive: true });
19
- const stream = createWriteStream(filePath, { flags: "a" });
20
-
21
- let closed = false;
22
- let pending = 0;
23
-
24
- /**
25
- * 이벤트를 JSONL 한 줄로 기록.
26
- * @param {string} event — 이벤트 타입 (spawn, health, kill, stateChange, ...)
27
- * @param {object} [data] — 이벤트 페이로드
28
- */
29
- function append(event, data = {}) {
30
- if (closed) return;
31
- const entry = {
32
- ts: new Date().toISOString(),
33
- ...(sessionId ? { session: sessionId } : {}),
34
- event,
35
- ...data,
36
- };
37
- pending += 1;
38
- stream.write(JSON.stringify(entry) + "\n", () => {
39
- pending -= 1;
40
- });
41
- }
42
-
43
- /**
44
- * 버퍼된 이벤트를 디스크에 flush.
45
- * @returns {Promise<void>}
46
- */
47
- function flush() {
48
- if (closed) return Promise.resolve();
49
- return new Promise((resolve, reject) => {
50
- stream.once("error", reject);
51
- stream.write("", () => {
52
- stream.removeListener("error", reject);
53
- resolve();
54
- });
55
- });
56
- }
57
-
58
- /**
59
- * 스트림 종료. flush 후 close.
60
- * @returns {Promise<void>}
61
- */
62
- function close() {
63
- if (closed) return Promise.resolve();
64
- closed = true;
65
- return new Promise((resolve) => {
66
- stream.end(() => resolve());
67
- });
68
- }
69
-
70
- return Object.freeze({
71
- append,
72
- flush,
73
- close,
74
- get filePath() {
75
- return filePath;
76
- },
77
- get pending() {
78
- return pending;
79
- },
80
- get closed() {
81
- return closed;
82
- },
83
- });
84
- }
@@ -1,324 +0,0 @@
1
- // hub/team/execution-mode.mjs — headless vs interactive execution mode selection
2
-
3
- import { existsSync, readFileSync } from "node:fs";
4
- import { dirname, resolve as pathResolve } from "node:path";
5
- import { resolveNestedCodexAgentProfile } from "../../scripts/lib/agent-route-policy.mjs";
6
- import { codexProfileConfigOverrides } from "../../scripts/lib/codex-profile-config.mjs";
7
- import {
8
- resolveGeminiModel,
9
- resolveGeminiProfileForPurpose,
10
- } from "../../scripts/lib/gemini-profiles.mjs";
11
- import {
12
- buildDisabledCliError,
13
- normalizeCliName,
14
- resolveCliPolicy,
15
- } from "../../scripts/lib/machine-profile.mjs";
16
- import { whichCommand } from "@triflux/core/hub/platform.mjs";
17
-
18
- const WIN32_EXT_PRECEDENCE = [".cmd", ".exe", ".bat", ".ps1"];
19
-
20
- // #128: parse npm-cmd-shim wrapper to extract the underlying .js entry point.
21
- // npm shim wrappers end with: "%_prog%" "%dp0%\node_modules\...\<cli>.js" %*
22
- // %* is cmd batch arg pass-through which mangles multi-line / fenced prompts.
23
- // Bypass cmd entirely by spawning node + the .js directly.
24
- export function unwrapCmdToJsScript(cmdPath, opts = {}) {
25
- const readFile = opts.readFile || readFileSync;
26
- const existsFn = opts.existsSyncFn || existsSync;
27
- try {
28
- const content = readFile(cmdPath, "utf8");
29
- const match = content.match(/"%dp0%[\\/]([^"]+\.[cm]?js)"/i);
30
- if (!match) return null;
31
- const dir = dirname(cmdPath);
32
- const jsPath = pathResolve(dir, match[1].replace(/\\/g, "/"));
33
- return existsFn(jsPath) ? jsPath : null;
34
- } catch {
35
- return null;
36
- }
37
- }
38
-
39
- export const MODES = Object.freeze({
40
- HEADLESS: "headless",
41
- INTERACTIVE: "interactive",
42
- AUTO: "auto",
43
- });
44
-
45
- function asPrompt(prompt) {
46
- return typeof prompt === "string" ? prompt : "";
47
- }
48
-
49
- function pushFlag(args, flag, value) {
50
- if (typeof value === "string" && value.length > 0) {
51
- args.push(flag, value);
52
- }
53
- }
54
-
55
- // #116-C: codex headless 의 argv-inline prompt 가 macOS oh-my-codex hook
56
- // SessionStart Failed 회귀를 유발 (codex CLI v0.130.0). cli-adapter-base 의
57
- // buildExecCommand 가 shell command 형태에서 동일 fix 를 적용했고
58
- // (TFX_CODEX_STDIN_PROMPT=0 opt-out), 본 함수의 spawn-args 형태에서도 같은
59
- // 정신으로 prompt 를 stdin transport 로 분리한다. 동일 env var 를 재사용해
60
- // 사용자가 한 곳만 끄면 양쪽 path 모두 비활성.
61
- export function resolveStdinPromptMode(explicit, envSource) {
62
- if (typeof explicit === "boolean") return explicit;
63
- const env = (envSource || process.env).TFX_CODEX_STDIN_PROMPT;
64
- if (env === "0" || env === "false") return false;
65
- return true;
66
- }
67
-
68
- function normalizeCli(cli) {
69
- const value = String(cli || "codex").toLowerCase();
70
- if (value === "gemini" || value === "antigravity" || value === "agy") {
71
- return "antigravity";
72
- }
73
- return value;
74
- }
75
-
76
- export function resolveCliExecutable(cli, opts = {}) {
77
- const name = String(cli || "codex");
78
- const resolveCommand = opts.resolveCommand || whichCommand;
79
- const resolved = resolveCommand(name) || name;
80
-
81
- // Windows: Node spawn({ shell: false }) calls CreateProcess directly and does NOT
82
- // search PATHEXT. npm-installed CLIs (codex, gemini) live at `<npm>/codex` (Git Bash
83
- // shell script) alongside `<npm>/codex.cmd` (Windows batch wrapper). whichCommand
84
- // returns the extensionless path, which Windows cannot execute → ENOENT "The system
85
- // cannot find the file specified." Append the correct extension when the resolved
86
- // path has none.
87
- const platform = opts.platform || process.platform;
88
- if (platform === "win32" && resolved) {
89
- const hasExt = /\.[^\\/.]+$/.test(resolved);
90
- if (!hasExt) {
91
- const existsFn = opts.existsSyncFn || existsSync;
92
- for (const ext of WIN32_EXT_PRECEDENCE) {
93
- const candidate = `${resolved}${ext}`;
94
- try {
95
- if (existsFn(candidate)) return candidate;
96
- } catch {
97
- // ignore stat failures, try next extension
98
- }
99
- }
100
- }
101
- }
102
-
103
- return resolved;
104
- }
105
-
106
- export function buildSpawnSpecForMode(mode, opts = {}) {
107
- const cli = normalizeCli(opts.cli || "codex");
108
-
109
- // TFX_DISABLE_* SSOT 집행. route 를 우회하는 spawn 경로도 tfx-route.sh 와 같은
110
- // 정책을 따른다. 정책 판독은 런타임 process.env 와 machine profile 만 쓰고,
111
- // 자식에게 넘길 세션 env(opts.env)는 쓰지 않는다. 주입 시임은 opts.cliPolicy
112
- // (계산된 정책)와 opts.policyEnv(env 객체) 둘뿐이다.
113
- // claude 처럼 정책 대상이 아닌 CLI 는 프로파일 파일을 읽지 않는다.
114
- if (normalizeCliName(cli)) {
115
- const cliPolicy =
116
- opts.cliPolicy ?? resolveCliPolicy(opts.policyEnv ?? process.env);
117
- const blockedCli = buildDisabledCliError(cli, cliPolicy);
118
- if (blockedCli) throw blockedCli;
119
- }
120
-
121
- const prompt = asPrompt(opts.prompt);
122
- const commandName = cli === "antigravity" ? "agy" : cli;
123
- const resolvedCommand = resolveCliExecutable(commandName, opts);
124
- const platform = opts.platform || process.platform;
125
-
126
- // Node v20.12+ (CVE-2024-27980) rejects spawn of .cmd/.bat files with shell:false
127
- // (EINVAL). npm-installed Windows wrappers (e.g. codex.cmd) hit this.
128
- //
129
- // #128 (BUG-A): the cmd /c wrapper used to be the workaround, but cmd batch
130
- // %* arg pass-through mangles multi-line and fenced (```bash) prompts before
131
- // they reach the underlying .js. We now unwrap the .cmd to the node script
132
- // it launches and spawn `node <script>` directly, bypassing cmd entirely.
133
- // Falls back to cmd /c if the .cmd cannot be parsed.
134
- const needsCmdWrap =
135
- platform === "win32" && /\.(cmd|bat)$/i.test(resolvedCommand);
136
- const unwrappedJs = needsCmdWrap
137
- ? (opts.unwrapCmdFn || unwrapCmdToJsScript)(resolvedCommand)
138
- : null;
139
- const wrap = (args) => {
140
- if (unwrappedJs) {
141
- const nodeBin = opts.nodeExecPath || process.execPath;
142
- return { command: nodeBin, args: [unwrappedJs, ...args] };
143
- }
144
- if (needsCmdWrap) {
145
- return { command: "cmd", args: ["/c", resolvedCommand, ...args] };
146
- }
147
- return { command: resolvedCommand, args };
148
- };
149
-
150
- if (cli === "antigravity") {
151
- // agy 1.1.27: --print 는 값(프롬프트)을 받는 마지막 인자다. stdin 프롬프트는
152
- // "empty prompt" 로 거부되므로 argv 로 넘긴다.
153
- // effort 는 역할별 SSOT(scripts/lib/gemini-profiles.mjs). 명시 model 이 우선한다.
154
- const model =
155
- typeof opts.model === "string" && opts.model.trim()
156
- ? resolveGeminiModel(opts.model.trim(), {
157
- profilesPath: opts.geminiProfilesPath,
158
- })
159
- : resolveGeminiModel(
160
- resolveGeminiProfileForPurpose(opts.role || opts.agent),
161
- { profilesPath: opts.geminiProfilesPath },
162
- );
163
- const args = ["--dangerously-skip-permissions"];
164
- if (model) args.push("--model", model);
165
- args.push("--print", prompt);
166
- return {
167
- ...wrap(args),
168
- useExec: true,
169
- shell: false,
170
- stdinPrompt: false,
171
- prompt,
172
- };
173
- }
174
-
175
- if (mode === MODES.INTERACTIVE || mode === MODES.AUTO) {
176
- return { ...wrap([]), useExec: false, shell: false };
177
- }
178
-
179
- if (cli === "claude") {
180
- const args = [];
181
- pushFlag(args, "--model", opts.model);
182
- args.push("-p", prompt);
183
- return { ...wrap(args), useExec: true, shell: false };
184
- }
185
-
186
- const args = [];
187
- args.push(
188
- "exec",
189
- "--dangerously-bypass-approvals-and-sandbox",
190
- "--skip-git-repo-check",
191
- "--color",
192
- "never",
193
- );
194
- // Select the effort profile via `-c` config overrides instead of
195
- // `--profile <name>`: codex 0.134+ rejects `--profile X` whenever config.toml
196
- // still contains an inline [profiles.X] table (which codex re-injects on
197
- // config rewrite). These go straight to spawn argv (shell: false), so push
198
- // the raw TOML-scalar overrides without shell-quoting.
199
- const codexHome = opts.env?.CODEX_HOME;
200
- const codexProfile = resolveNestedCodexAgentProfile(opts.role || "executor", {
201
- profileOverride: opts.profile ?? "auto",
202
- globalProfile: opts.env?.TFX_CODEX_PROFILE ?? process.env.TFX_CODEX_PROFILE,
203
- codexHome,
204
- });
205
- for (const override of codexProfileConfigOverrides(codexProfile, {
206
- codexHome,
207
- disallowUltra: true,
208
- enforceCanonicalProfile: true,
209
- })) {
210
- args.push("-c", override);
211
- }
212
- if (Array.isArray(opts.mcpServers)) {
213
- const excludedMcpServers = new Set(
214
- (Array.isArray(opts.excludeMcpServers) ? opts.excludeMcpServers : [])
215
- .map((server) => String(server ?? "").trim())
216
- .filter(Boolean),
217
- );
218
- for (const rawServer of opts.mcpServers) {
219
- const server = String(rawServer ?? "").trim();
220
- if (!server) continue;
221
- if (excludedMcpServers.has(server)) {
222
- const message = `Skipping MCP server '${server}' because Codex preflight did not find an enabled config entry.`;
223
- if (typeof opts.onWarning === "function") {
224
- opts.onWarning(message, { type: "mcp_server_skipped", server });
225
- }
226
- continue;
227
- }
228
- args.push("-c", `mcp_servers.${server}.enabled=true`);
229
- }
230
- }
231
-
232
- // #116-C: codex headless 의 argv-inline prompt → macOS oh-my-codex hook
233
- // SessionStart Failed 회귀 (codex v0.130.0). default 가 stdin transport.
234
- // conductor.respawnSession 이 spec.prompt 를 child.stdin.write 로 주입한다.
235
- const useStdin =
236
- resolveStdinPromptMode(opts.stdinPrompt, opts.env) && prompt.length > 0;
237
- if (useStdin) {
238
- return {
239
- ...wrap(args),
240
- useExec: true,
241
- shell: false,
242
- stdinPrompt: true,
243
- prompt,
244
- };
245
- }
246
-
247
- args.push(prompt);
248
- return { ...wrap(args), useExec: true, shell: false };
249
- }
250
-
251
- /**
252
- * @param {{
253
- * cli: "codex"|"gemini"|"antigravity"|"agy"|"claude",
254
- * taskType?: "implement"|"review"|"research"|"test"|"analyze",
255
- * needsInput?: boolean,
256
- * estimatedDuration?: number,
257
- * hasHub?: boolean,
258
- * }} opts
259
- * @returns {{ mode: string, reason: string }}
260
- */
261
- export function selectExecutionMode(opts = {}) {
262
- const {
263
- cli: rawCli = "codex",
264
- taskType = "research",
265
- needsInput = false,
266
- estimatedDuration = 0,
267
- hasHub = false,
268
- } = opts;
269
-
270
- const cli = normalizeCli(rawCli);
271
-
272
- if (!hasHub) {
273
- return {
274
- mode: MODES.HEADLESS,
275
- reason: "interactive mode requires hub; falling back to headless",
276
- };
277
- }
278
-
279
- if (cli === "antigravity") {
280
- return {
281
- mode: MODES.HEADLESS,
282
- reason: "antigravity CLI uses headless stdin print mode",
283
- };
284
- }
285
-
286
- if (taskType === "implement" && !needsInput) {
287
- return {
288
- mode: MODES.HEADLESS,
289
- reason: "implementation without expected input fits headless execution",
290
- };
291
- }
292
-
293
- if (taskType === "review" || taskType === "analyze") {
294
- return {
295
- mode: MODES.HEADLESS,
296
- reason: "review and analyze tasks default to headless execution",
297
- };
298
- }
299
-
300
- if (needsInput === true) {
301
- return {
302
- mode: MODES.INTERACTIVE,
303
- reason: "task may require operator input during execution",
304
- };
305
- }
306
-
307
- if (estimatedDuration > 300) {
308
- return {
309
- mode: MODES.INTERACTIVE,
310
- reason: "long-running work benefits from monitored interactive mode",
311
- };
312
- }
313
-
314
- return {
315
- mode: MODES.HEADLESS,
316
- reason: "defaulting to headless execution",
317
- };
318
- }
319
-
320
- // buildCommandForMode 는 caller 가 없어 제거됨 (v10.20.x). argv-inline 프롬프트와
321
- // `-s danger-full-access` flag 가 PR #252 의 stdin redirect 패턴과 정합하지
322
- // 않아 회귀 위험이 있었다. 실제 헤드리스 spawn 은 buildSpawnSpecForMode
323
- // (conductor.mjs:544) 가 담당하며, 셸 명령 형태가 필요한 경우는
324
- // hub/cli-adapter-base.mjs:buildExecCommand 를 사용한다.
@@ -1,110 +0,0 @@
1
- // hub/team/extract-completion-payload.mjs — worker stdout tail → completion JSON
2
- //
3
- // Two-tier extraction:
4
- // 1) Sentinel-framed (preferred, #125): worker emits the payload between
5
- // `<<<TFX_COMPLETION_BEGIN>>>` and `<<<TFX_COMPLETION_END>>>` markers
6
- // that each occupy their own line. If a standalone-line BEGIN is present
7
- // but END is missing, this is a deterministic truncation signal and we
8
- // return null instead of falling back. Substring matches inside log
9
- // lines (e.g. `[debug] saw <<<TFX_COMPLETION_BEGIN>>> earlier`) do NOT
10
- // trigger Tier 1 (Codex R1 LOW finding).
11
- // 2) Brace-scan fallback (legacy, #115): for workers that haven't adopted
12
- // the sentinel protocol yet (or for unit fixtures), scan the tail for
13
- // the latest `{...}` pair that parses as a JSON object.
14
- //
15
- // The hypervisor's F7 guard (validateWorkerCompletion) decides whether the
16
- // parsed payload is semantically valid — this layer only handles extraction.
17
-
18
- import { SENTINEL_BEGIN, SENTINEL_END } from "./sentinel-capture.mjs";
19
-
20
- function isLineEdgeChar(ch) {
21
- return ch === "\n" || ch === "\r";
22
- }
23
-
24
- function findStandaloneMarkerLast(text, marker) {
25
- let from = 0;
26
- let last = -1;
27
- while (from <= text.length - marker.length) {
28
- const idx = text.indexOf(marker, from);
29
- if (idx === -1) return last;
30
- const before = idx === 0 || isLineEdgeChar(text[idx - 1]);
31
- const afterIdx = idx + marker.length;
32
- const after = afterIdx === text.length || isLineEdgeChar(text[afterIdx]);
33
- if (before && after) last = idx;
34
- from = idx + 1;
35
- }
36
- return last;
37
- }
38
-
39
- function findStandaloneMarkerFrom(text, marker, fromIdx) {
40
- let from = fromIdx;
41
- while (from <= text.length - marker.length) {
42
- const idx = text.indexOf(marker, from);
43
- if (idx === -1) return -1;
44
- const before = idx === 0 || isLineEdgeChar(text[idx - 1]);
45
- const afterIdx = idx + marker.length;
46
- const after = afterIdx === text.length || isLineEdgeChar(text[afterIdx]);
47
- if (before && after) return idx;
48
- from = idx + 1;
49
- }
50
- return -1;
51
- }
52
-
53
- /**
54
- * @param {string} tail raw stdout (sentinel snapshot or 16 KiB sliding tail)
55
- * @returns {{payload: object} | null}
56
- */
57
- export function extractCompletionPayload(tail) {
58
- if (typeof tail !== "string" || tail.length === 0) return null;
59
-
60
- // Tier 1: sentinel-framed. Use last standalone-line BEGIN so a worker that
61
- // emitted multiple payloads (retry, debug log) yields the most recent one.
62
- const beginIdx = findStandaloneMarkerLast(tail, SENTINEL_BEGIN);
63
- if (beginIdx !== -1) {
64
- const innerStart = beginIdx + SENTINEL_BEGIN.length;
65
- const endIdx = findStandaloneMarkerFrom(tail, SENTINEL_END, innerStart);
66
- if (endIdx === -1) return null; // BEGIN without END → truncation/incomplete
67
- const inner = tail.slice(innerStart, endIdx).trim();
68
- try {
69
- const parsed = JSON.parse(inner);
70
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
71
- return { payload: parsed };
72
- }
73
- } catch {
74
- // Sentinel-framed but invalid JSON — do not fall back to brace scan,
75
- // because the worker explicitly tried (and failed) to use the protocol.
76
- }
77
- return null;
78
- }
79
-
80
- // Tier 2: brace-scan fallback. Iterate from the latest `}` backward; for
81
- // each close, try every `{` that sits before it (nearest first, widening
82
- // outward). Tolerates trailing noise containing `}` (`noise } more`) and
83
- // dangling `{` (`{ broken`) after the real payload.
84
- const closes = [];
85
- const opens = [];
86
- for (let i = 0; i < tail.length; i += 1) {
87
- const ch = tail[i];
88
- if (ch === "}") closes.push(i);
89
- else if (ch === "{") opens.push(i);
90
- }
91
- if (closes.length === 0 || opens.length === 0) return null;
92
-
93
- for (let c = closes.length - 1; c >= 0; c -= 1) {
94
- const closeIdx = closes[c];
95
- for (let o = opens.length - 1; o >= 0; o -= 1) {
96
- const openIdx = opens[o];
97
- if (openIdx >= closeIdx) continue;
98
- const candidate = tail.slice(openIdx, closeIdx + 1);
99
- try {
100
- const parsed = JSON.parse(candidate);
101
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
102
- return { payload: parsed };
103
- }
104
- } catch {
105
- // try an earlier `{` for this close, then an earlier `}`.
106
- }
107
- }
108
- }
109
- return null;
110
- }
@@ -1,17 +0,0 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
-
4
- export async function writeBridgeSession(sessionId, socketPath, baseDir) {
5
- const sessionsDir = path.join(baseDir, "sessions");
6
- await fs.mkdir(sessionsDir, { recursive: true });
7
-
8
- const sessionData = {
9
- session_id: sessionId,
10
- messagingSock: socketPath,
11
- status: "RUNNING",
12
- created_at: new Date().toISOString(),
13
- };
14
-
15
- const filePath = path.join(sessionsDir, `${sessionId}.json`);
16
- await fs.writeFile(filePath, JSON.stringify(sessionData, null, 2), "utf8");
17
- }