@triflux/remote 10.50.1 → 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.
- package/cto/brief.mjs +0 -12
- package/cto/collect.mjs +16 -247
- package/cto/current.schema.json +4 -20
- package/cto/events.mjs +13 -294
- package/cto/hygiene.mjs +26 -346
- package/cto/lake-root.mjs +114 -16
- package/cto/status.mjs +33 -128
- package/hub/index.mjs +0 -8
- package/hub/store-adapter.mjs +1 -1
- package/hub/team/agent-map.json +0 -1
- package/hub/team/ansi.mjs +1 -35
- package/hub/team/backend.mjs +9 -145
- package/hub/team/cli/commands/attach.mjs +1 -17
- package/hub/team/cli/commands/control.mjs +12 -38
- package/hub/team/cli/commands/debug.mjs +1 -37
- package/hub/team/cli/commands/focus.mjs +5 -31
- package/hub/team/cli/commands/interrupt.mjs +6 -25
- package/hub/team/cli/commands/kill.mjs +1 -30
- package/hub/team/cli/commands/list.mjs +0 -16
- package/hub/team/cli/commands/send.mjs +6 -26
- package/hub/team/cli/commands/start/index.mjs +41 -133
- package/hub/team/cli/commands/start/parse-args.mjs +15 -40
- package/hub/team/cli/commands/start/start-headless.mjs +138 -105
- package/hub/team/cli/commands/start/start-mux.mjs +0 -3
- package/hub/team/cli/commands/status.mjs +2 -57
- package/hub/team/cli/commands/stop.mjs +14 -19
- package/hub/team/cli/help.mjs +4 -5
- package/hub/team/cli/manifest.mjs +5 -1
- package/hub/team/cli/render.mjs +0 -12
- package/hub/team/cli/services/hub-client.mjs +2 -88
- package/hub/team/cli/services/runtime-mode.mjs +27 -98
- package/hub/team/handoff.mjs +3 -34
- package/hub/team/headless.mjs +14 -755
- package/hub/team/orchestrator.mjs +4 -30
- package/hub/team/pane.mjs +6 -28
- package/hub/team/psmux.mjs +16 -403
- package/hub/team/remote-session.mjs +1 -2
- package/hub/team/session.mjs +8 -119
- package/hub/team/swarm-intent.mjs +0 -198
- package/hub/team/terminal-opener.mjs +11 -35
- package/hub/team/tui-core.mjs +1 -1
- package/hub/team/tui-viewer.mjs +2 -2
- package/hub/team/tui.mjs +8 -269
- package/hub/team/wt-manager.mjs +2 -2
- package/hub/workers/factory.mjs +4 -37
- package/hub/workers/lib/jsonrpc-ws-uds.mjs +0 -2
- package/package.json +1 -1
- package/scripts/lib/agent-route-policy.mjs +162 -14
- package/scripts/lib/claudemd-scanner.mjs +2 -199
- package/scripts/lib/cli-claude.mjs +0 -2
- package/scripts/lib/context.mjs +3 -19
- package/scripts/lib/env-probe.mjs +5 -64
- package/scripts/lib/gemini-profiles.mjs +2 -69
- package/scripts/lib/legacy-mcp-cleanup.mjs +14 -3
- package/scripts/lib/machine-profile.mjs +3 -17
- package/scripts/lib/mcp-filter.mjs +109 -53
- package/scripts/lib/mcp-health.mjs +2 -2
- package/scripts/lib/mcp-server-catalog.mjs +2 -2
- package/scripts/lib/psmux-info.mjs +25 -104
- package/scripts/lib/skill-template.mjs +0 -271
- package/cto/hygiene-actions.mjs +0 -355
- package/cto/hygiene-notify.mjs +0 -170
- package/hub/team/build-worker-prompt.mjs +0 -92
- package/hub/team/check-mcp-hub.mjs +0 -49
- package/hub/team/claude-native-bridge.mjs +0 -987
- package/hub/team/cli/commands/start/start-in-process.mjs +0 -50
- package/hub/team/cli/commands/start/start-wt.mjs +0 -86
- package/hub/team/cli/services/native-control.mjs +0 -145
- package/hub/team/codex-review.mjs +0 -520
- package/hub/team/conductor-mesh-bridge.mjs +0 -122
- package/hub/team/conductor.mjs +0 -1529
- package/hub/team/consensus-meta.mjs +0 -176
- package/hub/team/cto-pull.mjs +0 -31
- package/hub/team/daemon-pty-tmux-bridge.mjs +0 -146
- package/hub/team/dashboard.mjs +0 -322
- package/hub/team/event-log.mjs +0 -84
- package/hub/team/execution-mode.mjs +0 -324
- package/hub/team/extract-completion-payload.mjs +0 -110
- package/hub/team/headless-bridge-session.mjs +0 -17
- package/hub/team/health-probe.mjs +0 -435
- package/hub/team/interactive-native-launcher.mjs +0 -119
- package/hub/team/interactive-native-worker.mjs +0 -80
- package/hub/team/interactive-tui-transport.mjs +0 -479
- package/hub/team/launcher-template.mjs +0 -144
- package/hub/team/mcp-selector.mjs +0 -145
- package/hub/team/native-supervisor.mjs +0 -460
- package/hub/team/native.mjs +0 -727
- package/hub/team/recovery-store.mjs +0 -149
- package/hub/team/remote-probe.mjs +0 -292
- package/hub/team/remote-watcher.mjs +0 -506
- package/hub/team/runtime-strategy.mjs +0 -200
- package/hub/team/schemas/worker-completion.json +0 -39
- package/hub/team/sentinel-capture.mjs +0 -127
- package/hub/team/swarm-cli.mjs +0 -390
- package/hub/team/swarm-hypervisor.mjs +0 -2206
- package/hub/team/swarm-locks.mjs +0 -335
- package/hub/team/swarm-planner.mjs +0 -371
- package/hub/team/swarm-preflight.mjs +0 -366
- package/hub/team/swarm-reconciler.mjs +0 -242
- package/hub/team/tui-remote-adapter.mjs +0 -377
- package/hub/team/worker-completion-validator.mjs +0 -79
- package/hub/team/worktree-lifecycle.mjs +0 -639
- package/hub/team/wt-templates.json +0 -43
- package/hub/workers/antigravity-route-worker.mjs +0 -179
- package/hub/workers/gemini-worker.mjs +0 -217
- package/scripts/lib/agent-json.mjs +0 -27
- package/scripts/lib/async.mjs +0 -174
- package/scripts/lib/cli-agy.mjs +0 -85
- package/scripts/lib/cli-codex.mjs +0 -62
- package/scripts/lib/cli-gemini.mjs +0 -37
- package/scripts/lib/dynamic-route-cli.mjs +0 -107
- package/scripts/lib/env.mjs +0 -14
- package/scripts/lib/hub.mjs +0 -48
- package/scripts/lib/pid.mjs +0 -63
- package/scripts/lib/quota.mjs +0 -47
- package/scripts/lib/skill-state.mjs +0 -220
- package/scripts/lib/team.mjs +0 -75
- package/scripts/lib/timeout.mjs +0 -66
- package/scripts/lib/tmp.mjs +0 -26
- package/scripts/lib/toml.mjs +0 -132
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
// hub/team/recovery-store.mjs — Best-effort preservation of worker changes
|
|
2
|
-
// before a dirty worktree is removed. Uses file-based `.patch` + JSON manifest
|
|
3
|
-
// instead of git stash, which is risky to pop under concurrent worktree use.
|
|
4
|
-
|
|
5
|
-
import { execFile } from "node:child_process";
|
|
6
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
-
import { join, resolve } from "node:path";
|
|
8
|
-
|
|
9
|
-
const DEFAULT_RECOVERY_DIR = ".codex-swarm/recovery";
|
|
10
|
-
|
|
11
|
-
function git(args, cwd) {
|
|
12
|
-
return new Promise((res) => {
|
|
13
|
-
execFile(
|
|
14
|
-
"git",
|
|
15
|
-
args,
|
|
16
|
-
{ cwd, windowsHide: true, timeout: 30_000, maxBuffer: 64 * 1024 * 1024 },
|
|
17
|
-
(err, stdout, stderr) => {
|
|
18
|
-
if (err) {
|
|
19
|
-
res({ ok: false, stderr: stderr?.toString() || err.message });
|
|
20
|
-
} else {
|
|
21
|
-
res({ ok: true, stdout });
|
|
22
|
-
}
|
|
23
|
-
},
|
|
24
|
-
);
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function manifestPath(recoveryDir) {
|
|
29
|
-
return join(recoveryDir, "manifest.json");
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function readManifestFile(recoveryDir) {
|
|
33
|
-
const path = manifestPath(recoveryDir);
|
|
34
|
-
if (!existsSync(path)) return { entries: [] };
|
|
35
|
-
try {
|
|
36
|
-
const raw = readFileSync(path, "utf8");
|
|
37
|
-
const parsed = JSON.parse(raw);
|
|
38
|
-
if (parsed && Array.isArray(parsed.entries)) return parsed;
|
|
39
|
-
return { entries: [] };
|
|
40
|
-
} catch {
|
|
41
|
-
return { entries: [] };
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function writeManifestFile(recoveryDir, data) {
|
|
46
|
-
writeFileSync(manifestPath(recoveryDir), JSON.stringify(data, null, 2));
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Preserve an uncommitted worker patch before the worktree is removed.
|
|
51
|
-
*
|
|
52
|
-
* Contract:
|
|
53
|
-
* - Never throws.
|
|
54
|
-
* - Dirty worktree → write {recoveryDir}/{shardId}.patch, append manifest entry.
|
|
55
|
-
* - Clean worktree → return {ok: true, skipped: true}, no files written.
|
|
56
|
-
* - Missing/invalid → return {ok: false, reason}, no throw.
|
|
57
|
-
*
|
|
58
|
-
* Patch captures `git diff HEAD` output (unified diff) — this covers both
|
|
59
|
-
* staged and unstaged changes relative to the shard's base commit.
|
|
60
|
-
*
|
|
61
|
-
* @param {object} opts
|
|
62
|
-
* @param {string} opts.worktreePath — absolute path to the worker worktree
|
|
63
|
-
* @param {string} opts.shardId — shard identifier (used for filename)
|
|
64
|
-
* @param {string} [opts.recoveryDir] — default: `<worktreePath>/.codex-swarm/recovery`
|
|
65
|
-
* when running inside worktree root;
|
|
66
|
-
* callers typically pass repo-root relative path.
|
|
67
|
-
* @returns {Promise<
|
|
68
|
-
* | { ok: true, patchPath: string, manifestPath: string }
|
|
69
|
-
* | { ok: true, skipped: true }
|
|
70
|
-
* | { ok: false, reason: string }
|
|
71
|
-
* >}
|
|
72
|
-
*/
|
|
73
|
-
export async function preserveWorktreePatch(opts = {}) {
|
|
74
|
-
const { worktreePath, shardId } = opts;
|
|
75
|
-
|
|
76
|
-
if (!worktreePath || typeof worktreePath !== "string") {
|
|
77
|
-
return { ok: false, reason: "missing_worktree_path" };
|
|
78
|
-
}
|
|
79
|
-
if (!shardId || typeof shardId !== "string") {
|
|
80
|
-
return { ok: false, reason: "missing_shard_id" };
|
|
81
|
-
}
|
|
82
|
-
if (!existsSync(worktreePath)) {
|
|
83
|
-
return { ok: false, reason: "worktree_path_not_found" };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const recoveryDir = opts.recoveryDir
|
|
87
|
-
? resolve(opts.recoveryDir)
|
|
88
|
-
: resolve(worktreePath, DEFAULT_RECOVERY_DIR);
|
|
89
|
-
|
|
90
|
-
// Quickly classify dirty vs clean. `git status --porcelain` is safe and fast.
|
|
91
|
-
const status = await git(["status", "--porcelain"], worktreePath);
|
|
92
|
-
if (!status.ok) {
|
|
93
|
-
return { ok: false, reason: `git_status_failed: ${status.stderr}` };
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (status.stdout.trim().length === 0) {
|
|
97
|
-
return { ok: true, skipped: true };
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Dirty — capture diff.
|
|
101
|
-
const diff = await git(["diff", "HEAD"], worktreePath);
|
|
102
|
-
if (!diff.ok) {
|
|
103
|
-
return { ok: false, reason: `git_diff_failed: ${diff.stderr}` };
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
try {
|
|
107
|
-
mkdirSync(recoveryDir, { recursive: true });
|
|
108
|
-
} catch (err) {
|
|
109
|
-
return { ok: false, reason: `mkdir_failed: ${err.message}` };
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const safeShardId = shardId.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
113
|
-
const patchPath = join(recoveryDir, `${safeShardId}.patch`);
|
|
114
|
-
try {
|
|
115
|
-
writeFileSync(patchPath, diff.stdout);
|
|
116
|
-
} catch (err) {
|
|
117
|
-
return { ok: false, reason: `patch_write_failed: ${err.message}` };
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const manifest = readManifestFile(recoveryDir);
|
|
121
|
-
manifest.entries.push({
|
|
122
|
-
shard: shardId,
|
|
123
|
-
patch: patchPath,
|
|
124
|
-
timestamp: new Date().toISOString(),
|
|
125
|
-
});
|
|
126
|
-
try {
|
|
127
|
-
writeManifestFile(recoveryDir, manifest);
|
|
128
|
-
} catch (err) {
|
|
129
|
-
return { ok: false, reason: `manifest_write_failed: ${err.message}` };
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return { ok: true, patchPath, manifestPath: manifestPath(recoveryDir) };
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Read the recovery manifest. Returns {entries: []} if missing or invalid.
|
|
137
|
-
* @param {string} recoveryDir
|
|
138
|
-
* @returns {{ entries: Array<{ shard: string, patch: string, timestamp: string }> }}
|
|
139
|
-
*/
|
|
140
|
-
export function readManifest(recoveryDir) {
|
|
141
|
-
if (!recoveryDir || typeof recoveryDir !== "string") {
|
|
142
|
-
return { entries: [] };
|
|
143
|
-
}
|
|
144
|
-
return readManifestFile(resolve(recoveryDir));
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export const RECOVERY_DEFAULTS = Object.freeze({
|
|
148
|
-
dir: DEFAULT_RECOVERY_DIR,
|
|
149
|
-
});
|
|
@@ -1,292 +0,0 @@
|
|
|
1
|
-
// hub/team/remote-probe.mjs — SSH 경유 원격 세션 health probe
|
|
2
|
-
// health-probe.mjs와 동일 인터페이스: { start, stop, probe, getStatus }
|
|
3
|
-
// child process PID 대신 SSH capture-pane 폴링으로 상태 추적.
|
|
4
|
-
//
|
|
5
|
-
// L0: SSH 연결 + psmux 세션 존재 확인
|
|
6
|
-
// L1: capture-pane 출력 변화 감지 (advancing)
|
|
7
|
-
// L1.5: INPUT_WAIT 패턴 감지 (detectInputWait 재사용)
|
|
8
|
-
// L3: 완료 토큰 감지 (__TRIFLUX_DONE__ 또는 프롬프트 idle)
|
|
9
|
-
|
|
10
|
-
import { execFileSync } from "node:child_process";
|
|
11
|
-
import { detectInputWait, PROBE_DEFAULTS } from "./health-probe.mjs";
|
|
12
|
-
|
|
13
|
-
/** 완료 토큰 패턴 */
|
|
14
|
-
const COMPLETION_TOKEN_RE = /__TRIFLUX_DONE__/;
|
|
15
|
-
|
|
16
|
-
/** 프롬프트 idle 패턴 (Claude Code 프롬프트 복귀) */
|
|
17
|
-
const _PROMPT_IDLE_RE = /(\u276f|\u2795|>\s*$)/;
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* SSH 경유로 원격 psmux capture-pane 실행.
|
|
21
|
-
* @param {string} host — SSH 호스트
|
|
22
|
-
* @param {string} paneTarget — psmux pane target (e.g. "session:0.0")
|
|
23
|
-
* @param {number} lines — 캡처할 줄 수
|
|
24
|
-
* @param {object} [deps] — 테스트용 의존성 주입
|
|
25
|
-
* @returns {string|null} 캡처 텍스트 또는 null (실패)
|
|
26
|
-
*/
|
|
27
|
-
export function sshCapturePane(host, paneTarget, lines = 20, deps = {}) {
|
|
28
|
-
const execFn = deps.execFileSync || execFileSync;
|
|
29
|
-
try {
|
|
30
|
-
const output = execFn(
|
|
31
|
-
"ssh",
|
|
32
|
-
[
|
|
33
|
-
"-o",
|
|
34
|
-
"ConnectTimeout=5",
|
|
35
|
-
"-o",
|
|
36
|
-
"BatchMode=yes",
|
|
37
|
-
host,
|
|
38
|
-
`psmux capture-pane -t ${paneTarget} -p -S -`,
|
|
39
|
-
],
|
|
40
|
-
{
|
|
41
|
-
encoding: "utf8",
|
|
42
|
-
timeout: 10_000,
|
|
43
|
-
windowsHide: true,
|
|
44
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
45
|
-
},
|
|
46
|
-
);
|
|
47
|
-
const nonEmpty = output.split("\n").filter((line) => line.trim() !== "");
|
|
48
|
-
return nonEmpty.slice(-lines).join("\n");
|
|
49
|
-
} catch {
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* SSH 경유로 원격 psmux 세션 존재 여부 확인.
|
|
56
|
-
* @param {string} host
|
|
57
|
-
* @param {string} sessionName
|
|
58
|
-
* @param {object} [deps]
|
|
59
|
-
* @returns {boolean}
|
|
60
|
-
*/
|
|
61
|
-
export function sshSessionExists(host, sessionName, deps = {}) {
|
|
62
|
-
const execFn = deps.execFileSync || execFileSync;
|
|
63
|
-
try {
|
|
64
|
-
execFn(
|
|
65
|
-
"ssh",
|
|
66
|
-
[
|
|
67
|
-
"-o",
|
|
68
|
-
"ConnectTimeout=5",
|
|
69
|
-
"-o",
|
|
70
|
-
"BatchMode=yes",
|
|
71
|
-
host,
|
|
72
|
-
`psmux has-session -t ${sessionName}`,
|
|
73
|
-
],
|
|
74
|
-
{
|
|
75
|
-
timeout: 10_000,
|
|
76
|
-
windowsHide: true,
|
|
77
|
-
stdio: ["ignore", "ignore", "ignore"],
|
|
78
|
-
},
|
|
79
|
-
);
|
|
80
|
-
return true;
|
|
81
|
-
} catch {
|
|
82
|
-
return false;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Remote health probe 팩토리.
|
|
88
|
-
* health-probe.mjs의 createHealthProbe와 동일 인터페이스를 제공하되,
|
|
89
|
-
* SSH capture-pane 폴링 기반으로 동작한다.
|
|
90
|
-
*
|
|
91
|
-
* @param {object} session — probe 대상 세션 정보
|
|
92
|
-
* @param {string} session.host — SSH 호스트
|
|
93
|
-
* @param {string} session.paneTarget — psmux pane target
|
|
94
|
-
* @param {string} session.sessionName — psmux 세션 이름
|
|
95
|
-
* @param {object} [opts] — PROBE_DEFAULTS 오버라이드
|
|
96
|
-
* @param {function} [opts.onProbe] — (result) => void 콜백
|
|
97
|
-
* @param {object} [opts.deps] — 테스트용 의존성 주입
|
|
98
|
-
* @returns {{ start, stop, probe, getStatus, started }}
|
|
99
|
-
*/
|
|
100
|
-
export function createRemoteProbe(session, opts = {}) {
|
|
101
|
-
const config = { ...PROBE_DEFAULTS, ...opts };
|
|
102
|
-
const deps = opts.deps || {};
|
|
103
|
-
let timer = null;
|
|
104
|
-
let started = false;
|
|
105
|
-
|
|
106
|
-
// L1 tracking — 출력 변화 감지
|
|
107
|
-
let lastCaptureHash = "";
|
|
108
|
-
let lastOutputChangeAt = Date.now();
|
|
109
|
-
|
|
110
|
-
// L3 tracking — 완료 토큰 / prompt idle
|
|
111
|
-
let promptAcked = false;
|
|
112
|
-
let spawnedAt = Date.now();
|
|
113
|
-
|
|
114
|
-
const status = {
|
|
115
|
-
l0: null,
|
|
116
|
-
l1: null,
|
|
117
|
-
l2: "skip", // 원격은 MCP L2 미지원
|
|
118
|
-
l3: null,
|
|
119
|
-
lastProbeAt: null,
|
|
120
|
-
inputWaitPattern: null,
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* L0: SSH 연결 + psmux 세션 존재 확인.
|
|
125
|
-
*/
|
|
126
|
-
function probeL0() {
|
|
127
|
-
const exists = sshSessionExists(session.host, session.sessionName, deps);
|
|
128
|
-
status.l0 = exists ? "ok" : "fail";
|
|
129
|
-
return status.l0;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* L1 + L1.5: capture-pane 출력 변화 + INPUT_WAIT 감지.
|
|
134
|
-
* @param {string|null} captured — 이미 캡처된 텍스트 (L0에서 재사용)
|
|
135
|
-
*/
|
|
136
|
-
function probeL1(captured) {
|
|
137
|
-
const now = Date.now();
|
|
138
|
-
|
|
139
|
-
if (captured == null) {
|
|
140
|
-
// 캡처 실패 — 이전 상태 유지
|
|
141
|
-
return status.l1 || null;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// 단순 해시: 길이 + 처음/끝 문자 조합 (crypto 불필요)
|
|
145
|
-
const hash = `${captured.length}:${captured.slice(0, 32)}:${captured.slice(-32)}`;
|
|
146
|
-
|
|
147
|
-
if (hash !== lastCaptureHash) {
|
|
148
|
-
lastCaptureHash = hash;
|
|
149
|
-
lastOutputChangeAt = now;
|
|
150
|
-
status.l1 = "ok";
|
|
151
|
-
status.inputWaitPattern = null;
|
|
152
|
-
return "ok";
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const silenceMs = now - lastOutputChangeAt;
|
|
156
|
-
|
|
157
|
-
if (silenceMs >= config.l1ThresholdMs) {
|
|
158
|
-
// L1.5: INPUT_WAIT 패턴 감지
|
|
159
|
-
const inputWait = detectInputWait(captured);
|
|
160
|
-
|
|
161
|
-
if (inputWait.detected) {
|
|
162
|
-
status.l1 = "input_wait";
|
|
163
|
-
status.inputWaitPattern = inputWait.pattern;
|
|
164
|
-
return "input_wait";
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
status.l1 = "stall";
|
|
168
|
-
status.inputWaitPattern = null;
|
|
169
|
-
return "stall";
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
status.l1 = "ok";
|
|
173
|
-
return "ok";
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* L3: 완료 토큰 또는 프롬프트 idle 감지.
|
|
178
|
-
* @param {string|null} captured
|
|
179
|
-
*/
|
|
180
|
-
function probeL3(captured) {
|
|
181
|
-
if (promptAcked) {
|
|
182
|
-
status.l3 = "ok";
|
|
183
|
-
return "ok";
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
if (captured != null && captured.length > 0) {
|
|
187
|
-
// 완료 토큰 감지 → 즉시 ok
|
|
188
|
-
if (COMPLETION_TOKEN_RE.test(captured)) {
|
|
189
|
-
promptAcked = true;
|
|
190
|
-
status.l3 = "completed";
|
|
191
|
-
return "completed";
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// 출력이 있으면 prompt acknowledged
|
|
195
|
-
promptAcked = true;
|
|
196
|
-
status.l3 = "ok";
|
|
197
|
-
return "ok";
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const elapsed = Date.now() - spawnedAt;
|
|
201
|
-
if (elapsed >= config.l3ThresholdMs) {
|
|
202
|
-
status.l3 = "timeout";
|
|
203
|
-
return "timeout";
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
status.l3 = null;
|
|
207
|
-
return null;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* 전체 probe 실행 (L0 → capture → L1 → L3).
|
|
212
|
-
* @returns {Promise<object>} probe 결과
|
|
213
|
-
*/
|
|
214
|
-
async function probe() {
|
|
215
|
-
const l0 = probeL0();
|
|
216
|
-
|
|
217
|
-
// L0 실패 시 나머지 probe 스킵
|
|
218
|
-
let captured = null;
|
|
219
|
-
if (l0 === "ok") {
|
|
220
|
-
captured = sshCapturePane(session.host, session.paneTarget, 20, deps);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const l1 = probeL1(captured);
|
|
224
|
-
const l3 = probeL3(captured);
|
|
225
|
-
|
|
226
|
-
const result = {
|
|
227
|
-
l0,
|
|
228
|
-
l1,
|
|
229
|
-
l2: "skip",
|
|
230
|
-
l3,
|
|
231
|
-
inputWaitPattern: status.inputWaitPattern,
|
|
232
|
-
ts: Date.now(),
|
|
233
|
-
};
|
|
234
|
-
status.lastProbeAt = result.ts;
|
|
235
|
-
|
|
236
|
-
if (typeof config.onProbe === "function") {
|
|
237
|
-
config.onProbe(result);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
return result;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function start() {
|
|
244
|
-
if (started) return;
|
|
245
|
-
started = true;
|
|
246
|
-
spawnedAt = Date.now();
|
|
247
|
-
lastOutputChangeAt = Date.now();
|
|
248
|
-
lastCaptureHash = "";
|
|
249
|
-
promptAcked = false;
|
|
250
|
-
|
|
251
|
-
timer = setInterval(() => {
|
|
252
|
-
void probe();
|
|
253
|
-
}, config.intervalMs);
|
|
254
|
-
timer.unref?.();
|
|
255
|
-
|
|
256
|
-
// 즉시 첫 probe 실행
|
|
257
|
-
void probe();
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function stop() {
|
|
261
|
-
if (!started) return;
|
|
262
|
-
started = false;
|
|
263
|
-
if (timer) {
|
|
264
|
-
clearInterval(timer);
|
|
265
|
-
timer = null;
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/** tracking 리셋 (restart 후 호출) */
|
|
270
|
-
function resetTracking() {
|
|
271
|
-
lastCaptureHash = "";
|
|
272
|
-
lastOutputChangeAt = Date.now();
|
|
273
|
-
promptAcked = false;
|
|
274
|
-
spawnedAt = Date.now();
|
|
275
|
-
status.l0 = null;
|
|
276
|
-
status.l1 = null;
|
|
277
|
-
status.l2 = "skip";
|
|
278
|
-
status.l3 = null;
|
|
279
|
-
status.inputWaitPattern = null;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
return Object.freeze({
|
|
283
|
-
start,
|
|
284
|
-
stop,
|
|
285
|
-
probe,
|
|
286
|
-
resetTracking,
|
|
287
|
-
getStatus: () => ({ ...status }),
|
|
288
|
-
get started() {
|
|
289
|
-
return started;
|
|
290
|
-
},
|
|
291
|
-
});
|
|
292
|
-
}
|