@camstack/system 1.2.23 → 1.2.24

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.
@@ -1,5 +1,5 @@
1
1
  const require_chunk = require("./chunk-Cek0wNdY.js");
2
- const require_manifest_python_deps = require("./manifest-python-deps-CqDPWqEE.js");
2
+ const require_manifest_python_deps = require("./manifest-python-deps-BbOZZbVt.js");
3
3
  const require_custom_action_registry = require("./custom-action-registry-jY0NOZK8.js");
4
4
  let node_fs = require("node:fs");
5
5
  node_fs = require_chunk.__toESM(node_fs);
@@ -1,4 +1,4 @@
1
- import { Ct as resolveAddonClass, I as createUdsLoggerWithControl, L as LocalChildClient, at as setWorkerNativeCapsChangeListener, bt as installManifestNativeDeps, i as createUdsAddonContext, nt as getWorkerNativeCapProvider, rt as getWorkerNativeCapSnapshot, st as validateProviderRegistrations, t as installManifestPythonDeps } from "./manifest-python-deps-57PA4rHm.mjs";
1
+ import { Ct as resolveAddonClass, I as createUdsLoggerWithControl, L as LocalChildClient, at as setWorkerNativeCapsChangeListener, bt as installManifestNativeDeps, i as createUdsAddonContext, nt as getWorkerNativeCapProvider, rt as getWorkerNativeCapSnapshot, st as validateProviderRegistrations, t as installManifestPythonDeps } from "./manifest-python-deps-C9qhvMUi.mjs";
2
2
  import { t as CustomActionRegistry } from "./custom-action-registry-F__gp_VX.mjs";
3
3
  import { register } from "node:module";
4
4
  import * as fs from "node:fs";
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_chunk = require("./chunk-Cek0wNdY.js");
3
3
  const require_dist = require("./dist-CS0pdC8l.js");
4
4
  const require_model_download_service = require("./model-download-service-hf0ookyy.js");
5
- const require_manifest_python_deps = require("./manifest-python-deps-CqDPWqEE.js");
5
+ const require_manifest_python_deps = require("./manifest-python-deps-BbOZZbVt.js");
6
6
  const require_resource_monitor = require("./resource-monitor-DNNomR-i.js");
7
7
  const require_builtins_sqlite_storage_filesystem_storage_addon = require("./builtins/sqlite-storage/filesystem-storage.addon.js");
8
8
  const require_builtins_sqlite_storage_sqlite_settings_addon = require("./builtins/sqlite-storage/sqlite-settings.addon.js");
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { a as __toCommonJS, i as __require, n as __esmMin, o as __toESM$1, r as __exportAll, t as __commonJSMin$1 } from "./chunk-CNf5ZN-e.mjs";
2
2
  import { B as logLevelAtMost, Dt as readinessKey, Et as parseJsonUnknown$1, F as isVoidInput, I as kebabToCamel, L as lifecycleJobSchema, M as isArrayOutputSchema, N as isCollectionArrayMethod, Ot as resolveCapMount, P as isObjectInput, Tt as parseJsonObject, V as looseSchema, W as objectInputDeclaresAddonId, _t as asString$1, c as RUNTIME_DEFAULTS, ct as DEVICE_STATUS_METHOD, ft as ReadinessRegistry, gt as asNumber, ht as asJsonObject$1, it as errMsg$1, jt as EventCategory$1, k as extractNestedAddonId, kt as scopeKey, m as addonSettingsCapability, ot as DATAPLANE_SECRET_HEADER$1, pt as ReadinessTimeoutError, q as procedureAuthKey, s as METHOD_ACCESS_MAP, st as DEVICE_SETTINGS_CONTRIBUTION_METHODS, t as ALL_CAPABILITY_DEFINITIONS, vt as createEvent, xt as expandCapMethods, yt as emitDownForOwnedCaps } from "./dist-CIek6F9X.mjs";
3
3
  import { a as downloadModel, c as getModelFilePath, d as contentTypeFor, f as createAuthenticatedFileServer, h as resolveFilePath, i as downloadFile, l as isModelDownloaded, m as parseTokenizedUrl, n as collectModelFiles, o as ensureModel, p as parseRangeHeader, r as deleteModelFromDisk, s as fetchJson, t as ModelDownloadService, u as createFileDataPlaneHandler } from "./model-download-service-Cp9f4dk6.mjs";
