@holokai/neuron-sdk
v0.1.1
Published
SDK for building Holokai neurons — connect to a BigBrain gateway over HTTP+SSE or Electron IPC and execute capability-typed tasks.
Readme
@holokai/neuron-sdk
SDK for building neurons — processes that connect to the BigBrain gateway and execute tasks on behalf of workflows.
A neuron advertises one or more capabilities (typed task handlers), opens a persistent connection to the gateway, and receives work via a pluggable Transport. Two transports ship in the SDK:
HttpTransport— SSE for inbound, HTTPS POST for outbound. The default for any neuron talking to a remote BigBrain gateway. Re-exported from the package root.IpcTransport— Electron renderer ↔ main IPC. For neurons running inside an app that embeds BigBrain in main; no network hop, no JWT in the renderer. Imported from the/ipcsubpath.
The gateway routes tasks to the right neuron based on capability type and scope. The built-in apps/worker uses this SDK over HTTP; the in-process Holokai Desktop renderer uses it over IPC; both share the same Neuron class and handler API.
Installation
pnpm add @holokai/neuron-sdk
# or: npm install @holokai/neuron-sdk
# or: yarn add @holokai/neuron-sdkRuns 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.
Quick start
import { Neuron } from '@holokai/neuron-sdk';
const neuron = new Neuron({
gatewayUrl: 'https://api.example.com',
neuronId: 'my-worker-001', // stable per process/device
auth: () => process.env.GATEWAY_TOKEN!, // or an async refresh function
});
neuron.handle(
{ type: 'core/script.python', scope: 'any', concurrency: 4 },
async (input, ctx) => {
ctx.log.info({}, 'running python script');
const result = await runPython(input as { code: string }, ctx.signal);
return { output: result };
},
);
await neuron.start();
process.on('SIGTERM', () => neuron.stop({ drain: true, timeoutMs: 30_000 }));Core concepts
Capability
A capability is a named contract a neuron can fulfill. It has:
| Field | Type | Description |
|---|---|---|
| type | string | Namespaced identifier: {owner}/{package}[/{name}][.{variant}] |
| scope | 'any' \| { onBehalfOf: userId } | Who this handler serves |
| concurrency | number | Max parallel tasks for this capability |
Examples:
core/script.python— server-side Python execution, any userdesktop/mcp/drive.read— user's Google Drive, scoped to that userorg/01HBQ8ZK/custom-tool— org-specific capability
Namespace authorization
The gateway enforces which identities may advertise which namespaces:
| Namespace prefix | Permitted advertisers | Required scope |
|---|---|---|
| core/* | Server service identities | any |
| desktop/* | Attested desktop binaries | { onBehalfOf: userId } |
| org/{orgId}/* | Tenant-installed extensions | any within org |
| user/{userId}/* | Personal extensions | { onBehalfOf: userId } |
Registration is rejected (HTTP 403) if the JWT is not authorized for the namespace.
Scope
'any'— the neuron handles tasks for any user. Shared queue; no per-user binding.{ onBehalfOf: userId }— the neuron handles tasks for that specific user only. The JWT's user binding must match; the gateway enforces this at registration time.
One neuron may advertise mixed-scope capabilities:
neuron.handle({ type: 'desktop/mcp/drive.read', scope: { onBehalfOf: 'user:abc' }, concurrency: 1 }, driveHandler);
neuron.handle({ type: 'core/script.python', scope: 'any', concurrency: 4 }, pythonHandler);Lease
A lease is how the gateway delivers a task. When the broker dispatches a task to a neuron, the gateway:
- Writes a DB row with
leaseIdand expiry. - Sends a
leaseframe over SSE.
The SDK validates input, invokes your handler, and sends either ack (success) or nack (failure) over HTTPS POST. Until the ack arrives, the message remains unacknowledged in the broker — if the neuron disconnects, the broker redelivers automatically.
Handlers must be idempotent. A task that completed a side effect but failed to ack will be retried. Use capability-native idempotency keys (e.g., Google Drive requestId, HTTP Idempotency-Key).
Neuron ID
neuronId must be stable across restarts for the same process/device. The gateway uses it to correlate reconnects. Pick something meaningful and persistent:
// Server process
neuronId: `worker-${process.env.HOSTNAME}-${process.pid}`
// Desktop app — persist a UUID to disk
neuronId: await readOrCreateStoredId('/var/lib/myapp/neuron-id')API reference
new Neuron(opts: NeuronOptions)
NeuronOptions is a discriminated union — pick the convenience shape (HTTP) or pass a transport explicitly.
type NeuronOptions = NeuronOptionsWithHttp | NeuronOptionsWithTransport;
interface NeuronCommonOptions {
neuronId: string; // Stable per process/device
logger?: NeuronLogger; // Plug in pino, console, etc. Default: silent
heartbeatIntervalMs?: number; // Default 10_000 (10s)
}
// Convenience: SDK builds an HttpTransport for you. Most common path.
interface NeuronOptionsWithHttp extends NeuronCommonOptions {
gatewayUrl: string; // Base URL, e.g. 'https://api.example.com'
auth: AuthCallback; // Token provider (see below)
initialReconnectDelayMs?: number; // Default 500ms
maxReconnectDelayMs?: number; // Default 30_000 (30s)
fetch?: typeof fetch; // Override for tests
}
// Power-user: bring your own Transport. Used for IpcTransport, custom
// transports, or HttpTransport instances built explicitly.
interface NeuronOptionsWithTransport extends NeuronCommonOptions {
transport: Transport;
}See the Transports section for HttpTransport, IpcTransport, and the Transport interface.
neuron.handle(reg, handler)
Register a handler for a capability. Must be called before neuron.start().
neuron.handle(reg: CapabilityRegistration, handler: Handler): voidinterface CapabilityRegistration {
type: string; // Namespaced capability type
scope: 'any' | { onBehalfOf: string };
concurrency: number; // Max parallel executions
inputSchema?: JsonSchema; // Optional; validated before invoking handler
outputSchema?: JsonSchema; // Optional; validated before acking
}
type Handler<TInput = unknown, TOutput = unknown> = (
input: TInput,
ctx: HandlerContext,
) => Promise<TOutput> | TOutput;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.
neuron.start()
Opens the SSE stream, posts register with all advertised capabilities, and starts the heartbeat loop.
await neuron.start(): Promise<void>Throws if no handlers have been registered. Subsequent calls are no-ops.
neuron.stop(opts?)
Graceful shutdown: refuse new leases, drain in-flight handlers, disconnect.
await neuron.stop({ drain?: boolean, timeoutMs?: number }): Promise<void>| Option | Default | Description |
|---|---|---|
| drain | true | Wait for in-flight handlers to finish |
| timeoutMs | 30_000 | After this, abort remaining in-flight tasks |
Queued leases (received but not yet started due to concurrency limits) are nacked retryable on shutdown so the broker redelivers immediately.
neuron.updateCapability(reg)
Change concurrency for an already-registered capability at runtime. Useful when available resources change (e.g., desktop plugged into power).
await neuron.updateCapability({ type: string, concurrency: number }): Promise<void>Notifies the gateway via update-capability so it can tune AMQP prefetch. Safe to call while tasks are in flight — in-flight tasks finish at the old concurrency; new slots open at the new limit.
HandlerContext
The second argument to every handler:
interface HandlerContext {
task: TaskPayload; // Full task metadata
leaseId: string; // Gateway lease ID (for logging/debugging)
signal: AbortSignal; // Fires on gateway cancel or SDK shutdown
log: NeuronLogger; // Logger scoped to this lease
progress(update: { percent?: number; message?: string; data?: unknown }): void;
fail(reason: string, opts?: { code?: string }): never;
}ctx.signal
Pass this to any fetch/HTTP calls so they abort cleanly:
const resp = await fetch(url, { signal: ctx.signal });When the gateway sends a cancel frame (workflow was cancelled by the user), ctx.signal fires and your handler should throw/return promptly. An AbortError from a cancelled signal is treated as nack(kind='cancelled') — benign, not counted as a failure.
ctx.progress()
Report progress for long-running tasks. Non-blocking; safe to call many times.
ctx.progress({ percent: 50, message: 'halfway done' });
ctx.progress({ data: { rowsProcessed: 1200 } });ctx.fail()
Terminal failure — nacks the lease with kind='terminal' (no retry). Use when the task can never succeed regardless of retries.
if (!validApiKey) {
ctx.fail('invalid API key', { code: 'INVALID_API_KEY' });
// unreachable — fail() throws internally
}AuthCallback
type AuthCallback = () => Promise<string> | string;Called on every POST and SSE open. The SDK retries once on 401, so implement token refresh inside the callback:
let token = await fetchInitialToken();
const auth = async () => {
if (isExpired(token)) {
token = await refreshToken();
}
return token.accessToken;
};NeuronLogger
interface NeuronLogger {
debug(meta: Record<string, unknown>, msg?: string): void;
info(meta: Record<string, unknown>, msg?: string): void;
warn(meta: Record<string, unknown>, msg?: string): void;
error(meta: Record<string, unknown>, msg?: string): void;
}Matches pino's call signature directly. To use pino:
import pino from 'pino';
const logger = pino({ level: 'info' });
const neuron = new Neuron({ ..., logger });defineCapability()
A typed helper that produces a capability definition from zod schemas, with handler inputs/outputs fully inferred:
import { defineCapability, Neuron } from '@holokai/neuron-sdk';
import { z } from 'zod';
const pythonCap = defineCapability({
type: 'core/script.python',
description: 'Run a Python script in a sandbox',
input: z.object({ code: z.string(), args: z.record(z.unknown()).optional() }),
output: z.object({ stdout: z.string(), exitCode: z.number() }),
scope: 'any', // optional default; overridable at handle() time
concurrency: 4, // optional default; overridable at handle() time
});
// Handler input/output inferred — no manual type annotations:
neuron.handle(pythonCap, async (input, ctx) => {
// input: { code: string; args?: Record<string, unknown> }
return { stdout: '...', exitCode: 0 };
});
// Or override defaults per deployment:
neuron.handle(pythonCap, { scope: { onBehalfOf: 'user-123' }, concurrency: 8 }, fn);Validation uses the original zod schemas (so .refine(), .transform(), .preprocess(), .brand() all run as expected). The JSON Schema produced by zod-to-json-schema is shipped on the wire to the gateway and planner but is not used as the runtime validator — that avoids silently dropping zod features that don't translate.
The fall-back neuron.handle(registration, fn) shape (raw JSON Schema + capability fields) is still accepted for low-level callers that don't have zod schemas on hand.
Events
Neuron extends EventEmitter from eventemitter3 — a browser-clean, type-safe drop-in for Node's EventEmitter. Subscribe with .on(), .once(), or .off():
const off = neuron.on('frame', (frame) => {
log.append(frame); // see every parsed SSE frame
});
off(); // unsubscribe
neuron.once('connection:registered', ({ sessionId }) => {
console.log(`registered as ${sessionId}`);
});
neuron.on('connection:reconnecting', ({ attempt, delayMs, reason }) => {
statusBar.show(`reconnecting (attempt ${attempt}, ${delayMs}ms)`, reason);
});The full event map:
| Event | Payload | When |
|---|---|---|
| connection:open | none | SSE handshake succeeded; before register is posted |
| connection:registered | { sessionId: string; capabilities: number } | Gateway accepted the register frame |
| connection:close | reason: string | SSE stream ended (clean shutdown or transient error) |
| connection:reconnecting | { attempt, delayMs, reason } | Reconnect scheduled, before backoff delay |
| error | err: Error | Transport / parse error (reconnect handling is automatic) |
| frame | frame: ServerToNeuronFrame | Pass-through of every parsed inbound SSE frame |
The frame event is a firehose mirror of the gateway's /neuron/events SSE stream. It fires for every frame including ones the SDK handles internally (lease, cancel) — useful for debug UIs, observability, and audit logs. The high-level neuron.handle() API is implemented as a frame listener filtered to lease frames, so handle() and direct frame subscribers compose without conflict.
Subscribe before calling neuron.start() if you care about events that fire as part of the connection sequence (connection:open, the initial registered frame). Late subscribers will still see subsequent events.
The event surface is designed to grow — future entries (e.g. 'workflow:task-completed' for cross-neuron notifications) slot in without breaking subscribers.
Transports
A Transport is the wire-level abstraction the rest of the SDK builds on. It carries typed frames in both directions: inbound ServerToNeuronFrames via callbacks, outbound ClientFrames via post(...). The Neuron class only ever talks to a Transport — adding a new transport (e.g. WebSocket, native messaging) means implementing one interface.
interface Transport {
subscribe(callbacks: TransportCallbacks): void;
start(): Promise<void>;
stop(): Promise<void>;
post<T extends ClientFrame>(path: PostPath, frame: T): Promise<unknown>;
isConnected(): boolean;
}
interface TransportCallbacks {
onOpen?: () => void;
onClose?: (reason: string) => void;
onFrame?: (frame: ServerToNeuronFrame) => void;
onError?: (err: Error) => void;
onReconnecting?: (info: { attempt: number; delayMs: number; reason: string }) => void;
}Both Transport and TransportCallbacks are exported from the package root.
HttpTransport
Default transport. SSE for inbound, HTTPS POST for outbound. Reconnects with exponential backoff (default: 500ms → 30s ceiling). Refreshes the auth token via your AuthCallback and retries once on 401.
import { Neuron, HttpTransport } from '@holokai/neuron-sdk';
const transport = new HttpTransport({
gatewayUrl: 'https://api.example.com',
neuronId: 'my-worker-001',
auth: () => process.env.GATEWAY_TOKEN!,
// optional:
initialReconnectDelayMs: 500,
maxReconnectDelayMs: 30_000,
});
const neuron = new Neuron({ neuronId: 'my-worker-001', transport });In practice you rarely construct HttpTransport directly — passing gatewayUrl + auth to Neuron builds it for you. Construct explicitly when you want to share a fetch implementation, swap the auth strategy, or instrument the transport in tests.
interface HttpTransportOptions {
gatewayUrl: string;
neuronId: string;
auth: AuthCallback;
logger?: NeuronLogger;
initialReconnectDelayMs?: number; // default 500ms
maxReconnectDelayMs?: number; // default 30_000ms
fetch?: typeof fetch;
}IpcTransport
For renderer-process neurons running inside an Electron app that hosts BigBrain in the main process. Leases arrive over Electron IPC, not over the network. Imported from the /ipc subpath so non-Electron consumers don't pay for it:
import { Neuron } from '@holokai/neuron-sdk';
import { IpcTransport } from '@holokai/neuron-sdk/ipc';
// `bridge` comes from the renderer's preload — see below.
const transport = new IpcTransport({ bridge: window.bigbrainNeuron, neuronId });
const neuron = new Neuron({ neuronId, transport });
neuron.handle({ type: 'desktop/local-tool', scope: { onBehalfOf: userId }, concurrency: 1 }, handler);
await neuron.start();Differences from HttpTransport:
- No reconnect logic — IPC has no transient socket. If the bridge throws, the failure surfaces via
onErrorand the consumer decides what to do. - No auth callback — the renderer never sees the JWT. The Electron main process attaches
Authorizationheaders when it forwards POSTs to the embedded Fastify gateway. - No keep-alive — IPC channels are process-local; nothing to time out.
The SDK does not depend on Electron. The renderer's preload script is responsible for constructing a concrete bridge that satisfies the NeuronIpcBridge shape and exposing it via contextBridge:
interface NeuronIpcBridge {
attach(neuronId: string): Promise<{ channel: string }>;
detach(neuronId: string): Promise<void>;
subscribe(channel: string, cb: (frame: ServerToNeuronFrame) => void): () => void;
invoke<T extends ClientFrame>(path: PostPath, frame: T): Promise<unknown>;
}Typical preload wiring (Electron-side, app code — not part of the SDK):
// preload.ts
import { contextBridge, ipcRenderer } from 'electron';
contextBridge.exposeInMainWorld('bigbrainNeuron', {
attach: (neuronId) => ipcRenderer.invoke('bigbrain:attach', neuronId),
detach: (neuronId) => ipcRenderer.invoke('bigbrain:detach', neuronId),
subscribe: (channel, cb) => {
const listener = (_e, frame) => cb(frame);
ipcRenderer.on(channel, listener);
return () => ipcRenderer.off(channel, listener);
},
invoke: (path, frame) => ipcRenderer.invoke('bigbrain:post', path, frame),
});interface IpcTransportOptions {
bridge: NeuronIpcBridge;
neuronId: string;
logger?: NeuronLogger;
}Examples
Simple any-scoped capability
import { Neuron } from '@holokai/neuron-sdk';
const neuron = new Neuron({
gatewayUrl: process.env.GATEWAY_URL!,
neuronId: `worker-${process.env.HOSTNAME}`,
auth: () => process.env.GATEWAY_TOKEN!,
});
neuron.handle(
{ type: 'core/http', scope: 'any', concurrency: 8 },
async (input, ctx) => {
const { url, method = 'GET', headers = {}, body } = input as {
url: string;
method?: string;
headers?: Record<string, string>;
body?: string;
};
const resp = await fetch(url, { method, headers, body, signal: ctx.signal });
const text = await resp.text();
return { status: resp.status, body: text };
},
);
await neuron.start();User-scoped capability (desktop / MCP)
const userId = await getUserIdFromToken(authToken);
neuron.handle(
{ type: 'desktop/mcp/drive.read', scope: { onBehalfOf: userId }, concurrency: 1 },
async (input, ctx) => {
const { fileId } = input as { fileId: string };
ctx.progress({ message: 'fetching file metadata' });
const file = await driveClient.files.get({ fileId, signal: ctx.signal });
ctx.progress({ percent: 50, message: 'downloading content' });
const content = await driveClient.files.download({ fileId, signal: ctx.signal });
return { name: file.name, mimeType: file.mimeType, content };
},
);Long-running task with progress
neuron.handle(
{ type: 'core/data.transform', scope: 'any', concurrency: 2 },
async (input, ctx) => {
const { rows } = input as { rows: unknown[] };
const results: unknown[] = [];
for (let i = 0; i < rows.length; i++) {
if (ctx.signal.aborted) break; // respect cancellation
results.push(await processRow(rows[i]));
ctx.progress({ percent: Math.round(((i + 1) / rows.length) * 100) });
}
return { results };
},
);Error handling
neuron.handle(
{ type: 'core/llm', scope: 'any', concurrency: 4 },
async (input, ctx) => {
const { prompt, model } = input as { prompt: string; model: string };
let response;
try {
response = await llmClient.complete({ prompt, model, signal: ctx.signal });
} catch (err) {
if ((err as Error).name === 'AbortError') {
throw err; // let the SDK handle cancellation
}
if ((err as any).status === 400) {
// Bad request — no point retrying
ctx.fail(`invalid LLM request: ${(err as Error).message}`, { code: 'INVALID_REQUEST' });
}
// Anything else: throw to get a retryable nack
throw err;
}
return { text: response.text, tokensUsed: response.usage.total };
},
);Graceful shutdown
await neuron.start();
const shutdown = async (signal: string) => {
console.log(`${signal} received — draining...`);
await neuron.stop({ drain: true, timeoutMs: 30_000 });
process.exit(0);
};
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));Runtime concurrency update
import { powerMonitor } from 'electron'; // or any power/resource API
powerMonitor.on('on-ac', async () => {
await neuron.updateCapability({ type: 'core/script.python', concurrency: 8 });
});
powerMonitor.on('on-battery', async () => {
await neuron.updateCapability({ type: 'core/script.python', concurrency: 1 });
});Configuration reference
| Option | Default | Applies to | Notes |
|---|---|---|---|
| neuronId | — | both | Stable per device/process; reused on reconnect |
| logger | silent no-op | both | Plug in pino or console |
| heartbeatIntervalMs | 10_000 | both | Sent every 10s; renews lease expiries |
| transport | — | with-transport shape | Pre-built Transport (e.g. IpcTransport or a custom HttpTransport) |
| gatewayUrl | — | with-http shape | Base URL, no trailing slash |
| auth | — | with-http shape | Called on every POST + SSE open |
| initialReconnectDelayMs | 500 | with-http shape | First reconnect delay (HTTP only — IPC doesn't reconnect) |
| maxReconnectDelayMs | 30_000 | with-http shape | Reconnect delay ceiling (exponential backoff) |
| fetch | global fetch | with-http shape | Override for tests |
Protocol details
This section covers enough to debug; the wire-level frame schemas are bundled into the SDK and exported alongside the public types.
Transport
The SDK abstracts the wire as a Transport (see Transports above). The frame types below are the same regardless of transport — only the carrier differs.
HttpTransport (default):
- Server → Neuron: SSE stream at
GET /neuron/events?neuronId={id} - Neuron → Server: HTTPS POST endpoints (one per frame type)
- No WebSockets — SSE + HTTPS POST traverses enterprise proxies cleanly.
IpcTransport (Electron renderer):
- Server → Neuron: per-neuron Electron IPC channel obtained from
bridge.attach(). - Neuron → Server:
bridge.invoke(path, frame), which the main process forwards to the embedded Fastify gateway viaapp.inject(...)— same auth and validation chain as HTTP. - No reconnect, no auth callback, no keep-alive.
Frame types
Neuron → Server (POST):
| Frame | Path | When |
|---|---|---|
| register | /neuron/register | On SSE open |
| update-capability | /neuron/update-capability | On concurrency change |
| heartbeat | /neuron/heartbeat | Every 10s while connected |
| ack | /neuron/ack | Handler succeeded |
| nack | /neuron/nack | Handler failed |
| progress | /neuron/progress | During handler execution |
Server → Neuron (SSE):
| Frame | When |
|---|---|
| registered | Gateway accepted the register frame; effective capabilities returned |
| lease | Task to execute |
| cancel | Gateway cancels a specific lease (workflow cancelled) |
| capability-revoked | A capability was deauthorized mid-session |
| disconnect | Gateway is closing the connection cleanly |
Heartbeat
Every ~10s the SDK posts:
{
"type": "heartbeat",
"neuronId": "worker-host-1234",
"inFlight": [
{ "leaseId": "01HB...", "taskId": "task-abc", "startedAt": "2024-01-01T12:00:00Z" }
],
"sentAt": "2024-01-01T12:00:10Z"
}The gateway renews lease expiries for every leaseId in inFlight in a single DB round-trip. Only running leases appear in inFlight — queued leases (admitted but waiting for a concurrency slot) are excluded so the reaper doesn't mistakenly extend their TTL.
Lease lifecycle
Gateway Neuron SDK
│ │
│── lease (SSE) ──────────────────────────▶ │
│ │ validate input schema
│ │ wait for pool slot
│ │ invoke handler
│◀── heartbeats (POST every 10s) ────────── │
│ │ handler returns
│ │ validate output schema
│◀── ack (POST) ────────────────────────── │
│
│ [or on failure]
│◀── nack(kind='retryable') ────────────── │ broker redelivers
│◀── nack(kind='terminal') ─────────────── │ workflow fails
│◀── nack(kind='schema-mismatch') ──────── │ structured error surfaced
│◀── nack(kind='cancelled') ────────────── │ benign; signal firedReconnection
The SDK uses exponential backoff (default: 500ms initial, doubling, 30s ceiling). On reconnect:
- SSE stream re-opens.
registeris posted with the full capability list.- Heartbeat loop resumes.
In-flight leases from before the disconnect were never acked — the broker redelivers them. The reaper flips stale assigned DB rows back to pending. Your handlers will receive the same task again; make them idempotent.
Troubleshooting
Neuron.handle() must be called before start()
Register all capabilities before calling start(). For runtime changes, use updateCapability().
Neuron.start() requires at least one handler
You must call neuron.handle(...) at least once before start().
Registration rejected with HTTP 403
Your JWT is not authorized for the namespace you're trying to advertise. Check:
- The capability type uses the correct namespace prefix (
core/*requires a service identity, not a user JWT). - For user-scoped capabilities, the
onBehalfOfvalue matches the user in your JWT.
Tasks not being received
- Check the
registeredlog line — it shows the effective capabilities after the gateway's policy/auth filters. If a capability is missing, the gateway filtered it. - Check
neuronId— if it changes across restarts, the gateway may have stale session state. - Check heartbeat logs — if heartbeats are failing, the lease reaper may be reclaiming tasks faster than they're running.
Handler keeps getting re-delivered
The handler completed but the ack POST failed (gateway returned 4xx or 5xx). Look for ack POST rejected by gateway in logs. Root causes:
- Output failed the
outputSchemacheck — fix your handler's return shape. - Gateway was restarting — the next delivery will succeed once it's back.
Tasks stuck in assigned state
Heartbeats aren't reaching the gateway, so the reaper hasn't reclaimed them. Check:
- Is the SSE stream still connected? (
transport reconnectinglog entries) - Are heartbeat POSTs failing? (
heartbeat post failedlog entries) - Is
heartbeatIntervalMsmisconfigured to a very large value?
Auth 401 on every request
The auth callback isn't returning a valid token. If using a refresh flow, ensure the callback fetches a fresh token on each call (the SDK caches nothing — it's your callback's job).
