@dev-anywhere/proxy 0.1.9 → 0.2.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 (39) hide show
  1. package/dist/{chunk-BMVYMCKF.js → chunk-3ZUZ22V6.js} +2 -2
  2. package/dist/{chunk-7XMJMVIL.js → chunk-4YQ2JUM7.js} +41 -6
  3. package/dist/chunk-4YQ2JUM7.js.map +1 -0
  4. package/dist/chunk-7UOPAMX7.js +220 -0
  5. package/dist/chunk-7UOPAMX7.js.map +1 -0
  6. package/dist/chunk-NBRBO5GS.js +1032 -0
  7. package/dist/chunk-NBRBO5GS.js.map +1 -0
  8. package/dist/chunk-NQDJ6QAM.js +18 -0
  9. package/dist/chunk-NQDJ6QAM.js.map +1 -0
  10. package/dist/chunk-OBYEKZWC.js +104 -0
  11. package/dist/chunk-OBYEKZWC.js.map +1 -0
  12. package/dist/chunk-PWG6K5QB.js +204 -0
  13. package/dist/chunk-PWG6K5QB.js.map +1 -0
  14. package/dist/{chunk-DCDXAM76.js → chunk-RIQ6OL7X.js} +9 -6
  15. package/dist/chunk-RIQ6OL7X.js.map +1 -0
  16. package/dist/{chunk-6O6JTF24.js → chunk-WUBRUO3G.js} +1 -1
  17. package/dist/index.js +5 -5
  18. package/dist/{relay-token-Z4JZFPQ5.js → relay-token-RKAVVQHE.js} +5 -4
  19. package/dist/{relay-token-Z4JZFPQ5.js.map → relay-token-RKAVVQHE.js.map} +1 -1
  20. package/dist/serve.js +538 -431
  21. package/dist/serve.js.map +1 -1
  22. package/dist/session-worker.js +99 -32
  23. package/dist/session-worker.js.map +1 -1
  24. package/dist/{terminal-FJAIRC73.js → terminal-YO2D2OJU.js} +194 -151
  25. package/dist/terminal-YO2D2OJU.js.map +1 -0
  26. package/package.json +3 -3
  27. package/dist/chunk-2JUB4LDU.js +0 -84
  28. package/dist/chunk-2JUB4LDU.js.map +0 -1
  29. package/dist/chunk-7XMJMVIL.js.map +0 -1
  30. package/dist/chunk-DCDXAM76.js.map +0 -1
  31. package/dist/chunk-ORZTFYXR.js +0 -123
  32. package/dist/chunk-ORZTFYXR.js.map +0 -1
  33. package/dist/chunk-QFYI6AMN.js +0 -870
  34. package/dist/chunk-QFYI6AMN.js.map +0 -1
  35. package/dist/chunk-U5T7ZYXT.js +0 -346
  36. package/dist/chunk-U5T7ZYXT.js.map +0 -1
  37. package/dist/terminal-FJAIRC73.js.map +0 -1
  38. /package/dist/{chunk-BMVYMCKF.js.map → chunk-3ZUZ22V6.js.map} +0 -0
  39. /package/dist/{chunk-6O6JTF24.js.map → chunk-WUBRUO3G.js.map} +0 -0
package/dist/serve.js CHANGED
@@ -3,17 +3,15 @@ import {
3
3
  ContentBlockDeltaSchema,
4
4
  IGNORED_EVENT_TYPES,
5
5
  KnownContentBlockSchema,
6
- SeqCounter,
7
- StreamJsonEventSchema
8
- } from "./chunk-7XMJMVIL.js";
6
+ StreamJsonEventSchema,
7
+ disposeSeqCounter,
8
+ getSeqCounterFor
9
+ } from "./chunk-4YQ2JUM7.js";
9
10
  import {
10
- createFSM,
11
- defineFSM,
11
+ decidePtySemanticTransition,
12
12
  extractOscSequences,
13
- extractOscSignals,
14
- shouldReleaseApprovalWait,
15
- stateAfterApprovalRelease
16
- } from "./chunk-ORZTFYXR.js";
13
+ extractOscSignals
14
+ } from "./chunk-OBYEKZWC.js";
17
15
  import {
18
16
  spawnScript
19
17
  } from "./chunk-ZUWAB67J.js";
@@ -21,47 +19,56 @@ import {
21
19
  CLAUDE_PROVIDER,
22
20
  CODEX_PROVIDER,
23
21
  detectAgentCliStatus
24
- } from "./chunk-6O6JTF24.js";
22
+ } from "./chunk-WUBRUO3G.js";
25
23
  import {
24
+ ControlErrorCode,
25
+ MessageEnvelopeSchema,
26
+ RelayControlSchema,
27
+ SessionState,
28
+ buildMessage,
29
+ createFSM,
26
30
  createIpcReader,
27
31
  createWorkerReader,
32
+ defineFSM,
33
+ encodeBinaryFrame,
34
+ providerValues,
35
+ serializeControl,
28
36
  serializeIpc,
29
37
  serializeWorkerMsg
30
- } from "./chunk-U5T7ZYXT.js";
38
+ } from "./chunk-NBRBO5GS.js";
31
39
  import {
32
40
  buildProviderEnv,
33
41
  loadConfig,
34
42
  saveAgentCliPath
35
- } from "./chunk-DCDXAM76.js";
43
+ } from "./chunk-RIQ6OL7X.js";
44
+ import {
45
+ atomicWriteFileSync
46
+ } from "./chunk-NQDJ6QAM.js";
36
47
  import {
48
+ flushLogger,
37
49
  serviceLogger
38
- } from "./chunk-2JUB4LDU.js";
50
+ } from "./chunk-7UOPAMX7.js";
39
51
  import {
40
- ControlErrorCode,
41
52
  DATA_DIR,
42
53
  DEFAULT_PROXY_PROFILE,
43
54
  HOOK_REGISTRY_PATH,
44
- MessageEnvelopeSchema,
45
55
  PID_PATH,
46
56
  PROFILE_NAME,
47
57
  PROXY_ID_PATH,
48
58
  SESSIONS_PATH,
49
59
  SOCK_PATH,
50
60
  STOPPED_PATH,
51
- SessionState,
52
- buildMessage,
53
61
  ensureProfileWorkspace,
54
62
  sessionPaths,
55
63
  tildify
56
- } from "./chunk-QFYI6AMN.js";
64
+ } from "./chunk-PWG6K5QB.js";
57
65
 
58
66
  // src/serve.ts
59
67
  import { createServer as createServer2 } from "net";
60
- import { unlinkSync as unlinkSync3, writeFileSync as writeFileSync5, chmodSync, rmSync as rmSync2 } from "fs";
68
+ import { unlinkSync as unlinkSync4, writeFileSync as writeFileSync3, chmodSync, rmSync as rmSync2 } from "fs";
61
69
 
62
70
  // src/serve/session-manager.ts
63
- import { mkdirSync, readFileSync, renameSync, writeFileSync, existsSync } from "fs";
64
- import { dirname } from "path";
71
+ import { readFileSync, existsSync } from "fs";
65
72
  import { nanoid } from "nanoid";
