@triflux/core 10.42.0 → 10.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/hub/router.mjs CHANGED
@@ -1,7 +1,15 @@
1
1
  // hub/router.mjs — 실시간 라우팅/수신함 상태 관리자
2
2
  // SQLite는 감사 로그만 담당하고, 실제 배달 상태는 메모리에서 관리한다.
3
3
  import { EventEmitter, once } from "node:events";
4
+ import {
5
+ clampDurationMs,
6
+ DEFAULT_ASSIGN_TIMEOUT_MS,
7
+ DEFAULT_ASSIGN_TTL_MS,
8
+ DEFAULT_HANDOFF_TTL_MS,
9
+ resolveHardCeilingMs,
10
+ } from "./lib/timeout-defaults.mjs";
4
11
  import { uuidv7 } from "./lib/uuidv7.mjs";
12
+ import { normalizeRoleKey } from "./role-contract.mjs";
5
13
 
6
14
  const ASSIGN_PENDING_STATUSES = new Set(["queued", "running"]);
7
15
  const ROLE_TOPICS = new Set(["cto"]);
@@ -9,6 +17,258 @@ const ROLE_SOURCE_RANK = Object.freeze({
9
17
  explicit: 2,
10
18
  "topic-fallback": 1,
11
19
  });
20
+ const TRANSPORT_LOCATOR_SCHEMA_VERSION = "tfx.transport-locator.v1";
21
+ const TRANSPORT_KINDS = new Set(["claude-uds", "tmux"]);
22
+ const TRANSPORT_CAPABILITIES = new Set([
23
+ "probe",
24
+ "wake",
25
+ "activate",
26
+ "interrupt",
27
+ ]);
28
+ const HOST_ID_RE = /^hst_[A-Za-z0-9_-]+$/u;
29
+
30
+ function registrationError(code, message = code) {
31
+ const error = new Error(message);
32
+ error.code = code;
33
+ return error;
34
+ }
35
+
36
+ function registrationString(value, code, { maxLength = 256 } = {}) {
37
+ if (typeof value !== "string" || !value.trim() || value.length > maxLength) {
38
+ throw registrationError(code);
39
+ }
40
+ return value.trim();
41
+ }
42
+
43
+ function registrationLogicalRef(value, code, { maxLength = 128 } = {}) {
44
+ const normalized = registrationString(value, code, { maxLength });
45
+ if (!/^[A-Za-z0-9._:-]+$/u.test(normalized)) {
46
+ throw registrationError(code);
47
+ }
48
+ return normalized;
49
+ }
50
+
51
+ function assertExactFields(value, allowed, code) {
52
+ if (
53
+ value === null ||
54
+ typeof value !== "object" ||
55
+ Array.isArray(value) ||
56
+ Object.keys(value).some((field) => !allowed.has(field))
57
+ ) {
58
+ throw registrationError(code);
59
+ }
60
+ }
61
+
62
+ function normalizeTransportLocator(
63
+ value,
64
+ { cli, hostId, now, leaseExpiresMs },
65
+ ) {
66
+ assertExactFields(
67
+ value,
68
+ new Set([
69
+ "schema_version",
70
+ "transport_id",
71
+ "kind",
72
+ "host_id",
73
+ "capabilities",
74
+ "priority",
75
+ "expires_at_ms",
76
+ "locator",
77
+ ]),
78
+ "TRANSPORT_LOCATOR_INVALID",
79
+ );
80
+ if (value.schema_version !== TRANSPORT_LOCATOR_SCHEMA_VERSION) {
81
+ throw registrationError("TRANSPORT_LOCATOR_SCHEMA_UNSUPPORTED");
82
+ }
83
+ const transportId = registrationLogicalRef(
84
+ value.transport_id,
85
+ "TRANSPORT_ID_INVALID",
86
+ { maxLength: 128 },
87
+ );
88
+ if (!TRANSPORT_KINDS.has(value.kind)) {
89
+ throw registrationError("TRANSPORT_KIND_UNSUPPORTED");
90
+ }
91
+ if (cli === "codex" && value.kind !== "tmux") {
92
+ throw registrationError("CODEX_TRANSPORT_UNSUPPORTED");
93
+ }
94
+ const locatorHostId = registrationString(value.host_id, "HOST_ID_INVALID", {
95
+ maxLength: 128,
96
+ });
97
+ if (!HOST_ID_RE.test(locatorHostId) || (hostId && locatorHostId !== hostId)) {
98
+ throw registrationError("HOST_ID_INVALID");
99
+ }
100
+ if (!Array.isArray(value.capabilities)) {
101
+ throw registrationError("TRANSPORT_CAPABILITIES_INVALID");
102
+ }
103
+ const capabilities = uniqueStrings(value.capabilities);
104
+ if (
105
+ capabilities.length !== value.capabilities.length ||
106
+ capabilities.some((capability) => !TRANSPORT_CAPABILITIES.has(capability))
107
+ ) {
108
+ throw registrationError("TRANSPORT_CAPABILITIES_INVALID");
109
+ }
110
+ if (!Number.isSafeInteger(value.priority)) {
111
+ throw registrationError("TRANSPORT_PRIORITY_INVALID");
112
+ }
113
+ if (
114
+ !Number.isSafeInteger(value.expires_at_ms) ||
115
+ value.expires_at_ms <= now ||
116
+ value.expires_at_ms > leaseExpiresMs
117
+ ) {
118
+ throw registrationError("TRANSPORT_EXPIRY_INVALID");
119
+ }
120
+
121
+ let locator;
122
+ if (value.kind === "claude-uds") {
123
+ assertExactFields(
124
+ value.locator,
125
+ new Set(["session_id", "short", "bridge_id"]),
126
+ "TRANSPORT_LOCATOR_INVALID",
127
+ );
128
+ const sessionId = value.locator.session_id;
129
+ const short = value.locator.short;
130
+ if (!sessionId && !short) {
131
+ throw registrationError("CLAUDE_UDS_SESSION_REQUIRED");
132
+ }
133
+ locator = {
134
+ ...(sessionId
135
+ ? {
136
+ session_id: registrationLogicalRef(
137
+ sessionId,
138
+ "CLAUDE_UDS_SESSION_INVALID",
139
+ ),
140
+ }
141
+ : {}),
142
+ ...(short
143
+ ? {
144
+ short: registrationLogicalRef(short, "CLAUDE_UDS_SHORT_INVALID", {
145
+ maxLength: 128,
146
+ }),
147
+ }
148
+ : {}),
149
+ bridge_id: registrationLogicalRef(
150
+ value.locator.bridge_id,
151
+ "CLAUDE_UDS_BRIDGE_INVALID",
152
+ { maxLength: 128 },
153
+ ),
154
+ };
155
+ } else {
156
+ assertExactFields(
157
+ value.locator,
158
+ new Set(["session_name", "mux_server_id"]),
159
+ "TRANSPORT_LOCATOR_INVALID",
160
+ );
161
+ locator = {
162
+ session_name: registrationLogicalRef(
163
+ value.locator.session_name,
164
+ "TMUX_SESSION_INVALID",
165
+ { maxLength: 128 },
166
+ ),
167
+ mux_server_id: registrationLogicalRef(
168
+ value.locator.mux_server_id,
169
+ "TMUX_SERVER_INVALID",
170
+ { maxLength: 128 },
171
+ ),
172
+ };
173
+ }
174
+
175
+ return {
176
+ schema_version: TRANSPORT_LOCATOR_SCHEMA_VERSION,
177
+ transport_id: transportId,
178
+ kind: value.kind,
179
+ host_id: locatorHostId,
180
+ capabilities,
181
+ priority: value.priority,
182
+ expires_at_ms: value.expires_at_ms,
183
+ locator,
184
+ };
185
+ }
186
+
187
+ export function normalizeRegistrationIdentity(args = {}, now = Date.now()) {
188
+ const hasProjectId = Object.hasOwn(args, "project_id");
189
+ const hasSessionId = Object.hasOwn(args, "session_id");
190
+ const hasHostId = Object.hasOwn(args, "host_id");
191
+ const hasLocatorJson = Object.hasOwn(args, "transport_locators_json");
192
+ const hasLocators = Object.hasOwn(args, "transport_locators");
193
+ if (
194
+ !hasProjectId &&
195
+ !hasSessionId &&
196
+ !hasHostId &&
197
+ !hasLocatorJson &&
198
+ !hasLocators
199
+ ) {
200
+ return null;
201
+ }
202
+
203
+ const projectId = hasProjectId
204
+ ? normalizeRoleKey({ project_id: args.project_id, role_kind: "cto" })
205
+ .project_id
206
+ : null;
207
+ const sessionId = hasSessionId
208
+ ? registrationString(args.session_id, "SESSION_ID_INVALID")
209
+ : null;
210
+ const hostId = hasHostId
211
+ ? registrationString(args.host_id, "HOST_ID_INVALID", { maxLength: 128 })
212
+ : null;
213
+ if (hostId && !HOST_ID_RE.test(hostId)) {
214
+ throw registrationError("HOST_ID_INVALID");
215
+ }
216
+
217
+ let rawLocators = [];
218
+ if (hasLocatorJson) {
219
+ if (Array.isArray(args.transport_locators_json)) {
220
+ rawLocators = args.transport_locators_json;
221
+ } else if (typeof args.transport_locators_json === "string") {
222
+ try {
223
+ rawLocators = JSON.parse(args.transport_locators_json);
224
+ } catch {
225
+ throw registrationError("TRANSPORT_LOCATORS_JSON_INVALID");
226
+ }
227
+ } else {
228
+ throw registrationError("TRANSPORT_LOCATORS_JSON_INVALID");
229
+ }
230
+ } else if (hasLocators) {
231
+ rawLocators = args.transport_locators;
232
+ }
233
+ if (!Array.isArray(rawLocators)) {
234
+ throw registrationError("TRANSPORT_LOCATORS_INVALID");
235
+ }
236
+
237
+ const ttlMs = Number(args.heartbeat_ttl_ms ?? 30000);
238
+ if (rawLocators.length > 0 && (!Number.isFinite(ttlMs) || ttlMs <= 0)) {
239
+ throw registrationError("HEARTBEAT_TTL_INVALID");
240
+ }
241
+ const leaseExpiresMs = now + ttlMs;
242
+ const normalizedLocators = rawLocators
243
+ .map((locator) =>
244
+ normalizeTransportLocator(locator, {
245
+ cli: args.cli,
246
+ hostId,
247
+ now,
248
+ leaseExpiresMs,
249
+ }),
250
+ )
251
+ .sort(
252
+ (left, right) =>
253
+ right.priority - left.priority ||
254
+ left.transport_id.localeCompare(right.transport_id),
255
+ );
256
+ const resolvedHostId = hostId || normalizedLocators[0]?.host_id || null;
257
+ if (
258
+ resolvedHostId &&
259
+ normalizedLocators.some((locator) => locator.host_id !== resolvedHostId)
260
+ ) {
261
+ throw registrationError("HOST_ID_INVALID");
262
+ }
263
+
264
+ return {
265
+ project_id: projectId,
266
+ session_id: sessionId,
267
+ host_id: resolvedHostId,
268
+ transport_locators_json:
269
+ hasLocatorJson || hasLocators ? JSON.stringify(normalizedLocators) : null,
270
+ };
271
+ }
12
272
 
