@holokai/neuron-sdk 0.3.0 → 0.5.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/README.md CHANGED
@@ -21,6 +21,16 @@ pnpm add @holokai/neuron-sdk
21
21
 
22
22
  Runs in **Node 20+** (uses the global `fetch` and `ReadableStream`) and **modern browsers** (Chromium, Firefox, Safari latest). The package has no `node:*` imports — Holokai Desktop / Electron and the in-browser neuron-tester both consume the same SDK build.
23
23
 
24
+ > **Gateway compatibility (protocol 1.1.0, HOL-2926).** Since protocol
25
+ > 1.1.0 the SDK advertises each capability's contract (`description` +
26
+ > input/output JSON Schemas) on the register frame. The gateway validates
27
+ > register frames **strictly**, so a gateway older than HOL-2926 rejects
28
+ > them with `INVALID_FRAME` — **deploy the gateway before publishing or
29
+ > rolling out any 1.1.0-protocol SDK (including neuron-tester OTA)**. A
30
+ > rejected registration leaves the SSE stream open but the neuron
31
+ > lease-less; the SDK surfaces it via the `error` event
32
+ > (`registration rejected by gateway: ...`) so apps can react.
33
+
24
34
  ---
25
35
 
26
36
  ## Quick start
@@ -29,7 +39,7 @@ Runs in **Node 20+** (uses the global `fetch` and `ReadableStream`) and **modern
29
39
  import { Neuron } from '@holokai/neuron-sdk';
30
40
 
31
41
  const neuron = new Neuron({
32
- gatewayUrl: 'https://api.example.com',
42
+ gatewayUrl: 'https://bigbrain.holokai.dev',
33
43
  neuronId: 'my-worker-001', // stable per process/device
34
44
  auth: () => process.env.GATEWAY_TOKEN!, // or an async refresh function
35
45
  });
@@ -75,7 +85,7 @@ import { Neuron } from '@holokai/neuron-sdk';
75
85
  import { createClientCredentialsAuth, defaultCredentialPath } from '@holokai/neuron-sdk/auth';
76
86
 
77
87
  const neuron = new Neuron({
78
- gatewayUrl: 'https://api.example.com',
88
+ gatewayUrl: 'https://bigbrain.holokai.dev',
79
89
  neuronId: 'warehouse-connector-001',
80
90
  auth: createClientCredentialsAuth(defaultCredentialPath()),
81
91
  });