4
- import { $ as buildNativeCapProxy, A as createHubCapForwardService, B as AGENT_CAP_FWD_ACTION, C as brokerTransportLink, Ct as resolveAddonClass, D as localProviderLink, E as ipcParentLink, F as createUdsLogger, G as callWithServiceDiscovery, H as CapRouteResolver, I as createUdsLoggerWithControl, J as UdsLocalTransportServer, K as createLocalTransport, L as LocalChildClient, M as createUdsEventBridge, N as createUdsEventBus, O as HUB_CAP_FWD_ACTION, P as udsChildLogToWorkerEntry, Q as encodeFrame, R as LocalChildRegistry, S as brokerCallForCap, St as runNpm, T as ipcChildLink, U as CapRouteError, V as AGENT_CAP_FWD_SERVICE, W as classifyCapRoute, X as localEndpointPath, Y as SocketChannel, Z as FrameDecoder, _ as createKernelHwAccel, _t as CapabilityHandle, a as getWorkerDeviceRegistry, b as __resetCapUsageRegistryForTests, bt as installManifestNativeDeps, c as setHubConnected, ct as NATIVE_PROVIDER_SERVICE_INFIX, d as getBrokerEventBus, dt as capBareAction, et as buildUdsNativeCapProxy, f as getMoleculerEventStats, ft as capServiceName, g as AddonDepsManager, gt as DeviceRegistry, h as subscribePassthrough, ht as serializeTypedArrays, i as createUdsAddonContext, it as mountNativeCapService, j as createParentUnownedCallHandler, k as HUB_CAP_FWD_SERVICE, l as EVENT_TOPIC_PREFIX, lt as capActionName, m as setNodeEventInterest, mt as deserializeTypedArrays, n as adaptBrokerToCluster, o as getOrInitReadinessRegistry, ot as createAddonService, p as registerEventBusService, pt as parseCapAction, q as UdsLocalTransportClient, r as createAddonContext, s as getOrInitReadinessRegistryForClient, st as validateProviderRegistrations, t as installManifestPythonDeps, tt as createBrokerDeviceManagerApi, u as clusterEventTopic, ut as capActionSuffix, v as resolveHwAccel, vt as CapabilityUnavailableError, w as buildLinkChain, wt as createAddonDataPlaneFacility, x as getCapUsageRegistry, xt as resolveNpmInvocation, y as CapUsageRegistry, yt as copyBundledNativeModules, z as UDS_NO_ROUTE_PREFIX } from "./manifest-python-deps-57PA4rHm.mjs";
4
+ import { $ as buildNativeCapProxy, A as createHubCapForwardService, B as AGENT_CAP_FWD_ACTION, C as brokerTransportLink, Ct as resolveAddonClass, D as localProviderLink, E as ipcParentLink, F as createUdsLogger, G as callWithServiceDiscovery, H as CapRouteResolver, I as createUdsLoggerWithControl, J as UdsLocalTransportServer, K as createLocalTransport, L as LocalChildClient, M as createUdsEventBridge, N as createUdsEventBus, O as HUB_CAP_FWD_ACTION, P as udsChildLogToWorkerEntry, Q as encodeFrame, R as LocalChildRegistry, S as brokerCallForCap, St as runNpm, T as ipcChildLink, U as CapRouteError, V as AGENT_CAP_FWD_SERVICE, W as classifyCapRoute, X as localEndpointPath, Y as SocketChannel, Z as FrameDecoder, _ as createKernelHwAccel, _t as CapabilityHandle, a as getWorkerDeviceRegistry, b as __resetCapUsageRegistryForTests, bt as installManifestNativeDeps, c as setHubConnected, ct as NATIVE_PROVIDER_SERVICE_INFIX, d as getBrokerEventBus, dt as capBareAction, et as buildUdsNativeCapProxy, f as getMoleculerEventStats, ft as capServiceName, g as AddonDepsManager, gt as DeviceRegistry, h as subscribePassthrough, ht as serializeTypedArrays, i as createUdsAddonContext, it as mountNativeCapService, j as createParentUnownedCallHandler, k as HUB_CAP_FWD_SERVICE, l as EVENT_TOPIC_PREFIX, lt as capActionName, m as setNodeEventInterest, mt as deserializeTypedArrays, n as adaptBrokerToCluster, o as getOrInitReadinessRegistry, ot as createAddonService, p as registerEventBusService, pt as parseCapAction, q as UdsLocalTransportClient, r as createAddonContext, s as getOrInitReadinessRegistryForClient, st as validateProviderRegistrations, t as installManifestPythonDeps, tt as createBrokerDeviceManagerApi, u as clusterEventTopic, ut as capActionSuffix, v as resolveHwAccel, vt as CapabilityUnavailableError, w as buildLinkChain, wt as createAddonDataPlaneFacility, x as getCapUsageRegistry, xt as resolveNpmInvocation, y as CapUsageRegistry, yt as copyBundledNativeModules, z as UDS_NO_ROUTE_PREFIX } from "./manifest-python-deps-C9qhvMUi.mjs";
5
5
  import { n as getSinglePidStats, t as getPidStats } from "./resource-monitor-BkP504Vq.mjs";