13
273
  function uniqueStrings(values = []) {
14
274
  return Array.from(
@@ -49,17 +309,6 @@ function rolePriority(metadata = {}, roleName = "") {
49
309
  return 0;
50
310
  }
51
311
 
52
- function clampAssignDuration(
53
- value,
54
- fallback = 600000,
55
- min = 1000,
56
- max = 86400000,
57
- ) {
58
- const num = Number(value);
59
- if (!Number.isFinite(num)) return fallback;
60
- return Math.max(min, Math.min(Math.trunc(num), max));
61
- }
62
-
63
312
  function normalizeAssignTerminalStatus(input, metadata = {}) {
64
313
  const status = String(input || "")
65
314
  .trim()
@@ -127,6 +376,18 @@ export function createRouter(store) {
127
376
  const queuesByAgent = new Map();
128
377
  const liveMessages = new Map();
129
378
  const roleStates = new Map();
379
+ const updateRegistrationIdentity = store.db?.prepare
380
+ ? store.db.prepare(`
381
+ UPDATE agents SET
382
+ project_id=COALESCE(@project_id, project_id),
383
+ session_id=COALESCE(@session_id, session_id),
384
+ host_id=COALESCE(@host_id, host_id),
385
+ transport_locators_json=COALESCE(
386
+ @transport_locators_json,
387
+ transport_locators_json
388
+ )
389
+ WHERE agent_id=@agent_id`)
390
+ : null;
130
391
  const MAX_LATENCY_SAMPLES = 100;
131
392
  let latencyIdx = 0;
132
393
  const deliveryLatencies = new Array(MAX_LATENCY_SAMPLES).fill(0);
@@ -606,11 +867,31 @@ export function createRouter(store) {
606
867
  record.message.status = "delivered";
607
868
  store.updateMessageStatus(messageId, "delivered");
608
869
  recordLatency(delivery.delivered_at_ms - record.message.created_at_ms);
870
+ rearmAssignDeadlineOnDispatch(record.message, delivery.delivered_at_ms);
609
871
  return true;
610
872
  }
611
873
  return false;
612
874
  }
613
875
 
876
+ function rearmAssignDeadlineOnDispatch(message, deliveredAtMs) {
877
+ if (message?.payload?.kind !== "assign.job") return;
878
+ const jobId = message.payload.assign_job_id;
879
+ if (!jobId) return;
880
+ const job = store.getAssign(jobId);
881
+ if (
882
+ !job ||
883
+ !ASSIGN_PENDING_STATUSES.has(job.status) ||
884
+ job.dispatched_at_ms
885
+ )
886
+ return;
887
+ if (Number(message.payload.attempt) !== job.attempt) return;
888
+ store.updateAssignStatus(job.job_id, job.status, {
889
+ dispatched_at_ms: deliveredAtMs,
890
+ deadline_ms:
891
+ deliveredAtMs + clampDurationMs(job.timeout_ms, job.timeout_ms),
892
+ });
893
+ }
894
+
614
895
  function ackMessages(ids, agentId) {
615
896
  const now = Date.now();
616
897
  let count = 0;
@@ -706,7 +987,7 @@ export function createRouter(store) {
706
987
  to: job.supervisor_agent,
707
988
  topic: "assign.result",
708
989
  priority: Math.max(5, job.priority || 5),
709
- ttl_ms: job.ttl_ms || job.timeout_ms || 600000,
990
+ ttl_ms: job.ttl_ms || job.timeout_ms || DEFAULT_ASSIGN_TTL_MS,
710
991
  payload: {
711
992
  event,
712
993
  ...buildAssignSnapshot(job),
@@ -725,7 +1006,7 @@ export function createRouter(store) {
725
1006
  to: job.worker_agent,
726
1007
  topic: job.topic || "assign.job",
727
1008
  priority: job.priority || 5,
728
- ttl_ms: job.ttl_ms || job.timeout_ms || 600000,
1009
+ ttl_ms: job.ttl_ms || job.timeout_ms || DEFAULT_ASSIGN_TTL_MS,
729
1010
  payload: {
730
1011
  kind: "assign.job",
731
1012
  reason,
@@ -797,6 +1078,24 @@ export function createRouter(store) {
797
1078
  const timedOut = store.updateAssignStatus(job.job_id, "timed_out", {
798
1079
  error: job.error ?? { message: "assign job timed out" },
799
1080
  });
1081
+ if (timedOut?.worker_agent) {
1082
+ dispatchMessage({
1083
+ type: "event",
1084
+ from: "assign-router",
1085
+ to: timedOut.worker_agent,
1086
+ topic: "assign.cancel",
1087
+ priority: 8,
1088
+ ttl_ms: clampDurationMs(timedOut.ttl_ms, DEFAULT_ASSIGN_TTL_MS),
1089
+ payload: {
1090
+ kind: "assign.cancel",
1091
+ assign_job_id: timedOut.job_id,
1092
+ attempt: timedOut.attempt,
1093
+ reason: "timed_out",
1094
+ },
1095
+ trace_id: timedOut.trace_id,
1096
+ correlation_id: timedOut.correlation_id,
1097
+ });
1098
+ }
800
1099
 
801
1100
  if (timedOut.retry_count < timedOut.max_retries) {
802
1101
  return scheduleAssignRetry(
@@ -821,10 +1120,17 @@ export function createRouter(store) {
821
1120
  deliveryEmitter,
822
1121
 
823
1122
  registerAgent(args) {
1123
+ const identity = normalizeRegistrationIdentity(args);
824
1124
  const result = store.registerAgent(args);
825
1125
  if (!registerPresenceIsEffective(result)) {
826
1126
  return registerPresenceFailure(args.agent_id, result);
827
1127
  }
1128
+ if (identity && updateRegistrationIdentity) {
1129
+ updateRegistrationIdentity.run({
1130
+ agent_id: args.agent_id,
1131
+ ...identity,
1132
+ });
1133
+ }
828
1134
  upsertRuntimeTopics(args.agent_id, args.topics || [], { replace: true });
829
1135
  refreshRoleCandidateForAgent(args.agent_id);
830
1136
  for (const roleName of ROLE_TOPICS) {
@@ -1146,7 +1452,7 @@ export function createRouter(store) {
1146
1452
  acceptance_criteria,
1147
1453
  context_refs,
1148
1454
  priority = 5,
1149
- ttl_ms = 600000,
1455
+ ttl_ms = DEFAULT_HANDOFF_TTL_MS,
1150
1456
  trace_id,
1151
1457
  correlation_id,
1152
1458
  }) {
@@ -1174,8 +1480,8 @@ export function createRouter(store) {
1174
1480
  task = "",
1175
1481
  payload = {},
1176
1482
  priority = 5,
1177
- ttl_ms = 600000,
1178
- timeout_ms = 600000,
1483
+ ttl_ms = DEFAULT_ASSIGN_TTL_MS,
1484
+ timeout_ms = DEFAULT_ASSIGN_TIMEOUT_MS,
1179
1485
  max_retries = 0,
1180
1486
  trace_id,
1181
1487
  correlation_id,
@@ -1232,16 +1538,6 @@ export function createRouter(store) {
1232
1538
  },
1233
1539
  };
1234
1540
  }
1235
- if (Number.isFinite(Number(attempt)) && Number(attempt) !== job.attempt) {
1236
- return {
1237
- ok: false,
1238
- error: {
1239
- code: "ASSIGN_ATTEMPT_MISMATCH",
1240
- message: `stale assign result for attempt ${attempt} (current ${job.attempt})`,
1241
- },
1242
- };
1243
- }
1244
-
1245
1541
  const mergedMetadata = {
1246
1542
  ...(payload?.metadata || {}),
1247
1543
  ...(metadata || {}),
@@ -1255,11 +1551,37 @@ export function createRouter(store) {
1255
1551
  (Object.hasOwn(payload || {}, "result") ? payload.result : payload);
1256
1552
  const nextError = error ?? payload?.error ?? null;
1257
1553
 
1554
+ if (Number.isFinite(Number(attempt)) && Number(attempt) !== job.attempt) {
1555
+ const preserved = notifyAssignSupervisor(job, "late_result", {
1556
+ stale_attempt: Number(attempt),
1557
+ late_status: normalizedStatus,
1558
+ late_result: nextResult,
1559
+ late_error: nextError,
1560
+ reported_by: worker_agent || job.worker_agent,
1561
+ });
1562
+ return {
1563
+ ok: false,
1564
+ error: {
1565
+ code: "ASSIGN_ATTEMPT_MISMATCH",
1566
+ message: `stale assign result for attempt ${attempt} (current ${job.attempt})`,
1567
+ details: {
1568
+ preserved: Boolean(preserved),
1569
+ preserved_message_id: preserved?.id ?? null,
1570
+ },
1571
+ },
1572
+ };
1573
+ }
1574
+
1258
1575
  if (normalizedStatus === "running") {
1576
+ const nowMs = Date.now();
1577
+ const ceilingAtMs =
1578
+ (job.dispatched_at_ms || job.created_at_ms) + resolveHardCeilingMs();
1259
1579
  const running = store.updateAssignStatus(job.job_id, "running", {
1260
- started_at_ms: job.started_at_ms || Date.now(),
1261
- deadline_ms:
1262
- Date.now() + clampAssignDuration(job.timeout_ms, job.timeout_ms),
1580
+ started_at_ms: job.started_at_ms || nowMs,
1581
+ deadline_ms: Math.min(
1582
+ nowMs + clampDurationMs(job.timeout_ms, job.timeout_ms),
1583
+ ceilingAtMs,
1584
+ ),
1263
1585
  result: nextResult,
1264
1586
  error: nextError,
1265
1587
  });
@@ -1328,12 +1650,44 @@ export function createRouter(store) {
1328
1650
  sweepExpired() {
1329
1651
  const now = Date.now();
1330
1652
  let expired = 0;
1653
+ const deadLettered = [];
1331
1654
  for (const [messageId, record] of Array.from(liveMessages.entries())) {
1332
1655
  if (record.message.expires_at_ms > now) continue;
1333
1656
  store.moveToDeadLetter(messageId, "ttl_expired", null);
1657
+ deadLettered.push(record.message);
1334
1658
  removeMessage(messageId);
1335
1659
  expired += 1;
1336
1660
  }
1661
+ for (const message of deadLettered) {
1662
+ if (
1663
+ !["handoff", "request"].includes(message.type) ||
1664
+ message.payload?.kind === "assign.job"
1665
+ )
1666
+ continue;
1667
+ if (
1668
+ !message.from_agent ||
1669
+ String(message.from_agent).startsWith("topic:")
1670
+ )
1671
+ continue;
1672
+ dispatchMessage({
1673
+ type: "event",
1674
+ from: "hub-sweeper",
1675
+ to: message.from_agent,
1676
+ topic: "handoff.dead_letter",
1677
+ priority: 7,
1678
+ ttl_ms: 300000,
1679
+ payload: {
1680
+ kind: "handoff.dead_letter",
1681
+ message_id: message.id,
1682
+ original_topic: message.topic,
1683
+ original_to: message.to_agent,
1684
+ reason: "ttl_expired",
1685
+ task: message.payload?.task ?? null,
1686
+ },
1687
+ trace_id: message.trace_id,
1688
+ correlation_id: message.correlation_id,
1689
+ });
1690
+ }
1337
1691
  return { messages: expired };
1338
1692
  },
1339
1693
 
@@ -464,28 +464,41 @@ export function sendClaudeControlRequest(
464
464
 
465
465
  // claude daemon 은 control.sock 의 mutating op(dispatch 등)에 control-key
466
466
  // 인증을 강제한다 (미제시 시 EAUTH "didn't present the daemon control key").
467
- // key 는 <configDir>/daemon/control.key (configDir 스코프별). 파일이 없으면
468
- // 인증 미강제 daemon 으로 보고 auth 필드를 생략한다 (fail-open — 구버전 호환).
467
+ // OMC runtime config 는 .omc-launch-profile.json 의 sourceConfigDir 에서 key 를
468
+ // 발급한다. control.sock 은 runtime config hash 를 쓰더라도 auth 는 발급 config
469
+ // 에서 읽어야 한다. 일반 daemon 은 <configDir>/daemon/control.key 를 사용한다.
470
+ // 어느 경로에도 파일이 없으면 인증 미강제 daemon 으로 보고 auth 를 생략한다
471
+ // (fail-open — 구버전 호환).
469
472
  export async function readDaemonControlKey(
470
473
  configDir = resolveClaudeConfigDir(),
471
474
  { diagnostics } = {},
472
475
  ) {
473
476
  if (!configDir) return undefined;
474
- const keyPath = path.join(configDir, "daemon", "control.key");
475
- try {
476
- const key = await fs.readFile(keyPath, "utf8");
477
- return key.trim() || undefined;
478
- } catch (error) {
479
- if (error?.code === "ENOENT") return undefined;
480
- if (Array.isArray(diagnostics)) {
481
- diagnostics.push({
482
- code: error?.code || "UNKNOWN",
483
- path: keyPath,
484
- message: error?.message || String(error),
485
- });
477
+
478
+ const runtimeConfigDir = path.resolve(configDir);
479
+ const omcProfile = await readOmcLaunchProfile(runtimeConfigDir);
480
+ const keyConfigDirs = [omcProfile?.sourceConfigDir, runtimeConfigDir].filter(
481
+ (candidate, index, entries) =>
482
+ candidate && entries.indexOf(candidate) === index,
483
+ );
484
+
485
+ for (const keyConfigDir of keyConfigDirs) {
486
+ const keyPath = path.join(keyConfigDir, "daemon", "control.key");
487
+ try {
488
+ const key = await fs.readFile(keyPath, "utf8");
489
+ if (key.trim()) return key.trim();
490
+ } catch (error) {
491
+ if (error?.code === "ENOENT") continue;
492
+ if (Array.isArray(diagnostics)) {
493
+ diagnostics.push({
494
+ code: error?.code || "UNKNOWN",
495
+ path: keyPath,
496
+ message: error?.message || String(error),
497
+ });
498
+ }
486
499
  }
487
- return undefined;
488
500
  }
501
+ return undefined;
489
502
  }
490
503
 
491
504
  export async function buildDaemonControlAuth(configDir, opts = {}) {