@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.
Files changed (155) hide show
  1. package/hub/pipe.mjs +239 -121
  2. package/hub/public/dashboard.html +68 -7
  3. package/hub/server.mjs +2181 -701
  4. package/hub/store-adapter.mjs +101 -37
  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 +84 -17
  9. package/hub/team/build-worker-prompt.mjs +42 -0
  10. package/hub/team/check-mcp-hub.mjs +49 -0
  11. package/hub/team/claude-daemon-control.mjs +219 -0
  12. package/hub/team/claude-native-bridge.mjs +784 -0
  13. package/hub/team/claude-session-projection.mjs +65 -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 +154 -33
  23. package/hub/team/cli/commands/start/parse-args.mjs +89 -12
  24. package/hub/team/cli/commands/start/start-headless.mjs +127 -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/index.mjs +2 -2
  33. package/hub/team/cli/manifest.mjs +38 -8
  34. package/hub/team/cli/render.mjs +52 -30
  35. package/hub/team/cli/services/attach-fallback.mjs +74 -54
  36. package/hub/team/cli/services/hub-client.mjs +45 -17
  37. package/hub/team/cli/services/member-selector.mjs +37 -30
  38. package/hub/team/cli/services/native-control.mjs +144 -116
  39. package/hub/team/cli/services/runtime-mode.mjs +55 -1
  40. package/hub/team/cli/services/state-store.mjs +66 -9
  41. package/hub/team/cli/services/task-model.mjs +40 -30
  42. package/hub/team/codex-review.mjs +521 -0
  43. package/hub/team/conductor-mesh-bridge.mjs +25 -24
  44. package/hub/team/conductor-registry.mjs +121 -0
  45. package/hub/team/conductor.mjs +1379 -671
  46. package/hub/team/consensus-meta.mjs +176 -0
  47. package/hub/team/dashboard-anchor.mjs +4 -5
  48. package/hub/team/dashboard-layout.mjs +3 -1
  49. package/hub/team/dashboard-open.mjs +21 -119
  50. package/hub/team/dashboard.mjs +76 -28
  51. package/hub/team/event-log.mjs +18 -10
  52. package/hub/team/execution-mode.mjs +241 -0
  53. package/hub/team/extract-completion-payload.mjs +110 -0
  54. package/hub/team/git-preflight.mjs +286 -0
  55. package/hub/team/handoff.mjs +49 -18
  56. package/hub/team/headless-bridge-session.mjs +17 -0
  57. package/hub/team/headless.mjs +2065 -1156
  58. package/hub/team/health-probe.mjs +232 -69
  59. package/hub/team/launcher-template.mjs +23 -13
  60. package/hub/team/lead-control.mjs +91 -4
  61. package/hub/team/mcp-selector.mjs +145 -0
  62. package/hub/team/native-supervisor.mjs +90 -26
  63. package/hub/team/native.mjs +74 -35
  64. package/hub/team/nativeProxy.mjs +184 -113
  65. package/hub/team/notify.mjs +147 -78
  66. package/hub/team/orchestrator.mjs +173 -161
  67. package/hub/team/pane.mjs +43 -15
  68. package/hub/team/process-cleanup.mjs +69 -25
  69. package/hub/team/psmux.mjs +1067 -264
  70. package/hub/team/recovery-store.mjs +149 -0
  71. package/hub/team/remote-probe.mjs +69 -53
  72. package/hub/team/remote-session.mjs +131 -59
  73. package/hub/team/remote-watcher.mjs +62 -34
  74. package/hub/team/retry-state-machine.mjs +267 -0
  75. package/hub/team/routing.mjs +51 -25
  76. package/hub/team/runtime-strategy.mjs +200 -0
  77. package/hub/team/schemas/worker-completion.json +39 -0
  78. package/hub/team/sentinel-capture.mjs +127 -0
  79. package/hub/team/session-sync.mjs +153 -6
  80. package/hub/team/session.mjs +605 -611
  81. package/hub/team/shared.mjs +13 -13
  82. package/hub/team/staleState.mjs +72 -30
  83. package/hub/team/swarm-cli.mjs +390 -0
  84. package/hub/team/swarm-hypervisor.mjs +1526 -198
  85. package/hub/team/swarm-intent.mjs +216 -0
  86. package/hub/team/swarm-locks.mjs +140 -25
  87. package/hub/team/swarm-planner.mjs +121 -21
  88. package/hub/team/swarm-preflight.mjs +317 -0
  89. package/hub/team/swarm-reconciler.mjs +128 -23
  90. package/hub/team/synapse-cli.mjs +458 -0
  91. package/hub/team/synapse-http.mjs +60 -0
  92. package/hub/team/synapse-registry.mjs +329 -0
  93. package/hub/team/terminal-opener.mjs +216 -0
  94. package/hub/team/tui-core.mjs +288 -0
  95. package/hub/team/tui-lite.mjs +234 -146
  96. package/hub/team/tui-remote-adapter.mjs +21 -37
  97. package/hub/team/tui-synapse.mjs +223 -0
  98. package/hub/team/tui-viewer.mjs +104 -45
  99. package/hub/team/tui-widgets.mjs +262 -0
  100. package/hub/team/tui.mjs +936 -449
  101. package/hub/team/worker-completion-validator.mjs +84 -0
  102. package/hub/team/worker-sandbox.mjs +93 -0
  103. package/hub/team/worker-signal.mjs +263 -0
  104. package/hub/team/worker-signal.types.d.ts +52 -0
  105. package/hub/team/worktree-lifecycle.mjs +518 -64
  106. package/hub/team/wt-manager.mjs +297 -26
  107. package/hub/tools.mjs +474 -252
  108. package/hub/tray.mjs +411 -368
  109. package/hub/workers/claude-worker.mjs +162 -118
  110. package/hub/workers/codex-app-server-worker.mjs +1015 -0
  111. package/hub/workers/codex-mcp.mjs +724 -507
  112. package/hub/workers/delegator-mcp.mjs +663 -337
  113. package/hub/workers/factory.mjs +100 -21
  114. package/hub/workers/gemini-worker.mjs +179 -86
  115. package/hub/workers/interface.mjs +23 -2
  116. package/hub/workers/lib/jsonrpc-stdio.mjs +463 -0
  117. package/hub/workers/worker-utils.mjs +22 -16
  118. package/package.json +4 -3
  119. package/scripts/lib/agent-json.mjs +27 -0
  120. package/scripts/lib/async.mjs +174 -0
  121. package/scripts/lib/claudemd-scanner.mjs +223 -0
  122. package/scripts/lib/cli-agy.mjs +62 -0
  123. package/scripts/lib/cli-claude.mjs +78 -0
  124. package/scripts/lib/cli-codex.mjs +199 -0
  125. package/scripts/lib/cli-gemini.mjs +67 -0
  126. package/scripts/lib/codex-recovery.sh +50 -0
  127. package/scripts/lib/context.mjs +67 -0
  128. package/scripts/lib/cross-review-utils.mjs +59 -0
  129. package/scripts/lib/doctor-env-checks.mjs +121 -0
  130. package/scripts/lib/dynamic-route-cli.mjs +107 -0
  131. package/scripts/lib/env-probe.mjs +269 -0
  132. package/scripts/lib/env.mjs +14 -0
  133. package/scripts/lib/gemini-profiles.mjs +119 -0
  134. package/scripts/lib/handoff.mjs +187 -0
  135. package/scripts/lib/hook-utils.mjs +16 -0
  136. package/scripts/lib/hub.mjs +48 -0
  137. package/scripts/lib/keyword-rules.mjs +195 -0
  138. package/scripts/lib/logger.mjs +105 -0
  139. package/scripts/lib/mcp-filter.mjs +883 -0
  140. package/scripts/lib/mcp-gateway-servers.mjs +70 -0
  141. package/scripts/lib/mcp-guard-engine.mjs +2120 -0
  142. package/scripts/lib/mcp-health.mjs +773 -0
  143. package/scripts/lib/mcp-manifest.mjs +89 -0
  144. package/scripts/lib/mcp-server-catalog.mjs +355 -0
  145. package/scripts/lib/pid.mjs +63 -0
  146. package/scripts/lib/process-utils.mjs +26 -0
  147. package/scripts/lib/psmux-info.mjs +179 -0
  148. package/scripts/lib/quota.mjs +47 -0
  149. package/scripts/lib/remote-spawn-transfer.mjs +226 -0
  150. package/scripts/lib/skill-state.mjs +220 -0
  151. package/scripts/lib/skill-template.mjs +375 -0
  152. package/scripts/lib/team.mjs +75 -0
  153. package/scripts/lib/timeout.mjs +66 -0
  154. package/scripts/lib/tmp.mjs +26 -0
  155. package/scripts/lib/toml.mjs +113 -0