6
6
  import { FilesystemStorageAddon, t as FilesystemStorageProvider } from "./builtins/sqlite-storage/filesystem-storage.addon.mjs";
7
7
  import { SqliteSettingsAddon, t as SqliteSettingsBackend } from "./builtins/sqlite-storage/sqlite-settings.addon.mjs";
@@ -95,6 +95,18 @@ export interface LocalChildRegistryOptions {
95
95
  * everything). The kernel has no cap registry, so the wiring side injects it.
96
96
  */
97
97
  readonly isAggregatedCollectionMethod?: (capName: string, method: string) => boolean;
98
+ /**
99
+ * Per-cap-method bound for UDS cap calls, in ms — read from the cap
100
+ * definition's declared `timeoutMs`. The SAME resolver the remote plane uses
101
+ * (`CapRouteResolver.capTimeoutMs`), so the two planes agree by construction
102
+ * instead of by coincidence.
103
+ *
104
+ * Without this the channel default (60 s) would apply to everything, and a
105
+ * legitimately long method — `applyServerUpdate` declares 16 min because it
106
+ * awaits a full npm install — would be cut off mid-run. Omitted ⇒ the
107
+ * channel default for every cap call.
108
+ */
109
+ readonly capTimeoutMs?: (capName: string, method: string) => number | undefined;
98
110
  }
99
111
  /**
100
112
  * Parent-side authority for local addon-runners reachable over a
@@ -118,6 +130,8 @@ export declare class LocalChildRegistry {
118
130
  private readonly ownNodeId?;
119
131
  /** See {@link LocalChildRegistryOptions.isAggregatedCollectionMethod}. */
120
132
  private readonly isAggregatedCollectionMethod?;
133
+ /** See {@link LocalChildRegistryOptions.capTimeoutMs}. */
134
+ private readonly capTimeoutMs?;
121
135
  /** Tracks capNames already logged as UDS-routed; one INFO line per capName per process. */
122
136
  private readonly egressRoutedCaps;
123
137
  /** Active event fan-out mode, read once from `CAMSTACK_UDS_EVENT_FANOUT`. */