@@ -83,7 +93,7 @@ const neuron = new Neuron({
83
93
 
84
94
  `createClientCredentialsAuth` mints `aud=bigbrain` tokens via the OAuth2 `client_credentials` grant, caches them against the response's `expires_in` (proactive refresh, single-flight), and re-reads the credential file on `invalid_client` so an operator-rotated secret is picked up without a restart. If the credential was revoked in Moku it throws `CredentialRevokedError` — re-enroll to recover.
85
95
 
86
- > `@holokai/neuron-sdk/auth` is **Node-only** (it reads the credential file); the root and `./ipc` entries remain browser-clean. Python SDK parity for enrollment/minting is a planned follow-up in `holokai-neuron-sdk` — until then, Python neurons keep using an externally supplied token.
96
+ > `@holokai/neuron-sdk/auth` is **Node-only** (it reads the credential file); the root and `./ipc` entries remain browser-clean. Python SDK parity ships in `holokai-neuron-sdk` ≥ 0.2.0 (`holokai_neuron_sdk.auth`: `holokai-neuron-sdk enroll` + `create_client_credentials_auth`) — same credential file, interchangeable across both SDKs.
87
97
 
88
98
  ---
89
99
 
@@ -170,7 +180,7 @@ interface NeuronCommonOptions {
170
180
 
171
181
  // Convenience: SDK builds an HttpTransport for you. Most common path.
172
182
  interface NeuronOptionsWithHttp extends NeuronCommonOptions {
173
- gatewayUrl: string; // Base URL, e.g. 'https://api.example.com'
183
+ gatewayUrl: string; // Base URL, e.g. 'https://bigbrain.holokai.dev'
174
184
  auth: AuthCallback; // Token provider (see below)
175
185
  initialReconnectDelayMs?: number; // Default 500ms
176
186
  maxReconnectDelayMs?: number; // Default 30_000 (30s)
@@ -213,6 +223,19 @@ type Handler<TInput = unknown, TOutput = unknown> = (
213
223
 
214
224
  If `inputSchema` is provided and the lease input fails validation, the handler is never called — the SDK sends a `schema-mismatch` nack automatically. Same for `outputSchema` on the return value.
215
225
 
226
+ **Schema precedence.** For each of input and output the SDK picks exactly one validator:
227
+
228
+ 1. The zod schema from `defineCapability`, when the capability was registered that way. JSON Schema can't express `.refine()`, `.transform()`, or `.preprocess()`, so the zod contract stays authoritative and its transforms are what the handler sees and what goes on the wire.
229
+ 2. Otherwise the JSON Schema passed here on `CapabilityRegistration`.
230
+
231
+ A capability registered with neither is not validated client-side — its handler runs on whatever arrives.
232
+
233
+ The `inputSchema` / `outputSchema` that ride on the lease frame are **not** consulted. They can only ever be your own schemas handed back to you — the register frame carries no schemas, so the server's only source is the `POST /plan` handler catalog the client supplies. And the planner doesn't persist those onto task rows, so on real workflow tasks the lease frame's schemas are absent entirely; the only case where they arrive populated is the dev-only `/__dev__/inject-task` route used by neuron-tester, which echoes back what the capability registered.
234
+
235
+ > **This is the only validation that runs.** The gateway's ack-time check validates against that same (empty) `lease.outputSchema`, so it does not currently catch anything. If a capability declares no schema here, its input and output are unchecked end to end.
236
+
237
+ > **Note:** the Go and Python SDKs currently prefer the lease frame's schema and fall back to the registered one. TypeScript deliberately diverges here; the other two are expected to follow.
238
+
216
239
  ---
217
240
 
218
241
  ### `neuron.start()`
@@ -450,7 +473,7 @@ Default transport. SSE for inbound, HTTPS POST for outbound. Reconnects with exp
450
473
  import { Neuron, HttpTransport } from '@holokai/neuron-sdk';
451
474
 
452
475
  const transport = new HttpTransport({
453
- gatewayUrl: 'https://api.example.com',
476
+ gatewayUrl: 'https://bigbrain.holokai.dev',
454
477
  neuronId: 'my-worker-001',
455
478
  auth: () => process.env.GATEWAY_TOKEN!,
456
479
  // optional:
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { J as JsonSchema, C as Capability, a as CapabilityHandleOptions, S as ServerToNeuronFrame, N as NeuronLogger, T as Transport, A as AuthCallback, H as Handler, b as CapabilityRegistration, c as TransportCallbacks, d as ClientFrame, P as PostPath } from './transport-QqkooS7K.mjs';
2
- export { e as HandlerContext, f as HeartbeatInFlightEntry, L as LeasePayload, g as TaskPayload } from './transport-QqkooS7K.mjs';
1
+ import { J as JsonSchema, C as Capability, a as CapabilityHandleOptions, S as ServerToNeuronFrame, N as NeuronLogger, T as Transport, A as AuthCallback, H as Handler, b as CapabilityRegistration, c as TransportCallbacks, d as ClientFrame, P as PostPath } from './transport-Ckcwj-Do.mjs';
2
+ export { e as HandlerContext, f as HeartbeatInFlightEntry, L as LeasePayload, g as TaskPayload } from './transport-Ckcwj-Do.mjs';
3
3
  import { EventEmitter } from 'eventemitter3';
4
4
  import { z } from 'zod';
5
5
  import { ErrorObject } from 'ajv';
@@ -97,6 +97,11 @@ declare function toPlanShape(def: CapabilityDef): CapabilityPlanShape;
97
97
  * required at registration time — if neither the def nor `opts` provides
98
98
  * them, this throws so the misconfiguration surfaces at start time
99
99
  * rather than as a confusing wire-validation error.
100
+ *
101
+ * Since HOL-2926 the frame also carries the contract (`description` +
102
+ * compiled JSON Schemas): the gateway forwards it to Moku's capability
103
+ * catalog so registry-sourced capabilities render with a real contract
104
+ * in the supervisor catalog instead of type-only stubs.
100
105
  */
101
106
  declare function toRegistrationCapability(def: CapabilityDef, opts?: CapabilityHandleOptions): Capability;
102
107
 
@@ -141,7 +146,12 @@ interface NeuronEvents {
141
146
  delayMs: number;
142
147
  reason: string;
143
148
  }) => void;
144
- /** Fired on transport / parse errors. Reconnect handling is automatic. */
149
+ /**
150
+ * Fired on transport / parse errors (reconnect handling is automatic)
151
+ * and on register rejection (HOL-2926 — NOT automatically recovered:
152
+ * the SSE stream stays open, so the neuron sits connected but
153
+ * lease-less until the app reacts, e.g. by calling `stop()`).
154
+ */
145
155
  error: (err: Error) => void;
146
156
  /** Pass-through of every parsed inbound SSE frame. */
147
157
  frame: (frame: ServerToNeuronFrame) => void;
@@ -176,7 +186,7 @@ interface NeuronOptionsWithTransport extends NeuronCommonOptions {
176
186
  * surface so existing call sites keep compiling unchanged.
177
187
  */
178
188
  interface NeuronOptionsWithHttp extends NeuronCommonOptions {
179
- /** Base URL of the gateway, e.g. `https://api.holokai.dev`. */
189
+ /** Base URL of the gateway, e.g. `https://bigbrain.holokai.dev`. */
180
190
  gatewayUrl: string;
181
191
  /** Token provider — see `AuthCallback` for refresh semantics. */
182
192
  auth: AuthCallback;
@@ -276,7 +286,7 @@ declare const SSE_EVENT_NAMES: {
276
286
  };
277
287
 
278
288
  interface HttpTransportOptions {
279
- /** Base URL of the gateway (e.g. `https://api.holokai.dev`). */
289
+ /** Base URL of the gateway (e.g. `https://bigbrain.holokai.dev`). */
280
290
  gatewayUrl: string;
281
291
  /** Stable per-process identifier sent as `?neuronId=` on the SSE GET. */
282
292
  neuronId: string;
@@ -395,12 +405,15 @@ declare class ShutdownError extends Error {
395
405
  * the same schemas so a misbehaving party can't sneak by either side.
396
406
  */
397
407
  declare class SchemaValidator {
408
+ private readonly onCompileError?;
398
409
  private readonly ajv;
399
410
  private readonly cache;
400
- constructor();
411
+ private readonly uncompilable;
412
+ constructor(onCompileError?: ((message: string) => void) | undefined);
401
413
  /**
402
- * Returns null if the value matches the schema (or no schema given);
403
- * otherwise an array of structured errors.
414
+ * Returns null if the value matches the schema, no schema was given, or
415
+ * the schema cannot be compiled in this runtime (fail-open — the gateway
416
+ * remains the enforcement point); otherwise an array of structured errors.
404
417
  */
405
418
  validate(schema: JsonSchema | undefined, data: unknown): ErrorObject[] | null;
406
419
  }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { J as JsonSchema, C as Capability, a as CapabilityHandleOptions, S as ServerToNeuronFrame, N as NeuronLogger, T as Transport, A as AuthCallback, H as Handler, b as CapabilityRegistration, c as TransportCallbacks, d as ClientFrame, P as PostPath } from './transport-QqkooS7K.js';
2
- export { e as HandlerContext, f as HeartbeatInFlightEntry, L as LeasePayload, g as TaskPayload } from './transport-QqkooS7K.js';
1
+ import { J as JsonSchema, C as Capability, a as CapabilityHandleOptions, S as ServerToNeuronFrame, N as NeuronLogger, T as Transport, A as AuthCallback, H as Handler, b as CapabilityRegistration, c as TransportCallbacks, d as ClientFrame, P as PostPath } from './transport-Ckcwj-Do.js';
2
+ export { e as HandlerContext, f as HeartbeatInFlightEntry, L as LeasePayload, g as TaskPayload } from './transport-Ckcwj-Do.js';
3
3
  import { EventEmitter } from 'eventemitter3';
4
4
  import { z } from 'zod';
5
5
  import { ErrorObject } from 'ajv';
@@ -97,6 +97,11 @@ declare function toPlanShape(def: CapabilityDef): CapabilityPlanShape;
97
97
  * required at registration time — if neither the def nor `opts` provides
98
98
  * them, this throws so the misconfiguration surfaces at start time
99
99
  * rather than as a confusing wire-validation error.
100
+ *
101
+ * Since HOL-2926 the frame also carries the contract (`description` +
102
+ * compiled JSON Schemas): the gateway forwards it to Moku's capability
103
+ * catalog so registry-sourced capabilities render with a real contract
104
+ * in the supervisor catalog instead of type-only stubs.
100
105
  */
101
106
  declare function toRegistrationCapability(def: CapabilityDef, opts?: CapabilityHandleOptions): Capability;
102
107
 
@@ -141,7 +146,12 @@ interface NeuronEvents {
141
146
  delayMs: number;
142
147
  reason: string;
143
148
  }) => void;
144
- /** Fired on transport / parse errors. Reconnect handling is automatic. */
149
+ /**
150
+ * Fired on transport / parse errors (reconnect handling is automatic)
151
+ * and on register rejection (HOL-2926 — NOT automatically recovered:
152
+ * the SSE stream stays open, so the neuron sits connected but
153
+ * lease-less until the app reacts, e.g. by calling `stop()`).
154
+ */
145
155
  error: (err: Error) => void;
146
156
  /** Pass-through of every parsed inbound SSE frame. */
147
157
  frame: (frame: ServerToNeuronFrame) => void;
@@ -176,7 +186,7 @@ interface NeuronOptionsWithTransport extends NeuronCommonOptions {
176
186
  * surface so existing call sites keep compiling unchanged.
177
187
  */
178
188
  interface NeuronOptionsWithHttp extends NeuronCommonOptions {
179
- /** Base URL of the gateway, e.g. `https://api.holokai.dev`. */
189
+ /** Base URL of the gateway, e.g. `https://bigbrain.holokai.dev`. */
180
190
  gatewayUrl: string;
181
191
  /** Token provider — see `AuthCallback` for refresh semantics. */
182
192
  auth: AuthCallback;
@@ -276,7 +286,7 @@ declare const SSE_EVENT_NAMES: {
276
286
  };
277
287
 
278
288
  interface HttpTransportOptions {
279
- /** Base URL of the gateway (e.g. `https://api.holokai.dev`). */
289
+ /** Base URL of the gateway (e.g. `https://bigbrain.holokai.dev`). */
280
290
  gatewayUrl: string;
281
291
  /** Stable per-process identifier sent as `?neuronId=` on the SSE GET. */
282
292
  neuronId: string;
@@ -395,12 +405,15 @@ declare class ShutdownError extends Error {
395
405
  * the same schemas so a misbehaving party can't sneak by either side.
396
406
  */
397
407
  declare class SchemaValidator {
408
+ private readonly onCompileError?;
398
409
  private readonly ajv;
399
410
  private readonly cache;
400
- constructor();
411
+ private readonly uncompilable;
412
+ constructor(onCompileError?: ((message: string) => void) | undefined);
401
413
  /**
402
- * Returns null if the value matches the schema (or no schema given);
403
- * otherwise an array of structured errors.
414
+ * Returns null if the value matches the schema, no schema was given, or
415
+ * the schema cannot be compiled in this runtime (fail-open — the gateway
416
+ * remains the enforcement point); otherwise an array of structured errors.
404
417
  */
405
418
  validate(schema: JsonSchema | undefined, data: unknown): ErrorObject[] | null;
406
419
  }
package/dist/index.js CHANGED
@@ -17,6 +17,7 @@ var CapabilityTypeSchema = zod.z.string().min(1).regex(
17
17
  "capability type must be namespaced: {owner}/{package}[/{name}][.{variant}]"
18
18
  );
19
19
  var UserIdSchema = zod.z.string().min(1);
20
+ var JsonSchemaSchema = zod.z.unknown();
20
21
  var CapabilityScopeSchema = zod.z.union([
21
22
  zod.z.literal("any"),
22
23
  zod.z.object({ onBehalfOf: UserIdSchema }).strict()
@@ -25,6 +26,36 @@ var CapabilitySchema = zod.z.object({
25
26
  type: CapabilityTypeSchema,
26
27
  scope: CapabilityScopeSchema,
27
28
  concurrency: zod.z.number().int().positive(),
29
+ /**
30
+ * Plain-language contract advertisement (HOL-2926). All three fields
31
+ * are optional and additive: older neurons keep sending
32
+ * `{ type, scope, concurrency }` and the gateway accepts both shapes.
33
+ * When present they flow (fire-and-forget) to Moku's
34
+ * `capability_catalog` so registry-sourced capabilities render with a
35
+ * real contract in the supervisor catalog instead of type-only stubs.
36
+ *
37
+ * Omit rather than null — the fields are `.optional()`, not
38
+ * `.nullable()`, and a `null` description fails validation.
39
+ */
40
+ description: zod.z.string().optional(),
41
+ /**
42
+ * JSON Schema for the capability's input payload — opaque on the wire,
43
+ * but `null` is rejected: the reconcile push to Moku is last-writer-wins
44
+ * per present key, so a null here could wipe a stored schema.
45
+ * (`description` already rejects null via `z.string()`.)
46
+ */
47
+ // The `: boolean` return annotations stop TS (5.5+) from inferring the
48
+ // lambdas as type predicates, which would narrow the field from
49
+ // `unknown` to `{}` and ripple into every consumer.
50
+ inputSchema: JsonSchemaSchema.refine(
51
+ (v) => v !== null,
52
+ "omit inputSchema instead of sending null"
53
+ ).optional(),
54
+ /** JSON Schema for the capability's output payload — same rules as inputSchema. */
55
+ outputSchema: JsonSchemaSchema.refine(
56
+ (v) => v !== null,
57
+ "omit outputSchema instead of sending null"
58
+ ).optional(),
28
59
  /**
29
60
  * Per-capability lease TTL override in milliseconds (HOL-356).
30
61
  * The gateway uses this when minting a lease's `expiresAt`; absent
@@ -40,7 +71,6 @@ var CapabilitySchema = zod.z.object({
40
71
  */
41
72
  leaseTtlMs: zod.z.number().int().min(1e3).max(60 * 60 * 1e3).optional()
42
73
  }).strict();
43
- var JsonSchemaSchema = zod.z.unknown();
44
74
  var LeaseIdSchema = zod.z.string().min(1);
45
75
  var TaskPayloadSchema = zod.z.object({
46
76
  taskId: zod.z.string().min(1),
@@ -62,9 +92,10 @@ var NeuronIdSchema = zod.z.string().min(1);
62
92
  var RegisterFrameSchema = zod.z.object({
63
93
  type: zod.z.literal("register"),
64
94
  neuronId: NeuronIdSchema,
65
- // Free-form by design — the gateway negotiates compatibility against its
66
- // own PROTOCOL_VERSION (semver), so a v1.1 server can still accept a v1.0
67
- // neuron. Do not pin this to the exported constant.
95
+ // Free-form by design — the gateway records this verbatim and performs
96
+ // no version negotiation; compatibility comes from additive-only frame
97
+ // evolution (see PROTOCOL_VERSION in transport.ts). Do not pin this to
98
+ // the exported constant.
68
99
  protocolVersion: zod.z.string().min(1),
69
100
  capabilities: zod.z.array(CapabilitySchema).min(1).refine(
70
101
  (caps) => new Set(caps.map((c) => c.type)).size === caps.length,
@@ -178,7 +209,7 @@ zod.z.discriminatedUnion("type", [
178
209
  ]);
179
210
 
180
211
  // ../neuron-protocol/src/transport.ts
181
- var PROTOCOL_VERSION = "1.0.0";
212
+ var PROTOCOL_VERSION = "1.1.0";
182
213
  var SSE_PATH = "/neuron/events";
183
214
  var SSE_EVENT = {
184
215
  registered: "registered",
@@ -233,6 +264,12 @@ function toRegistrationCapability(def, opts) {
233
264
  type: def.type,
234
265
  scope,
235
266
  concurrency,
267
+ // Contract advertisement (HOL-2926). `description` is required on the
268
+ // def, so it is always present here; `opts.description` lets a
269
+ // deployment reword it without redefining the capability.
270
+ description: opts?.description ?? def.description,
271
+ inputSchema: def.inputSchema,
272
+ outputSchema: def.outputSchema,
236
273
  ...typeof leaseTtlMs === "number" ? { leaseTtlMs } : {}
237
274
  };
238
275
  }
@@ -252,7 +289,6 @@ var HeartbeatLoop = class {
252
289
  this.timer = setInterval(() => {
253
290
  void this.tick();
254
291
  }, this.intervalMs);
255
- if (typeof this.timer.unref === "function") this.timer.unref();
256
292
  }
257
293
  stop() {
258
294
  if (this.timer) {
@@ -503,9 +539,6 @@ var HttpTransport = class {
503
539
  this.reconnectTimer = null;
504
540
  void this.openStream();
505
541
  }, delay);
506
- if (typeof this.reconnectTimer === "object" && this.reconnectTimer !== null && "unref" in this.reconnectTimer && typeof this.reconnectTimer.unref === "function") {
507
- this.reconnectTimer.unref();
508
- }
509
542
  }
510
543
  };
511
544
  var TransportError = class extends Error {
@@ -525,6 +558,7 @@ var TerminalFailure = class extends Error {
525
558
  this.name = "TerminalFailure";
526
559
  }
527
560
  };
561
+ var DEFAULT_PROGRESS_TIMEOUT_MS = 5e3;
528
562
  function validateInput(zod, validator, jsonSchema, data) {
529
563
  if (zod) {
530
564
  const result = zod.safeParse(data);
@@ -537,8 +571,20 @@ function validateInput(zod, validator, jsonSchema, data) {
537
571
  return errors === null ? { ok: true, value: data } : { ok: false, errors };
538
572
  }
539
573
  async function runLease(opts) {
540
- const { lease, handler, zodInput, zodOutput, transport, validator, logger, signal } = opts;
541
- const inputCheck = validateInput(zodInput, validator, lease.inputSchema, lease.task.input);
574
+ const {
575
+ lease,
576
+ handler,
577
+ zodInput,
578
+ zodOutput,
579
+ inputSchema,
580
+ outputSchema,
581
+ transport,
582
+ validator,
583
+ logger,
584
+ signal,
585
+ progressTimeoutMs = DEFAULT_PROGRESS_TIMEOUT_MS
586
+ } = opts;
587
+ const inputCheck = validateInput(zodInput, validator, inputSchema, lease.task.input);
542
588
  if (!inputCheck.ok) {
543
589
  await sendNack(
544
590
  transport,
@@ -551,13 +597,13 @@ async function runLease(opts) {
551
597
  return;
552
598
  }
553
599
  const parsedInput = inputCheck.value;
554
- const progressFn = makeProgressFn(transport, lease.leaseId, logger);
600
+ const reporter = makeProgressReporter(transport, lease.leaseId, logger, signal, progressTimeoutMs);
555
601
  const ctx = {
556
602
  task: lease.task,
557
603
  leaseId: lease.leaseId,
558
604
  signal,
559
605
  log: scopedLogger(logger, lease),
560
- progress: progressFn,
606
+ progress: reporter.progress,
561
607
  fail: (reason, opts2) => {
562
608
  throw new TerminalFailure(reason, opts2?.code);
563
609
  }
@@ -566,7 +612,9 @@ async function runLease(opts) {
566
612
  try {
567
613
  output = await handler(parsedInput, ctx);
568
614
  } catch (err) {
569
- if (signal.aborted || err.name === "AbortError") {
615
+ const cancelled = signal.aborted || err.name === "AbortError";
616
+ if (!cancelled) await reporter.drained();
617
+ if (cancelled) {
570
618
  await sendNack(
571
619
  transport,
572
620
  lease.leaseId,
@@ -600,7 +648,8 @@ async function runLease(opts) {
600
648
  );
601
649
  return;
602
650
  }
603
- const outputCheck = validateInput(zodOutput, validator, lease.outputSchema, output);
651
+ await reporter.drained();
652
+ const outputCheck = validateInput(zodOutput, validator, outputSchema, output);
604
653
  if (!outputCheck.ok) {
605
654
  await sendNack(
606
655
  transport,
@@ -641,20 +690,37 @@ async function sendNack(transport, leaseId, kind, reason, extras = {}, logger) {
641
690
  );
642
691
  });
643
692
  }
644
- function makeProgressFn(transport, leaseId, logger) {
645
- return (update) => {
646
- void transport.post(POST_PATH.progress, {
647
- type: "progress",
648
- leaseId,
649
- percent: update.percent,
650
- message: update.message,
651
- data: update.data,
652
- sentAt: (/* @__PURE__ */ new Date()).toISOString()
653
- }).catch((err) => {
654
- logger.warn({ err, leaseId }, "progress POST failed; continuing");
655
- });
693
+ function makeProgressReporter(transport, leaseId, logger, signal, timeoutMs) {
694
+ let tail = Promise.resolve();
695
+ return {
696
+ progress: (update) => {
697
+ const frame = {
698
+ type: "progress",
699
+ leaseId,
700
+ percent: update.percent,
701
+ message: update.message,
702
+ data: update.data,
703
+ sentAt: (/* @__PURE__ */ new Date()).toISOString()
704
+ };
705
+ tail = tail.then(async () => {
706
+ if (signal.aborted) return;
707
+ try {
708
+ await withTimeout(transport.post(POST_PATH.progress, frame), timeoutMs);
709
+ } catch (err) {
710
+ logger.warn({ err, leaseId }, "progress POST failed; continuing");
711
+ }
712
+ });
713
+ },
714
+ drained: () => tail
656
715
  };
657
716
  }
717
+ function withTimeout(work, ms) {
718
+ let timer;
719
+ const timeout = new Promise((_resolve, reject) => {
720
+ timer = setTimeout(() => reject(new Error(`timed out after ${ms}ms`)), ms);
721
+ });
722
+ return Promise.race([work, timeout]).finally(() => clearTimeout(timer));
723
+ }
658
724
  function scopedLogger(logger, lease) {
659
725
  const scope = { leaseId: lease.leaseId, taskId: lease.task.taskId };
660
726
  return {
@@ -667,32 +733,39 @@ function scopedLogger(logger, lease) {
667
733
  var Ajv = AjvDefault__default.default.default ?? AjvDefault__default.default;
668
734
  var addFormats = addFormatsDefault__default.default.default ?? addFormatsDefault__default.default;
669
735
  var SchemaValidator = class {
670
- ajv;
671
- cache = /* @__PURE__ */ new WeakMap();
672
- constructor() {
736
+ constructor(onCompileError) {
737
+ this.onCompileError = onCompileError;
673
738
  this.ajv = new Ajv({ allErrors: true, strict: false });
674
739
  addFormats(this.ajv);
675
740
  }
741
+ ajv;
742
+ cache = /* @__PURE__ */ new WeakMap();
743
+ // Schemas whose compilation threw. Cannot-compile is an environment or
744
+ // schema-authoring problem, not evidence about the data — so validate()
745
+ // FAILS OPEN for these (returns null) instead of reporting a mismatch.
746
+ // Nacking on compile failure turned every lease into an
747
+ // INPUT_SCHEMA_MISMATCH retry loop on runtimes where ajv's codegen is
748
+ // unavailable (Electron renderers with CSP `script-src 'self'` block
749
+ // `new Function`) — HOL-2937.
750
+ uncompilable = /* @__PURE__ */ new WeakSet();
676
751
  /**
677
- * Returns null if the value matches the schema (or no schema given);
678
- * otherwise an array of structured errors.
752
+ * Returns null if the value matches the schema, no schema was given, or
753
+ * the schema cannot be compiled in this runtime (fail-open — the gateway
754
+ * remains the enforcement point); otherwise an array of structured errors.
679
755
  */
680
756
  validate(schema, data) {
681
757
  if (!schema || typeof schema !== "object") return null;
758
+ if (this.uncompilable.has(schema)) return null;
682
759
  let validator = this.cache.get(schema);
683
760
  if (!validator) {
684
761
  try {
685
762
  validator = this.ajv.compile(schema);
686
763
  } catch (err) {
687
- return [
688
- {
689
- instancePath: "",
690
- schemaPath: "",
691
- keyword: "schema",
692
- params: {},
693
- message: `schema failed to compile: ${err instanceof Error ? err.message : String(err)}`
694
- }
695
- ];
764
+ this.uncompilable.add(schema);
765
+ this.onCompileError?.(
766
+ `schema failed to compile \u2014 skipping SDK-side validation for this schema: ${err instanceof Error ? err.message : String(err)}`
767
+ );
768
+ return null;
696
769
  }
697
770
  this.cache.set(schema, validator);
698
771
  }
@@ -825,7 +898,9 @@ var Neuron = class extends eventemitter3.EventEmitter {
825
898
  super();
826
899
  this.logger = opts.logger ?? noopLogger;
827
900
  this.neuronId = opts.neuronId;
828
- this.validator = new SchemaValidator();
901
+ this.validator = new SchemaValidator(
902
+ (message) => this.logger.warn({ neuronId: this.neuronId }, message)
903
+ );
829
904
  this.transport = isWithTransport(opts) ? opts.transport : new HttpTransport({
830
905
  gatewayUrl: opts.gatewayUrl,
831
906
  neuronId: opts.neuronId,
@@ -897,7 +972,15 @@ var Neuron = class extends eventemitter3.EventEmitter {
897
972
  );
898
973
  }
899
974
  handler = optsOrHandler;
900
- capability = { type: reg.type, scope: reg.scope, concurrency: reg.concurrency };
975
+ capability = {
976
+ type: reg.type,
977
+ scope: reg.scope,
978
+ concurrency: reg.concurrency,
979
+ ...reg.description !== void 0 ? { description: reg.description } : {},
980
+ ...reg.inputSchema !== void 0 ? { inputSchema: reg.inputSchema } : {},
981
+ ...reg.outputSchema !== void 0 ? { outputSchema: reg.outputSchema } : {},
982
+ ...typeof reg.leaseTtlMs === "number" ? { leaseTtlMs: reg.leaseTtlMs } : {}
983
+ };
901
984
  inputSchema = reg.inputSchema;
902
985
  outputSchema = reg.outputSchema;
903
986
  }
@@ -1008,7 +1091,15 @@ var Neuron = class extends eventemitter3.EventEmitter {
1008
1091
  this.hasOpenedOnce = true;
1009
1092
  this.heartbeat.start();
1010
1093
  } catch (err) {
1011
- this.logger.error({ err }, "register POST failed; transport will retry on reconnect");
1094
+ const cause = err instanceof Error ? err : new Error(String(err));
1095
+ this.logger.error(
1096
+ { err: cause },
1097
+ "register POST rejected; SSE is open but the neuron is NOT registered and will receive no leases"
1098
+ );
1099
+ this.emit(
1100
+ "error",
1101
+ new Error(`registration rejected by gateway: ${cause.message}`, { cause })
1102
+ );
1012
1103
  }
1013
1104
  }
1014
1105
  handleConnectionClose(reason) {
@@ -1085,6 +1176,8 @@ var Neuron = class extends eventemitter3.EventEmitter {
1085
1176
  handler: handler.handler,
1086
1177
  zodInput: handler.zodInput,
1087
1178
  zodOutput: handler.zodOutput,
1179
+ inputSchema: handler.inputSchema,
1180
+ outputSchema: handler.outputSchema,
1088
1181
  transport: this.transport,
1089
1182
  validator: this.validator,
1090
1183
  logger: this.logger,