@triflux/remote 10.1.0 → 10.26.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 (162) hide show
  1. package/hub/pipe.mjs +239 -121
  2. package/hub/public/dashboard.html +61 -0
  3. package/hub/server.mjs +2179 -709
  4. package/hub/store-adapter.mjs +59 -26
  5. package/hub/store.mjs +356 -153
  6. package/hub/team/agent-map.json +26 -7
  7. package/hub/team/ansi.mjs +245 -146
  8. package/hub/team/backend.mjs +78 -17
  9. package/hub/team/build-worker-prompt.mjs +73 -0
  10. package/hub/team/check-mcp-hub.mjs +49 -0
  11. package/hub/team/claude-daemon-control.mjs +1130 -0
  12. package/hub/team/claude-native-bridge.mjs +1011 -0
  13. package/hub/team/claude-session-projection.mjs +74 -0
  14. package/hub/team/cli/commands/attach.mjs +57 -37
  15. package/hub/team/cli/commands/control.mjs +29 -8
  16. package/hub/team/cli/commands/debug.mjs +95 -74
  17. package/hub/team/cli/commands/focus.mjs +80 -53
  18. package/hub/team/cli/commands/interrupt.mjs +18 -6
  19. package/hub/team/cli/commands/kill.mjs +16 -5
  20. package/hub/team/cli/commands/list.mjs +31 -24
  21. package/hub/team/cli/commands/send.mjs +19 -6
  22. package/hub/team/cli/commands/start/index.mjs +151 -33
  23. package/hub/team/cli/commands/start/parse-args.mjs +97 -13
  24. package/hub/team/cli/commands/start/start-headless.mjs +155 -37
  25. package/hub/team/cli/commands/start/start-in-process.mjs +50 -40
  26. package/hub/team/cli/commands/start/start-mux.mjs +122 -73
  27. package/hub/team/cli/commands/start/start-wt.mjs +86 -69
  28. package/hub/team/cli/commands/status.mjs +43 -14
  29. package/hub/team/cli/commands/stop.mjs +11 -4
  30. package/hub/team/cli/commands/task.mjs +8 -3
  31. package/hub/team/cli/commands/tasks.mjs +13 -13
  32. package/hub/team/cli/help.mjs +1 -1
  33. package/hub/team/cli/index.mjs +2 -2
  34. package/hub/team/cli/manifest.mjs +38 -8
  35. package/hub/team/cli/render.mjs +52 -30
  36. package/hub/team/cli/services/attach-fallback.mjs +74 -54
  37. package/hub/team/cli/services/hub-client.mjs +44 -16
  38. package/hub/team/cli/services/member-selector.mjs +37 -30
  39. package/hub/team/cli/services/native-control.mjs +144 -116
  40. package/hub/team/cli/services/runtime-mode.mjs +61 -4
  41. package/hub/team/cli/services/state-store.mjs +66 -9
  42. package/hub/team/cli/services/task-model.mjs +40 -30
  43. package/hub/team/codex-review.mjs +521 -0
  44. package/hub/team/conductor-mesh-bridge.mjs +25 -24
  45. package/hub/team/conductor-registry.mjs +121 -0
  46. package/hub/team/conductor.mjs +1379 -671
  47. package/hub/team/consensus-meta.mjs +176 -0
  48. package/hub/team/dashboard-anchor.mjs +4 -5
  49. package/hub/team/dashboard-layout.mjs +3 -1
  50. package/hub/team/dashboard-open.mjs +20 -118
  51. package/hub/team/dashboard.mjs +76 -28
  52. package/hub/team/event-log.mjs +18 -10
  53. package/hub/team/execution-mode.mjs +256 -0
  54. package/hub/team/extract-completion-payload.mjs +110 -0
  55. package/hub/team/git-preflight.mjs +286 -0
  56. package/hub/team/handoff.mjs +49 -18
  57. package/hub/team/headless-bridge-session.mjs +17 -0
  58. package/hub/team/headless.mjs +2213 -1156
  59. package/hub/team/health-probe.mjs +232 -69
  60. package/hub/team/interactive-native-launcher.mjs +106 -0
  61. package/hub/team/interactive-native-worker.mjs +80 -0
  62. package/hub/team/interactive-tui-transport.mjs +465 -0
  63. package/hub/team/launcher-template.mjs +49 -17
  64. package/hub/team/lead-control.mjs +91 -4
  65. package/hub/team/mcp-selector.mjs +145 -0
  66. package/hub/team/native-supervisor.mjs +90 -26
  67. package/hub/team/native.mjs +120 -42
  68. package/hub/team/nativeProxy.mjs +184 -113
  69. package/hub/team/notify.mjs +147 -78
  70. package/hub/team/orchestrator.mjs +173 -161
  71. package/hub/team/pane.mjs +43 -20
  72. package/hub/team/process-cleanup.mjs +69 -25
  73. package/hub/team/psmux.mjs +1022 -281
  74. package/hub/team/recovery-store.mjs +149 -0
  75. package/hub/team/remote-probe.mjs +68 -52
  76. package/hub/team/remote-session.mjs +131 -59
  77. package/hub/team/remote-watcher.mjs +61 -33
  78. package/hub/team/retry-state-machine.mjs +336 -0
  79. package/hub/team/routing.mjs +51 -25
  80. package/hub/team/runtime-strategy.mjs +200 -0
  81. package/hub/team/schemas/worker-completion.json +39 -0
  82. package/hub/team/sentinel-capture.mjs +127 -0
  83. package/hub/team/session-sync.mjs +153 -6
  84. package/hub/team/session.mjs +574 -611
  85. package/hub/team/shared.mjs +13 -13
  86. package/hub/team/staleState.mjs +72 -30
  87. package/hub/team/swarm-cli.mjs +390 -0
  88. package/hub/team/swarm-hypervisor.mjs +1544 -193
  89. package/hub/team/swarm-intent.mjs +216 -0
  90. package/hub/team/swarm-locks.mjs +140 -25
  91. package/hub/team/swarm-planner.mjs +121 -21
  92. package/hub/team/swarm-preflight.mjs +319 -0
  93. package/hub/team/swarm-reconciler.mjs +128 -23
  94. package/hub/team/synapse-cli.mjs +458 -0
  95. package/hub/team/synapse-http.mjs +60 -0
  96. package/hub/team/synapse-registry.mjs +329 -0
  97. package/hub/team/terminal-opener.mjs +216 -0
  98. package/hub/team/tui-core.mjs +292 -0
  99. package/hub/team/tui-lite.mjs +237 -148
  100. package/hub/team/tui-remote-adapter.mjs +18 -34
  101. package/hub/team/tui-synapse.mjs +223 -0
  102. package/hub/team/tui-viewer.mjs +108 -48
  103. package/hub/team/tui-widgets.mjs +262 -0
  104. package/hub/team/tui.mjs +931 -439
  105. package/hub/team/worker-completion-validator.mjs +84 -0
  106. package/hub/team/worker-sandbox.mjs +97 -0
  107. package/hub/team/worker-signal.mjs +263 -0
  108. package/hub/team/worker-signal.types.d.ts +52 -0
  109. package/hub/team/worktree-lifecycle.mjs +517 -63
  110. package/hub/team/wt-manager.mjs +297 -26
  111. package/hub/tools.mjs +477 -252
  112. package/hub/tray.mjs +411 -368
  113. package/hub/workers/antigravity-route-worker.mjs +169 -0
  114. package/hub/workers/claude-worker.mjs +162 -118
  115. package/hub/workers/codex-app-server-worker.mjs +1015 -0
  116. package/hub/workers/codex-mcp.mjs +724 -507
  117. package/hub/workers/delegator-mcp.mjs +570 -499
  118. package/hub/workers/factory.mjs +103 -21
  119. package/hub/workers/gemini-worker.mjs +126 -284
  120. package/hub/workers/interface.mjs +24 -2
  121. package/hub/workers/lib/jsonrpc-stdio.mjs +463 -0
  122. package/hub/workers/worker-utils.mjs +23 -17
  123. package/package.json +4 -3
  124. package/scripts/lib/agent-json.mjs +27 -0
  125. package/scripts/lib/async.mjs +174 -0
  126. package/scripts/lib/claudemd-scanner.mjs +223 -0
  127. package/scripts/lib/cli-agy.mjs +62 -0
  128. package/scripts/lib/cli-claude.mjs +78 -0
  129. package/scripts/lib/cli-codex.mjs +199 -0
  130. package/scripts/lib/cli-gemini.mjs +37 -0
  131. package/scripts/lib/codex-recovery.sh +50 -0
  132. package/scripts/lib/context.mjs +67 -0
  133. package/scripts/lib/cross-review-utils.mjs +59 -0
  134. package/scripts/lib/doctor-env-checks.mjs +121 -0
  135. package/scripts/lib/dynamic-route-cli.mjs +107 -0
  136. package/scripts/lib/env-probe.mjs +269 -0
  137. package/scripts/lib/env.mjs +14 -0
  138. package/scripts/lib/gemini-profiles.mjs +119 -0
  139. package/scripts/lib/handoff.mjs +187 -0
  140. package/scripts/lib/hook-utils.mjs +16 -0
  141. package/scripts/lib/hub.mjs +48 -0
  142. package/scripts/lib/keyword-rules.mjs +195 -0
  143. package/scripts/lib/logger.mjs +105 -0
  144. package/scripts/lib/mcp-filter.mjs +883 -0
  145. package/scripts/lib/mcp-gateway-health-check.mjs +159 -0
  146. package/scripts/lib/mcp-gateway-servers.mjs +70 -0
  147. package/scripts/lib/mcp-gateway-wrapper-check.mjs +35 -0
  148. package/scripts/lib/mcp-guard-engine.mjs +2120 -0
  149. package/scripts/lib/mcp-health.mjs +773 -0
  150. package/scripts/lib/mcp-manifest.mjs +89 -0
  151. package/scripts/lib/mcp-server-catalog.mjs +355 -0
  152. package/scripts/lib/pid.mjs +63 -0
  153. package/scripts/lib/process-utils.mjs +26 -0
  154. package/scripts/lib/psmux-info.mjs +224 -0
  155. package/scripts/lib/quota.mjs +47 -0
  156. package/scripts/lib/remote-spawn-transfer.mjs +226 -0
  157. package/scripts/lib/skill-state.mjs +220 -0
  158. package/scripts/lib/skill-template.mjs +375 -0
  159. package/scripts/lib/team.mjs +75 -0
  160. package/scripts/lib/timeout.mjs +66 -0
  161. package/scripts/lib/tmp.mjs +26 -0
  162. package/scripts/lib/toml.mjs +113 -0