@@ -227,6 +241,12 @@ export declare class LocalChildRegistry {
227
241
  */
228
242
  isChildKnown(childId: string): boolean;
229
243
  childProvides(childId: string, capName: string, deviceId?: number): boolean;
244
+ /**
245
+ * Per-request options for a cap call: the cap method's declared `timeoutMs`
246
+ * when one exists, else `undefined` so the channel applies its default.
247
+ * Mirrors `CapRouteResolver.callTimeout` for the remote plane.
248
+ */
249
+ private capRequestOpts;
230
250
  /** Forward a cap method call to a SPECIFIC child by id over UDS; rejects if that child is absent. */
231
251
  callCapOnChild(childId: string, input: CapCallInput): Promise<unknown>;
232
252
  /** Forward a cap method call to the owning child over UDS; rejects if none. */
@@ -19,9 +19,19 @@ export type Frame = {
19
19
  };
20
20
  export type RequestHandler = (body: unknown) => Promise<unknown>;
21
21
  export type EventHandler = (body: unknown) => void;
22
+ /** Per-request options a {@link LocalChannel} implementation may honour. */
23
+ export interface LocalRequestOptions {
24
+ /**
25
+ * Bound on this request in ms; `0` or negative disables it. Omitted ⇒ the
26
+ * implementation's default. Cap calls pass the cap method's declared
27
+ * `timeoutMs` so a legitimately long method (`applyServerUpdate`, 16 min) is
28
+ * not cut short.
29
+ */
30
+ readonly timeoutMs?: number;
31
+ }
22
32
  /** A bidirectional request/response + one-way event channel over one socket. */
23
33
  export interface LocalChannel {
24
- request(body: unknown): Promise<unknown>;
34
+ request(body: unknown, opts?: LocalRequestOptions): Promise<unknown>;
25
35
  emit(body: unknown): void;
26
36
  onRequest(handler: RequestHandler): void;
27
37
  onEvent(handler: EventHandler): void;
@@ -1,5 +1,21 @@
1
1
  import { Socket } from 'node:net';
2
2
  import { LocalChannel, RequestHandler, EventHandler } from './local-transport.js';
3
+ /**
4
+ * Default bound on one UDS request, when the caller names none.
5
+ *
6
+ * Chosen to match `REMOTE_CALL_TIMEOUT_MS` in `cap-route-resolver.ts`: the SAME
7
+ * cap method already dies at 60 s when it resolves to a remote node, so
8
+ * applying the same bound locally makes the two planes consistent rather than
9
+ * newly restrictive. Callers with a legitimately longer method (the cap
10
+ * layer's declared `timeoutMs`, e.g. `applyServerUpdate` at 16 min) pass it
11
+ * explicitly.
12
+ */
13
+ export declare const DEFAULT_UDS_REQUEST_TIMEOUT_MS = 60000;
14
+ /** Per-request options for {@link SocketChannel.request}. */
15
+ export interface SocketRequestOptions {
16
+ /** Bound in ms; `0` or negative disables the timer entirely. */
17
+ readonly timeoutMs?: number;
18
+ }
3
19
  export declare class SocketChannel implements LocalChannel {
4
20
  private readonly socket;
5
21
  private nextId;
@@ -11,7 +27,18 @@ export declare class SocketChannel implements LocalChannel {
11
27
  private closed;
12
28
  private closeFired;
13
29
  constructor(socket: Socket);
14
- request(body: unknown): Promise<unknown>;
30
+ /**
31
+ * Send a request and wait for the peer's response.
32
+ *
33
+ * Bounded. Without a timer this settles on exactly two events — a matching
34
+ * `res` frame, or the socket's `close` (which drains `pending`) — so a peer
35
+ * that receives the request and then neither answers nor closes leaves the
36
+ * caller pending forever. That is a wedged handler, or a half-open socket
37
+ * after a child died without a clean teardown, and it is how camera 615 spent
38
+ * 30+ minutes with every inference permit held by a call that was never
39
+ * coming back.
40
+ */
41
+ request(body: unknown, opts?: SocketRequestOptions): Promise<unknown>;
15
42
  emit(body: unknown): void;
16
43
  /** Replace the request handler. Only one handler is active at a time. */
17
44
  onRequest(handler: RequestHandler): void;
@@ -3526,8 +3526,6 @@ function localEndpointPath(nodeId) {
3526
3526
  if (process.platform === "win32") return `\\\\.\\pipe\\camstack-${safe}`;
3527
3527
  return (0, node_path.join)((0, node_os.tmpdir)(), `camstack-${safe}.sock`);
3528
3528
  }
3529
- //#endregion
3530
- //#region src/kernel/transport/socket-channel.ts
3531
3529
  var SocketChannel = class {
3532
3530
  socket;
3533
3531
  nextId = 1;
@@ -3546,13 +3544,34 @@ var SocketChannel = class {
3546
3544
  socket.on("close", () => this.handleClose());
3547
3545
  socket.on("error", () => {});
3548
3546
  }
3549
- request(body) {
3547
+ /**
3548
+ * Send a request and wait for the peer's response.
3549
+ *
3550
+ * Bounded. Without a timer this settles on exactly two events — a matching
3551
+ * `res` frame, or the socket's `close` (which drains `pending`) — so a peer
3552
+ * that receives the request and then neither answers nor closes leaves the
3553
+ * caller pending forever. That is a wedged handler, or a half-open socket
3554
+ * after a child died without a clean teardown, and it is how camera 615 spent
3555
+ * 30+ minutes with every inference permit held by a call that was never
3556
+ * coming back.
3557
+ */
3558
+ request(body, opts) {
3550
3559
  if (this.closed) return Promise.reject(/* @__PURE__ */ new Error("channel closed"));
3551
3560
  const id = this.nextId++;
3561
+ const timeoutMs = opts?.timeoutMs ?? 6e4;
3552
3562
  return new Promise((resolve, reject) => {
3563
+ let timer = null;
3564
+ if (timeoutMs > 0) {
3565
+ timer = setTimeout(() => {
3566
+ this.pending.delete(id);
3567
+ reject(/* @__PURE__ */ new Error(`UDS request timed out after ${timeoutMs}ms`));
3568
+ }, timeoutMs);
3569
+ timer.unref?.();
3570
+ }
3553
3571
  this.pending.set(id, {
3554
3572
  resolve,
3555
- reject
3573
+ reject,
3574
+ timer
3556
3575
  });
3557
3576
  this.send({
3558
3577
  k: "req",
@@ -3616,6 +3635,7 @@ var SocketChannel = class {
3616
3635
  const p = this.pending.get(frame.id);
3617
3636
  if (!p) return;
3618
3637
  this.pending.delete(frame.id);
3638
+ if (p.timer !== null) clearTimeout(p.timer);
3619
3639
  if (frame.ok) p.resolve(frame.body);
3620
3640
  else p.reject(new Error(frame.err));
3621
3641
  return;
@@ -3626,7 +3646,10 @@ var SocketChannel = class {
3626
3646
  if (this.closeFired) return;
3627
3647
  this.closeFired = true;
3628
3648
  this.closed = true;
3629
- for (const [, p] of this.pending) p.reject(/* @__PURE__ */ new Error("channel closed"));
3649
+ for (const [, p] of this.pending) {
3650
+ if (p.timer !== null) clearTimeout(p.timer);
3651
+ p.reject(/* @__PURE__ */ new Error("channel closed"));
3652
+ }
3630
3653
  this.pending.clear();
3631
3654
  this.closeHandler();
3632
3655
  }
@@ -4305,6 +4328,8 @@ var LocalChildRegistry = class {
4305
4328
  ownNodeId;
4306
4329
  /** See {@link LocalChildRegistryOptions.isAggregatedCollectionMethod}. */
4307
4330
  isAggregatedCollectionMethod;
4331
+ /** See {@link LocalChildRegistryOptions.capTimeoutMs}. */
4332
+ capTimeoutMs;
4308
4333
  /** Tracks capNames already logged as UDS-routed; one INFO line per capName per process. */
4309
4334
  egressRoutedCaps = /* @__PURE__ */ new Set();
4310
4335
  /** Active event fan-out mode, read once from `CAMSTACK_UDS_EVENT_FANOUT`. */
@@ -4334,6 +4359,7 @@ var LocalChildRegistry = class {
4334
4359
  this.resolveChildIdForAddon = opts.resolveChildIdForAddon;
4335
4360
  this.ownNodeId = opts.ownNodeId;
4336
4361
  this.isAggregatedCollectionMethod = opts.isAggregatedCollectionMethod;
4362
+ this.capTimeoutMs = opts.capTimeoutMs;
4337
4363
  } else {
4338
4364
  this.server = serverOrOptions;
4339
4365
  this.onUnownedCall = onUnownedCallArg;
@@ -4496,11 +4522,20 @@ var LocalChildRegistry = class {
4496
4522
  if (deviceId !== void 0 && entry.caps.some((cap) => cap.capName === capName && cap.deviceId === deviceId)) return true;
4497
4523
  return entry.caps.some((cap) => cap.capName === capName && cap.deviceId === void 0);
4498
4524
  }
4525
+ /**
4526
+ * Per-request options for a cap call: the cap method's declared `timeoutMs`
4527
+ * when one exists, else `undefined` so the channel applies its default.
4528
+ * Mirrors `CapRouteResolver.callTimeout` for the remote plane.
4529
+ */
4530
+ capRequestOpts(input) {
4531
+ const declared = this.capTimeoutMs?.(input.capName, input.method);
4532
+ return typeof declared === "number" ? { timeoutMs: declared } : void 0;
4533
+ }
4499
4534
  /** Forward a cap method call to a SPECIFIC child by id over UDS; rejects if that child is absent. */
4500
4535
  async callCapOnChild(childId, input) {
4501
4536
  const entry = this.children.get(childId);
4502
4537
  if (!entry) throw new Error(`no local child "${childId}" for cap "${input.capName}"`);
4503
- return entry.channel.request(this.toCapCall(input));
4538
+ return entry.channel.request(this.toCapCall(input), this.capRequestOpts(input));
4504
4539
  }
4505
4540
  /** Forward a cap method call to the owning child over UDS; rejects if none. */
4506
4541
  async callCap(input) {
@@ -4510,7 +4545,7 @@ var LocalChildRegistry = class {
4510
4545
  const where = input.deviceId === void 0 ? "" : ` for device ${input.deviceId}`;
4511
4546
  throw new Error(`no local child provides cap "${input.capName}"${where}`);
4512
4547
  }
4513
- return entry.channel.request(this.toCapCall(input));
4548
+ return entry.channel.request(this.toCapCall(input), this.capRequestOpts(input));
4514
4549
  }
4515
4550
  /**
4516
4551
  * Forward an ADDON-LEVEL call (routes / custom-action) to a SPECIFIC child
@@ -3524,8 +3524,6 @@ function localEndpointPath(nodeId) {
3524
3524
  if (process.platform === "win32") return `\\\\.\\pipe\\camstack-${safe}`;
3525
3525
  return join(tmpdir(), `camstack-${safe}.sock`);
3526
3526
  }
3527
- //#endregion
3528
- //#region src/kernel/transport/socket-channel.ts
3529
3527
  var SocketChannel = class {
3530
3528
  socket;
3531
3529
  nextId = 1;
@@ -3544,13 +3542,34 @@ var SocketChannel = class {
3544
3542
  socket.on("close", () => this.handleClose());
3545
3543
  socket.on("error", () => {});
3546
3544
  }
3547
- request(body) {
3545
+ /**
3546
+ * Send a request and wait for the peer's response.
3547
+ *
3548
+ * Bounded. Without a timer this settles on exactly two events — a matching
3549
+ * `res` frame, or the socket's `close` (which drains `pending`) — so a peer
3550
+ * that receives the request and then neither answers nor closes leaves the
3551
+ * caller pending forever. That is a wedged handler, or a half-open socket
3552
+ * after a child died without a clean teardown, and it is how camera 615 spent
3553
+ * 30+ minutes with every inference permit held by a call that was never
3554
+ * coming back.
3555
+ */
3556
+ request(body, opts) {
3548
3557
  if (this.closed) return Promise.reject(/* @__PURE__ */ new Error("channel closed"));
3549
3558
  const id = this.nextId++;
3559
+ const timeoutMs = opts?.timeoutMs ?? 6e4;
3550
3560
  return new Promise((resolve, reject) => {
3561
+ let timer = null;
3562
+ if (timeoutMs > 0) {
3563
+ timer = setTimeout(() => {
3564
+ this.pending.delete(id);
3565
+ reject(/* @__PURE__ */ new Error(`UDS request timed out after ${timeoutMs}ms`));
3566
+ }, timeoutMs);
3567
+ timer.unref?.();
3568
+ }
3551
3569
  this.pending.set(id, {
3552
3570
  resolve,
3553
- reject
3571
+ reject,
3572
+ timer
3554
3573
  });
3555
3574
  this.send({
3556
3575
  k: "req",
@@ -3614,6 +3633,7 @@ var SocketChannel = class {
3614
3633
  const p = this.pending.get(frame.id);
3615
3634
  if (!p) return;
3616
3635
  this.pending.delete(frame.id);
3636
+ if (p.timer !== null) clearTimeout(p.timer);
3617
3637
  if (frame.ok) p.resolve(frame.body);
3618
3638
  else p.reject(new Error(frame.err));
3619
3639
  return;
@@ -3624,7 +3644,10 @@ var SocketChannel = class {
3624
3644
  if (this.closeFired) return;
3625
3645
  this.closeFired = true;
3626
3646
  this.closed = true;
3627
- for (const [, p] of this.pending) p.reject(/* @__PURE__ */ new Error("channel closed"));
3647
+ for (const [, p] of this.pending) {
3648
+ if (p.timer !== null) clearTimeout(p.timer);
3649
+ p.reject(/* @__PURE__ */ new Error("channel closed"));
3650
+ }
3628
3651
  this.pending.clear();
3629
3652
  this.closeHandler();
3630
3653
  }
@@ -4303,6 +4326,8 @@ var LocalChildRegistry = class {
4303
4326
  ownNodeId;
4304
4327
  /** See {@link LocalChildRegistryOptions.isAggregatedCollectionMethod}. */
4305
4328
  isAggregatedCollectionMethod;
4329
+ /** See {@link LocalChildRegistryOptions.capTimeoutMs}. */
4330
+ capTimeoutMs;
4306
4331
  /** Tracks capNames already logged as UDS-routed; one INFO line per capName per process. */
4307
4332
  egressRoutedCaps = /* @__PURE__ */ new Set();
4308
4333
  /** Active event fan-out mode, read once from `CAMSTACK_UDS_EVENT_FANOUT`. */
@@ -4332,6 +4357,7 @@ var LocalChildRegistry = class {
4332
4357
  this.resolveChildIdForAddon = opts.resolveChildIdForAddon;
4333
4358
  this.ownNodeId = opts.ownNodeId;
4334
4359
  this.isAggregatedCollectionMethod = opts.isAggregatedCollectionMethod;
4360
+ this.capTimeoutMs = opts.capTimeoutMs;
4335
4361
  } else {
4336
4362
  this.server = serverOrOptions;
4337
4363
  this.onUnownedCall = onUnownedCallArg;
@@ -4494,11 +4520,20 @@ var LocalChildRegistry = class {
4494
4520
  if (deviceId !== void 0 && entry.caps.some((cap) => cap.capName === capName && cap.deviceId === deviceId)) return true;
4495
4521
  return entry.caps.some((cap) => cap.capName === capName && cap.deviceId === void 0);
4496
4522
  }
4523
+ /**
4524
+ * Per-request options for a cap call: the cap method's declared `timeoutMs`
4525
+ * when one exists, else `undefined` so the channel applies its default.
4526
+ * Mirrors `CapRouteResolver.callTimeout` for the remote plane.
4527
+ */
4528
+ capRequestOpts(input) {
4529
+ const declared = this.capTimeoutMs?.(input.capName, input.method);
4530
+ return typeof declared === "number" ? { timeoutMs: declared } : void 0;
4531
+ }
4497
4532
  /** Forward a cap method call to a SPECIFIC child by id over UDS; rejects if that child is absent. */
4498
4533
  async callCapOnChild(childId, input) {
4499
4534
  const entry = this.children.get(childId);
4500
4535
  if (!entry) throw new Error(`no local child "${childId}" for cap "${input.capName}"`);
4501
- return entry.channel.request(this.toCapCall(input));
4536
+ return entry.channel.request(this.toCapCall(input), this.capRequestOpts(input));
4502
4537
  }
4503
4538
  /** Forward a cap method call to the owning child over UDS; rejects if none. */
4504
4539
  async callCap(input) {
@@ -4508,7 +4543,7 @@ var LocalChildRegistry = class {
4508
4543
  const where = input.deviceId === void 0 ? "" : ` for device ${input.deviceId}`;
4509
4544
  throw new Error(`no local child provides cap "${input.capName}"${where}`);
4510
4545
  }
4511
- return entry.channel.request(this.toCapCall(input));
4546
+ return entry.channel.request(this.toCapCall(input), this.capRequestOpts(input));
4512
4547
  }
4513
4548
  /**
4514
4549
  * Forward an ADDON-LEVEL call (routes / custom-action) to a SPECIFIC child
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/system",
3
- "version": "1.2.23",
3
+ "version": "1.2.24",
4
4
  "description": "Core addon for CamStack — builtins, pipeline, process management, auth, logging, events",
5
5
  "keywords": [
6
6
  "camstack",