66
73
  var PTY_TRANSITIONS = {
67
74
  [SessionState.IDLE]: [
@@ -278,8 +285,6 @@ var SessionManager = class {
278
285
  }
279
286
  }
280
287
  save() {
281
- const dir = dirname(this.persistPath);
282
- mkdirSync(dir, { recursive: true });
283
288
  const persisted = Array.from(this.sessions.values()).map((s) => ({
284
289
  id: s.id,
285
290
  mode: s.mode,
@@ -292,9 +297,7 @@ var SessionManager = class {
292
297
  ...s.claudeSessionId !== void 0 ? { claudeSessionId: s.claudeSessionId } : {}
293
298
  }));
294
299
  const data = JSON.stringify(persisted, null, 2);
295
- const tmpPath = this.persistPath + ".tmp";
296
- writeFileSync(tmpPath, data, "utf-8");
297
- renameSync(tmpPath, this.persistPath);
300
+ atomicWriteFileSync(this.persistPath, data, { ensureDir: true });
298
301
  }
299
302
  load() {
300
303
  if (!existsSync(this.persistPath)) {
@@ -305,22 +308,28 @@ var SessionManager = class {
305
308
  try {
306
309
  parsed = JSON.parse(raw);
307
310
  } catch (err) {
308
- throw new Error(`Failed to parse session persistence file at ${this.persistPath}`, {
309
- cause: err
310
- });
311
+ serviceLogger.warn(
312
+ { path: this.persistPath, error: String(err) },
313
+ "Session persistence file unparseable, starting with empty state"
314
+ );
315
+ return;
311
316
  }
312
317
  if (!Array.isArray(parsed)) {
313
- throw new Error(
314
- `Session persistence file has invalid format at ${this.persistPath}: expected array`
318
+ serviceLogger.warn(
319
+ { path: this.persistPath },
320
+ "Session persistence file has unexpected format (not array), starting with empty state"
315
321
  );
322
+ return;
316
323
  }
317
324
  for (const item of parsed) {
318
325
  if (item && typeof item === "object" && "state" in item) {
319
- throw new Error(
320
- `Session persistence file has invalid persisted state for session ${String(
321
- item.id
322
- )}`
326
+ const sessionId = String(item.id);
327
+ serviceLogger.warn(
328
+ { sessionId },
329
+ "Session persistence record has unexpected state field, skipping"
323
330
  );
331
+ this.onSessionRemoved?.(sessionId);
332
+ continue;
324
333
  }
325
334
  const info = item;
326
335
  if (!isProviderId(info.provider)) {
@@ -366,9 +375,9 @@ var SessionManager = class {
366
375
 
367
376
  // src/serve/relay-connection.ts
368
377
  import WebSocket from "ws";
369
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync2 } from "fs";
378
+ import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
370
379
  import { homedir } from "os";
371
- import { dirname as dirname2, join } from "path";
380
+ import { join } from "path";
372
381
  import { nanoid as nanoid2 } from "nanoid";
373
382
  import { EventEmitter } from "events";
374
383
 
@@ -400,6 +409,7 @@ var DEFAULT_PROXY_ID_PATH = join(homedir(), ".dev-anywhere", "proxy-id");
400
409
  var MAX_BACKOFF_MS = 3e4;
401
410
  var BASE_BACKOFF_MS = 1e3;
402
411
  var MAX_QUEUE_SIZE = 1e4;
412
+ var MAX_JSON_MESSAGE_SIZE = 1 * 1024 * 1024;
403
413
  var RelayConnectionState = {
404
414
  DISCONNECTED: "disconnected",
405
415
  CONNECTING: "connecting",
@@ -467,11 +477,7 @@ var RelayConnection = class extends EventEmitter {
467
477
  }
468
478
  }
469
479
  const id = nanoid2(21);
470
- const dir = dirname2(idPath);
471
- if (!existsSync2(dir)) {
472
- mkdirSync2(dir, { recursive: true });
473
- }
474
- writeFileSync2(idPath, id, "utf-8");
480
+ atomicWriteFileSync(idPath, id, { ensureDir: true });
475
481
  return id;
476
482
  }
477
483
  // 连接到 relay server
@@ -492,7 +498,7 @@ var RelayConnection = class extends EventEmitter {
492
498
  "Connected to relay server"
493
499
  );
494
500
  this.ws.send(
495
- JSON.stringify({
501
+ serializeControl({
496
502
  type: "proxy_register",
497
503
  proxyId: this.proxyId,
498
504
  ...this.name ? { name: this.name } : {}
@@ -500,7 +506,15 @@ var RelayConnection = class extends EventEmitter {
500
506
  );
501
507
  });
502
508
  this.ws.on("message", (data) => {
503
- const raw = data.toString();
509
+ const buf = data;
510
+ if (buf.length > MAX_JSON_MESSAGE_SIZE) {
511
+ serviceLogger.warn(
512
+ { size: buf.length },
513
+ "JSON message from relay rejected: exceeds max size"
514
+ );
515
+ return;
516
+ }
517
+ const raw = buf.toString();
504
518
  let msg;
505
519
  try {
506
520
  msg = JSON.parse(raw);
@@ -518,15 +532,16 @@ var RelayConnection = class extends EventEmitter {
518
532
  }
519
533
  this.emit("message", msg);
520
534
  });
521
- this.ws.on("close", () => {
535
+ this.ws.on("close", (code, reason) => {
522
536
  this.ws = null;
537
+ const closeMeta = { code, reason: reason.toString() || void 0 };
523
538
  if (this.fsm.current() !== RelayConnectionState.CLOSED) {
524
539
  this.fsm.tryTransitionTo(RelayConnectionState.WAITING_RECONNECT);
525
- serviceLogger.info("Relay connection closed unexpectedly");
540
+ serviceLogger.info(closeMeta, "Relay connection closed unexpectedly");
526
541
  this.emit("disconnected");
527
542
  this.scheduleReconnect();
528
543
  } else {
529
- serviceLogger.info("Relay connection closed");
544
+ serviceLogger.info(closeMeta, "Relay connection closed");
530
545
  }
531
546
  });
532
547
  this.ws.on("error", (err) => {
@@ -565,6 +580,8 @@ var RelayConnection = class extends EventEmitter {
565
580
  this.sendRaw(raw);
566
581
  }
567
582
  // 发送 binary PTY 帧到 relay,断线时直接丢弃不入队
583
+ // 接受 Uint8Array 而非强制 Buffer:encodeBinaryFrame 在 shared 包返回 Uint8Array,
584
+ // ws.send 在底层同样支持 Uint8Array,无需额外 Buffer.from 拷贝。
568
585
  sendBinary(data) {
569
586
  if (this.fsm.current() === RelayConnectionState.SYNCED && this.ws?.readyState === WebSocket.OPEN) {
570
587
  this.ws.send(data);
@@ -599,7 +616,7 @@ var RelayConnection = class extends EventEmitter {
599
616
  }
600
617
  if (this.ws) {
601
618
  if (this.ws.readyState === WebSocket.OPEN) {
602
- this.ws.send(JSON.stringify({ type: "proxy_disconnect", proxyId: this.proxyId }));
619
+ this.ws.send(serializeControl({ type: "proxy_disconnect", proxyId: this.proxyId }));
603
620
  }
604
621
  this.ws.close();
605
622
  this.ws = null;
@@ -744,7 +761,9 @@ function decodeHistoryCursor(cursor, fileSize) {
744
761
  if (!Number.isInteger(parsed) || parsed < 0) return fileSize;
745
762
  return Math.min(parsed, fileSize);
746
763
  }
764
+ var SAFE_SESSION_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
747
765
  async function findClaudeSessionFile(claudeSessionId) {
766
+ if (!SAFE_SESSION_ID_PATTERN.test(claudeSessionId)) return null;
748
767
  let projectDirs;
749
768
  try {
750
769
  projectDirs = await readdir(claudeProjectsDir());
@@ -1270,7 +1289,7 @@ function createControlMessageHandlers(send, sessionManager) {
1270
1289
  try {
1271
1290
  const commands = await discoverCommands(workDir);
1272
1291
  send(
1273
- JSON.stringify({
1292
+ serializeControl({
1274
1293
  type: "command_list_push",
1275
1294
  commands
1276
1295
  })
@@ -1285,7 +1304,7 @@ function createControlMessageHandlers(send, sessionManager) {
1285
1304
  async handleDirListRequest(msg) {
1286
1305
  if (!isPathSafe(msg.path)) {
1287
1306
  send(
1288
- JSON.stringify({
1307
+ serializeControl({
1289
1308
  type: "dir_list_response",
1290
1309
  requestId: msg.requestId,
1291
1310
  path: msg.path,
@@ -1300,7 +1319,7 @@ function createControlMessageHandlers(send, sessionManager) {
1300
1319
  try {
1301
1320
  const entries = await scanDir(msg.path);
1302
1321
  send(
1303
- JSON.stringify({
1322
+ serializeControl({
1304
1323
  type: "dir_list_response",
1305
1324
  requestId: msg.requestId,
1306
1325
  path: msg.path,
@@ -1310,7 +1329,7 @@ function createControlMessageHandlers(send, sessionManager) {
1310
1329
  serviceLogger.debug({ path: msg.path, count: entries.length }, "Dir list response sent");
1311
1330
  } catch (err) {
1312
1331
  send(
1313
- JSON.stringify({
1332
+ serializeControl({
1314
1333
  type: "dir_list_response",
1315
1334
  requestId: msg.requestId,
1316
1335
  path: msg.path,
@@ -1325,7 +1344,7 @@ function createControlMessageHandlers(send, sessionManager) {
1325
1344
  async handleDirCreateRequest(msg) {
1326
1345
  if (!isPathSafe(msg.path)) {
1327
1346
  send(
1328
- JSON.stringify({
1347
+ serializeControl({
1329
1348
  type: "dir_create_response",
1330
1349
  requestId: msg.requestId,
1331
1350
  path: msg.path,
@@ -1340,7 +1359,7 @@ function createControlMessageHandlers(send, sessionManager) {
1340
1359
  try {
1341
1360
  await mkdir(msg.path, { recursive: true });
1342
1361
  send(
1343
- JSON.stringify({
1362
+ serializeControl({
1344
1363
  type: "dir_create_response",
1345
1364
  requestId: msg.requestId,
1346
1365
  path: msg.path,
@@ -1350,7 +1369,7 @@ function createControlMessageHandlers(send, sessionManager) {
1350
1369
  serviceLogger.info({ path: msg.path }, "Directory created");
1351
1370
  } catch (err) {
1352
1371
  send(
1353
- JSON.stringify({
1372
+ serializeControl({
1354
1373
  type: "dir_create_response",
1355
1374
  requestId: msg.requestId,
1356
1375
  path: msg.path,
@@ -1366,7 +1385,7 @@ function createControlMessageHandlers(send, sessionManager) {
1366
1385
  try {
1367
1386
  const sessions = await scanSessionHistory();
1368
1387
  send(
1369
- JSON.stringify({
1388
+ serializeControl({
1370
1389
  type: "session_history_response",
1371
1390
  requestId: msg.requestId,
1372
1391
  sessions
@@ -1375,7 +1394,7 @@ function createControlMessageHandlers(send, sessionManager) {
1375
1394
  serviceLogger.debug({ count: sessions.length }, "Session history response sent");
1376
1395
  } catch (err) {
1377
1396
  send(
1378
- JSON.stringify({
1397
+ serializeControl({
1379
1398
  type: "session_history_response",
1380
1399
  requestId: msg.requestId,
1381
1400
  sessions: []
@@ -1395,7 +1414,7 @@ function createControlMessageHandlers(send, sessionManager) {
1395
1414
  const groups = groupsResult.status === "fulfilled" ? groupsResult.value : [];
1396
1415
  const failedReason = commandsResult.status === "rejected" ? commandsResult.reason : groupsResult.status === "rejected" ? groupsResult.reason : void 0;
1397
1416
  send(
1398
- JSON.stringify({
1417
+ serializeControl({
1399
1418
  type: "session_resources_response",
1400
1419
  requestId: msg.requestId,
1401
1420
  sessionId: msg.sessionId,
@@ -1416,7 +1435,7 @@ function createControlMessageHandlers(send, sessionManager) {
1416
1435
  try {
1417
1436
  const commands = await discoverCommands(workDir);
1418
1437
  send(
1419
- JSON.stringify({
1438
+ serializeControl({
1420
1439
  type: "command_list_push",
1421
1440
  commands
1422
1441
  })
@@ -1433,7 +1452,7 @@ function createControlMessageHandlers(send, sessionManager) {
1433
1452
  try {
1434
1453
  const groups = await getFileTree(workDir);
1435
1454
  send(
1436
- JSON.stringify({
1455
+ serializeControl({
1437
1456
  type: "file_tree_push",
1438
1457
  groups
1439
1458
  })
@@ -1451,7 +1470,7 @@ function createControlMessageHandlers(send, sessionManager) {
1451
1470
  const activeSessions = sessionManager.listSessions().filter((s) => s.state !== "terminated");
1452
1471
  if (activeSessions.length > 0) {
1453
1472
  send(
1454
- JSON.stringify({
1473
+ serializeControl({
1455
1474
  type: "session_sync",
1456
1475
  sessions: activeSessions.map((s) => ({
1457
1476
  id: s.id,
@@ -1471,14 +1490,14 @@ function createControlMessageHandlers(send, sessionManager) {
1471
1490
  try {
1472
1491
  const commands = await discoverCommands(workDir);
1473
1492
  send(
1474
- JSON.stringify({
1493
+ serializeControl({
1475
1494
  type: "command_list_push",
1476
1495
  commands
1477
1496
  })
1478
1497
  );
1479
1498
  const groups = await getFileTree(workDir);
1480
1499
  send(
1481
- JSON.stringify({
1500
+ serializeControl({
1482
1501
  type: "file_tree_push",
1483
1502
  groups
1484
1503
  })
@@ -1585,7 +1604,16 @@ var WorkerRegistry = class {
1585
1604
  const sock = connect(sockPath);
1586
1605
  sock.on("connect", () => {
1587
1606
  this.sockets.set(sessionId, sock);
1588
- createWorkerReader(sock, (msg) => this.handleWorkerMessage(sessionId, msg));
1607
+ createWorkerReader(
1608
+ sock,
1609
+ (msg) => this.handleWorkerMessage(sessionId, msg),
1610
+ (err, line) => {
1611
+ serviceLogger.warn(
1612
+ { sessionId, err: err.message, lineLen: line.length },
1613
+ "Worker IPC message dropped (parse/schema error)"
1614
+ );
1615
+ }
1616
+ );
1589
1617
  sock.on("close", () => this.onDisconnect(sessionId));
1590
1618
  sock.on("error", () => this.onDisconnect(sessionId));
1591
1619
  resolve3(sock);
@@ -1690,6 +1718,9 @@ var WorkerRegistry = class {
1690
1718
  onDisconnect(sessionId) {
1691
1719
  this.sockets.delete(sessionId);
1692
1720
  this.deps.permissionBroker.cleanupSession(sessionId, "Worker disconnected");
1721
+ if (this.deps.sessionManager.getSession(sessionId)) {
1722
+ this.deps.jsonObserver.onChannelBroken(sessionId);
1723
+ }
1693
1724
  }
1694
1725
  // 对齐 Claude CLI stream-json 输出,按 type 分发:
1695
1726
  // stream_event.content_block_delta → 增量 text/thinking envelope(仅 streamDelta 会话产生)
@@ -1802,7 +1833,7 @@ var WorkerRegistry = class {
1802
1833
  if (ev.type === "result") {
1803
1834
  const resultText = typeof ev.result === "string" ? ev.result : void 0;
1804
1835
  relay.sendRaw(
1805
- JSON.stringify({
1836
+ serializeControl({
1806
1837
  type: "turn_result",
1807
1838
  sessionId,
1808
1839
  success: ev.subtype === "success",
@@ -1820,7 +1851,7 @@ var WorkerRegistry = class {
1820
1851
  );
1821
1852
  this.deps.jsonObserver.onApprovalRequested(sessionId);
1822
1853
  try {
1823
- const approvalSeq = this.deps.nextSeq?.(sessionId) ?? new SeqCounter(sessionId).next();
1854
+ const approvalSeq = this.deps.nextSeq?.(sessionId) ?? getSeqCounterFor(sessionId).next();
1824
1855
  const envelope = buildMessage(
1825
1856
  "tool_use_request",
1826
1857
  sessionId,
@@ -1887,6 +1918,7 @@ function terminateSessionByOwnership(deps, sessionId) {
1887
1918
  });
1888
1919
  deps.controlHandlers.cleanup(sessionId);
1889
1920
  deps.agentStatusRegistry.delete(sessionId);
1921
+ deps.broadcastSessionList();
1890
1922
  serviceLogger.info(
1891
1923
  { sessionId, success: result.success },
1892
1924
  "Local terminal session detached from remote view"
@@ -1904,6 +1936,7 @@ function terminateSessionByOwnership(deps, sessionId) {
1904
1936
  const result = deps.sessionManager.terminateSession(sessionId);
1905
1937
  deps.controlHandlers.cleanup(sessionId);
1906
1938
  deps.agentStatusRegistry.delete(sessionId);
1939
+ deps.broadcastSessionList();
1907
1940
  serviceLogger.info({ sessionId, success: result.success }, "JSON worker session terminated");
1908
1941
  return { success: result.success, action: "terminate_json_worker" };
1909
1942
  }
@@ -1915,7 +1948,7 @@ function terminateSessionByOwnership(deps, sessionId) {
1915
1948
  }
1916
1949
 
1917
1950
  // src/serve/clipboard-image-upload.ts
1918
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync4, statSync, writeFileSync as writeFileSync3 } from "fs";
1951
+ import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync4, statSync, writeFileSync } from "fs";
1919
1952
  import { isAbsolute as isAbsolute2, join as join5, relative, resolve } from "path";
1920
1953
  import { nanoid as nanoid3 } from "nanoid";
1921
1954
  var MAX_CLIPBOARD_IMAGE_BYTES = 10 * 1024 * 1024;
@@ -1971,7 +2004,7 @@ function ensureProjectClipboardIgnored(cwd) {
1971
2004
  const alreadyIgnored = current.split(/\r?\n/).some((line) => normalizeGitignoreLine(line) === ".dev-anywhere");
1972
2005
  if (alreadyIgnored) return;
1973
2006
  const separator = current.length > 0 && !current.endsWith("\n") ? "\n" : "";
1974
- writeFileSync3(gitignorePath, `${current}${separator}.dev-anywhere/
2007
+ writeFileSync(gitignorePath, `${current}${separator}.dev-anywhere/
1975
2008
  `);
1976
2009
  } catch {
1977
2010
  }
@@ -1984,11 +2017,15 @@ function trySaveProjectClipboardImage(options) {
1984
2017
  const clipboardRoot = resolve(cwd, ".dev-anywhere", "clipboard");
1985
2018
  const uploadDir = resolveChildDir(clipboardRoot, options.sessionId);
1986
2019
  const path = join5(uploadDir, options.fileName);
1987
- mkdirSync3(uploadDir, { recursive: true });
1988
- writeFileSync3(path, options.buffer, { mode: 384 });
2020
+ mkdirSync(uploadDir, { recursive: true });
2021
+ writeFileSync(path, options.buffer, { mode: 384 });
1989
2022
  ensureProjectClipboardIgnored(cwd);
1990
2023
  return { success: true, path: relative(cwd, path) };
1991
- } catch {
2024
+ } catch (err) {
2025
+ serviceLogger.warn(
2026
+ { sessionId: options.sessionId, cwd: options.cwd, error: String(err) },
2027
+ "Project clipboard image write failed; falling back to data dir"
2028
+ );
1992
2029
  return null;
1993
2030
  }
1994
2031
  }
@@ -2017,8 +2054,8 @@ function saveClipboardImageUpload(request, options = {}) {
2017
2054
  const dataDir = options.dataDir ?? DATA_DIR;
2018
2055
  const uploadDir = resolveSessionClipboardDir(dataDir, request.sessionId);
2019
2056
  const path = join5(uploadDir, fileName);
2020
- mkdirSync3(uploadDir, { recursive: true });
2021
- writeFileSync3(path, buffer, { mode: 384 });
2057
+ mkdirSync(uploadDir, { recursive: true });
2058
+ writeFileSync(path, buffer, { mode: 384 });
2022
2059
  return { success: true, path };
2023
2060
  } catch (err) {
2024
2061
  return {
@@ -2144,17 +2181,15 @@ var RelayInputHandlers = class {
2144
2181
  }
2145
2182
  deps;
2146
2183
  onUserInput(msg) {
2147
- const sessionId = msg.sessionId;
2184
+ const { sessionId } = msg;
2148
2185
  if (!sessionId) return;
2149
2186
  const session = this.deps.sessionManager.getSession(sessionId);
2150
2187
  if (!session) {
2151
2188
  serviceLogger.warn({ sessionId }, "Remote input dropped: session not found");
2152
2189
  return;
2153
2190
  }
2154
- const payload = msg.payload;
2155
- const text = payload?.text ?? "";
2191
+ const text = msg.payload.text;
2156
2192
  if (session.mode === "json") {
2157
- this.deps.jsonObserver.onTurnStart(sessionId);
2158
2193
  const sent = this.deps.workerRegistry.send(sessionId, {
2159
2194
  type: "worker_input",
2160
2195
  content: text
@@ -2163,18 +2198,16 @@ var RelayInputHandlers = class {
2163
2198
  serviceLogger.warn({ sessionId }, "Remote input dropped: JSON worker socket not available");
2164
2199
  return;
2165
2200
  }
2166
- const timestamp = typeof msg.timestamp === "number" && Number.isFinite(msg.timestamp) ? msg.timestamp : Date.now();
2167
- const seq = typeof msg.seq === "number" && Number.isInteger(msg.seq) && msg.seq >= 0 ? msg.seq : 0;
2168
- const version = typeof msg.version === "string" ? msg.version : "1";
2169
- const messageId = typeof payload?.messageId === "string" && payload.messageId.length > 0 ? payload.messageId : `${sessionId}-user-${timestamp}`;
2201
+ this.deps.jsonObserver.onTurnStart(sessionId);
2202
+ const messageId = msg.payload.messageId && msg.payload.messageId.length > 0 ? msg.payload.messageId : `${sessionId}-user-${msg.timestamp}`;
2170
2203
  this.deps.relayConnection.sendEnvelope(
2171
2204
  MessageEnvelopeSchema.parse({
2172
2205
  type: "user_input",
2173
2206
  sessionId,
2174
- seq,
2175
- timestamp,
2207
+ seq: msg.seq,
2208
+ timestamp: msg.timestamp,
2176
2209
  source: "proxy",
2177
- version,
2210
+ version: msg.version,
2178
2211
  payload: { text, messageId }
2179
2212
  })
2180
2213
  );
@@ -2187,8 +2220,7 @@ var RelayInputHandlers = class {
2187
2220
  );
2188
2221
  }
2189
2222
  onRemoteInputRaw(msg) {
2190
- const sessionId = msg.sessionId;
2191
- const data = msg.data;
2223
+ const { sessionId, data } = msg;
2192
2224
  if (!sessionId || data === void 0) return;
2193
2225
  const ts = this.deps.terminalSockets.get(sessionId);
2194
2226
  if (!ts?.writable && this.deps.hostedPtyRegistry.write(sessionId, data)) {
@@ -2206,13 +2238,12 @@ var RelayInputHandlers = class {
2206
2238
  serviceLogger.info({ sessionId, bytes: data.length }, "Raw PTY input forwarded");
2207
2239
  }
2208
2240
  onClipboardImageUpload(msg) {
2209
- const sessionId = msg.sessionId;
2210
- const requestId = msg.requestId;
2241
+ const { sessionId, requestId } = msg;
2211
2242
  if (!sessionId) return;
2212
2243
  const session = this.deps.sessionManager.getSession(sessionId);
2213
2244
  if (!session) {
2214
2245
  this.deps.relayConnection.sendRaw(
2215
- JSON.stringify({
2246
+ serializeControl({
2216
2247
  type: "clipboard_image_upload_response",
2217
2248
  requestId,
2218
2249
  sessionId,
@@ -2228,16 +2259,16 @@ var RelayInputHandlers = class {
2228
2259
  const result = saveClipboardImageUpload(
2229
2260
  {
2230
2261
  sessionId,
2231
- mimeType: typeof msg.mimeType === "string" ? msg.mimeType : "",
2232
- dataBase64: typeof msg.dataBase64 === "string" ? msg.dataBase64 : "",
2233
- fileName: typeof msg.fileName === "string" ? msg.fileName : void 0
2262
+ mimeType: msg.mimeType,
2263
+ dataBase64: msg.dataBase64,
2264
+ fileName: msg.fileName
2234
2265
  },
2235
2266
  {
2236
2267
  cwd: session.cwd
2237
2268
  }
2238
2269
  );
2239
2270
  this.deps.relayConnection.sendRaw(
2240
- JSON.stringify({
2271
+ serializeControl({
2241
2272
  type: "clipboard_image_upload_response",
2242
2273
  requestId,
2243
2274
  sessionId,
@@ -2247,14 +2278,12 @@ var RelayInputHandlers = class {
2247
2278
  serviceLogger.info({ sessionId, success: result.success }, "Clipboard image upload handled");
2248
2279
  }
2249
2280
  onImagePreviewRequest(msg) {
2250
- const sessionId = msg.sessionId;
2251
- const requestId = msg.requestId;
2252
- const path = msg.path;
2281
+ const { sessionId, requestId, path } = msg;
2253
2282
  if (!sessionId || !path) return;
2254
2283
  const session = this.deps.sessionManager.getSession(sessionId);
2255
2284
  if (!session) {
2256
2285
  this.deps.relayConnection.sendRaw(
2257
- JSON.stringify({
2286
+ serializeControl({
2258
2287
  type: "image_preview_response",
2259
2288
  requestId,
2260
2289
  sessionId,
@@ -2275,7 +2304,7 @@ var RelayInputHandlers = class {
2275
2304
  }
2276
2305
  );
2277
2306
  this.deps.relayConnection.sendRaw(
2278
- JSON.stringify({
2307
+ serializeControl({
2279
2308
  type: "image_preview_response",
2280
2309
  requestId,
2281
2310
  ...result
@@ -2292,16 +2321,13 @@ var RelayHistoryHandlers = class {
2292
2321
  }
2293
2322
  deps;
2294
2323
  onSessionMessagesRequest(msg) {
2295
- const sid = msg.sessionId;
2324
+ const { sessionId: sid, requestId, before, limit } = msg;
2296
2325
  if (!sid) return;
2297
- const requestId = msg.requestId;
2298
- const before = msg.before;
2299
- const limit = msg.limit;
2300
2326
  const session = this.deps.sessionManager.getSession(sid);
2301
2327
  if (session?.claudeSessionId) {
2302
2328
  readSessionMessagesPage(session.claudeSessionId, { before, limit }).then((page) => {
2303
2329
  this.deps.relaySend(
2304
- JSON.stringify({
2330
+ serializeControl({
2305
2331
  type: "session_history_messages",
2306
2332
  requestId,
2307
2333
  sessionId: sid,
@@ -2327,7 +2353,7 @@ var RelayHistoryHandlers = class {
2327
2353
  "Failed to read session history page on request"
2328
2354
  );
2329
2355
  this.deps.relaySend(
2330
- JSON.stringify({
2356
+ serializeControl({
2331
2357
  type: "session_history_messages",
2332
2358
  requestId,
2333
2359
  sessionId: sid,
@@ -2339,7 +2365,7 @@ var RelayHistoryHandlers = class {
2339
2365
  });
2340
2366
  } else {
2341
2367
  this.deps.relaySend(
2342
- JSON.stringify({
2368
+ serializeControl({
2343
2369
  type: "session_history_messages",
2344
2370
  requestId,
2345
2371
  sessionId: sid,
@@ -2355,7 +2381,7 @@ var RelayHistoryHandlers = class {
2355
2381
  input: approval.input
2356
2382
  }));
2357
2383
  this.deps.relaySend(
2358
- JSON.stringify({ type: "pending_approvals_push", sessionId: sid, approvals })
2384
+ serializeControl({ type: "pending_approvals_push", sessionId: sid, approvals })
2359
2385
  );
2360
2386
  serviceLogger.info({ sessionId: sid, count: approvals.length }, "Pending approvals pushed");
2361
2387
  }
@@ -2368,8 +2394,7 @@ var RelayPermissionHandlers = class {
2368
2394
  }
2369
2395
  deps;
2370
2396
  onToolApprove(msg) {
2371
- const sessionId = msg.sessionId;
2372
- const payload = msg.payload;
2397
+ const { sessionId, payload } = msg;
2373
2398
  if (!sessionId || !payload?.toolId) return;
2374
2399
  const pending = this.deps.permissionBroker.get(payload.toolId);
2375
2400
  if (!pending) {
@@ -2421,8 +2446,7 @@ var RelayPermissionHandlers = class {
2421
2446
  );
2422
2447
  }
2423
2448
  onToolDeny(msg) {
2424
- const sessionId = msg.sessionId;
2425
- const payload = msg.payload;
2449
+ const { sessionId, payload } = msg;
2426
2450
  if (!sessionId || !payload?.toolId) return;
2427
2451
  const reason = payload.reason ?? "Denied by remote user";
2428
2452
  const pending = this.deps.permissionBroker.get(payload.toolId);
@@ -2460,15 +2484,14 @@ var RelayPermissionHandlers = class {
2460
2484
  serviceLogger.info({ sessionId, toolId: payload.toolId }, "Tool denied via relay");
2461
2485
  }
2462
2486
  onPermissionRequestDelivered(msg) {
2463
- const sid = msg.sessionId;
2464
- const requestId = msg.requestId;
2487
+ const { sessionId: sid, requestId } = msg;
2465
2488
  if (!sid || !requestId) return;
2466
2489
  const marked = this.deps.permissionBroker.markDelivered(requestId);
2467
2490
  serviceLogger.info({ sessionId: sid, requestId, marked }, "Permission request delivered");
2468
2491
  }
2469
2492
  pushPermissionDecisionResult(sessionId, requestId, outcome, delivered, message) {
2470
2493
  this.deps.relaySend(
2471
- JSON.stringify({
2494
+ serializeControl({
2472
2495
  type: "permission_decision_result",
2473
2496
  sessionId,
2474
2497
  requestId,
@@ -2501,7 +2524,7 @@ var RelayResourceHandlers = class {
2501
2524
  deps;
2502
2525
  onProxyInfoRequest(msg) {
2503
2526
  this.deps.relaySend(
2504
- JSON.stringify({
2527
+ serializeControl({
2505
2528
  type: "proxy_info",
2506
2529
  requestId: msg.requestId,
2507
2530
  homePath: homedir4() || "/",
@@ -2512,12 +2535,11 @@ var RelayResourceHandlers = class {
2512
2535
  );
2513
2536
  }
2514
2537
  onAgentCliConfigUpdate(msg) {
2515
- const requestId = msg.requestId;
2516
- const provider = msg.provider;
2538
+ const { requestId, provider } = msg;
2517
2539
  const rawPath = msg.path;
2518
2540
  if (provider !== "claude" && provider !== "codex") {
2519
2541
  this.deps.relaySend(
2520
- JSON.stringify({
2542
+ serializeControl({
2521
2543
  type: "agent_cli_config_update_response",
2522
2544
  requestId,
2523
2545
  provider: "claude",
@@ -2535,7 +2557,7 @@ var RelayResourceHandlers = class {
2535
2557
  suggestions: this.deps.getAgentCliSuggestions()
2536
2558
  });
2537
2559
  this.deps.relaySend(
2538
- JSON.stringify({
2560
+ serializeControl({
2539
2561
  type: "agent_cli_config_update_response",
2540
2562
  requestId,
2541
2563
  provider,
@@ -2546,7 +2568,7 @@ var RelayResourceHandlers = class {
2546
2568
  } catch (err) {
2547
2569
  const error = errorMessage(err);
2548
2570
  this.deps.relaySend(
2549
- JSON.stringify({
2571
+ serializeControl({
2550
2572
  type: "agent_cli_config_update_response",
2551
2573
  requestId,
2552
2574
  provider,
@@ -2576,7 +2598,7 @@ var RelayResourceHandlers = class {
2576
2598
  if (!session?.cwd) {
2577
2599
  serviceLogger.warn({ sessionId: sid }, "Session resources request: no cwd available");
2578
2600
  this.deps.relaySend(
2579
- JSON.stringify({
2601
+ serializeControl({
2580
2602
  type: "session_resources_response",
2581
2603
  requestId: msg.requestId,
2582
2604
  sessionId: sid,
@@ -2712,7 +2734,7 @@ var HostedPtyRegistry = class {
2712
2734
  hosted.child.resize(cols, rows);
2713
2735
  hosted.terminal.resize(cols, rows);
2714
2736
  this.deps.relayConnection.sendRaw(
2715
- JSON.stringify({ type: "terminal_resize", sessionId, cols, rows })
2737
+ serializeControl({ type: "terminal_resize", sessionId, cols, rows })
2716
2738
  );
2717
2739
  serviceLogger.info({ sessionId, cols, rows }, "Hosted PTY resized");
2718
2740
  return true;
@@ -2722,14 +2744,14 @@ var HostedPtyRegistry = class {
2722
2744
  if (!hosted) return false;
2723
2745
  const data = hosted.serializeAddon.serialize();
2724
2746
  this.deps.relayConnection.sendRaw(
2725
- JSON.stringify({
2747
+ serializeControl({
2726
2748
  type: "session_snapshot",
2727
2749
  sessionId,
2728
2750
  cols: hosted.terminal.cols,
2729
2751
  rows: hosted.terminal.rows,
2730
2752
  data,
2731
2753
  outputSeq: hosted.outputSeq,
2732
- requestId
2754
+ ...requestId !== void 0 ? { requestId } : {}
2733
2755
  })
2734
2756
  );
2735
2757
  serviceLogger.info(
@@ -2770,46 +2792,15 @@ var HostedPtyRegistry = class {
2770
2792
  if (signal?.title) {
2771
2793
  this.sendTerminalTitle(sessionId, signal.title);
2772
2794
  }
2773
- if (signal?.state === "approval_wait") {
2774
- hosted.currentState = "approval_wait";
2775
- this.deps.changeSessionState(sessionId, SessionState.WAITING_APPROVAL);
2776
- this.sendPtyState(sessionId, "approval_wait", { title: signal?.title, tool: signal?.tool });
2777
- return;
2778
- }
2779
- if (shouldReleaseApprovalWait({
2795
+ const decision = decidePtySemanticTransition({
2780
2796
  currentState: hosted.currentState,
2781
- signalState: signal?.state
2782
- })) {
2783
- const nextState = stateAfterApprovalRelease(signal?.state);
2784
- hosted.currentState = nextState;
2785
- if (nextState === "turn_complete") {
2786
- this.deps.onTurnComplete(sessionId);
2787
- this.deps.changeSessionState(sessionId, SessionState.IDLE);
2788
- } else {
2789
- this.deps.changeSessionState(sessionId, SessionState.WORKING);
2790
- }
2791
- this.sendPtyState(sessionId, nextState, { title: signal?.title, tool: signal?.tool });
2792
- return;
2793
- }
2794
- if ((session?.state === SessionState.WAITING_APPROVAL || hosted.currentState === "approval_wait") && signal?.state !== "turn_complete") {
2795
- hosted.currentState = "approval_wait";
2796
- this.sendPtyState(sessionId, "approval_wait", { title: signal?.title, tool: signal?.tool });
2797
- return;
2798
- }
2799
- if (signal && signal.state !== "working") {
2800
- hosted.currentState = signal.state;
2801
- if (signal.state === "turn_complete") {
2802
- this.deps.onTurnComplete(sessionId);
2803
- this.deps.changeSessionState(sessionId, SessionState.IDLE);
2804
- }
2805
- this.sendPtyState(sessionId, signal.state, { title: signal.title, tool: signal.tool });
2806
- return;
2807
- }
2808
- if (hosted.currentState !== "working") {
2809
- hosted.currentState = "working";
2810
- this.deps.changeSessionState(sessionId, SessionState.WORKING);
2811
- this.sendPtyState(sessionId, "working");
2812
- }
2797
+ signal: signal ?? null,
2798
+ sessionStateIsWaitingApproval: session?.state === SessionState.WAITING_APPROVAL
2799
+ });
2800
+ hosted.currentState = decision.nextState;
2801
+ if (!decision.emit) return;
2802
+ this.sendPtyState(sessionId, decision.nextState, decision.meta);
2803
+ this.deps.applyPtyStateToSession(sessionId, decision.nextState);
2813
2804
  }
2814
2805
  checkIdle(sessionId) {
2815
2806
  const hosted = this.sessions.get(sessionId);
@@ -2820,9 +2811,8 @@ var HostedPtyRegistry = class {
2820
2811
  hosted.lastOutputTime = 0;
2821
2812
  if (hosted.currentState !== "working") return;
2822
2813
  hosted.currentState = "turn_complete";
2823
- this.deps.onTurnComplete(sessionId);
2824
- this.deps.changeSessionState(sessionId, SessionState.IDLE);
2825
2814
  this.sendPtyState(sessionId, "turn_complete");
2815
+ this.deps.applyPtyStateToSession(sessionId, "turn_complete");
2826
2816
  }
2827
2817
  sendPtyState(sessionId, state, meta) {
2828
2818
  const payload = {
@@ -2831,7 +2821,7 @@ var HostedPtyRegistry = class {
2831
2821
  ...meta?.tool !== void 0 ? { tool: meta.tool } : {}
2832
2822
  };
2833
2823
  this.deps.relayConnection.sendRaw(
2834
- JSON.stringify({
2824
+ serializeControl({
2835
2825
  type: "pty_state",
2836
2826
  sessionId,
2837
2827
  payload
@@ -2846,7 +2836,7 @@ var HostedPtyRegistry = class {
2846
2836
  }
2847
2837
  sendTerminalTitle(sessionId, title) {
2848
2838
  this.deps.relayConnection.sendRaw(
2849
- JSON.stringify({
2839
+ serializeControl({
2850
2840
  type: "terminal_title",
2851
2841
  sessionId,
2852
2842
  title
@@ -2854,13 +2844,7 @@ var HostedPtyRegistry = class {
2854
2844
  );
2855
2845
  }
2856
2846
  sendBinary(sessionId, data, outputSeq) {
2857
- const sessionIdBuf = Buffer.from(sessionId, "utf-8");
2858
- const frame = Buffer.alloc(1 + sessionIdBuf.length + 4 + data.length);
2859
- frame[0] = sessionIdBuf.length;
2860
- sessionIdBuf.copy(frame, 1);
2861
- frame.writeUInt32LE(outputSeq, 1 + sessionIdBuf.length);
2862
- data.copy(frame, 1 + sessionIdBuf.length + 4);
2863
- this.deps.relayConnection.sendBinary(frame);
2847
+ this.deps.relayConnection.sendBinary(encodeBinaryFrame(sessionId, outputSeq, data));
2864
2848
  }
2865
2849
  close(sessionId, options) {
2866
2850
  const hosted = this.sessions.get(sessionId);
@@ -2907,16 +2891,25 @@ var RelaySessionCreateHandler = class {
2907
2891
  this.deps = deps;
2908
2892
  }
2909
2893
  deps;
2894
+ // 跟踪每个 pendingId 当前挂起的 retry timer。SIGTERM 抵达时 destroy() 会 clear
2895
+ // 这些 timer 并执行 cleanupPendingJsonSession,否则 worker 子进程在窗口期内可能成为孤儿
2896
+ // (setTimeout 回调命中时 workerRegistry 已经 destroyAll,但 worker 进程并未被 kill)。
2897
+ pendingTimers = /* @__PURE__ */ new Map();
2898
+ destroy() {
2899
+ for (const [pendingId, timer] of this.pendingTimers) {
2900
+ clearTimeout(timer);
2901
+ this.cleanupPendingJsonSession(pendingId);
2902
+ }
2903
+ this.pendingTimers.clear();
2904
+ }
2910
2905
  onSessionCreate(msg) {
2911
- const requestId = msg.requestId;
2912
- const cwd = msg.cwd;
2906
+ const { requestId, cwd } = msg;
2913
2907
  const cwdError = validateSessionCwd(cwd);
2914
2908
  if (cwdError) {
2915
2909
  this.deps.relaySend(
2916
- JSON.stringify({
2910
+ serializeControl({
2917
2911
  type: "session_create_response",
2918
2912
  requestId,
2919
- sessionId: "",
2920
2913
  error: cwdError.message,
2921
2914
  errorCode: cwdError.code
2922
2915
  })
@@ -2934,10 +2927,9 @@ var RelaySessionCreateHandler = class {
2934
2927
  }
2935
2928
  if (provider !== "claude") {
2936
2929
  this.deps.relaySend(
2937
- JSON.stringify({
2930
+ serializeControl({
2938
2931
  type: "session_create_response",
2939
2932
  requestId,
2940
- sessionId: "",
2941
2933
  errorCode: ControlErrorCode.PROVIDER_UNSUPPORTED,
2942
2934
  error: provider === "codex" ? "Codex chat sessions are not supported yet; start a Codex terminal session instead." : "Unsupported provider for JSON session."
2943
2935
  })
@@ -2946,7 +2938,7 @@ var RelaySessionCreateHandler = class {
2946
2938
  return;
2947
2939
  }
2948
2940
  const resumeSessionId = msg.resumeSessionId;
2949
- const streamDelta = msg.streamDelta === true;
2941
+ const streamDelta = false;
2950
2942
  const name = tildify(sessionCwd);
2951
2943
  const pendingId = nanoid4();
2952
2944
  const hook = this.deps.createHookContext(pendingId, provider);
@@ -2960,7 +2952,12 @@ var RelaySessionCreateHandler = class {
2960
2952
  const paths = sessionPaths(pendingId);
2961
2953
  let attempt = 0;
2962
2954
  const maxRetries = 20;
2955
+ const scheduleAttempt = (delayMs) => {
2956
+ const timer = setTimeout(tryConnect, delayMs);
2957
+ this.pendingTimers.set(pendingId, timer);
2958
+ };
2963
2959
  const tryConnect = () => {
2960
+ this.pendingTimers.delete(pendingId);
2964
2961
  attempt++;
2965
2962
  this.deps.workerRegistry.connect(pendingId, paths.workerSock).then((sock) => {
2966
2963
  if (sock) {
@@ -2976,7 +2973,11 @@ var RelaySessionCreateHandler = class {
2976
2973
  this.deps.sessionManager.setClaudeSessionId(session.id, resumeSessionId);
2977
2974
  }
2978
2975
  this.deps.relaySend(
2979
- JSON.stringify({ type: "session_create_response", requestId, sessionId: session.id })
2976
+ serializeControl({
2977
+ type: "session_create_response",
2978
+ requestId,
2979
+ sessionId: session.id
2980
+ })
2980
2981
  );
2981
2982
  if (resumeSessionId) {
2982
2983
  this.pushHistoryMessages(session.id, resumeSessionId);
@@ -2989,11 +2990,11 @@ var RelaySessionCreateHandler = class {
2989
2990
  this.deps.broadcastSessionSync(session);
2990
2991
  this.deps.broadcastSessionList();
2991
2992
  } else if (attempt < maxRetries) {
2992
- setTimeout(tryConnect, Math.min(100 * attempt, 2e3));
2993
+ scheduleAttempt(Math.min(100 * attempt, 2e3));
2993
2994
  } else {
2994
2995
  this.cleanupPendingJsonSession(pendingId);
2995
2996
  this.deps.relaySend(
2996
- JSON.stringify({
2997
+ serializeControl({
2997
2998
  type: "session_create_response",
2998
2999
  requestId,
2999
3000
  sessionId: pendingId,
@@ -3005,7 +3006,7 @@ var RelaySessionCreateHandler = class {
3005
3006
  }
3006
3007
  });
3007
3008
  };
3008
- setTimeout(tryConnect, 100);
3009
+ scheduleAttempt(100);
3009
3010
  }
3010
3011
  cleanupPendingJsonSession(sessionId) {
3011
3012
  const killed = this.deps.workerRegistry.terminateProcess(sessionId);
@@ -3022,10 +3023,9 @@ var RelaySessionCreateHandler = class {
3022
3023
  createHostedPtySession(msg, cwd, provider, permissionMode) {
3023
3024
  if (provider !== "claude" && provider !== "codex") {
3024
3025
  this.deps.relaySend(
3025
- JSON.stringify({
3026
+ serializeControl({
3026
3027
  type: "session_create_response",
3027
3028
  requestId: msg.requestId,
3028
- sessionId: "",
3029
3029
  errorCode: ControlErrorCode.PROVIDER_UNSUPPORTED,
3030
3030
  error: "Unsupported provider for PTY session."
3031
3031
  })
@@ -3058,7 +3058,7 @@ var RelaySessionCreateHandler = class {
3058
3058
  this.deps.sessionManager.setClaudeSessionId(session.id, resumeSessionId);
3059
3059
  }
3060
3060
  this.deps.relaySend(
3061
- JSON.stringify({
3061
+ serializeControl({
3062
3062
  type: "session_create_response",
3063
3063
  requestId: msg.requestId,
3064
3064
  sessionId: session.id,
@@ -3075,10 +3075,9 @@ var RelaySessionCreateHandler = class {
3075
3075
  } catch (err) {
3076
3076
  const error = err instanceof Error ? err.message : String(err);
3077
3077
  this.deps.relaySend(
3078
- JSON.stringify({
3078
+ serializeControl({
3079
3079
  type: "session_create_response",
3080
3080
  requestId: msg.requestId,
3081
- sessionId: "",
3082
3081
  errorCode: ControlErrorCode.PROCESS_START_FAILED,
3083
3082
  error
3084
3083
  })
@@ -3101,7 +3100,7 @@ var RelaySessionCreateHandler = class {
3101
3100
  readSessionMessagesPage(resumeSessionId).then((page) => {
3102
3101
  if (page.messages.length === 0) return;
3103
3102
  this.deps.relaySend(
3104
- JSON.stringify({
3103
+ serializeControl({
3105
3104
  type: "session_history_messages",
3106
3105
  sessionId,
3107
3106
  messages: page.messages,
@@ -3180,56 +3179,120 @@ var RelayRouter = class {
3180
3179
  permissionHandlers;
3181
3180
  resourceHandlers;
3182
3181
  sessionCreateHandler;
3183
- handle(parsed) {
3184
- const type = parsed.type;
3185
- if (!type) {
3186
- serviceLogger.warn("Relay message without type discriminator");
3182
+ // shutdown 链路上提供单一 destroy 入口:把 sessionCreateHandler 内部 pending retry timer 清掉
3183
+ // cleanup 已 spawn 但未 connect 的 worker 子进程,避免在 SIGTERM 之后变成孤儿。
3184
+ destroy() {
3185
+ this.sessionCreateHandler.destroy();
3186
+ }
3187
+ // 入站消息统一入口:proxy 收两类消息——relay control 与 envelope(user_input 这一种)。
3188
+ // 先按 envelope 试解析(discriminated union),失败再按 control 解析;各 handler 拿到
3189
+ // 强类型窄化后的消息,不再需要 `as string | undefined` / `as { ... }` 裸 cast。
3190
+ handle(rawMsg) {
3191
+ const asEnvelope = MessageEnvelopeSchema.safeParse(rawMsg);
3192
+ if (asEnvelope.success && asEnvelope.data.type === "user_input") {
3193
+ try {
3194
+ this.inputHandlers.onUserInput(asEnvelope.data);
3195
+ } catch (err) {
3196
+ serviceLogger.warn({ type: "user_input", error: String(err) }, "Relay handler threw");
3197
+ }
3187
3198
  return;
3188
3199
  }
3189
- const handler = this.handlers[type];
3190
- if (!handler) {
3191
- serviceLogger.warn({ type }, "Unhandled relay message type");
3200
+ const asControl = RelayControlSchema.safeParse(rawMsg);
3201
+ if (!asControl.success) {
3202
+ serviceLogger.warn(
3203
+ {
3204
+ type: typeof rawMsg.type === "string" ? rawMsg.type : "<missing>",
3205
+ controlIssues: asControl.error.issues.slice(0, 3)
3206
+ },
3207
+ "Relay message rejected by both envelope and control schemas"
3208
+ );
3192
3209
  return;
3193
3210
  }
3211
+ const msg = asControl.data;
3194
3212
  try {
3195
- handler.call(this, parsed);
3213
+ this.dispatch(msg);
3196
3214
  } catch (err) {
3197
- serviceLogger.warn({ type, error: String(err) }, "Relay handler threw");
3198
- }
3199
- }
3200
- handlers = {
3201
- user_input: (msg) => this.inputHandlers.onUserInput(msg),
3202
- remote_input_raw: (msg) => this.inputHandlers.onRemoteInputRaw(msg),
3203
- clipboard_image_upload: (msg) => this.inputHandlers.onClipboardImageUpload(msg),
3204
- image_preview_request: (msg) => this.inputHandlers.onImagePreviewRequest(msg),
3205
- tool_approve: (msg) => this.permissionHandlers.onToolApprove(msg),
3206
- tool_deny: (msg) => this.permissionHandlers.onToolDeny(msg),
3207
- proxy_info_request: (msg) => this.resourceHandlers.onProxyInfoRequest(msg),
3208
- agent_cli_config_update: (msg) => this.resourceHandlers.onAgentCliConfigUpdate(msg),
3209
- dir_list_request: (msg) => this.resourceHandlers.onDirListRequest(msg),
3210
- dir_create_request: (msg) => this.resourceHandlers.onDirCreateRequest(msg),
3211
- session_create: (msg) => this.sessionCreateHandler.onSessionCreate(msg),
3212
- session_messages_request: (msg) => this.historyHandlers.onSessionMessagesRequest(msg),
3213
- session_resources_request: (msg) => this.resourceHandlers.onSessionResourcesRequest(msg),
3214
- agent_status_request: (msg) => this.onAgentStatusRequest(msg),
3215
- permission_request_delivered: (msg) => this.permissionHandlers.onPermissionRequestDelivered(msg),
3216
- session_terminate: (msg) => this.onSessionTerminate(msg),
3217
- session_worker_abort: (msg) => this.onSessionWorkerAbort(msg),
3218
- session_history_request: (msg) => this.deps.controlHandlers.handleSessionHistoryRequest({
3219
- requestId: msg.requestId
3220
- }),
3221
- session_list: () => this.onSessionList(),
3222
- permission_mode_change: (msg) => this.onPermissionModeChange(msg),
3223
- session_subscribe: (msg) => this.onSessionSubscribe(msg),
3224
- terminal_resize_request: (msg) => this.onTerminalResizeRequest(msg)
3225
- };
3215
+ serviceLogger.warn({ type: msg.type, error: String(err) }, "Relay handler threw");
3216
+ }
3217
+ }
3218
+ dispatch(msg) {
3219
+ switch (msg.type) {
3220
+ case "remote_input_raw":
3221
+ this.inputHandlers.onRemoteInputRaw(msg);
3222
+ return;
3223
+ case "clipboard_image_upload":
3224
+ this.inputHandlers.onClipboardImageUpload(msg);
3225
+ return;
3226
+ case "image_preview_request":
3227
+ this.inputHandlers.onImagePreviewRequest(msg);
3228
+ return;
3229
+ case "tool_approve":
3230
+ this.permissionHandlers.onToolApprove(msg);
3231
+ return;
3232
+ case "tool_deny":
3233
+ this.permissionHandlers.onToolDeny(msg);
3234
+ return;
3235
+ case "proxy_info_request":
3236
+ this.resourceHandlers.onProxyInfoRequest(msg);
3237
+ return;
3238
+ case "agent_cli_config_update":
3239
+ this.resourceHandlers.onAgentCliConfigUpdate(msg);
3240
+ return;
3241
+ case "dir_list_request":
3242
+ this.resourceHandlers.onDirListRequest(msg);
3243
+ return;
3244
+ case "dir_create_request":
3245
+ this.resourceHandlers.onDirCreateRequest(msg);
3246
+ return;
3247
+ case "session_create":
3248
+ this.sessionCreateHandler.onSessionCreate(msg);
3249
+ return;
3250
+ case "session_messages_request":
3251
+ this.historyHandlers.onSessionMessagesRequest(msg);
3252
+ return;
3253
+ case "session_resources_request":
3254
+ this.resourceHandlers.onSessionResourcesRequest(msg);
3255
+ return;
3256
+ case "agent_status_request":
3257
+ this.onAgentStatusRequest(msg);
3258
+ return;
3259
+ case "permission_request_delivered":
3260
+ this.permissionHandlers.onPermissionRequestDelivered(msg);
3261
+ return;
3262
+ case "session_terminate":
3263
+ this.onSessionTerminate(msg);
3264
+ return;
3265
+ case "session_worker_abort":
3266
+ this.onSessionWorkerAbort(msg);
3267
+ return;
3268
+ case "session_history_request":
3269
+ this.deps.controlHandlers.handleSessionHistoryRequest({ requestId: msg.requestId });
3270
+ return;
3271
+ case "session_list":
3272
+ this.onSessionList();
3273
+ return;
3274
+ case "permission_mode_change":
3275
+ this.onPermissionModeChange(msg);
3276
+ return;
3277
+ case "session_subscribe":
3278
+ this.onSessionSubscribe(msg);
3279
+ return;
3280
+ case "terminal_resize_request":
3281
+ this.onTerminalResizeRequest(msg);
3282
+ return;
3283
+ default:
3284
+ return;
3285
+ }
3286
+ }
3226
3287
  onAgentStatusRequest(msg) {
3227
3288
  const sid = msg.sessionId;
3228
3289
  const requestId = msg.requestId;
3229
3290
  if (sid) {
3230
3291
  const status = this.deps.agentStatusRegistry.get(sid);
3231
3292
  const statuses2 = status && this.deps.sessionManager.getSession(sid) ? [{ sessionId: sid, payload: status }] : [];
3232
- this.deps.relaySend(JSON.stringify({ type: "agent_status_response", requestId, statuses: statuses2 }));
3293
+ this.deps.relaySend(
3294
+ serializeControl({ type: "agent_status_response", requestId, statuses: statuses2 })
3295
+ );
3233
3296
  serviceLogger.info({ sessionId: sid, count: statuses2.length }, "Agent status snapshot sent");
3234
3297
  return;
3235
3298
  }
@@ -3238,7 +3301,9 @@ var RelayRouter = class {
3238
3301
  if (!this.deps.sessionManager.getSession(sessionId)) continue;
3239
3302
  statuses.push({ sessionId, payload: status });
3240
3303
  }
3241
- this.deps.relaySend(JSON.stringify({ type: "agent_status_response", requestId, statuses }));
3304
+ this.deps.relaySend(
3305
+ serializeControl({ type: "agent_status_response", requestId, statuses })
3306
+ );
3242
3307
  serviceLogger.info({ count: statuses.length }, "Agent status snapshot sent");
3243
3308
  }
3244
3309
  onSessionTerminate(msg) {
@@ -3249,7 +3314,6 @@ var RelayRouter = class {
3249
3314
  { sessionId: sid, success: result.success, action: result.action },
3250
3315
  "Session termination handled via relay"
3251
3316
  );
3252
- if (result.action !== "terminate_hosted_pty") this.deps.broadcastSessionList();
3253
3317
  }
3254
3318
  onSessionWorkerAbort(msg) {
3255
3319
  const sid = msg.sessionId;
@@ -3382,15 +3446,27 @@ var JsonObserver = class {
3382
3446
  };
3383
3447
 
3384
3448
  // src/serve/permission-broker.ts
3449
+ var DUPLICATE_DECISION = {
3450
+ behavior: "deny",
3451
+ message: "Duplicate permission request id."
3452
+ };
3453
+ function snapshot(pending) {
3454
+ return {
3455
+ requestId: pending.requestId,
3456
+ sessionId: pending.sessionId,
3457
+ provider: pending.provider,
3458
+ source: pending.source,
3459
+ toolName: pending.toolName,
3460
+ input: pending.input,
3461
+ createdAt: pending.createdAt,
3462
+ ...pending.deliveredAt !== void 0 ? { deliveredAt: pending.deliveredAt } : {}
3463
+ };
3464
+ }
3385
3465
  var PermissionBroker = class {
3386
3466
  pending = /* @__PURE__ */ new Map();
3387
3467
  request(request) {
3388
- const existing = this.pending.get(request.requestId);
3389
- if (existing) {
3390
- return Promise.resolve({
3391
- behavior: "deny",
3392
- message: "Duplicate permission request id."
3393
- });
3468
+ if (this.pending.has(request.requestId)) {
3469
+ return Promise.resolve(DUPLICATE_DECISION);
3394
3470
  }
3395
3471
  return new Promise((resolve3) => {
3396
3472
  this.pending.set(request.requestId, {
@@ -3402,12 +3478,8 @@ var PermissionBroker = class {
3402
3478
  });
3403
3479
  }
3404
3480
  registerWorkerRequest(request, onDecision) {
3405
- const existing = this.pending.get(request.requestId);
3406
- if (existing) {
3407
- onDecision({
3408
- behavior: "deny",
3409
- message: "Duplicate permission request id."
3410
- });
3481
+ if (this.pending.has(request.requestId)) {
3482
+ onDecision(DUPLICATE_DECISION);
3411
3483
  return false;
3412
3484
  }
3413
3485
  this.pending.set(request.requestId, {
@@ -3433,17 +3505,7 @@ var PermissionBroker = class {
3433
3505
  }
3434
3506
  get(requestId) {
3435
3507
  const pending = this.pending.get(requestId);
3436
- if (!pending) return null;
3437
- return {
3438
- requestId: pending.requestId,
3439
- sessionId: pending.sessionId,
3440
- provider: pending.provider,
3441
- source: pending.source,
3442
- toolName: pending.toolName,
3443
- input: pending.input,
3444
- createdAt: pending.createdAt,
3445
- ...pending.deliveredAt !== void 0 ? { deliveredAt: pending.deliveredAt } : {}
3446
- };
3508
+ return pending ? snapshot(pending) : null;
3447
3509
  }
3448
3510
  cleanupSession(sessionId, reason) {
3449
3511
  for (const [requestId, pending] of this.pending) {
@@ -3457,31 +3519,27 @@ var PermissionBroker = class {
3457
3519
  const out = [];
3458
3520
  for (const pending of this.pending.values()) {
3459
3521
  if (pending.sessionId !== sessionId) continue;
3460
- out.push({
3461
- requestId: pending.requestId,
3462
- sessionId: pending.sessionId,
3463
- provider: pending.provider,
3464
- source: pending.source,
3465
- toolName: pending.toolName,
3466
- input: pending.input,
3467
- createdAt: pending.createdAt,
3468
- ...pending.deliveredAt !== void 0 ? { deliveredAt: pending.deliveredAt } : {}
3469
- });
3522
+ out.push(snapshot(pending));
3470
3523
  }
3471
3524
  return out;
3472
3525
  }
3473
3526
  };
3474
3527
 
3475
- // src/serve/hook-event-router.ts
3476
- function hookPayloadRecord(value) {
3528
+ // src/serve/hook-payload-helpers.ts
3529
+ function asRecord(value) {
3477
3530
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
3478
3531
  }
3532
+ function asProvider(value) {
3533
+ return providerValues.includes(value) ? value : null;
3534
+ }
3479
3535
  function toolNameFromPayload(payload) {
3480
3536
  return typeof payload.toolName === "string" ? payload.toolName : typeof payload.tool_name === "string" ? payload.tool_name : "unknown";
3481
3537
  }
3482
3538
  function toolInputFromPayload(payload) {
3483
- return hookPayloadRecord(payload.input ?? payload.tool_input);
3539
+ return asRecord(payload.input ?? payload.tool_input);
3484
3540
  }
3541
+
3542
+ // src/serve/hook-event-router.ts
3485
3543
  var HookEventRouter = class {
3486
3544
  constructor(deps) {
3487
3545
  this.deps = deps;
@@ -3521,9 +3579,13 @@ var HookEventRouter = class {
3521
3579
  }
3522
3580
  }
3523
3581
  onPermissionResolved(sessionId, provider, requestId, outcome, context) {
3524
- this.deps.changeSessionState(sessionId, SessionState.WORKING);
3525
3582
  if (outcome === "deny") {
3526
3583
  this.deps.changeSessionState(sessionId, SessionState.IDLE);
3584
+ } else {
3585
+ const mode = this.deps.getSessionMode?.(sessionId);
3586
+ if (mode === "pty") {
3587
+ this.deps.changeSessionState(sessionId, SessionState.WORKING);
3588
+ }
3527
3589
  }
3528
3590
  this.forwardAgentStatus(
3529
3591
  {
@@ -3556,7 +3618,7 @@ var HookEventRouter = class {
3556
3618
  input
3557
3619
  }
3558
3620
  });
3559
- const seq = this.deps.nextSeq?.(event.sessionId) ?? new SeqCounter(event.sessionId).next();
3621
+ const seq = this.deps.nextSeq?.(event.sessionId) ?? getSeqCounterFor(event.sessionId).next();
3560
3622
  const envelope = buildMessage(
3561
3623
  "tool_use_request",
3562
3624
  event.sessionId,
@@ -3588,7 +3650,7 @@ var HookEventRouter = class {
3588
3650
  };
3589
3651
  this.deps.agentStatusRegistry.set(event.sessionId, payload);
3590
3652
  this.deps.relayConnection.sendRaw(
3591
- JSON.stringify({
3653
+ serializeControl({
3592
3654
  type: "agent_status",
3593
3655
  sessionId: event.sessionId,
3594
3656
  payload
@@ -3596,7 +3658,7 @@ var HookEventRouter = class {
3596
3658
  );
3597
3659
  }
3598
3660
  nextSeq(sessionId) {
3599
- return this.deps.nextSeq?.(sessionId) ?? new SeqCounter(sessionId).next();
3661
+ return this.deps.nextSeq?.(sessionId) ?? getSeqCounterFor(sessionId).next();
3600
3662
  }
3601
3663
  };
3602
3664
 
@@ -3619,6 +3681,52 @@ var AgentStatusRegistry = class {
3619
3681
  }
3620
3682
  };
3621
3683
 
3684
+ // src/serve/pty-state-guard.ts
3685
+ function shouldPromotePtyActivityToWorking(session, pendingApprovalCount) {
3686
+ if (!session || session.mode !== "pty") return false;
3687
+ if (pendingApprovalCount > 0) return false;
3688
+ return session.state === SessionState.IDLE || session.state === SessionState.WAITING_APPROVAL;
3689
+ }
3690
+
3691
+ // src/serve/pty-semantic-lifecycle.ts
3692
+ function resolvePtySemanticSessionTransitions(currentState, semanticState) {
3693
+ if (semanticState !== "turn_complete") return [];
3694
+ if (currentState === SessionState.WAITING_APPROVAL) {
3695
+ return [SessionState.IDLE];
3696
+ }
3697
+ if (currentState === SessionState.WORKING) {
3698
+ return [SessionState.IDLE];
3699
+ }
3700
+ return [];
3701
+ }
3702
+
3703
+ // src/serve/pty-session-bridge.ts
3704
+ function applyPtyStateToSession(deps, sessionId, ptyState) {
3705
+ const session = deps.getSession(sessionId);
3706
+ if (!session || session.state === SessionState.TERMINATED) return;
3707
+ switch (ptyState) {
3708
+ case "approval_wait":
3709
+ deps.changeSessionState(sessionId, SessionState.WAITING_APPROVAL);
3710
+ break;
3711
+ case "working": {
3712
+ const pending = deps.getPendingApprovalCount(sessionId);
3713
+ if (shouldPromotePtyActivityToWorking(session, pending)) {
3714
+ deps.changeSessionState(sessionId, SessionState.WORKING);
3715
+ }
3716
+ break;
3717
+ }
3718
+ case "turn_complete": {
3719
+ deps.resolveInterruptedApprovals(sessionId);
3720
+ const transitions = resolvePtySemanticSessionTransitions(session.state, ptyState);
3721
+ for (const next of transitions) {
3722
+ deps.changeSessionState(sessionId, next);
3723
+ }
3724
+ deps.emitAgentStatus(sessionId, "idle");
3725
+ break;
3726
+ }
3727
+ }
3728
+ }
3729
+
3622
3730
  // src/serve/session-broadcast.ts
3623
3731
  var ACTIVITY_STATUS_PUSH_INTERVAL_MS = 15e3;
3624
3732
  function toSessionListPayload(s) {
@@ -3649,21 +3757,18 @@ function pushSessionStatus(relay, sessionManager, sessionId) {
3649
3757
  }
3650
3758
  }
3651
3759
  function broadcastSessionList(relay, sessionManager) {
3652
- relay.sendRaw(
3653
- JSON.stringify({
3654
- type: "session_list",
3655
- sessionId: "",
3656
- seq: 0,
3657
- timestamp: Date.now(),
3658
- source: "proxy",
3659
- version: "1",
3660
- payload: { sessions: sessionManager.listSessions().map(toSessionListPayload) }
3661
- })
3760
+ const envelope = buildMessage(
3761
+ "session_list",
3762
+ null,
3763
+ 0,
3764
+ { sessions: sessionManager.listSessions().map(toSessionListPayload) },
3765
+ "proxy"
3662
3766
  );
3767
+ relay.sendEnvelope(envelope);
3663
3768
  }
3664
3769
  function broadcastSessionSync(relay, session) {
3665
3770
  relay.sendRaw(
3666
- JSON.stringify({
3771
+ serializeControl({
3667
3772
  type: "session_sync",
3668
3773
  sessions: [
3669
3774
  {
@@ -3689,6 +3794,37 @@ function touchSessionActivity(sessionManager, relay, sessionId, now = Date.now()
3689
3794
  return touched;
3690
3795
  }
3691
3796
 
3797
+ // src/serve/event-bridge.ts
3798
+ function createEventBridge(deps) {
3799
+ const changeState = (sessionId, next) => changeSessionState(deps.sessionManager, deps.relayConnection, sessionId, next);
3800
+ const touchActivity = (sessionId) => touchSessionActivity(deps.sessionManager, deps.relayConnection, sessionId);
3801
+ const emitAgentStatus = (sessionId, phase) => {
3802
+ const session = deps.sessionManager.getSession(sessionId);
3803
+ if (!session) return;
3804
+ const payload = {
3805
+ provider: session.provider,
3806
+ phase,
3807
+ seq: getSeqCounterFor(sessionId).next(),
3808
+ updatedAt: Date.now()
3809
+ };
3810
+ deps.agentStatusRegistry.set(sessionId, payload);
3811
+ deps.relayConnection.sendRaw(serializeControl({ type: "agent_status", sessionId, payload }));
3812
+ };
3813
+ const cleanupSessionResources = (sessionId) => {
3814
+ deps.controlHandlers.cleanup(sessionId);
3815
+ deps.agentStatusRegistry.delete(sessionId);
3816
+ disposeSeqCounter(sessionId);
3817
+ deps.permissionBroker.cleanupSession(sessionId, "Session closed");
3818
+ broadcastSessionList(deps.relayConnection, deps.sessionManager);
3819
+ };
3820
+ return {
3821
+ changeSessionState: changeState,
3822
+ touchSessionActivity: touchActivity,
3823
+ emitAgentStatus,
3824
+ cleanupSessionResources
3825
+ };
3826
+ }
3827
+
3692
3828
  // src/serve/service-files.ts
3693
3829
  import { execSync } from "child_process";
3694
3830
  import { existsSync as existsSync5, readFileSync as readFileSync6, unlinkSync as unlinkSync2 } from "fs";
@@ -3717,6 +3853,7 @@ async function cleanupStaleResources() {
3717
3853
  const msg = `Another service is already running on ${SOCK_PATH}`;
3718
3854
  serviceLogger.error(msg);
3719
3855
  console.error(msg);
3856
+ await flushLogger(serviceLogger);
3720
3857
  process.exit(1);
3721
3858
  }
3722
3859
  unlinkSync2(SOCK_PATH);
@@ -3729,6 +3866,7 @@ async function cleanupStaleResources() {
3729
3866
  const msg = `Another service is already running with PID ${pid}`;
3730
3867
  serviceLogger.error(msg);
3731
3868
  console.error(msg);
3869
+ await flushLogger(serviceLogger);
3732
3870
  process.exit(1);
3733
3871
  }
3734
3872
  unlinkSync2(PID_PATH);
@@ -3751,25 +3889,6 @@ function getComputerName() {
3751
3889
  }
3752
3890
  }
3753
3891
 
3754
- // src/serve/pty-state-guard.ts
3755
- function shouldPromotePtyActivityToWorking(session, pendingApprovalCount) {
3756
- if (!session || session.mode !== "pty") return false;
3757
- if (pendingApprovalCount > 0) return false;
3758
- return session.state === SessionState.IDLE || session.state === SessionState.WAITING_APPROVAL;
3759
- }
3760
-
3761
- // src/serve/pty-semantic-lifecycle.ts
3762
- function resolvePtySemanticSessionTransitions(currentState, semanticState) {
3763
- if (semanticState !== "turn_complete") return [];
3764
- if (currentState === SessionState.WAITING_APPROVAL) {
3765
- return [SessionState.IDLE];
3766
- }
3767
- if (currentState === SessionState.WORKING) {
3768
- return [SessionState.IDLE];
3769
- }
3770
- return [];
3771
- }
3772
-
3773
3892
  // src/serve/terminal-ipc.ts
3774
3893
  function handleTerminalConnection(socket, deps) {
3775
3894
  const {
@@ -3784,8 +3903,16 @@ function handleTerminalConnection(socket, deps) {
3784
3903
  createHookContext,
3785
3904
  emitAgentStatus,
3786
3905
  resolveInterruptedApprovals: resolveInterruptedApprovals2,
3906
+ cleanupSessionResources,
3787
3907
  config
3788
3908
  } = deps;
3909
+ const bridgeDeps = {
3910
+ changeSessionState: (sessionId, next) => changeSessionState(sessionManager, relayConnection, sessionId, next),
3911
+ getSession: (sessionId) => sessionManager.getSession(sessionId),
3912
+ getPendingApprovalCount: (sessionId) => permissionBroker.listSession(sessionId).length,
3913
+ resolveInterruptedApprovals: resolveInterruptedApprovals2,
3914
+ emitAgentStatus
3915
+ };
3789
3916
  createIpcReader(
3790
3917
  socket,
3791
3918
  (msg) => {
@@ -3852,7 +3979,7 @@ function handleTerminalConnection(socket, deps) {
3852
3979
  case "pty_title_change": {
3853
3980
  if (!sessionManager.getSession(msg.sessionId)) break;
3854
3981
  relayConnection.sendRaw(
3855
- JSON.stringify({
3982
+ serializeControl({
3856
3983
  type: "terminal_title",
3857
3984
  sessionId: msg.sessionId,
3858
3985
  title: msg.title
@@ -3873,45 +4000,9 @@ function handleTerminalConnection(socket, deps) {
3873
4000
  } else {
3874
4001
  serviceLogger.debug(logPayload, "PTY semantic event received");
3875
4002
  }
3876
- if (msg.state === "approval_wait") {
3877
- changeSessionState(
3878
- sessionManager,
3879
- relayConnection,
3880
- msg.sessionId,
3881
- SessionState.WAITING_APPROVAL
3882
- );
3883
- } else if (msg.state === "working" || msg.state === "mid_pause") {
3884
- const session = sessionManager.getSession(msg.sessionId);
3885
- const pendingApprovals = permissionBroker.listSession(msg.sessionId);
3886
- if (shouldPromotePtyActivityToWorking(session, pendingApprovals.length)) {
3887
- changeSessionState(
3888
- sessionManager,
3889
- relayConnection,
3890
- msg.sessionId,
3891
- SessionState.WORKING
3892
- );
3893
- } else if (session?.state === SessionState.WAITING_APPROVAL) {
3894
- serviceLogger.debug(
3895
- {
3896
- sessionId: msg.sessionId,
3897
- ptyState: msg.state,
3898
- pendingApprovals: pendingApprovals.length
3899
- },
3900
- "PTY working signal ignored while permission approval is pending"
3901
- );
3902
- }
3903
- }
3904
- if (msg.state === "turn_complete") {
3905
- resolveInterruptedApprovals2(msg.sessionId);
3906
- const session = sessionManager.getSession(msg.sessionId);
3907
- const transitions = resolvePtySemanticSessionTransitions(session?.state, msg.state);
3908
- for (const next of transitions) {
3909
- changeSessionState(sessionManager, relayConnection, msg.sessionId, next);
3910
- }
3911
- emitAgentStatus(msg.sessionId, "idle");
3912
- }
4003
+ applyPtyStateToSession(bridgeDeps, msg.sessionId, msg.state);
3913
4004
  relayConnection.sendRaw(
3914
- JSON.stringify({
4005
+ serializeControl({
3915
4006
  type: "pty_state",
3916
4007
  sessionId: msg.sessionId,
3917
4008
  payload: {
@@ -3926,7 +4017,7 @@ function handleTerminalConnection(socket, deps) {
3926
4017
  case "pty_resize": {
3927
4018
  if (!sessionManager.getSession(msg.sessionId)) break;
3928
4019
  relayConnection.sendRaw(
3929
- JSON.stringify({
4020
+ serializeControl({
3930
4021
  type: "terminal_resize",
3931
4022
  sessionId: msg.sessionId,
3932
4023
  cols: msg.cols,
@@ -3943,7 +4034,8 @@ function handleTerminalConnection(socket, deps) {
3943
4034
  controlHandlers,
3944
4035
  terminalSockets,
3945
4036
  hostedPtyRegistry,
3946
- agentStatusRegistry
4037
+ agentStatusRegistry,
4038
+ broadcastSessionList: () => broadcastSessionList(relayConnection, sessionManager)
3947
4039
  },
3948
4040
  msg.sessionId
3949
4041
  );
@@ -3986,7 +4078,7 @@ function handleTerminalConnection(socket, deps) {
3986
4078
  }
3987
4079
  case "pty_deregister": {
3988
4080
  relayConnection.sendRaw(
3989
- JSON.stringify({
4081
+ serializeControl({
3990
4082
  type: "pty_state",
3991
4083
  sessionId: msg.sessionId,
3992
4084
  payload: { state: "turn_complete" }
@@ -3994,9 +4086,7 @@ function handleTerminalConnection(socket, deps) {
3994
4086
  );
3995
4087
  sessionManager.terminateSession(msg.sessionId);
3996
4088
  terminalSockets.delete(msg.sessionId);
3997
- controlHandlers.cleanup(msg.sessionId);
3998
- agentStatusRegistry.delete(msg.sessionId);
3999
- broadcastSessionList(relayConnection, sessionManager);
4089
+ cleanupSessionResources(msg.sessionId);
4000
4090
  serviceLogger.info({ sessionId: msg.sessionId }, "PTY session deregistered");
4001
4091
  break;
4002
4092
  }
@@ -4024,14 +4114,14 @@ function handleTerminalConnection(socket, deps) {
4024
4114
  case "pty_snapshot": {
4025
4115
  if (!sessionManager.getSession(msg.sessionId)) break;
4026
4116
  relayConnection.sendRaw(
4027
- JSON.stringify({
4117
+ serializeControl({
4028
4118
  type: "session_snapshot",
4029
4119
  sessionId: msg.sessionId,
4030
4120
  cols: msg.cols,
4031
4121
  rows: msg.rows,
4032
4122
  data: msg.data,
4033
4123
  outputSeq: msg.outputSeq,
4034
- requestId: msg.requestId
4124
+ ...msg.requestId !== void 0 ? { requestId: msg.requestId } : {}
4035
4125
  })
4036
4126
  );
4037
4127
  serviceLogger.info(
@@ -4048,13 +4138,13 @@ function handleTerminalConnection(socket, deps) {
4048
4138
  (sessionId, data, outputSeq) => {
4049
4139
  if (!sessionManager.getSession(sessionId)) return;
4050
4140
  touchSessionActivity(sessionManager, relayConnection, sessionId);
4051
- const sessionIdBuf = Buffer.from(sessionId, "utf-8");
4052
- const wsFrame = Buffer.alloc(1 + sessionIdBuf.length + 4 + data.length);
4053
- wsFrame[0] = sessionIdBuf.length;
4054
- sessionIdBuf.copy(wsFrame, 1);
4055
- wsFrame.writeUInt32LE(outputSeq, 1 + sessionIdBuf.length);
4056
- data.copy(wsFrame, 1 + sessionIdBuf.length + 4);
4057
- relayConnection.sendBinary(wsFrame);
4141
+ relayConnection.sendBinary(encodeBinaryFrame(sessionId, outputSeq, data));
4142
+ },
4143
+ (err, line) => {
4144
+ serviceLogger.warn(
4145
+ { err: err.message, lineLen: line.length },
4146
+ "Terminal IPC message dropped (parse/schema error)"
4147
+ );
4058
4148
  }
4059
4149
  );
4060
4150
  socket.on("close", () => {
@@ -4074,16 +4164,14 @@ function handleTerminalConnection(socket, deps) {
4074
4164
  continue;
4075
4165
  }
4076
4166
  relayConnection.sendRaw(
4077
- JSON.stringify({
4167
+ serializeControl({
4078
4168
  type: "pty_state",
4079
4169
  sessionId,
4080
4170
  payload: { state: "turn_complete" }
4081
4171
  })
4082
4172
  );
4083
4173
  sessionManager.terminateSession(sessionId);
4084
- controlHandlers.cleanup(sessionId);
4085
- agentStatusRegistry.delete(sessionId);
4086
- broadcastSessionList(relayConnection, sessionManager);
4174
+ cleanupSessionResources(sessionId);
4087
4175
  serviceLogger.info(
4088
4176
  { sessionId },
4089
4177
  "PTY session cleaned up on socket close (crash fallback)"
@@ -4098,8 +4186,8 @@ function handleTerminalConnection(socket, deps) {
4098
4186
 
4099
4187
  // src/serve/hook-registry.ts
4100
4188
  import { createHash, randomBytes } from "crypto";
4101
- import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync7, renameSync as renameSync2, writeFileSync as writeFileSync4 } from "fs";
4102
- import { dirname as dirname3 } from "path";
4189
+ import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync7, renameSync, writeFileSync as writeFileSync2 } from "fs";
4190
+ import { dirname } from "path";
4103
4191
  import { z } from "zod";
4104
4192
  var PersistedHookSessionBindingSchema = z.object({
4105
4193
  sessionId: z.string(),
@@ -4178,9 +4266,9 @@ var HookRegistry = class {
4178
4266
  save() {
4179
4267
  if (!this.persistPath) return;
4180
4268
  try {
4181
- mkdirSync4(dirname3(this.persistPath), { recursive: true });
4269
+ mkdirSync2(dirname(this.persistPath), { recursive: true });
4182
4270
  const tmpPath = `${this.persistPath}.${process.pid}.${Date.now()}.tmp`;
4183
- writeFileSync4(
4271
+ writeFileSync2(
4184
4272
  tmpPath,
4185
4273
  JSON.stringify(
4186
4274
  {
@@ -4191,7 +4279,7 @@ var HookRegistry = class {
4191
4279
  2
4192
4280
  )
4193
4281
  );
4194
- renameSync2(tmpPath, this.persistPath);
4282
+ renameSync(tmpPath, this.persistPath);
4195
4283
  } catch (err) {
4196
4284
  serviceLogger.warn(
4197
4285
  { path: this.persistPath, error: String(err) },
@@ -4208,12 +4296,6 @@ function getBearerToken(req) {
4208
4296
  if (!header?.startsWith("Bearer ")) return null;
4209
4297
  return header.slice("Bearer ".length).trim() || null;
4210
4298
  }
4211
- function asProvider(value) {
4212
- return value === "claude" || value === "codex" ? value : null;
4213
- }
4214
- function asRecord(value) {
4215
- return value && typeof value === "object" && !Array.isArray(value) ? value : {};
4216
- }
4217
4299
  var HookServer = class {
4218
4300
  constructor(options) {
4219
4301
  this.options = options;
@@ -4313,7 +4395,7 @@ var HookServer = class {
4313
4395
  }
4314
4396
  async handlePermissionRequest(event, res) {
4315
4397
  const requestId = event.requestId ?? (typeof event.payload.tool_use_id === "string" ? event.payload.tool_use_id : void 0) ?? `${event.sessionId}:${Date.now()}`;
4316
- const toolName = typeof event.payload.toolName === "string" ? event.payload.toolName : typeof event.payload.tool_name === "string" ? event.payload.tool_name : "unknown";
4398
+ const toolName = toolNameFromPayload(event.payload);
4317
4399
  const input = asRecord(event.payload.input ?? event.payload.tool_input);
4318
4400
  this.options.onEvent?.({ ...event, requestId });
4319
4401
  const decision = await this.options.permissionBroker.request({
@@ -4396,7 +4478,8 @@ async function createProviderHookRuntime(options) {
4396
4478
  const hookEventRouter = new HookEventRouter({
4397
4479
  relayConnection: options.relayConnection,
4398
4480
  agentStatusRegistry: options.agentStatusRegistry,
4399
- changeSessionState: options.changeSessionState
4481
+ changeSessionState: options.changeSessionState,
4482
+ getSessionMode: (sessionId) => options.sessionManager.getSession(sessionId)?.mode
4400
4483
  });
4401
4484
  const port = options.hookPort ?? 17654;
4402
4485
  const hookServer = new HookServer({
@@ -4423,6 +4506,7 @@ async function createProviderHookRuntime(options) {
4423
4506
  const msg = `Failed to start hook server on 127.0.0.1:${port}: ${String(err)}`;
4424
4507
  serviceLogger.error(msg);
4425
4508
  console.error(msg);
4509
+ await flushLogger(serviceLogger);
4426
4510
  process.exit(1);
4427
4511
  }
4428
4512
  const hookUrl = `http://127.0.0.1:${hookServer.getListeningPort() ?? port}/hook`;
@@ -4444,6 +4528,35 @@ async function createProviderHookRuntime(options) {
4444
4528
  };
4445
4529
  }
4446
4530
 
4531
+ // src/serve/shutdown.ts
4532
+ import { unlinkSync as unlinkSync3 } from "fs";
4533
+ function createServeShutdown(deps) {
4534
+ const exit = deps.exit ?? ((code) => process.exit(code));
4535
+ let shuttingDown = false;
4536
+ return async () => {
4537
+ if (shuttingDown) return;
4538
+ shuttingDown = true;
4539
+ deps.logger.info("Shutting down service");
4540
+ deps.sessionManagerStopReaper();
4541
+ deps.relayRouterDestroy();
4542
+ await deps.hookServerClose();
4543
+ deps.relayConnectionClose();
4544
+ deps.workerRegistryDestroyAll();
4545
+ deps.hostedPtyRegistryDestroyAll();
4546
+ deps.ipcServerClose();
4547
+ try {
4548
+ unlinkSync3(deps.sockPath);
4549
+ } catch {
4550
+ }
4551
+ try {
4552
+ unlinkSync3(deps.pidPath);
4553
+ } catch {
4554
+ }
4555
+ await flushLogger(deps.logger);
4556
+ exit(0);
4557
+ };
4558
+ }
4559
+
4447
4560
  // src/serve.ts
4448
4561
  function resolveInterruptedApprovals(permissionBroker, hookEventRouter, relay, sessionId) {
4449
4562
  const approvals = permissionBroker.listSession(sessionId);
@@ -4459,7 +4572,7 @@ function resolveInterruptedApprovals(permissionBroker, hookEventRouter, relay, s
4459
4572
  { toolName: approval.toolName, toolInput: approval.input }
4460
4573
  );
4461
4574
  relay.sendRaw(
4462
- JSON.stringify({
4575
+ serializeControl({
4463
4576
  type: "permission_decision_result",
4464
4577
  sessionId: approval.sessionId,
4465
4578
  requestId: approval.requestId,
@@ -4499,7 +4612,7 @@ async function startService(options) {
4499
4612
  ensureProfileWorkspace();
4500
4613
  await cleanupStaleResources();
4501
4614
  try {
4502
- unlinkSync3(STOPPED_PATH);
4615
+ unlinkSync4(STOPPED_PATH);
4503
4616
  } catch {
4504
4617
  }
4505
4618
  const permissionBroker = new PermissionBroker();
@@ -4560,6 +4673,7 @@ async function startService(options) {
4560
4673
  const msg = `Relay URL is required. Set relays.${proxyConfig.relayName}.url in ~/.dev-anywhere/config.json or pass --relay <name>.`;
4561
4674
  serviceLogger.error(msg);
4562
4675
  console.error(msg);
4676
+ await flushLogger(serviceLogger);
4563
4677
  process.exit(1);
4564
4678
  }
4565
4679
  const relayConnection = new RelayConnection(relayUrl, {
@@ -4569,23 +4683,16 @@ async function startService(options) {
4569
4683
  });
4570
4684
  const relaySend = (data) => relayConnection.sendRaw(data);
4571
4685
  const controlHandlers = createControlMessageHandlers(relaySend, sessionManager);
4572
- const observerChangeState = (sessionId, next) => changeSessionState(sessionManager, relayConnection, sessionId, next);
4573
- const observerTouchActivity = (sessionId) => touchSessionActivity(sessionManager, relayConnection, sessionId);
4574
- const emitAgentStatus = (sessionId, phase) => {
4575
- const session = sessionManager.getSession(sessionId);
4576
- if (!session) return;
4577
- const payload = {
4578
- provider: session.provider,
4579
- phase,
4580
- seq: new SeqCounter(sessionId).next(),
4581
- updatedAt: Date.now()
4582
- };
4583
- agentStatusRegistry.set(sessionId, payload);
4584
- relayConnection.sendRaw(JSON.stringify({ type: "agent_status", sessionId, payload }));
4585
- };
4686
+ const eventBridge = createEventBridge({
4687
+ sessionManager,
4688
+ relayConnection,
4689
+ agentStatusRegistry,
4690
+ controlHandlers,
4691
+ permissionBroker
4692
+ });
4586
4693
  const jsonObserver = new JsonObserver({
4587
- changeSessionState: observerChangeState,
4588
- emitAgentStatus
4694
+ changeSessionState: eventBridge.changeSessionState,
4695
+ emitAgentStatus: eventBridge.emitAgentStatus
4589
4696
  });
4590
4697
  const hookRuntime = await createProviderHookRuntime({
4591
4698
  hookPort: proxyConfig.hookPort,
@@ -4593,7 +4700,7 @@ async function startService(options) {
4593
4700
  sessionManager,
4594
4701
  relayConnection,
4595
4702
  agentStatusRegistry,
4596
- changeSessionState: observerChangeState
4703
+ changeSessionState: eventBridge.changeSessionState
4597
4704
  });
4598
4705
  unregisterHookSession = (sessionId) => hookRuntime.hookRegistry.unregisterSession(sessionId);
4599
4706
  const workerRegistry = new WorkerRegistry({
@@ -4601,29 +4708,28 @@ async function startService(options) {
4601
4708
  permissionBroker,
4602
4709
  relayConnection,
4603
4710
  jsonObserver,
4604
- touchSessionActivity: observerTouchActivity,
4711
+ touchSessionActivity: eventBridge.touchSessionActivity,
4605
4712
  getProviderEnv
4606
4713
  });
4714
+ const ptyBridgeDeps = {
4715
+ changeSessionState: eventBridge.changeSessionState,
4716
+ getSession: (sessionId) => sessionManager.getSession(sessionId),
4717
+ getPendingApprovalCount: (sessionId) => permissionBroker.listSession(sessionId).length,
4718
+ resolveInterruptedApprovals: (sessionId) => resolveInterruptedApprovals(
4719
+ permissionBroker,
4720
+ hookRuntime.hookEventRouter,
4721
+ relayConnection,
4722
+ sessionId
4723
+ ),
4724
+ emitAgentStatus: eventBridge.emitAgentStatus
4725
+ };
4607
4726
  const hostedPtyRegistry = new HostedPtyRegistry({
4608
4727
  sessionManager,
4609
4728
  relayConnection,
4610
4729
  getProviderEnv,
4611
- changeSessionState: observerChangeState,
4612
- touchSessionActivity: observerTouchActivity,
4613
- onTurnComplete: (sessionId) => {
4614
- resolveInterruptedApprovals(
4615
- permissionBroker,
4616
- hookRuntime.hookEventRouter,
4617
- relayConnection,
4618
- sessionId
4619
- );
4620
- emitAgentStatus(sessionId, "idle");
4621
- },
4622
- onSessionClosed: (sessionId) => {
4623
- controlHandlers.cleanup(sessionId);
4624
- agentStatusRegistry.delete(sessionId);
4625
- broadcastSessionList(relayConnection, sessionManager);
4626
- }
4730
+ touchSessionActivity: eventBridge.touchSessionActivity,
4731
+ applyPtyStateToSession: (sessionId, ptyState) => applyPtyStateToSession(ptyBridgeDeps, sessionId, ptyState),
4732
+ onSessionClosed: eventBridge.cleanupSessionResources
4627
4733
  });
4628
4734
  relayConnection.connect();
4629
4735
  serviceLogger.info(
@@ -4660,7 +4766,12 @@ async function startService(options) {
4660
4766
  });
4661
4767
  relayConnection.on("message", (msg) => relayRouter.handle(msg));
4662
4768
  relayConnection.on("connected", () => {
4663
- controlHandlers.reinitializeOnReconnect();
4769
+ void controlHandlers.reinitializeOnReconnect().catch((err) => {
4770
+ serviceLogger.error(
4771
+ { error: err instanceof Error ? err.message : String(err), stack: err instanceof Error ? err.stack : void 0 },
4772
+ "reinitializeOnReconnect failed: client may see stale state until next manual sync"
4773
+ );
4774
+ });
4664
4775
  broadcastBridgeStatus(true);
4665
4776
  });
4666
4777
  relayConnection.on("disconnected", () => {
@@ -4685,7 +4796,8 @@ async function startService(options) {
4685
4796
  permissionBroker,
4686
4797
  hookEventRouter: hookRuntime.hookEventRouter,
4687
4798
  createHookContext: hookRuntime.createHookContext,
4688
- emitAgentStatus,
4799
+ emitAgentStatus: eventBridge.emitAgentStatus,
4800
+ cleanupSessionResources: eventBridge.cleanupSessionResources,
4689
4801
  config: statusConfig,
4690
4802
  resolveInterruptedApprovals: (sessionId) => resolveInterruptedApprovals(
4691
4803
  permissionBroker,
@@ -4696,41 +4808,36 @@ async function startService(options) {
4696
4808
  });
4697
4809
  });
4698
4810
  server.listen(SOCK_PATH, () => {
4699
- writeFileSync5(PID_PATH, String(process.pid));
4811
+ writeFileSync3(PID_PATH, String(process.pid));
4700
4812
  chmodSync(SOCK_PATH, 384);
4701
4813
  serviceLogger.info({ pid: process.pid, sock: SOCK_PATH }, "Service started");
4702
4814
  });
4703
- async function shutdown() {
4704
- serviceLogger.info("Shutting down service");
4705
- sessionManager.stopReaper();
4706
- await hookRuntime.hookServer.close();
4707
- relayConnection.close();
4708
- workerRegistry.destroyAll();
4709
- hostedPtyRegistry.destroyAll();
4710
- server.close();
4711
- try {
4712
- unlinkSync3(SOCK_PATH);
4713
- } catch {
4714
- }
4715
- try {
4716
- unlinkSync3(PID_PATH);
4717
- } catch {
4718
- }
4719
- process.exit(0);
4720
- }
4815
+ const shutdown = createServeShutdown({
4816
+ logger: serviceLogger,
4817
+ sessionManagerStopReaper: () => sessionManager.stopReaper(),
4818
+ relayRouterDestroy: () => relayRouter.destroy(),
4819
+ hookServerClose: () => hookRuntime.hookServer.close(),
4820
+ relayConnectionClose: () => relayConnection.close(),
4821
+ workerRegistryDestroyAll: () => workerRegistry.destroyAll(),
4822
+ hostedPtyRegistryDestroyAll: () => hostedPtyRegistry.destroyAll(),
4823
+ ipcServerClose: () => server.close(),
4824
+ sockPath: SOCK_PATH,
4825
+ pidPath: PID_PATH
4826
+ });
4721
4827
  process.on("SIGTERM", () => {
4722
- shutdown();
4828
+ void shutdown();
4723
4829
  });
4724
4830
  process.on("SIGINT", () => {
4725
- shutdown();
4831
+ void shutdown();
4726
4832
  });
4727
4833
  }
4728
4834
  var isMainModule = process.argv[1] && (process.argv[1].endsWith("serve.js") || process.argv[1].endsWith("serve.ts"));
4729
4835
  if (isMainModule) {
4730
- startService(parseServiceOptions(process.argv.slice(2))).catch((err) => {
4836
+ startService(parseServiceOptions(process.argv.slice(2))).catch(async (err) => {
4731
4837
  const message = err instanceof Error ? err.message : String(err);
4732
4838
  serviceLogger.error({ err: message }, "Service failed to start");
4733
4839
  console.error(message);
4840
+ await flushLogger(serviceLogger);
4734
4841
  process.exit(1);
4735
4842
  });
4736
4843
  }