@@ -0,0 +1,149 @@
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 (git-preflight flags stash-pop/worktree-remove as risky).
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
+ });
@@ -7,8 +7,8 @@
7
7
  // L1.5: INPUT_WAIT 패턴 감지 (detectInputWait 재사용)
8
8
  // L3: 완료 토큰 감지 (__TRIFLUX_DONE__ 또는 프롬프트 idle)
9
9
 
10
- import { execFileSync } from 'node:child_process';
11
- import { detectInputWait, PROBE_DEFAULTS } from './health-probe.mjs';
10
+ import { execFileSync } from "node:child_process";
11
+ import { detectInputWait, PROBE_DEFAULTS } from "./health-probe.mjs";
12
12
 
13
13
  /** 완료 토큰 패턴 */
14
14
  const COMPLETION_TOKEN_RE = /__TRIFLUX_DONE__/;
@@ -27,19 +27,25 @@ const _PROMPT_IDLE_RE = /(\u276f|\u2795|>\s*$)/;
27
27
  export function sshCapturePane(host, paneTarget, lines = 20, deps = {}) {
28
28
  const execFn = deps.execFileSync || execFileSync;
29
29
  try {
30
- const output = execFn('ssh', [
31
- '-o', 'ConnectTimeout=5',
32
- '-o', 'BatchMode=yes',
33
- host,
34
- `psmux capture-pane -t ${paneTarget} -p -S -`,
35
- ], {
36
- encoding: 'utf8',
37
- timeout: 10_000,
38
- windowsHide: true,
39
- stdio: ['ignore', 'pipe', 'ignore'],
40
- });
41
- const nonEmpty = output.split('\n').filter((line) => line.trim() !== '');
42
- return nonEmpty.slice(-lines).join('\n');
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");
43
49
  } catch {
44
50
  return null;
45
51
  }
@@ -55,16 +61,22 @@ export function sshCapturePane(host, paneTarget, lines = 20, deps = {}) {
55
61
  export function sshSessionExists(host, sessionName, deps = {}) {
56
62
  const execFn = deps.execFileSync || execFileSync;
57
63
  try {
58
- execFn('ssh', [
59
- '-o', 'ConnectTimeout=5',
60
- '-o', 'BatchMode=yes',
61
- host,
62
- `psmux has-session -t ${sessionName}`,
63
- ], {
64
- timeout: 10_000,
65
- windowsHide: true,
66
- stdio: ['ignore', 'ignore', 'ignore'],
67
- });
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
+ );
68
80
  return true;
69
81
  } catch {
70
82
  return false;
@@ -92,7 +104,7 @@ export function createRemoteProbe(session, opts = {}) {
92
104
  let started = false;
93
105
 
94
106
  // L1 tracking — 출력 변화 감지
95
- let lastCaptureHash = '';
107
+ let lastCaptureHash = "";
96
108
  let lastOutputChangeAt = Date.now();
97
109
 
98
110
  // L3 tracking — 완료 토큰 / prompt idle
@@ -102,7 +114,7 @@ export function createRemoteProbe(session, opts = {}) {
102
114
  const status = {
103
115
  l0: null,
104
116
  l1: null,
105
- l2: 'skip', // 원격은 MCP L2 미지원
117
+ l2: "skip", // 원격은 MCP L2 미지원
106
118
  l3: null,
107
119
  lastProbeAt: null,
108
120
  inputWaitPattern: null,
@@ -113,7 +125,7 @@ export function createRemoteProbe(session, opts = {}) {
113
125
  */
114
126
  function probeL0() {
115
127
  const exists = sshSessionExists(session.host, session.sessionName, deps);
116
- status.l0 = exists ? 'ok' : 'fail';
128
+ status.l0 = exists ? "ok" : "fail";
117
129
  return status.l0;
118
130
  }
119
131
 
@@ -135,9 +147,9 @@ export function createRemoteProbe(session, opts = {}) {
135
147
  if (hash !== lastCaptureHash) {
136
148
  lastCaptureHash = hash;
137
149
  lastOutputChangeAt = now;
138
- status.l1 = 'ok';
150
+ status.l1 = "ok";
139
151
  status.inputWaitPattern = null;
140
- return 'ok';
152
+ return "ok";
141
153
  }
142
154
 
143
155
  const silenceMs = now - lastOutputChangeAt;
@@ -147,18 +159,18 @@ export function createRemoteProbe(session, opts = {}) {
147
159
  const inputWait = detectInputWait(captured);
148
160
 
149
161
  if (inputWait.detected) {
150
- status.l1 = 'input_wait';
162
+ status.l1 = "input_wait";
151
163
  status.inputWaitPattern = inputWait.pattern;
152
- return 'input_wait';
164
+ return "input_wait";
153
165
  }
154
166
 
155
- status.l1 = 'stall';
167
+ status.l1 = "stall";
156
168
  status.inputWaitPattern = null;
157
- return 'stall';
169
+ return "stall";
158
170
  }
159
171
 
160
- status.l1 = 'ok';
161
- return 'ok';
172
+ status.l1 = "ok";
173
+ return "ok";
162
174
  }
163
175
 
164
176
  /**
@@ -167,28 +179,28 @@ export function createRemoteProbe(session, opts = {}) {
167
179
  */
168
180
  function probeL3(captured) {
169
181
  if (promptAcked) {
170
- status.l3 = 'ok';
171
- return 'ok';
182
+ status.l3 = "ok";
183
+ return "ok";
172
184
  }
173
185
 
174
186
  if (captured != null && captured.length > 0) {
175
187
  // 완료 토큰 감지 → 즉시 ok
176
188
  if (COMPLETION_TOKEN_RE.test(captured)) {
177
189
  promptAcked = true;
178
- status.l3 = 'completed';
179
- return 'completed';
190
+ status.l3 = "completed";
191
+ return "completed";
180
192
  }
181
193
 
182
194
  // 출력이 있으면 prompt acknowledged
183
195
  promptAcked = true;
184
- status.l3 = 'ok';
185
- return 'ok';
196
+ status.l3 = "ok";
197
+ return "ok";
186
198
  }
187
199
 
188
200
  const elapsed = Date.now() - spawnedAt;
189
201
  if (elapsed >= config.l3ThresholdMs) {
190
- status.l3 = 'timeout';
191
- return 'timeout';
202
+ status.l3 = "timeout";
203
+ return "timeout";
192
204
  }
193
205
 
194
206
  status.l3 = null;
@@ -204,7 +216,7 @@ export function createRemoteProbe(session, opts = {}) {
204
216
 
205
217
  // L0 실패 시 나머지 probe 스킵
206
218
  let captured = null;
207
- if (l0 === 'ok') {
219
+ if (l0 === "ok") {
208
220
  captured = sshCapturePane(session.host, session.paneTarget, 20, deps);
209
221
  }
210
222
 
@@ -214,14 +226,14 @@ export function createRemoteProbe(session, opts = {}) {
214
226
  const result = {
215
227
  l0,
216
228
  l1,
217
- l2: 'skip',
229
+ l2: "skip",
218
230
  l3,
219
231
  inputWaitPattern: status.inputWaitPattern,
220
232
  ts: Date.now(),
221
233
  };
222
234
  status.lastProbeAt = result.ts;
223
235
 
224
- if (typeof config.onProbe === 'function') {
236
+ if (typeof config.onProbe === "function") {
225
237
  config.onProbe(result);
226
238
  }
227
239
 
@@ -233,10 +245,12 @@ export function createRemoteProbe(session, opts = {}) {
233
245
  started = true;
234
246
  spawnedAt = Date.now();
235
247
  lastOutputChangeAt = Date.now();
236
- lastCaptureHash = '';
248
+ lastCaptureHash = "";
237
249
  promptAcked = false;
238
250
 
239
- timer = setInterval(() => { void probe(); }, config.intervalMs);
251
+ timer = setInterval(() => {
252
+ void probe();
253
+ }, config.intervalMs);
240
254
  timer.unref?.();
241
255
 
242
256
  // 즉시 첫 probe 실행
@@ -254,13 +268,13 @@ export function createRemoteProbe(session, opts = {}) {
254
268
 
255
269
  /** tracking 리셋 (restart 후 호출) */
256
270
  function resetTracking() {
257
- lastCaptureHash = '';
271
+ lastCaptureHash = "";
258
272
  lastOutputChangeAt = Date.now();
259
273
  promptAcked = false;
260
274
  spawnedAt = Date.now();
261
275
  status.l0 = null;
262
276
  status.l1 = null;
263
- status.l2 = 'skip';
277
+ status.l2 = "skip";
264
278
  status.l3 = null;
265
279
  status.inputWaitPattern = null;
266
280
  }
@@ -271,6 +285,8 @@ export function createRemoteProbe(session, opts = {}) {
271
285
  probe,
272
286
  resetTracking,
273
287
  getStatus: () => ({ ...status }),
274
- get started() { return started; },
288
+ get started() {
289
+ return started;
290
+ },
275
291
  });
276
292
  }