@triflux/remote 10.50.2 → 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.
Files changed (134) hide show
  1. package/cto/brief.mjs +0 -20
  2. package/cto/collect.mjs +1 -200
  3. package/cto/current.schema.json +2 -23
  4. package/cto/hygiene.mjs +2 -61
  5. package/cto/lake-root.mjs +0 -56
  6. package/cto/status.mjs +5 -187
  7. package/hub/index.mjs +0 -17
  8. package/hub/team/agent-map.json +0 -1
  9. package/hub/team/agents-row-attach.py +120 -0
  10. package/hub/team/agents-row.mjs +200 -0
  11. package/hub/team/ansi.mjs +1 -35
  12. package/hub/team/backend.mjs +9 -145
  13. package/hub/team/claude-daemon-control.mjs +1 -2
  14. package/hub/team/claude-session-projection.mjs +5 -73
  15. package/hub/team/cli/commands/attach.mjs +1 -17
  16. package/hub/team/cli/commands/control.mjs +12 -38
  17. package/hub/team/cli/commands/debug.mjs +2 -39
  18. package/hub/team/cli/commands/focus.mjs +5 -31
  19. package/hub/team/cli/commands/interrupt.mjs +6 -25
  20. package/hub/team/cli/commands/kill.mjs +1 -30
  21. package/hub/team/cli/commands/list.mjs +0 -16
  22. package/hub/team/cli/commands/send.mjs +6 -26
  23. package/hub/team/cli/commands/start/index.mjs +42 -133
  24. package/hub/team/cli/commands/start/parse-args.mjs +15 -40
  25. package/hub/team/cli/commands/start/start-headless.mjs +138 -105
  26. package/hub/team/cli/commands/start/start-mux.mjs +2 -7
  27. package/hub/team/cli/commands/status.mjs +2 -57
  28. package/hub/team/cli/commands/stop.mjs +14 -19
  29. package/hub/team/cli/help.mjs +4 -5
  30. package/hub/team/cli/manifest.mjs +5 -1
  31. package/hub/team/cli/render.mjs +0 -12
  32. package/hub/team/cli/services/runtime-mode.mjs +27 -98
  33. package/hub/team/cli/services/state-store.mjs +5 -20
  34. package/hub/team/handoff.mjs +3 -34
  35. package/hub/team/headless.mjs +108 -1296
  36. package/hub/team/orchestrator.mjs +4 -30
  37. package/hub/team/pane.mjs +6 -28
  38. package/hub/team/psmux.mjs +3 -34
  39. package/hub/team/remote-session.mjs +1 -2
  40. package/hub/team/session-context.mjs +1 -1
  41. package/hub/team/session.mjs +7 -91
  42. package/hub/team/tui-core.mjs +1 -1
  43. package/hub/team/tui-viewer.mjs +11 -58
  44. package/hub/team/tui.mjs +8 -306
  45. package/hub/team/uds-orchestrator.mjs +2 -580
  46. package/hub/team/wt-manager.mjs +2 -2
  47. package/hub/workers/factory.mjs +6 -84
  48. package/hub/workers/interface.mjs +1 -7
  49. package/hub/workers/lib/jsonrpc-core.mjs +11 -0
  50. package/hub/workers/lib/jsonrpc-ws-uds.mjs +5 -8
  51. package/package.json +3 -8
  52. package/scripts/lib/agent-route-policy.mjs +143 -62
  53. package/scripts/lib/cli-claude.mjs +0 -14
  54. package/scripts/lib/env-probe.mjs +2 -131
  55. package/scripts/lib/gemini-profiles.mjs +2 -69
  56. package/scripts/lib/legacy-mcp-cleanup.mjs +283 -0
  57. package/scripts/lib/machine-profile.mjs +3 -17
  58. package/scripts/lib/mcp-filter.mjs +109 -53
  59. package/scripts/lib/mcp-guard-engine.mjs +40 -98
  60. package/scripts/lib/mcp-health.mjs +2 -2
  61. package/scripts/lib/mcp-server-catalog.mjs +2 -2
  62. package/scripts/lib/psmux-info.mjs +11 -59
  63. package/scripts/lib/skill-template.mjs +0 -245
  64. package/hub/hub-lifecycle.mjs +0 -248
  65. package/hub/pipe.mjs +0 -825
  66. package/hub/promote-penalties.mjs +0 -138
  67. package/hub/public/dashboard.html +0 -416
  68. package/hub/schema.sql +0 -209
  69. package/hub/server.mjs +0 -3048
  70. package/hub/store-adapter.mjs +0 -182
  71. package/hub/store.mjs +0 -1736
  72. package/hub/team/build-worker-prompt.mjs +0 -92
  73. package/hub/team/check-mcp-hub.mjs +0 -49
  74. package/hub/team/claude-native-bridge.mjs +0 -987
  75. package/hub/team/cli/commands/start/start-in-process.mjs +0 -50
  76. package/hub/team/cli/commands/start/start-wt.mjs +0 -86
  77. package/hub/team/cli/services/hub-client.mjs +0 -264
  78. package/hub/team/cli/services/native-control.mjs +0 -145
  79. package/hub/team/codex-review.mjs +0 -520
  80. package/hub/team/conductor-mesh-bridge.mjs +0 -122
  81. package/hub/team/conductor-registry.mjs +0 -121
  82. package/hub/team/conductor.mjs +0 -1529
  83. package/hub/team/consensus-meta.mjs +0 -176
  84. package/hub/team/cto-auto-collect.mjs +0 -115
  85. package/hub/team/cto-pull.mjs +0 -31
  86. package/hub/team/daemon-pty-tmux-bridge.mjs +0 -146
  87. package/hub/team/dashboard.mjs +0 -322
  88. package/hub/team/event-log.mjs +0 -84
  89. package/hub/team/execution-mode.mjs +0 -324
  90. package/hub/team/extract-completion-payload.mjs +0 -110
  91. package/hub/team/headless-bridge-session.mjs +0 -17
  92. package/hub/team/health-probe.mjs +0 -435
  93. package/hub/team/interactive-native-launcher.mjs +0 -119
  94. package/hub/team/interactive-native-worker.mjs +0 -80
  95. package/hub/team/interactive-tui-transport.mjs +0 -479
  96. package/hub/team/launcher-template.mjs +0 -144
  97. package/hub/team/lead-control.mjs +0 -191
  98. package/hub/team/mcp-selector.mjs +0 -145
  99. package/hub/team/native-supervisor.mjs +0 -460
  100. package/hub/team/native.mjs +0 -727
  101. package/hub/team/recovery-store.mjs +0 -149
  102. package/hub/team/remote-probe.mjs +0 -292
  103. package/hub/team/remote-watcher.mjs +0 -506
  104. package/hub/team/runtime-strategy.mjs +0 -200
  105. package/hub/team/schemas/worker-completion.json +0 -39
  106. package/hub/team/sentinel-capture.mjs +0 -127
  107. package/hub/team/session-sync.mjs +0 -316
  108. package/hub/team/swarm-cli.mjs +0 -390
  109. package/hub/team/swarm-hypervisor.mjs +0 -2206
  110. package/hub/team/swarm-intent.mjs +0 -216
  111. package/hub/team/swarm-locks.mjs +0 -335
  112. package/hub/team/swarm-planner.mjs +0 -371
  113. package/hub/team/swarm-preflight.mjs +0 -366
  114. package/hub/team/swarm-reconciler.mjs +0 -242
  115. package/hub/team/synapse-cli.mjs +0 -458
  116. package/hub/team/synapse-http.mjs +0 -311
  117. package/hub/team/synapse-registry.mjs +0 -618
  118. package/hub/team/tui-remote-adapter.mjs +0 -377
  119. package/hub/team/tui-synapse.mjs +0 -223
  120. package/hub/team/worker-completion-validator.mjs +0 -79
  121. package/hub/team/worker-sandbox.mjs +0 -125
  122. package/hub/team/worktree-lifecycle.mjs +0 -639
  123. package/hub/team/wt-templates.json +0 -43
  124. package/hub/tools.mjs +0 -900
  125. package/hub/workers/antigravity-route-worker.mjs +0 -179
  126. package/hub/workers/codex-app-server-worker.mjs +0 -1067
  127. package/hub/workers/delegator-mcp.mjs +0 -1098
  128. package/hub/workers/lib/jsonrpc-stdio.mjs +0 -204
  129. package/scripts/lib/claudemd-scanner.mjs +0 -223
  130. package/scripts/lib/context.mjs +0 -67
  131. package/scripts/lib/dynamic-route-cli.mjs +0 -107
  132. package/scripts/lib/handoff.mjs +0 -187
  133. package/scripts/lib/logger.mjs +0 -122
  134. package/scripts/lib/stealth-fetch.mjs +0 -176
@@ -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
- }