@@ -0,0 +1,463 @@
1
+ // hub/workers/lib/jsonrpc-stdio.mjs
2
+ // Minimal line-delimited JSON-RPC 2.0 client over stdio.
3
+ // Replaces vscode-jsonrpc for the codex app-server transport.
4
+ //
5
+ // Wire format (Issue #95 P1 #1): OpenAI App Server JSONL variant omits the
6
+ // top-level `"jsonrpc": "2.0"` header on outbound frames. Inbound decode is
7
+ // lenient — frames with or without the header are accepted for forward compat.
8
+ //
9
+ // Contract:
10
+ // new JsonRpcStdioClient({ stdin, stdout, onError, maxLineSize })
11
+ // request(method, params, timeoutMs=60000) -> Promise<result>
12
+ // notify(method, params) -> void
13
+ // onNotification(method, cb) -> unsubscribe() ('*' = catch-all)
14
+ // close(reason) -> void (idempotent; optional reason marks closing state)
15
+ // isOpen() -> boolean
16
+ //
17
+ // Lifecycle (Issue #95 P1 #3):
18
+ // State machine: running | closing | closed
19
+ // - `running` is the default. Parse/EOF/max-line errors reject in-flight
20
+ // requests and transition to `closed`.
21
+ // - `closing` is entered via `close("closing")` before a graceful shutdown;
22
+ // EOF in `closing` is a normal termination and does NOT fail-fast.
23
+ //
24
+ // AC18: any single line whose raw-byte length would exceed maxLineSize is
25
+ // rejected at the stream layer (before readline emits it) to defend against
26
+ // OOM/DoS from an unresponsive or malicious peer.
27
+
28
+ import { createInterface } from "node:readline";
29
+
30
+ const DEFAULT_MAX_LINE_SIZE = 1024 * 1024; // 1 MiB
31
+ const CLOSED_MESSAGE = "JsonRpcStdioClient closed";
32
+
33
+ /**
34
+ * Thrown (via onError) when an inbound line would exceed maxLineSize.
35
+ */
36
+ export class MaxLineSizeExceededError extends Error {
37
+ /**
38
+ * @param {number} size Bytes observed so far in the offending line.
39
+ * @param {number} max Configured max.
40
+ */
41
+ constructor(size, max) {
42
+ super(`JSON-RPC line exceeded max size: ${size} > ${max}`);
43
+ this.name = "MaxLineSizeExceededError";
44
+ this.size = size;
45
+ this.max = max;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Thrown (and used to reject in-flight execute()) when the peer emits a
51
+ * malformed frame or the transport layer hits a structural error.
52
+ */
53
+ export class JsonRpcProtocolError extends Error {
54
+ /** @param {string} message @param {{ cause?: unknown }} [options] */
55
+ constructor(message, options = {}) {
56
+ super(message, { cause: options.cause });
57
+ this.name = "JsonRpcProtocolError";
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Thrown when the underlying stream closes unexpectedly (EOF outside `closing`).
63
+ */
64
+ export class JsonRpcTransportError extends Error {
65
+ /** @param {string} message @param {{ cause?: unknown }} [options] */
66
+ constructor(message, options = {}) {
67
+ super(message, { cause: options.cause });
68
+ this.name = "JsonRpcTransportError";
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Line-delimited JSON-RPC 2.0 client over a pair of Node streams.
74
+ */
75
+ export class JsonRpcStdioClient {
76
+ /**
77
+ * @param {object} options
78
+ * @param {NodeJS.ReadableStream} options.stdin Server -> client bytes.
79
+ * @param {NodeJS.WritableStream} options.stdout Client -> server bytes.
80
+ * @param {(err: Error) => void} [options.onError] Protocol error sink.
81
+ * @param {number} [options.maxLineSize] Max bytes per inbound line.
82
+ */
83
+ constructor({ stdin, stdout, onError, maxLineSize = DEFAULT_MAX_LINE_SIZE }) {
84
+ if (!stdin || typeof stdin.on !== "function") {
85
+ throw new TypeError("JsonRpcStdioClient requires a readable stdin");
86
+ }
87
+ if (!stdout || typeof stdout.write !== "function") {
88
+ throw new TypeError("JsonRpcStdioClient requires a writable stdout");
89
+ }
90
+
91
+ this._stdin = stdin;
92
+ this._stdout = stdout;
93
+ this._onError = typeof onError === "function" ? onError : null;
94
+ this._maxLineSize =
95
+ Number.isFinite(maxLineSize) && maxLineSize > 0
96
+ ? maxLineSize
97
+ : DEFAULT_MAX_LINE_SIZE;
98
+
99
+ /** @type {'running'|'closing'|'closed'} */
100
+ this._state = "running";
101
+ this._nextRequestId = 1;
102
+ /** @type {Map<number, { resolve: Function, reject: Function, timer: any, method: string }>} */
103
+ this._pendingRequests = new Map();
104
+ /** @type {Map<string, Set<Function>>} */
105
+ this._notificationHandlers = new Map();
106
+
107
+ // AC18: track bytes since last newline at the raw stream layer so an
108
+ // oversized line is rejected *before* readline concatenates it internally.
109
+ this._pendingLineSize = 0;
110
+ this._oversized = false;
111
+
112
+ this._onStdinData = (chunk) => this._trackRawBytes(chunk);
113
+ this._stdin.on("data", this._onStdinData);
114
+
115
+ // Without these handlers, an EPIPE/ERR_STREAM_DESTROYED on either pipe
116
+ // would bubble up as an unhandled 'error' and take down the hub process.
117
+ this._onStdinError = (err) => this._handleStreamError("stdin", err);
118
+ this._onStdoutError = (err) => this._handleStreamError("stdout", err);
119
+ if (typeof this._stdin.on === "function") {
120
+ this._stdin.on("error", this._onStdinError);
121
+ }
122
+ if (typeof this._stdout.on === "function") {
123
+ this._stdout.on("error", this._onStdoutError);
124
+ }
125
+
126
+ this._rl = createInterface({ input: this._stdin, crlfDelay: Infinity });
127
+ this._rl.on("line", (line) => this._handleLine(line));
128
+ // readline re-emits the input stream 'error' on itself; the raw stdin
129
+ // handler above already converts it into a JsonRpcTransportError, so
130
+ // suppress the re-emit to avoid an unhandled 'error' on the Interface.
131
+ this._rl.on("error", () => {});
132
+ this._rl.on("close", () => {
133
+ // P1 #3 fail-fast: EOF during `running` is a transport error. Pending
134
+ // requests are rejected with JsonRpcTransportError. EOF during `closing`
135
+ // is a normal shutdown — pending requests are rejected with the generic
136
+ // CLOSED_MESSAGE via close().
137
+ if (this._state === "running") {
138
+ const err = new JsonRpcTransportError(
139
+ "JSON-RPC stream closed unexpectedly (EOF during running state)",
140
+ );
141
+ this._emitError(err);
142
+ this._closeWith("closed", err);
143
+ } else if (this._state !== "closed") {
144
+ this._closeWith("closed");
145
+ }
146
+ });
147
+ }
148
+
149
+ /**
150
+ * Issue a JSON-RPC request and resolve with the server's `result`.
151
+ * Rejects on error response, timeout, malformed payload, or close().
152
+ * @param {string} method
153
+ * @param {unknown} params
154
+ * @param {number} [timeoutMs=60000]
155
+ * @returns {Promise<any>}
156
+ */
157
+ request(method, params, timeoutMs = 60000) {
158
+ if (this._state !== "running") {
159
+ return Promise.reject(new Error(CLOSED_MESSAGE));
160
+ }
161
+
162
+ const id = this._nextRequestId++;
163
+ // P1 #1 wire framing: omit `jsonrpc: "2.0"` on outbound. Peer decode remains
164
+ // lenient (OpenAI App Server JSONL variant spec).
165
+ const frame = { id, method };
166
+ if (params !== undefined) frame.params = params;
167
+
168
+ return new Promise((resolve, reject) => {
169
+ let timer = null;
170
+ if (Number.isFinite(timeoutMs) && timeoutMs > 0) {
171
+ timer = setTimeout(() => {
172
+ const pending = this._pendingRequests.get(id);
173
+ if (!pending) return;
174
+ this._pendingRequests.delete(id);
175
+ reject(
176
+ new Error(
177
+ `JSON-RPC request timed out after ${timeoutMs}ms: ${method}`,
178
+ ),
179
+ );
180
+ }, timeoutMs);
181
+ }
182
+
183
+ this._pendingRequests.set(id, { resolve, reject, timer, method });
184
+
185
+ try {
186
+ this._writeFrame(frame);
187
+ } catch (err) {
188
+ this._pendingRequests.delete(id);
189
+ if (timer) clearTimeout(timer);
190
+ reject(err);
191
+ }
192
+ });
193
+ }
194
+
195
+ /**
196
+ * Send a JSON-RPC notification (no id, no response expected).
197
+ * Silently drops if the client is not in `running`.
198
+ * @param {string} method
199
+ * @param {unknown} [params]
200
+ */
201
+ notify(method, params) {
202
+ if (this._state !== "running") return;
203
+ // P1 #1 wire framing: omit jsonrpc header (outbound).
204
+ const frame = { method };
205
+ if (params !== undefined) frame.params = params;
206
+ try {
207
+ this._writeFrame(frame);
208
+ } catch (err) {
209
+ this._emitError(err);
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Subscribe to inbound notifications. Use `"*"` for a catch-all handler
215
+ * which receives `(params, method)`. Targeted handlers receive `(params)`.
216
+ * @param {string} method
217
+ * @param {(params: any, method?: string) => void} callback
218
+ * @returns {() => void} unsubscribe
219
+ */
220
+ onNotification(method, callback) {
221
+ if (typeof callback !== "function") {
222
+ throw new TypeError("onNotification requires a callback function");
223
+ }
224
+ let set = this._notificationHandlers.get(method);
225
+ if (!set) {
226
+ set = new Set();
227
+ this._notificationHandlers.set(method, set);
228
+ }
229
+ set.add(callback);
230
+ return () => {
231
+ const handlers = this._notificationHandlers.get(method);
232
+ if (!handlers) return;
233
+ handlers.delete(callback);
234
+ if (handlers.size === 0) this._notificationHandlers.delete(method);
235
+ };
236
+ }
237
+
238
+ /**
239
+ * Close the client: reject all pending requests, stop tracking input,
240
+ * and release the readline interface. Idempotent.
241
+ *
242
+ * Optional `reason` = `"closing"` transitions to the intermediate `closing`
243
+ * state *without* terminating the readline loop, so a graceful shutdown can
244
+ * issue a final request (e.g. `thread/unsubscribe`) before EOF triggers
245
+ * full closure. Any subsequent EOF in `closing` is treated as normal.
246
+ *
247
+ * @param {string} [reason]
248
+ */
249
+ close(reason) {
250
+ if (this._state === "closed") return;
251
+
252
+ if (reason === "closing" && this._state === "running") {
253
+ this._state = "closing";
254
+ return;
255
+ }
256
+ this._closeWith("closed");
257
+ }
258
+
259
+ /**
260
+ * @returns {boolean} True if the client accepts new requests.
261
+ */
262
+ isOpen() {
263
+ return this._state === "running";
264
+ }
265
+
266
+ /**
267
+ * @returns {'running'|'closing'|'closed'}
268
+ */
269
+ getState() {
270
+ return this._state;
271
+ }
272
+
273
+ // --- internals ---------------------------------------------------------
274
+
275
+ _closeWith(target, rejectReason = null) {
276
+ if (this._state === "closed") return;
277
+ this._state = target;
278
+
279
+ const rejectErr =
280
+ rejectReason instanceof Error ? rejectReason : new Error(CLOSED_MESSAGE);
281
+
282
+ for (const [, pending] of this._pendingRequests) {
283
+ if (pending.timer) clearTimeout(pending.timer);
284
+ pending.reject(rejectErr);
285
+ }
286
+ this._pendingRequests.clear();
287
+
288
+ try {
289
+ this._stdin.off?.("data", this._onStdinData);
290
+ } catch {
291
+ /* ignore */
292
+ }
293
+ try {
294
+ this._rl.close();
295
+ } catch {
296
+ /* ignore */
297
+ }
298
+ }
299
+
300
+ _writeFrame(frame) {
301
+ if (this._state === "closed") return;
302
+ const line = `${JSON.stringify(frame)}\n`;
303
+ try {
304
+ this._stdout.write(line, (err) => {
305
+ if (err) this._handleStreamError("stdout-write", err);
306
+ });
307
+ } catch (err) {
308
+ this._handleStreamError("stdout-write", err);
309
+ }
310
+ }
311
+
312
+ /**
313
+ * Convert a raw stream error into a JsonRpcTransportError, emit it to the
314
+ * error sink, and close the client so pending requests are rejected.
315
+ * Idempotent: repeated errors after close are swallowed.
316
+ * @param {string} which identifier for the originating pipe/operation
317
+ * @param {unknown} err raw error from the stream
318
+ */
319
+ _handleStreamError(which, err) {
320
+ if (this._state === "closed") return;
321
+ const base = err instanceof Error ? err : new Error(String(err));
322
+ const wrapped = new JsonRpcTransportError(
323
+ `JSON-RPC stream error on ${which}: ${base.message}`,
324
+ );
325
+ wrapped.cause = base;
326
+ this._emitError(wrapped);
327
+ this._closeWith("closed", wrapped);
328
+ }
329
+
330
+ _trackRawBytes(chunk) {
331
+ if (this._oversized || this._state === "closed") return;
332
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
333
+ for (let i = 0; i < buf.length; i++) {
334
+ if (buf[i] === 0x0a /* \n */) {
335
+ this._pendingLineSize = 0;
336
+ continue;
337
+ }
338
+ this._pendingLineSize += 1;
339
+ if (this._pendingLineSize > this._maxLineSize) {
340
+ this._oversized = true;
341
+ const err = new MaxLineSizeExceededError(
342
+ this._pendingLineSize,
343
+ this._maxLineSize,
344
+ );
345
+ this._emitError(err);
346
+ // P1 #3 fail-fast: oversized line → reject pending with the actual error
347
+ this._closeWith("closed", err);
348
+ return;
349
+ }
350
+ }
351
+ }
352
+
353
+ _handleLine(line) {
354
+ if (this._state === "closed") return;
355
+ if (line.length === 0) return;
356
+
357
+ let frame;
358
+ try {
359
+ frame = JSON.parse(line);
360
+ } catch (err) {
361
+ const pErr = new JsonRpcProtocolError(
362
+ `JSON-RPC parse error: ${err instanceof Error ? err.message : String(err)}`,
363
+ { cause: err },
364
+ );
365
+ this._emitError(pErr);
366
+ // P1 #3 fail-fast: malformed frame during running → reject in-flight + close
367
+ if (this._state === "running") this._closeWith("closed", pErr);
368
+ return;
369
+ }
370
+
371
+ if (!frame || typeof frame !== "object") {
372
+ const pErr = new JsonRpcProtocolError(
373
+ "JSON-RPC protocol error: frame is not an object",
374
+ );
375
+ this._emitError(pErr);
376
+ if (this._state === "running") this._closeWith("closed", pErr);
377
+ return;
378
+ }
379
+
380
+ // Response: has id + (result | error)
381
+ if (
382
+ Object.hasOwn(frame, "id") &&
383
+ frame.id !== null &&
384
+ (Object.hasOwn(frame, "result") || Object.hasOwn(frame, "error"))
385
+ ) {
386
+ this._dispatchResponse(frame);
387
+ return;
388
+ }
389
+
390
+ // Notification: method + no id (or id === null for responses we treated above)
391
+ if (typeof frame.method === "string" && !Object.hasOwn(frame, "id")) {
392
+ this._dispatchNotification(frame);
393
+ return;
394
+ }
395
+
396
+ // Unknown / malformed envelope — surface but keep loop alive during running.
397
+ // Fail-fast only on structural errors (JSON parse, EOF, max-line).
398
+ this._emitError(
399
+ new JsonRpcProtocolError(
400
+ "JSON-RPC protocol error: unrecognized frame shape",
401
+ ),
402
+ );
403
+ }
404
+
405
+ _dispatchResponse(frame) {
406
+ const pending = this._pendingRequests.get(frame.id);
407
+ if (!pending) {
408
+ // Stray response — drop silently (notify() path, or late after timeout).
409
+ return;
410
+ }
411
+ this._pendingRequests.delete(frame.id);
412
+ if (pending.timer) clearTimeout(pending.timer);
413
+
414
+ if (Object.hasOwn(frame, "error") && frame.error) {
415
+ const { code, message, data } = frame.error;
416
+ const err = new Error(
417
+ `JSON-RPC error${typeof code === "number" ? ` ${code}` : ""}: ${message || "unknown"}`,
418
+ );
419
+ if (code !== undefined) err.code = code;
420
+ if (data !== undefined) err.data = data;
421
+ pending.reject(err);
422
+ return;
423
+ }
424
+
425
+ pending.resolve(frame.result);
426
+ }
427
+
428
+ _dispatchNotification(frame) {
429
+ const method = frame.method;
430
+ const params = frame.params;
431
+
432
+ const targeted = this._notificationHandlers.get(method);
433
+ if (targeted && targeted.size > 0) {
434
+ for (const cb of targeted) {
435
+ try {
436
+ cb(params);
437
+ } catch (err) {
438
+ this._emitError(err instanceof Error ? err : new Error(String(err)));
439
+ }
440
+ }
441
+ }
442
+
443
+ const wildcard = this._notificationHandlers.get("*");
444
+ if (wildcard && wildcard.size > 0) {
445
+ for (const cb of wildcard) {
446
+ try {
447
+ cb(params, method);
448
+ } catch (err) {
449
+ this._emitError(err instanceof Error ? err : new Error(String(err)));
450
+ }
451
+ }
452
+ }
453
+ }
454
+
455
+ _emitError(err) {
456
+ if (!this._onError) return;
457
+ try {
458
+ this._onError(err);
459
+ } catch {
460
+ // Never throw out of the dispatch loop.
461
+ }
462
+ }
463
+ }
@@ -6,9 +6,7 @@ export const DEFAULT_KILL_GRACE_MS = 1000;
6
6
 
7
7
  export function toStringList(value) {
8
8
  if (!Array.isArray(value)) return [];
9
- return value
10
- .map((item) => String(item ?? '').trim())
11
- .filter(Boolean);
9
+ return value.map((item) => String(item ?? "").trim()).filter(Boolean);
12
10
  }
13
11
 
14
12
  export function safeJsonParse(line) {
@@ -27,8 +25,7 @@ export function createWorkerError(message, details = {}) {
27
25
 
28
26
  function sleep(delayMs) {
29
27
  return new Promise((resolve) => {
30
- const timer = setTimeout(resolve, Math.max(0, delayMs));
31
- timer.unref?.();
28
+ setTimeout(resolve, Math.max(0, delayMs));
32
29
  });
33
30
  }
34
31
 
@@ -51,8 +48,9 @@ export async function withRetry(fn, opts = {}) {
51
48
  throw error;
52
49
  }
53
50
 
54
- const delay = Math.min(baseDelayMs * 2 ** (attempt - 1), maxDelayMs)
55
- * (0.5 + Math.random() * 0.5);
51
+ const delay =
52
+ Math.min(baseDelayMs * 2 ** (attempt - 1), maxDelayMs) *
53
+ (0.5 + Math.random() * 0.5);
56
54
  await sleep(delay);
57
55
  }
58
56
  }
@@ -63,7 +61,7 @@ export async function withRetry(fn, opts = {}) {
63
61
  export function appendTextFragments(value, parts) {
64
62
  if (value == null) return;
65
63
 
66
- if (typeof value === 'string') {
64
+ if (typeof value === "string") {
67
65
  const trimmed = value.trim();
68
66
  if (trimmed) parts.push(trimmed);
69
67
  return;
@@ -74,11 +72,13 @@ export function appendTextFragments(value, parts) {
74
72
  return;
75
73
  }
76
74
 
77
- if (typeof value !== 'object') return;
75
+ if (typeof value !== "object") return;
78
76
 
79
- if (typeof value.text === 'string') appendTextFragments(value.text, parts);
80
- if (typeof value.response === 'string') appendTextFragments(value.response, parts);
81
- if (typeof value.result === 'string') appendTextFragments(value.result, parts);
77
+ if (typeof value.text === "string") appendTextFragments(value.text, parts);
78
+ if (typeof value.response === "string")
79
+ appendTextFragments(value.response, parts);
80
+ if (typeof value.result === "string")
81
+ appendTextFragments(value.result, parts);
82
82
  if (value.content != null) appendTextFragments(value.content, parts);
83
83
  if (value.message != null) appendTextFragments(value.message, parts);
84
84
  }
@@ -86,18 +86,24 @@ export function appendTextFragments(value, parts) {
86
86
  export function extractText(value) {
87
87
  const parts = [];
88
88
  appendTextFragments(value, parts);
89
- return parts.join('\n').trim();
89
+ return parts.join("\n").trim();
90
90
  }
91
91
 
92
92
  export function terminateChild(child, killGraceMs) {
93
93
  if (!child || child.exitCode !== null || child.killed) return;
94
94
 
95
- try { child.stdin.end(); } catch {}
96
- try { child.kill(); } catch {}
95
+ try {
96
+ child.stdin.end();
97
+ } catch {}
98
+ try {
99
+ child.kill();
100
+ } catch {}
97
101
 
98
102
  const timer = setTimeout(() => {
99
103
  if (child.exitCode === null) {
100
- try { child.kill('SIGKILL'); } catch {}
104
+ try {
105
+ child.kill("SIGKILL");
106
+ } catch {}
101
107
  }
102
108
  }, killGraceMs);
103
109
  timer.unref?.();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@triflux/remote",
3
- "version": "10.0.1",
3
+ "version": "10.25.1",
4
4
  "description": "triflux remote — team mode, psmux, MCP workers, SQLite store.",
5
5
  "type": "module",
6
6
  "main": "hub/index.mjs",
@@ -16,11 +16,12 @@
16
16
  },
17
17
  "dependencies": {
18
18
  "@modelcontextprotocol/sdk": "^1.27.1",
19
- "better-sqlite3": "^12.6.2",
19
+ "better-sqlite3": "^12.10.0",
20
20
  "systray2": "^2.1.4"
21
21
  },
22
22
  "files": [
23
- "hub"
23
+ "hub",
24
+ "scripts/lib"
24
25
  ],
25
26
  "keywords": [
26
27
  "triflux",
@@ -0,0 +1,27 @@
1
+ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ export function agentJsonPath({ tmpDir, pid = process.pid }) {
5
+ return join(tmpDir, `tfx-agent-${pid}.json`);
6
+ }
7
+
8
+ export function registerAgent({
9
+ tmpDir,
10
+ pid = process.pid,
11
+ cli = "",
12
+ agent = "",
13
+ started = Math.floor(Date.now() / 1000),
14
+ }) {
15
+ mkdirSync(tmpDir, { recursive: true });
16
+ const file = agentJsonPath({ tmpDir, pid });
17
+ writeFileSync(
18
+ file,
19
+ `${JSON.stringify({ pid, cli, agent, started })}\n`,
20
+ "utf8",
21
+ );
22
+ return file;
23
+ }
24
+
25
+ export function removeAgent({ tmpDir, pid = process.pid }) {
26
+ rmSync(agentJsonPath({ tmpDir, pid }), { force: true });
27
+ }