@blocks-network/sdk 0.1.60 → 0.1.62

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
@@ -1,6 +1,6 @@
1
1
  # @blocks-network/sdk
2
2
 
3
- Blocks Network SDK for Node.js -- build and run A2A agents on PubNub.
3
+ Blocks Network SDK for Node.js
4
4
 
5
5
  ## Installation
6
6
 
@@ -89,29 +89,25 @@ The agent card follows the A2A specification with a `runtime` extension:
89
89
  Note: `identity.agentName` must use only alphanumeric characters and
90
90
  underscores (no hyphens). The pattern is `^[a-zA-Z0-9_]+$`.
91
91
 
92
- ## PubNub Subscribe Strategy
93
-
94
- The Node SDK uses PubNub Event Engine (`enableEventEngine: true`) on all
95
- subscribing PubNub clients (control client, per-task client, and per-stream
96
- client). Event Engine replaces the legacy subscribe manager with a
97
- deterministic state machine for subscribe, reconnect, and retry. The SDK
98
- does not set `autoNetworkDetection` or `restore` (both are browser-only
99
- settings).
100
-
101
- **Control client** (long-lived, drives agent online presence) is
102
- constructed with `subscribeRetryUnbounded: true`, which configures an
103
- `ExponentialRetryPolicy` with `maximumRetry: 43_200` (~30 days at the
104
- 60s cap). This prevents the Event Engine from parking in
105
- `RECEIVE_FAILED`/`HEARTBEAT_FAILED` after the default 6-attempt budget
106
- is exhausted, so the agent automatically resumes within seconds of a
107
- network outage ending. Transport-level retry activity is forwarded to
108
- the SDK log via the `onRetry` hook, surfacing as
109
- `event: 'pubnub_transport_retry'` warn lines so the agent's log stays
110
- active during outages instead of going silent.
111
-
112
- **Per-task and per-stream clients** keep PubNub's default 6-attempt
113
- retry budget. They are short-lived; a stuck task should fail cleanly
114
- rather than loop forever.
92
+ ## Subscribe Strategy
93
+
94
+ The SDK keeps the control client subscribed for the agent's lifetime
95
+ and configures the underlying realtime transport to retry indefinitely
96
+ on transient failures. The agent therefore resumes within seconds of a
97
+ network outage ending without any manual intervention.
98
+
99
+ Connectivity-state changes are surfaced through the SDK log:
100
+
101
+ - `event: 'transport_degraded'` (warn) — emitted when the transport
102
+ reports network down, network issues, timeout, or a malformed
103
+ response. The log line reads `connectivity degraded: <category>`,
104
+ where `<category>` is one of `network_down`, `network_issues`,
105
+ `timeout`, `malformed_response`.
106
+ - `event: 'transport_restored'` (info) — emitted on reconnection.
107
+
108
+ Per-task and per-stream clients keep the default short retry budget.
109
+ They are short-lived; a stuck task should fail cleanly rather than
110
+ loop forever.
115
111
 
116
112
  ## API
117
113
 
@@ -128,9 +124,19 @@ Go CLI's `blocks login` command writes the API key to `.env` when
128
124
  invoked with `--write-env` (or by answering "y" to its interactive
129
125
  prompt).
130
126
 
131
- PAM tokens for PubNub channel access are managed by the SDK at runtime
127
+ Access tokens for realtime channel access are managed by the SDK at runtime
132
128
  (granted at registration, refreshed per-task on the control channel).
133
- No CLI involvement is needed for PAM.
129
+ No CLI involvement is needed for token management.
130
+
131
+ ### Logging
132
+
133
+ The SDK respects two log-related environment variables:
134
+
135
+ - `LOG_LEVEL` (default: `info`) — `error` | `warn` | `info` | `debug`. Threshold for the SDK's own log lines (`[AgentInstance]`, `[Transport]`).
136
+ - `BLOCKS_DEBUG_INTERNAL` (default: unset) — comma-separated list of subsystems to enable. Neither subsystem is implied by `LOG_LEVEL=debug`.
137
+ - `diagnostics` — Blocks-authored transport-connectivity diagnostics (connection-state transitions, alive snapshots).
138
+ - `forward_transport` — routes the underlying transport's own log output through the Blocks logger with a `[Transport]` prefix, filtered by `LOG_LEVEL`. Off by default because the forwarded messages reference internal SDK config concepts that aren't meaningful to a Blocks developer. **Security note:** forwarded entries may include constructed transport URLs whose query strings carry the access-token (`auth=…`) or request-signature (`signature=…`) parameters. Do not enable in environments that ship logs to less-trusted sinks (third-party log shippers, public Sentry projects, support-ticket attachments).
139
+ - Example: `BLOCKS_DEBUG_INTERNAL=diagnostics,forward_transport`
134
140
 
135
141
  ## Examples
136
142
 
@@ -198,9 +204,9 @@ import type { TokenEndpointConfig } from '@blocks-network/sdk';
198
204
 
199
205
  const tokenEndpoint: TokenEndpointConfig = {
200
206
  url: '/api/blocks-token',
201
- credentials: 'include', // send session cookies
202
- headers: { 'X-CSRF-Token': readCsrfMeta() }, // merged with Content-Type
203
- body: { sessionId: getCurrentSessionId() }, // replaces the default {}
207
+ credentials: 'include', // send session cookies
208
+ headers: { 'X-CSRF-Token': readCsrfMeta() }, // merged with Content-Type
209
+ body: { sessionId: getCurrentSessionId() }, // replaces the default {}
204
210
  };
205
211
 
206
212
  const client = await TaskClient.create({
@@ -228,12 +234,12 @@ the client.
228
234
  Blocks API key, authenticates the browser caller however you
229
235
  choose (session cookie / OAuth / etc.), and forwards to the
230
236
  Blocks backend's `POST /api/v1/auth/agent/consumer-token`.
231
- 2. **Dashboard embedder (`afui_mvp` pattern).** The Blocks backend's
232
- own `POST /api/v1/auth/agent/consumer-token` endpoint, called
233
- directly from a signed-in dashboard with the user's session
234
- cookie plus `X-Active-Org` and `X-CSRF-Token` headers. No proxy,
235
- no API key in the browser. See `dev_docs/SDK_CONTRACT.md` §8.6.4g
236
- for the full wiring.
237
+ 2. **Dashboard embedder.** The Blocks backend's own
238
+ `POST /api/v1/auth/agent/consumer-token` endpoint, called directly
239
+ from a signed-in dashboard with the user's session cookie plus
240
+ `X-Active-Org` and `X-CSRF-Token` headers. No proxy, no API key in
241
+ the browser. Custom apps usually use the customer-owned backend
242
+ proxy shape.
237
243
 
238
244
  Both shapes speak the same Mode 2 contract and are consumed
239
245
  uniformly by this SDK.
@@ -387,7 +393,7 @@ extends `Uint8Array`.
387
393
 
388
394
  ### Stream Consumer APIs
389
395
 
390
- All consumer iterators (`bytes()`, `events()`, `readable()`, `inbound`) deliver messages in sequence order. The SDK's reorder buffer transparently corrects out-of-order PubNub delivery and drops duplicate messages. To customize or disable reordering, pass `reorderTimeoutMs` to `open()`:
396
+ All consumer iterators (`bytes()`, `events()`, `readable()`, `inbound`) deliver messages in sequence order. The SDK's reorder buffer transparently corrects out-of-order delivery and drops duplicate messages. To customize or disable reordering, pass `reorderTimeoutMs` to `open()`:
391
397
 
392
398
  ```typescript
393
399
  // Default: reorder buffer with 750ms gap timeout
@@ -427,19 +433,17 @@ readable.pipe(createWriteStream('./output.bin'));
427
433
  ### Handling Stream Errors
428
434
 
429
435
  Every `StreamClient` exposes an `onError(cb)` registration method.
430
- The callback fires whenever the stream's PubNub subscribe loop
431
- surfaces an error-category status event: PAM revocation, network
432
- issues, timeouts, or any other category the PubNub SDK marks as an
433
- error. The payload is a typed `StreamError`:
436
+ The callback fires whenever the stream's subscribe loop surfaces an
437
+ error-category status event: access revocation, network issues,
438
+ timeouts, or any other transport error category. The payload is a
439
+ typed `StreamError`:
434
440
 
435
441
  ```typescript
436
442
  import type { StreamError } from '@blocks-network/sdk/stream';
437
443
 
438
444
  const stream = ref.open();
439
445
  stream.onError((err: StreamError) => {
440
- console.warn(
441
- `[stream] ${err.category} fatal=${err.fatal} channel=${err.channel}`,
442
- );
446
+ console.warn(`[stream] ${err.category} fatal=${err.fatal} channel=${err.channel}`);
443
447
  });
444
448
 
445
449
  for await (const chunk of stream.bytes()) {
@@ -452,14 +456,15 @@ the stream so `for await` / `for msg in ...` loops exit cleanly
452
456
  instead of hanging waiting for a `stream_end` that will never
453
457
  arrive:
454
458
 
455
- - `PNAccessDeniedCategory` — PAM revocation (admin-terminate,
459
+ - `access_denied` — access grant revoked (admin-terminate or
456
460
  token denied). This is the signal that the server-side grant is
457
- gone even if the cached T7c's `exp` claim has not elapsed.
458
- - `PNBadRequestCategory` — auth configuration or malformed grant.
461
+ gone even if the cached token's `exp` claim has not elapsed.
462
+ - `bad_request` — auth configuration or malformed grant.
459
463
 
460
- All other error categories (network transients, timeouts, etc.)
461
- fire `onError` with `fatal: false` and leave the stream running so
462
- PubNub's built-in retry machinery can recover.
464
+ All other error categories (`network_down`, `network_issues`,
465
+ `timeout`, `malformed_response`, `other`) fire `onError` with
466
+ `fatal: false` and leave the stream running so the transport's
467
+ built-in retry machinery can recover.
463
468
 
464
469
  ### Opening Task Streams
465
470
 
@@ -484,10 +489,10 @@ session.onStream((ref) => {
484
489
  });
485
490
 
486
491
  // Option 2 — open every readable stream in one call, then branch
487
- await session.waitForStream(); // ensure at least one is announced
488
- const streams = session.openAllStreams(); // returns StreamClient[] in insertion order
492
+ await session.waitForStream(); // ensure at least one is announced
493
+ const streams = session.openAllStreams(); // returns StreamClient[] in insertion order
489
494
  for (const s of streams) {
490
- void consume(s, /* whichever ref you care about */);
495
+ void consume(s /* whichever ref you care about */);
491
496
  }
492
497
  ```
493
498
 
@@ -507,13 +512,13 @@ loops. Tune the window per session:
507
512
  const session = await client.sendMessage({
508
513
  agentName: 'llm_streamer',
509
514
  requestParts: [textPart('stream please')],
510
- drainWindowMs: 5_000, // 5 seconds
515
+ drainWindowMs: 5_000, // 5 seconds
511
516
  });
512
517
 
513
518
  // Wider window for long-tail consumers, or on connect()
514
519
  const resumed = await client.connect({
515
520
  taskId: 'task-abc',
516
- drainWindowMs: 60_000, // 60 seconds
521
+ drainWindowMs: 60_000, // 60 seconds
517
522
  });
518
523
  ```
519
524
 
@@ -521,8 +526,8 @@ The option is supported on both `sendMessage()` and `connect()`.
521
526
 
522
527
  ### Reconnecting to Terminal Tasks
523
528
 
524
- Stream data is **live-only** — PubNub does not persist stream
525
- payloads. When `client.connect({ taskId })` returns a session for a
529
+ Stream data is **live-only** — the realtime transport does not persist
530
+ stream payloads. When `client.connect({ taskId })` returns a session for a
526
531
  task that has already finished, a stream that was **never opened
527
532
  while the task was active** throws a typed `StreamUnavailableError`
528
533
  from `StreamRef.open()` instead of subscribing to a dead channel.
@@ -586,11 +591,11 @@ Import from the package root — no special browser entrypoint needed:
586
591
 
587
592
  import { TaskClient, TaskSession, StreamRef } from '@blocks-network/sdk';
588
593
 
589
- The Node SDK package.json declares `engines.node >= 20.0.0` (Node 20
590
- LTS) alongside the `browser` exports field. Node 20+ has native
594
+ The Node SDK package.json declares `engines.node >= 22.0.0` (Node 22
595
+ Maintenance LTS) alongside the `browser` exports field. Node 22+ has native
591
596
  `FormData`, `fetch`, `Blob`, `Uint8Array`, `TextEncoder`, and
592
597
  `TextDecoder`, which the SDK uses directly — no polyfill required on
593
- either platform.
598
+ either platform. CI tests against Node 22, 24, and 26.
594
599
 
595
600
  **Consumer APIs** (TaskClient, TaskSession, StreamRef, StreamClient)
596
601
  are browser-safe — no Node.js `Buffer` polyfill needed:
@@ -603,8 +608,8 @@ are browser-safe — no Node.js `Buffer` polyfill needed:
603
608
  `Blob` field. `fetch` computes the multipart boundary
604
609
  automatically — no manual `Content-Type: multipart/form-data`
605
610
  header, no `Buffer.concat`.
606
- - `downloadArtifact()` handles the three PubNub v10 download shapes
607
- (raw `Uint8Array`, `Blob`, and legacy `PubNubFile` with
611
+ - `downloadArtifact()` handles the three download shapes returned by
612
+ the transport (raw `Uint8Array`, `Blob`, and legacy file object with
608
613
  `toArrayBuffer()`) with typeof-guarded branches — no
609
614
  `Buffer.isBuffer` OR-order short-circuits.
610
615
  - `decodeInlineArtifact()` uses `atob` + `Uint8Array` and
@@ -645,4 +650,4 @@ the SDK does not read from `process.env` in browser environments:
645
650
 
646
651
  ## License
647
652
 
648
- PubNub
653
+ See [LICENSE](./LICENSE).
@@ -189,14 +189,10 @@ export interface ControlMessage {
189
189
  caller?: string;
190
190
  protocolVersion?: string;
191
191
  }
192
- /** Numeric precedence for LOG_LEVEL threshold filtering. */
193
- export declare const _LOG_LEVEL_ORDER: Record<string, number>;
194
- /** Resolve the effective LOG_LEVEL from env (default: "info"). */
195
- export declare function _resolveLogLevel(): number;
196
192
  /**
197
193
  * True when the diag entry is "parked" — past the staleness threshold
198
194
  * AND not currently in the connected state. A long-silent
199
- * PNConnectedCategory entry is healthy by definition: at LOG_LEVEL=info
195
+ * 'connected' entry is healthy by definition: at LOG_LEVEL=info
200
196
  * the SDK suppresses successful-heartbeat status events
201
197
  * (announceSuccessfulHeartbeats=false), so lastStatusAt does not
202
198
  * advance on a healthy idle client. Without the category gate, the
@@ -1 +1 @@
1
- import{StreamClient as e}from"../stream/index.js";import{buildArtifactRef as t,shouldInlineArtifact as a,decodeInlineArtifact as n}from"./artifacts.js";import{createPubNubClient as s}from"./pubnub-client.js";import{createChannelManager as r}from"./channel-manager.js";import{toPayload as o}from"./pubnub-types.js";import{connectAgent as i,getAgent as d}from"./agent-registry.js";import{parseStreamSetupResponse as c,parseStreamSetupError as l}from"./stream-setup-helper.js";import{DEFAULTS as u}from"../defaults.js";import{getEnv as m}from"../env.js";import{uploadFile as f}from"./file-upload.js";import{StaticAuthProvider as p}from"./auth-provider.js";import{StreamRegistry as g}from"./stream-registry.js";import{CredentialCache as k}from"./credential-cache.js";import{createStreamObject as y,createExternalStreamObject as h,runOnActivate as b}from"./stream-context.js";import{TaskClient as I}from"./task-client.js";import{ConsumerAuth as w}from"./consumer-auth.js";import{fetchCdmConfig as v}from"./cdm-config.js";import{AgentAuth as S,AgentAuthFatalError as T}from"./agent-auth.js";import{CURRENT_PROTOCOL_VERSION as C,SUPPORTED_PROTOCOL_VERSIONS as E,isProtocolVersionSupported as A}from"./protocol-version.js";const $=async(e,t,a,n,s,i=C)=>{const d=r(n);try{await e.publish({channel:d.taskChannel(t,a),message:o({...s,protocolVersion:i}),storeInHistory:!0,sendByPost:!0,meta:o({agentName:n,taskId:t,protocolVersion:i})})}catch{}};export const _LOG_LEVEL_ORDER={error:0,warn:1,info:2,debug:3};export function _resolveLogLevel(){const e=(m("LOG_LEVEL")??"info").toLowerCase();return _LOG_LEVEL_ORDER[e]??_LOG_LEVEL_ORDER.info}export function _isDiagEntryStale(e){const{lastStatusAt:t,lastCategory:a,now:n,thresholdMs:s}=e;return null!==t&&("PNConnectedCategory"!==a&&n-t>s)}const _=(e,t,a)=>{const n=_resolveLogLevel();if((_LOG_LEVEL_ORDER[e]??_LOG_LEVEL_ORDER.info)>n)return;const s={level:e,message:t,ts:Date.now(),...a};"error"===e?console.error("[AgentInstance]",s):"warn"===e?console.warn("[AgentInstance]",s):console.log("[AgentInstance]",s)};export function resolveMaxRunningTimeSec(e,t){return void 0!==e&&void 0!==t&&e!==t&&console.info(`[AgentInstance] opts.maxRunningTimeSec (${e}) overrides card.runtime.maxRunningTimeSec (${t}).`),e??t}export function computeStreamDurationMinutes(e,t,a){return"number"==typeof e?e:t?60:Math.ceil((a??3600)/60)}const K=async(e,n,s,r,o,i,d,c,l)=>{const u={type:"artifact",taskId:n};if(c&&(u.outputId=c),a(o.length))await $(e,n,s,r,{...u,artifactRef:t({data:o,mimeType:i,fileName:d})});else{if(!l)throw new Error("Artifact size exceeds the inline threshold but no baseUrl is configured. Set baseUrl in AgentInstanceOptions to enable the pre-signed URL upload flow.");await f(l,{role:"provider-output",taskId:n,fileName:d??`${n}-artifact`,fileSize:o.length,mimeType:i,outputId:c},o)}};async function x(e,t){const a=`setup.${t.orgId}.${t.taskId}`,n={type:"stream_setup",taskId:t.taskId,orgId:t.orgId,agentName:t.agentName,streamId:t.streamId,channel:t.channel,direction:t.direction,format:t.format,taskKind:t.taskKind,durationMinutes:t.durationMinutes,affinity:t.affinity,declaredStream:t.declaredStream??"_default",protocolVersion:C};t.phase&&(n.phase=t.phase),t.metadata&&(n.metadata=t.metadata);try{throw await e.publish({channel:a,message:o(n),storeInHistory:!1,sendByPost:!0}),new Error("stream_setup publish should have been aborted by Function")}catch(e){const t=l(e);if(t)throw new Error(`Stream setup failed: [${t.code}] ${t.message}`);const a=c(e);if(a)return{token:a.token,tokenTtlMinutes:a.tokenTtlMinutes};throw e}}export const startAgentInstance=async t=>{const a=t.agentName;if(!a)throw new Error("agentName is required: provide opts.agentName");if(!/^[a-zA-Z0-9_]+$/.test(a))throw new Error("agentName must contain only alphanumeric characters and underscores (no hyphens)");const o=r(a),c=t.instanceId??`AG-${a}-${crypto.randomUUID()}`,l=t.token;let f,R=null,M="playground",L=M;if(t.pubnub){const e=t.pubnub._configuration?.keySet,a={publishKey:e?.publishKey??"",subscribeKey:e?.subscribeKey??""};f={playground:a,network:a}}else R=await v(t.cdmUrl),f={playground:R.playground,network:R.network},console.log("[AgentInstance] CDM config loaded — environment switching enabled (playground + network)");const N=m("BLOCKS_API_KEY");if(!N)throw new Error("BLOCKS_API_KEY is required. Run 'blocks login --write-env' to set up credentials.");let P;const j=t.baseUrl??R?.api.baseUrl;let U,D;if(N&&j&&(P=new S(N,j),console.log("[AgentInstance] AgentAuth created — will authenticate via connect")),R){const e=t.baseUrl??R.api.baseUrl,n=await d(a,{baseUrl:e,apiKey:N});if(!n)throw new Error(`[AgentInstance] Agent "${a}" not found in registry. Register the agent (e.g. via \`blocks publish\`) before starting an instance.`);if(!n.billingMode)throw new Error(`[AgentInstance] Agent "${a}" registry entry is missing billingMode. Re-register the agent so the registry persists an explicit billingMode.`);U=n.billingMode,D=n.listing,M="paid"===U?"network":"playground",console.log(`[AgentInstance] Registry billingMode: ${U} — using ${M} environment`),L=M}const O=U??"free",q=[];let V=null;const B=(e,t)=>{const a={label:e,pn:t,listener:void 0,lastStatusAt:null,lastConnectedAt:null,lastMessageAt:null,lastCategory:null,lastOperation:null,lastStatusCode:null};a.listener=(e=>({status:t=>{const a=t,n=String(a.category??""),s=String(a.operation??""),r="number"==typeof a.statusCode?a.statusCode:null,o=Date.now(),i=e.lastCategory;e.lastStatusAt=o,e.lastCategory=n,e.lastOperation=s,e.lastStatusCode=r,"PNConnectedCategory"===n&&(e.lastConnectedAt=o),_("debug","pubnub status event",{event:"pubnub_status",client:e.label,category:n,operation:s,statusCode:r,error:Boolean(a.error),affectedChannels:Array.isArray(a.affectedChannels)?a.affectedChannels.length:void 0,instanceId:c}),n&&n!==i&&_("info",`pubnub status transition [${e.label}]: ${i??"(none)"} -> ${n}`,{event:"pubnub_status_transition",client:e.label,from:i,to:n,operation:s,statusCode:r,instanceId:c})},message:()=>{e.lastMessageAt=Date.now()}}))(a),q.push(a),t.addListener(a.listener)},F=e=>{const t=q.findIndex(t=>t.pn===e);if(t<0)return;const a=q[t];try{e.removeListener(a.listener)}catch{}q.splice(t,1)};let G,z=!1;if(t.pubnub)G=t.pubnub,z=!1,l&&G.setToken(l);else{const e=f[L];G=s({publishKey:e.publishKey,subscribeKey:e.subscribeKey,userId:c,presenceTimeout:20,announceSuccessfulHeartbeats:_resolveLogLevel()>=_LOG_LEVEL_ORDER.debug,subscribeRetryUnbounded:!0,onRetry:e=>{_("warn","pubnub transport retrying",{event:"pubnub_transport_retry",message:e,instanceId:c})}}),z=!0,l&&G.setToken(l)}let H=f[L].subscribeKey,Y=f[L].publishKey;const W=j?new w({apiKey:N,baseUrl:j}):void 0,Z=new I({billingMode:O,subscribeKey:H,publishKey:Y,authProvider:W,baseUrl:j,createPubNub:()=>{const e=s({publishKey:Y,subscribeKey:H,userId:`${c}-taskclient`,subscribeRetryUnbounded:!1});return B("consumer-task",e),e}}),J=t.concurrency??u.concurrency,Q=t.expectedInstances??u.expectedInstances,X=t.maxPendingBacklog,ee=resolveMaxRunningTimeSec(t.maxRunningTimeSec,t.card?.runtime?.maxRunningTimeSec),te=Date.now();let ae=0;const ne=new g,se=new k,re=new Map,oe=new Map,ie=new Map,de=new Map,ce=new Map,le=new Set,ue=new Set,me=new Map,fe=new Set,pe=new Map;let ge,ke;t.logChannel;const ye=async()=>{if("function"==typeof G.setState&&ke)try{await G.setState({channels:[ke],state:{instanceId:c,activeTasks:ae,activeStreams:ne.activeStreamCount,concurrency:J,startedAt:te,preferredProtocolVersion:C,protocolVersions:[...E]}})}catch{}},he=(e,t)=>{const a=re.get(e);a&&(a.delete(t),0===a.size&&re.delete(e))},be=async e=>{(e=>{for(const[t,a]of re)a.has(e)&&(a.delete(e),0===a.size&&re.delete(t))})(e);const t=ne.releaseAllForTask(e);for(const e of t)if(e.streamClient)try{await e.streamClient.end()}catch{}},Ie=async(e,t)=>{he(e,t);const a=ne.get(e);if(0===ne.release(e,t)&&a?.streamClient){console.log("[AgentInstance]",{level:"info",message:"last-ref teardown",event:"stream_registry_last_ref_teardown",streamId:e,affinity:a.affinity,releasingTaskId:t,ts:Date.now()});try{await a.streamClient.end()}catch{}}ye().catch(()=>{})},we=async(e,t)=>{const a=ne.forceRemove(e);if((e=>{re.delete(e)})(e),a){if(a.streamClient)try{await a.streamClient.end()}catch{}for(const e of a.taskIds){const a=se.get(e);if(a){try{const n=f[a.environment??M]??f[M],r=s({publishKey:n.publishKey,subscribeKey:n.subscribeKey,userId:c,subscribeRetryUnbounded:!1});r.setToken(a.writeToken),await $(r,e,a.orgId,a.agentName,{type:"terminal",taskId:e,state:"failed",error:t}),r.destroy()}catch{}se.remove(e)}}await ye()}},ve=async(e,t)=>{const a=se.get(e);if(!a)throw new Error(`No cached credentials for task ${e}`);const n=f[a.environment??M]??f[M],r=s({publishKey:n.publishKey,subscribeKey:n.subscribeKey,userId:c,subscribeRetryUnbounded:!1});r.setToken(a.writeToken);try{await $(r,e,a.orgId,a.agentName,{...t,type:"terminal",taskId:e})}finally{r.destroy()}await be(e),se.remove(e),await ye()},Se=async(s,r,i,d,c)=>{const u=a,m="pipe"===s.taskKind,f=m?"pipe":"request",g=computeStreamDurationMinutes(s.duration,m,ee);if(m&&"number"==typeof s.durationExpiresAtMs){const e=Math.max(0,s.durationExpiresAtMs-Date.now());if(e>0){const t=setTimeout(()=>{le.add(s.taskId),c.abort()},e);me.set(s.taskId,t)}}await $(r,s.taskId,d,u,{type:"progress",taskId:s.taskId,progress:0,state:"running"}),pe.set(s.taskId,0);let k=0,I=null,w=null;const v=()=>{if(null===I)return;const e=I;I=null,k=Date.now(),$(r,s.taskId,d,u,{type:"progress",taskId:s.taskId,message:e}).catch(()=>{})},S={taskId:s.taskId,requestParts:s.requestParts??[],reportStatus:e=>{de.set(s.taskId,e);const t=Date.now();if(t-k>=1e3)I=null,k=t,$(r,s.taskId,d,u,{type:"progress",taskId:s.taskId,message:e}).catch(()=>{});else if(I=e,!w){w=setTimeout(()=>{w=null,v()},1e3-(t-k))}},get isCancelled(){return c.signal.aborted},get isExpired(){return le.has(s.taskId)},hasStream:!!s.hasStream,cancelSignal:c.signal,taskClient:Z,consumerPublicKey:s.consumerPublicKey,downloadInputArtifact:async e=>{const t=e.artifactRef;if(!t)throw new Error(`Request part has no artifactRef (partId: ${e.partId??"none"})`);if("inline"===t.kind)return Buffer.from(n(t));if(!t.channel||!t.fileId||!t.fileName)throw new Error("File artifactRef missing channel, fileId, or fileName");if(!r.downloadFile)throw new Error("PubNub client does not support downloadFile");const a=await r.downloadFile({channel:t.channel,id:t.fileId,name:t.fileName}),s=a?.data;if(Buffer.isBuffer(s))return s;if(s instanceof Uint8Array)return Buffer.from(s);if(s&&"function"==typeof s.arrayBuffer){const e=await s.arrayBuffer();return Buffer.from(e)}throw new Error("Unexpected downloadFile response format")},publishArtifact:async(e,t)=>{const a="string"==typeof e?Buffer.from(e,"utf-8"):e,n=t?.mimeType??"application/octet-stream",o=j?{baseUrl:j,authProvider:l?new p(l):void 0,agentAuth:P}:void 0;await K(r,s.taskId,d,u,a,n,t?.fileName,t?.outputId,o)},createStream:async a=>{if(void 0!==a&&("object"!=typeof a||null===a||Array.isArray(a)))throw new TypeError("createStream expects an options object. The positional streamId argument was removed; move any declared-stream key into CreateStreamOptions.declaredStream. See SDK_CONTRACT §8.2.1.");if(!s.hasStream)throw new Error("Streaming was not negotiated for this task. Ensure the agent card is registered with streaming capability.");const n=t.card.streams;if(!n||0===Object.keys(n).length)throw new Error("Agent card has no streams block. Streaming requires declared streams in the card.");const i=Object.keys(n);let c;if(a?.declaredStream)c=a.declaredStream;else{if(1!==i.length)throw new Error(`Card declares multiple streams (${i.join(", ")}). Specify declaredStream in CreateStreamOptions.`);c=i[0]}const l=n[c];if(!l)throw new Error(`Stream "${c}" is not declared in the agent card. Available streams: ${i.join(", ")}`);const p=a?.direction??l.direction,k=a?.format??l.format;if(a?.direction&&a.direction!==l.direction)throw new Error(`Direction "${a.direction}" conflicts with card declaration "${l.direction}" for stream "${c}".`);if(a?.format&&a.format!==l.format)throw new Error(`Format "${a.format}" conflicts with card declaration "${l.format}" for stream "${c}".`);const I=a?.external??!1,w=a?.metadata,v=a?.onActivate,S=l.affinity??"dedicated";if("shared"===S&&I)throw new Error(`Shared-affinity external streams are not supported. Declared stream '${c}' has affinity: 'shared' and createStream was called with external: true. Shared affinity requires a single SDK-managed writer with per-task ref-counting; external streams delegate the writer entirely. Use affinity: 'dedicated' with external: true, or affinity: 'shared' without external.`);if(!m){if("shared"===S)throw new Error(`Shared-affinity streams are not supported on request tasks. Declared stream '${c}' has affinity: 'shared'. Request tasks are single-shot; cross-task broadcast is inherently a pipe-task concept. Use affinity: 'dedicated' or remove 'request' from the agent card's taskKinds.`);if("outbound"!==p)throw new Error("Request tasks only support outbound streams");if(I)throw new Error("Request tasks cannot use external streams")}let T;if("shared"===S)T=c;else{const e=(pe.get(s.taskId)??0)+1;pe.set(s.taskId,e),T=`${s.taskId}-${e}`}const C=o.streamChannel(T);if("shared"===S){const e=re.get(T)?.get(s.taskId);if(e)return e}const{entry:E,isNew:A,isNewForTask:$}=ne.acquire(T,s.taskId,{direction:p,format:k,external:I,affinity:S});se.addStream(s.taskId,T);const _=e=>{if("shared"!==S)return;let t=re.get(T);t||(t=new Map,re.set(T,t)),t.set(s.taskId,e)};if(!$){E.setupPromise&&await E.setupPromise;const e=re.get(T)?.get(s.taskId);if(e)return e;if(E.streamClient){const e=y(T,E.streamClient,s.taskId,{releaseStream:Ie});return _(e),e}throw new Error(`Stream "${T}" same-task reacquire reached an impossible state: setup completed but no client installed. This indicates a logic error in the shared-stream handle cache.`)}if(!A)try{if(E.setupPromise&&await E.setupPromise,"shared"===S&&await(async()=>{await x(r,{taskId:s.taskId,orgId:d,agentName:u,streamId:T,channel:C,direction:p,format:k,taskKind:f,durationMinutes:g,affinity:S,phase:"activate",metadata:w,declaredStream:c})})(),E.streamClient){const e=y(T,E.streamClient,s.taskId,{releaseStream:Ie});return _(e),e}if(I)return h(T,C,"",async()=>{throw new Error("External stream already activated")});throw new Error(`Stream "${T}" exists but has no client`)}catch(e){he(T,s.taskId);try{await ne.release(T,s.taskId)}catch{}throw e}let K,R;E.setupPromise=new Promise((e,t)=>{K=e,R=t}),E.setupPromise.catch(()=>{});const M=I?"token_request":"embedded";let L;try{L=await x(r,{taskId:s.taskId,orgId:d,agentName:u,streamId:T,channel:C,direction:p,format:k,taskKind:f,durationMinutes:g,affinity:S,phase:M,metadata:w,declaredStream:c})}catch(e){R(e),he(T,s.taskId);try{await ne.release(T,s.taskId)}catch{}throw e}if(I){K(),E.setupPromise=null;const e=L.token??"";return h(T,C,e,async e=>{await x(r,{taskId:s.taskId,orgId:d,agentName:u,streamId:T,channel:C,direction:p,format:k,taskKind:f,durationMinutes:g,affinity:S,phase:"activate",metadata:e?.metadata??w,declaredStream:c})})}const N=L.token;if(!N){const e=new Error("No T7a token received from stream setup handshake");R(e),he(T,s.taskId);try{await ne.release(T,s.taskId)}catch{}throw e}const P=new e({subscribeKey:H,publishKey:Y,token:N,agentName:u,streamId:T,channel:C,format:k,direction:p,affinity:S,gating:m,bundleSizeBytes:a?.bundleSizeBytes,maxLatencyMs:a?.maxLatencyMs});E.streamClient=P;const j=y(T,P,s.taskId,{releaseStream:Ie});if(_(j),K(),E.setupPromise=null,"outbound"===p||"bidirectional"===p){const e=a?.subscribeGraceMs??1e3;e>0&&await new Promise(t=>setTimeout(t,e))}return v&&(E.activated=!0,E.activatePromise=b(T,j,v,we)),j}};let T;try{T=t.handler?await t.handler(s,S):void 0}finally{pe.delete(s.taskId),w&&(clearTimeout(w),w=null),v()}const C=j?{baseUrl:j,authProvider:l?new p(l):void 0,agentAuth:P}:void 0;if(T?.artifacts&&T.artifacts.length>0)for(const e of T.artifacts){const t="string"==typeof e.data?Buffer.from(e.data,"utf-8"):e.data;await K(r,s.taskId,d,u,t,e.mimeType,e.fileName,e.outputId,C)}const E=c.signal.aborted;if(m){const e=le.has(s.taskId),t=ue.has(s.taskId);if(E||e||t){await be(s.taskId);const a=E&&!e?"canceled":"completed";await $(r,s.taskId,d,u,{type:"terminal",taskId:s.taskId,state:a,...e?{completionReason:"duration_expired"}:{},...t?{reason:"terminated"}:{}}),se.remove(s.taskId),_("info",`Task ${s.taskId} ${a}`,{taskId:s.taskId,agentName:u,owner:i})}else _("info",`Task ${s.taskId} handler returned (pipe, no auto-terminal)`,{taskId:s.taskId,agentName:u,owner:i})}else{const e=ue.has(s.taskId);await be(s.taskId);const t=E?"canceled":"completed";await $(r,s.taskId,d,u,{type:"terminal",taskId:s.taskId,state:t,...e?{reason:"terminated"}:{}}),se.remove(s.taskId),_("info",`Task ${s.taskId} ${t}`,{taskId:s.taskId,agentName:u,owner:i})}},Te=(()=>{let e=!1;return(t,a,n)=>{if(_("debug","pamDeniedHandler invoked",{event:"pam_handler_invoked",category:t,operation:a,statusCode:n,alreadyHandled:e,instanceId:c}),"PNAccessDeniedCategory"===t&&!e){e=!0,_("error","PAM access denied — destroying control client (agent will go silent)",{event:"pam_denied_destroy",category:t,operation:a,statusCode:n,instanceId:c,controlChannel:ke}),console.error(`[AgentInstance] PAM token expired or revoked — agent ${c} is no longer receiving tasks. Re-register the agent to resume.`);try{G.destroy()}catch{}}}})(),Ce={status:e=>{const t=e,a=String(t.category??""),n=String(t.operation??""),s="number"==typeof t.statusCode?t.statusCode:null;"PNConnectedCategory"===a&&console.log(`[AgentInstance] PubNub connected to ${ke}`),Te(a,n,s)},message:e=>{const n=e.message;if(!n)return;if("SwitchEnvironment"===n.type){const e=n,t=e.environment;return void(t&&t!==L&&t in f&&((e,t)=>{if(_("info",`SwitchEnvironment requested: ${L} -> ${e}`,{pamToken:t?"present":"absent"}),!t)return void _("error","SwitchEnvironment rejected: pamToken is required but was absent. The backend must include a pamToken in SwitchEnvironment messages.");const a=G,n=z;try{a.removeListener(Ce),ke&&a.unsubscribe({channels:[ke]})}catch{}F(a);const r=f[e];if(G=s({publishKey:r.publishKey,subscribeKey:r.subscribeKey,userId:c,presenceTimeout:20,announceSuccessfulHeartbeats:_resolveLogLevel()>=_LOG_LEVEL_ORDER.debug,subscribeRetryUnbounded:!0,onRetry:e=>{_("warn","pubnub transport retrying",{event:"pubnub_transport_retry",message:e,instanceId:c})}}),z=!0,ge=void 0,G.setToken(t),0!==Q&&"function"==typeof G.setFilterExpression&&G.setFilterExpression(`meta.instance == '${c}' || meta.broadcast == "true"`),G.addListener(Ce),B("control",G),ke&&G.subscribe({channels:[ke]}),n)try{a.destroy()}catch{}L=e,H=r.subscribeKey,Y=r.publishKey,Z.updateKeys(r.subscribeKey,r.publishKey),_("info",`Switched to ${e} environment`),ye().catch(()=>{})})(t,e.pamToken))}(async(e,n)=>{if("StartTask"===e.type){const r=L,o=((e,t)=>{if("string"==typeof e&&e.length>0)return e;const a=t?.sub;return"string"==typeof a&&a.length>0?a:"anonymous"})(e.ownerId,e.callerClaims),i=e.orgId??o;if(fe.has(e.taskId))return void _("warn",`Ignoring duplicate StartTask for in-flight task ${e.taskId}`,{taskId:e.taskId});const d=Boolean(n?.broadcast);if(e.protocolVersion&&!A(e.protocolVersion))return d?void _("debug",`Ignoring broadcast StartTask ${e.taskId} with unsupported protocolVersion ${e.protocolVersion}`,{taskId:e.taskId}):(await $(G,e.taskId,i,a,{type:"terminal",taskId:e.taskId,state:"failed",error:"unsupported_protocol_version"},e.protocolVersion),void _("warn",`Task ${e.taskId} rejected: unsupported protocolVersion ${e.protocolVersion}`,{taskId:e.taskId,instanceId:c}));if("pipe"===(e.taskKind||"request")){const t=e.duration,n=e.durationExpiresAtMs;if(null==t||!Number.isInteger(t)||t<1||t>43200||"number"!=typeof n||n<=0)return _("error",`Task ${e.taskId} rejected: pipe StartTask with invalid duration (duration=${t}, durationExpiresAtMs=${n})`,{taskId:e.taskId,instanceId:c}),void await $(G,e.taskId,i,a,{type:"terminal",taskId:e.taskId,state:"failed",error:"invalid_start_task"},e.protocolVersion)}const l=ae+ne.activeStreamCount;if(J>0&&l>=J)return d?void _("debug",`Task ${e.taskId} skipped (broadcast, at capacity ${l}/${J})`,{taskId:e.taskId,instanceId:c}):(await $(G,e.taskId,i,a,{type:"terminal",taskId:e.taskId,state:"failed",error:"agent_at_capacity"}),void _("warn",`Task ${e.taskId} rejected: agent at capacity`,{taskId:e.taskId,instanceId:c,activeTasks:ae}));oe.set(e.taskId,o),ie.set(e.taskId,i),fe.add(e.taskId),ae++,e.controlToken&&(ge=e.controlToken);const u=new AbortController;ce.set(e.taskId,u);let m=null;try{const n=f[r];if(!n.publishKey||!n.subscribeKey)throw new Error("Per-task PubNub client requires valid pub/sub keys. When using an injected PubNub client, ensure it was created with publishKey and subscribeKey.");m=s({publishKey:n.publishKey,subscribeKey:n.subscribeKey,userId:c,subscribeRetryUnbounded:!1}),e.writeToken&&m.setToken(e.writeToken),B(`task-sub:${e.taskId}`,m),se.set(e.taskId,{ownerId:o,orgId:i,writeToken:e.writeToken??"",agentName:a,environment:r}),await ye();const{writeToken:d,controlToken:l,...p}=e;t.onStartTask?await t.onStartTask(e,m):await Se(p,m,o,i,u)}catch(n){const s=n?.message??"Agent instance error",r=m??G;try{await be(e.taskId)}catch{}await $(r,e.taskId,i,a,{type:"terminal",taskId:e.taskId,state:"failed",error:s}),t.onError&&await t.onError(e.taskId,n),_("error",s,{taskId:e.taskId}),se.remove(e.taskId)}finally{const t=me.get(e.taskId);if(t&&(clearTimeout(t),me.delete(e.taskId)),fe.delete(e.taskId),oe.delete(e.taskId),ie.delete(e.taskId),de.delete(e.taskId),ce.delete(e.taskId),le.delete(e.taskId),ue.delete(e.taskId),m){F(m);try{m.destroy()}catch{}}ae--,0===ae&&ge&&G.setToken(ge),await ye()}}else if("CancelTask"===e.type){const t=ce.get(e.taskId);t?(t.abort(),_("info",`Task ${e.taskId} cancel requested (cooperative)`,{taskId:e.taskId,instanceId:c})):se.get(e.taskId)&&await ve(e.taskId,{state:"canceled"})}else if("ExpireTask"===e.type)ce.has(e.taskId)?(le.add(e.taskId),ce.get(e.taskId)?.abort(),_("info",`Task ${e.taskId} expired`,{taskId:e.taskId,instanceId:c})):se.get(e.taskId)&&await ve(e.taskId,{state:"completed",completionReason:"duration_expired"});else if("TerminateTask"===e.type)ce.has(e.taskId)?(ue.add(e.taskId),ce.get(e.taskId)?.abort()):se.get(e.taskId)&&await ve(e.taskId,{state:"canceled",reason:"terminated"});else if("PauseTask"===e.type){if(fe.has(e.taskId)){const t=ie.get(e.taskId)??"anonymous";await $(G,e.taskId,t,a,{type:"system",taskId:e.taskId,status:"paused"})}}else if("ResumeTask"===e.type){if(fe.has(e.taskId)){const t=ie.get(e.taskId)??"anonymous";await $(G,e.taskId,t,a,{type:"system",taskId:e.taskId,status:"resumed"})}}else"RetryTask"===e.type&&_("info","RetryTask received (no-op)",{taskId:e.taskId})})(n,e.userMetadata&&"object"==typeof e.userMetadata?e.userMetadata:void 0).catch(e=>{console.error("[AgentInstance] Unhandled error in message handler:",e)})}};G.addListener(Ce),B("control",G),null===V&&(V=setInterval(()=>{const e=Date.now();let t=!1;const a=q.map(a=>{const n=null!==a.lastStatusAt?e-a.lastStatusAt:null,s=_isDiagEntryStale({lastStatusAt:a.lastStatusAt,lastCategory:a.lastCategory,now:e,thresholdMs:6e4});s&&(t=!0);const r="function"==typeof a.pn.getSubscribedChannels?a.pn.getSubscribedChannels().length:null;return{client:a.label,lastCategory:a.lastCategory,lastOperation:a.lastOperation,lastStatusCode:a.lastStatusCode,msSinceStatus:n,msSinceConnected:null!==a.lastConnectedAt?e-a.lastConnectedAt:null,msSinceMessage:null!==a.lastMessageAt?e-a.lastMessageAt:null,subscribedChannels:r,stale:s}}),n={event:"pubnub_alive_snapshot",instanceId:c,clients:a};t?_("info","pubnub alive snapshot — STALE clients present",n):_("debug","pubnub alive snapshot",n)},1e4),"function"==typeof V.unref&&V.unref()),_("info","pubnub diagnostics armed",{event:"pubnub_diagnostics_armed",snapshotIntervalMs:1e4,staleThresholdMs:6e4,instanceId:c}),i(a,{instanceId:c,billingMode:O,description:t.description,scaling:{expectedInstances:Q,concurrency:J,maxPendingBacklog:X,maxRunningTimeSec:ee},card:t.card,cardRef:t.cardRef,cardSummary:t.cardSummary,listing:D??t.listing,actor:`agent-instance:${c}`,baseUrl:t.baseUrl??R?.api.baseUrl,agentAuth:P}).then(e=>{if(console.log(`[AgentInstance] Registered agent: ${a} (instance: ${c})`),!e.controlChannel)throw new Error("Connect response missing controlChannel — server may be outdated");ke=e.controlChannel,e.pamToken&&G.setToken(e.pamToken),0!==Q&&"function"==typeof G.setFilterExpression&&G.setFilterExpression(`meta.instance == '${c}' || meta.broadcast == "true"`),G.subscribe({channels:[ke]}),ye().catch(()=>{})}).catch(e=>{const t=e instanceof Error?e.message:String(e);console.error(`[AgentInstance] Failed to register agent: ${a} — ${t}`),e instanceof T&&process.exit(1)}),_("info",`Agent instance ${c} started (agent name: ${a})`,{agentName:a,event:"agent_instance_started",instanceId:c});return{stop:()=>{try{null!==V&&(clearInterval(V),V=null);for(const e of q)try{e.pn.removeListener(e.listener)}catch{}q.length=0,W?.destroy(),Z.destroy(),G.removeListener(Ce),ke&&G.unsubscribe({channels:[ke]}),z&&G.destroy();for(const e of ne.streamIds()){const t=ne.get(e);if(t?.streamClient)try{t.streamClient.end()}catch{}}ne.clear(),re.clear(),se.clear()}catch{}},agentName:a,instanceId:c,get controlChannel(){return ke},get pubnub(){return G},get subscribeKey(){return f[L].subscribeKey},taskClient:Z,publishTerminal:ve,failStream:we,cdmConfig:R}};export const artifactUtils={buildArtifactRef:t,shouldInlineArtifact:a};
1
+ import{StreamClient as e}from"../stream/index.js";import{buildArtifactRef as t,shouldInlineArtifact as a,decodeInlineArtifact as s}from"./artifacts.js";import{createPubNubClient as n}from"./pubnub-client.js";import{createChannelManager as r}from"./channel-manager.js";import{toPayload as o}from"./pubnub-types.js";import{connectAgent as i,getAgent as d}from"./agent-registry.js";import{parseStreamSetupResponse as c,parseStreamSetupError as l}from"./stream-setup-helper.js";import{DEFAULTS as u}from"../defaults.js";import{getEnv as m}from"../env.js";import{log as f,isDebugSubsystemEnabled as p,_resolveLogLevel as k,_LOG_LEVEL_ORDER as g}from"./logger.js";import{mapTransportCategory as h,mapTransportOperation as y,isAccessDeniedStatus as I,DEGRADED_TRANSPORT_CATEGORIES as b,RESTORED_TRANSPORT_CATEGORIES as w}from"./transport-categories.js";import{uploadFile as v}from"./file-upload.js";import{StaticAuthProvider as S}from"./auth-provider.js";import{StreamRegistry as T}from"./stream-registry.js";import{CredentialCache as C}from"./credential-cache.js";import{createStreamObject as $,createExternalStreamObject as _,runOnActivate as K}from"./stream-context.js";import{TaskClient as A}from"./task-client.js";import{ConsumerAuth as E}from"./consumer-auth.js";import{fetchCdmConfig as x}from"./cdm-config.js";import{AgentAuth as M,AgentAuthFatalError as N}from"./agent-auth.js";import{CURRENT_PROTOCOL_VERSION as R,SUPPORTED_PROTOCOL_VERSIONS as j,isProtocolVersionSupported as P}from"./protocol-version.js";const U=async(e,t,a,s,n,i=R)=>{const d=r(s);try{await e.publish({channel:d.taskChannel(t,a),message:o({...n,protocolVersion:i}),storeInHistory:!0,sendByPost:!0,meta:o({agentName:s,taskId:t,protocolVersion:i})})}catch{}};export function _isDiagEntryStale(e){const{lastStatusAt:t,lastCategory:a,now:s,thresholdMs:n}=e;return null!==t&&("connected"!==a&&s-t>n)}const q=(e,t,a)=>f("[AgentInstance]",e,t,a);export function resolveMaxRunningTimeSec(e,t){return void 0!==e&&void 0!==t&&e!==t&&f("[AgentInstance]","info",`opts.maxRunningTimeSec (${e}) overrides card.runtime.maxRunningTimeSec (${t})`,{event:"max_running_time_override",optsValue:e,cardValue:t}),e??t}export function computeStreamDurationMinutes(e,t,a){return"number"==typeof e?e:t?60:Math.ceil((a??3600)/60)}const B=async(e,s,n,r,o,i,d,c,l)=>{const u={type:"artifact",taskId:s};if(c&&(u.outputId=c),a(o.length))await U(e,s,n,r,{...u,artifactRef:t({data:o,mimeType:i,fileName:d})});else{if(!l)throw new Error("Artifact size exceeds the inline threshold but no baseUrl is configured. Set baseUrl in AgentInstanceOptions to enable the pre-signed URL upload flow.");await v(l,{role:"provider-output",taskId:s,fileName:d??`${s}-artifact`,fileSize:o.length,mimeType:i,outputId:c},o)}};async function D(e,t){const a=`setup.${t.orgId}.${t.taskId}`,s={type:"stream_setup",taskId:t.taskId,orgId:t.orgId,agentName:t.agentName,streamId:t.streamId,channel:t.channel,direction:t.direction,format:t.format,taskKind:t.taskKind,durationMinutes:t.durationMinutes,affinity:t.affinity,declaredStream:t.declaredStream??"_default",protocolVersion:R};t.phase&&(s.phase=t.phase),t.metadata&&(s.metadata=t.metadata);try{throw await e.publish({channel:a,message:o(s),storeInHistory:!1,sendByPost:!0}),new Error("stream_setup publish should have been aborted by Function")}catch(e){const t=l(e);if(t)throw new Error(`Stream setup failed: [${t.code}] ${t.message}`);const a=c(e);if(a)return{token:a.token,tokenTtlMinutes:a.tokenTtlMinutes};throw e}}export const startAgentInstance=async t=>{const a=t.agentName;if(!a)throw new Error("agentName is required: provide opts.agentName");if(!/^[a-zA-Z0-9_]+$/.test(a))throw new Error("agentName must contain only alphanumeric characters and underscores (no hyphens)");const o=r(a),c=t.instanceId??`AG-${a}-${crypto.randomUUID()}`,l=t.token;let f,v=null,L="playground",V=L;if(t.pubnub){const e=t.pubnub._configuration?.keySet,a={publishKey:e?.publishKey??"",subscribeKey:e?.subscribeKey??""};f={playground:a,network:a}}else v=await x(t.cdmUrl),f={playground:v.playground,network:v.network},q("info","CDM config loaded — environment switching enabled",{event:"cdm_config_loaded"});const F=m("BLOCKS_API_KEY");if(!F)throw new Error("BLOCKS_API_KEY is required. Run 'blocks login --write-env' to set up credentials.");let O;const z=t.baseUrl??v?.api.baseUrl;let H,G;if(F&&z&&(O=new M(F,z),q("info","AgentAuth created — will authenticate via connect",{event:"agent_auth_created"})),v){const e=t.baseUrl??v.api.baseUrl,s=await d(a,{baseUrl:e,apiKey:F});if(!s)throw new Error(`[AgentInstance] Agent "${a}" not found in registry. Register the agent (e.g. via \`blocks publish\`) before starting an instance.`);if(!s.billingMode)throw new Error(`[AgentInstance] Agent "${a}" registry entry is missing billingMode. Re-register the agent so the registry persists an explicit billingMode.`);H=s.billingMode,G=s.listing,L="paid"===H?"network":"playground",q("info",`registry billingMode: ${H} — using ${L} environment`,{event:"registry_billing_mode_resolved",billingMode:H,environment:L}),V=L}const Y=H??"free",W=[];let Z=null;const J=p("diagnostics"),Q=()=>{let e=!1;return{status:t=>{const a=h(t),s=b.has(a);s&&!e?(e=!0,q("warn",`connectivity degraded: ${a}`,{event:"transport_degraded",category:a,instanceId:c})):!s&&e&&w.has(a)&&(e=!1,q("info","connectivity restored",{event:"transport_restored",category:a,instanceId:c}))}}},X=(e,t)=>{if(!J)return;const a={label:e,pn:t,listener:void 0,lastStatusAt:null,lastConnectedAt:null,lastMessageAt:null,lastCategory:null,lastOperation:null,lastStatusCode:null};a.listener=(e=>({status:t=>{const a=t,s=h(a),n=y("string"==typeof a.operation?a.operation:void 0),r="number"==typeof a.statusCode?a.statusCode:null,o=Date.now(),i=e.lastCategory;e.lastStatusAt=o,e.lastCategory=s,e.lastOperation=n,e.lastStatusCode=r,"connected"===s&&(e.lastConnectedAt=o),q("debug","transport status event",{event:"transport_status",client:e.label,category:s,operation:n,statusCode:r,error:Boolean(a.error),affectedChannels:Array.isArray(a.affectedChannels)?a.affectedChannels.length:void 0,instanceId:c}),s&&s!==i&&q("info",`transport status transition [${e.label}]: ${i??"(none)"} -> ${s}`,{event:"transport_status_transition",client:e.label,from:i,to:s,operation:n,statusCode:r,instanceId:c})},message:()=>{e.lastMessageAt=Date.now()}}))(a),W.push(a),t.addListener(a.listener)},ee=e=>{if(!J)return;const t=W.findIndex(t=>t.pn===e);if(t<0)return;const a=W[t];try{e.removeListener(a.listener)}catch{}W.splice(t,1)};let te,ae=!1;if(t.pubnub)te=t.pubnub,ae=!1,l&&te.setToken(l);else{const e=f[V];te=n({publishKey:e.publishKey,subscribeKey:e.subscribeKey,userId:c,presenceTimeout:20,announceSuccessfulHeartbeats:k()>=g.debug,subscribeRetryUnbounded:!0}),ae=!0,l&&te.setToken(l)}let se=f[V].subscribeKey,ne=f[V].publishKey;const re=z?new E({apiKey:F,baseUrl:z}):void 0,oe=new A({billingMode:Y,subscribeKey:se,publishKey:ne,authProvider:re,baseUrl:z,createPubNub:()=>{const e=n({publishKey:ne,subscribeKey:se,userId:`${c}-taskclient`,subscribeRetryUnbounded:!1});return X("consumer-task",e),e}}),ie=t.concurrency??u.concurrency,de=t.expectedInstances??u.expectedInstances,ce=t.maxPendingBacklog,le=resolveMaxRunningTimeSec(t.maxRunningTimeSec,t.card?.runtime?.maxRunningTimeSec),ue=Date.now();let me=0;const fe=new T,pe=new C,ke=new Map,ge=new Map,he=new Map,ye=new Map,Ie=new Map,be=new Set,we=new Set,ve=new Map,Se=new Set,Te=new Map;let Ce,$e;t.logChannel;const _e=async()=>{if("function"==typeof te.setState&&$e)try{await te.setState({channels:[$e],state:{instanceId:c,activeTasks:me,activeStreams:fe.activeStreamCount,concurrency:ie,startedAt:ue,preferredProtocolVersion:R,protocolVersions:[...j]}})}catch{}},Ke=(e,t)=>{const a=ke.get(e);a&&(a.delete(t),0===a.size&&ke.delete(e))},Ae=async e=>{(e=>{for(const[t,a]of ke)a.has(e)&&(a.delete(e),0===a.size&&ke.delete(t))})(e);const t=fe.releaseAllForTask(e);for(const e of t)if(e.streamClient)try{await e.streamClient.end()}catch{}},Ee=async(e,t)=>{Ke(e,t);const a=fe.get(e);if(0===fe.release(e,t)&&a?.streamClient){q("info","last-ref teardown",{event:"stream_registry_last_ref_teardown",streamId:e,affinity:a.affinity,releasingTaskId:t});try{await a.streamClient.end()}catch{}}_e().catch(()=>{})},xe=async(e,t)=>{const a=fe.forceRemove(e);if((e=>{ke.delete(e)})(e),a){if(a.streamClient)try{await a.streamClient.end()}catch{}for(const e of a.taskIds){const a=pe.get(e);if(a){try{const s=f[a.environment??L]??f[L],r=n({publishKey:s.publishKey,subscribeKey:s.subscribeKey,userId:c,subscribeRetryUnbounded:!1});r.setToken(a.writeToken),await U(r,e,a.orgId,a.agentName,{type:"terminal",taskId:e,state:"failed",error:t}),r.destroy()}catch{}pe.remove(e)}}await _e()}},Me=async(e,t)=>{const a=pe.get(e);if(!a)throw new Error(`No cached credentials for task ${e}`);const s=f[a.environment??L]??f[L],r=n({publishKey:s.publishKey,subscribeKey:s.subscribeKey,userId:c,subscribeRetryUnbounded:!1});r.setToken(a.writeToken);try{await U(r,e,a.orgId,a.agentName,{...t,type:"terminal",taskId:e})}finally{r.destroy()}await Ae(e),pe.remove(e),await _e()},Ne=async(n,r,i,d,c)=>{const u=a,m="pipe"===n.taskKind,f=m?"pipe":"request",p=computeStreamDurationMinutes(n.duration,m,le);if(m&&"number"==typeof n.durationExpiresAtMs){const e=Math.max(0,n.durationExpiresAtMs-Date.now());if(e>0){const t=setTimeout(()=>{be.add(n.taskId),c.abort()},e);ve.set(n.taskId,t)}}await U(r,n.taskId,d,u,{type:"progress",taskId:n.taskId,progress:0,state:"running"}),Te.set(n.taskId,0);let k=0,g=null,h=null;const y=()=>{if(null===g)return;const e=g;g=null,k=Date.now(),U(r,n.taskId,d,u,{type:"progress",taskId:n.taskId,message:e}).catch(()=>{})},I={taskId:n.taskId,requestParts:n.requestParts??[],reportStatus:e=>{ye.set(n.taskId,e);const t=Date.now();if(t-k>=1e3)g=null,k=t,U(r,n.taskId,d,u,{type:"progress",taskId:n.taskId,message:e}).catch(()=>{});else if(g=e,!h){h=setTimeout(()=>{h=null,y()},1e3-(t-k))}},get isCancelled(){return c.signal.aborted},get isExpired(){return be.has(n.taskId)},hasStream:!!n.hasStream,cancelSignal:c.signal,taskClient:oe,consumerPublicKey:n.consumerPublicKey,downloadInputArtifact:async e=>{const t=e.artifactRef;if(!t)throw new Error(`Request part has no artifactRef (partId: ${e.partId??"none"})`);if("inline"===t.kind)return Buffer.from(s(t));if(!t.channel||!t.fileId||!t.fileName)throw new Error("File artifactRef missing channel, fileId, or fileName");if(!r.downloadFile)throw new Error("PubNub client does not support downloadFile");const a=await r.downloadFile({channel:t.channel,id:t.fileId,name:t.fileName}),n=a?.data;if(Buffer.isBuffer(n))return n;if(n instanceof Uint8Array)return Buffer.from(n);if(n&&"function"==typeof n.arrayBuffer){const e=await n.arrayBuffer();return Buffer.from(e)}throw new Error("Unexpected downloadFile response format")},publishArtifact:async(e,t)=>{const a="string"==typeof e?Buffer.from(e,"utf-8"):e,s=t?.mimeType??"application/octet-stream",o=z?{baseUrl:z,authProvider:l?new S(l):void 0,agentAuth:O}:void 0;await B(r,n.taskId,d,u,a,s,t?.fileName,t?.outputId,o)},createStream:async a=>{if(void 0!==a&&("object"!=typeof a||null===a||Array.isArray(a)))throw new TypeError("createStream expects an options object. The positional streamId argument was removed; move any declared-stream key into CreateStreamOptions.declaredStream. See SDK_CONTRACT §8.2.1.");if(!n.hasStream)throw new Error("Streaming was not negotiated for this task. Ensure the agent card is registered with streaming capability.");const s=t.card.streams;if(!s||0===Object.keys(s).length)throw new Error("Agent card has no streams block. Streaming requires declared streams in the card.");const i=Object.keys(s);let c;if(a?.declaredStream)c=a.declaredStream;else{if(1!==i.length)throw new Error(`Card declares multiple streams (${i.join(", ")}). Specify declaredStream in CreateStreamOptions.`);c=i[0]}const l=s[c];if(!l)throw new Error(`Stream "${c}" is not declared in the agent card. Available streams: ${i.join(", ")}`);const k=a?.direction??l.direction,g=a?.format??l.format;if(a?.direction&&a.direction!==l.direction)throw new Error(`Direction "${a.direction}" conflicts with card declaration "${l.direction}" for stream "${c}".`);if(a?.format&&a.format!==l.format)throw new Error(`Format "${a.format}" conflicts with card declaration "${l.format}" for stream "${c}".`);const h=a?.external??!1,y=a?.metadata,I=a?.onActivate,b=l.affinity??"dedicated";if("shared"===b&&h)throw new Error(`Shared-affinity external streams are not supported. Declared stream '${c}' has affinity: 'shared' and createStream was called with external: true. Shared affinity requires a single SDK-managed writer with per-task ref-counting; external streams delegate the writer entirely. Use affinity: 'dedicated' with external: true, or affinity: 'shared' without external.`);if(!m){if("shared"===b)throw new Error(`Shared-affinity streams are not supported on request tasks. Declared stream '${c}' has affinity: 'shared'. Request tasks are single-shot; cross-task broadcast is inherently a pipe-task concept. Use affinity: 'dedicated' or remove 'request' from the agent card's taskKinds.`);if("outbound"!==k)throw new Error("Request tasks only support outbound streams");if(h)throw new Error("Request tasks cannot use external streams")}let w;if("shared"===b)w=c;else{const e=(Te.get(n.taskId)??0)+1;Te.set(n.taskId,e),w=`${n.taskId}-${e}`}const v=o.streamChannel(w);if("shared"===b){const e=ke.get(w)?.get(n.taskId);if(e)return e}const{entry:S,isNew:T,isNewForTask:C}=fe.acquire(w,n.taskId,{direction:k,format:g,external:h,affinity:b});pe.addStream(n.taskId,w);const A=e=>{if("shared"!==b)return;let t=ke.get(w);t||(t=new Map,ke.set(w,t)),t.set(n.taskId,e)};if(!C){S.setupPromise&&await S.setupPromise;const e=ke.get(w)?.get(n.taskId);if(e)return e;if(S.streamClient){const e=$(w,S.streamClient,n.taskId,{releaseStream:Ee});return A(e),e}throw new Error(`Stream "${w}" same-task reacquire reached an impossible state: setup completed but no client installed. This indicates a logic error in the shared-stream handle cache.`)}if(!T)try{if(S.setupPromise&&await S.setupPromise,"shared"===b&&await(async()=>{await D(r,{taskId:n.taskId,orgId:d,agentName:u,streamId:w,channel:v,direction:k,format:g,taskKind:f,durationMinutes:p,affinity:b,phase:"activate",metadata:y,declaredStream:c})})(),S.streamClient){const e=$(w,S.streamClient,n.taskId,{releaseStream:Ee});return A(e),e}if(h)return _(w,v,"",async()=>{throw new Error("External stream already activated")});throw new Error(`Stream "${w}" exists but has no client`)}catch(e){Ke(w,n.taskId);try{await fe.release(w,n.taskId)}catch{}throw e}let E,x;S.setupPromise=new Promise((e,t)=>{E=e,x=t}),S.setupPromise.catch(()=>{});const M=h?"token_request":"embedded";let N;try{N=await D(r,{taskId:n.taskId,orgId:d,agentName:u,streamId:w,channel:v,direction:k,format:g,taskKind:f,durationMinutes:p,affinity:b,phase:M,metadata:y,declaredStream:c})}catch(e){x(e),Ke(w,n.taskId);try{await fe.release(w,n.taskId)}catch{}throw e}if(h){E(),S.setupPromise=null;const e=N.token??"";return _(w,v,e,async e=>{await D(r,{taskId:n.taskId,orgId:d,agentName:u,streamId:w,channel:v,direction:k,format:g,taskKind:f,durationMinutes:p,affinity:b,phase:"activate",metadata:e?.metadata??y,declaredStream:c})})}const R=N.token;if(!R){const e=new Error("No T7a token received from stream setup handshake");x(e),Ke(w,n.taskId);try{await fe.release(w,n.taskId)}catch{}throw e}const j=new e({subscribeKey:se,publishKey:ne,token:R,agentName:u,streamId:w,channel:v,format:g,direction:k,affinity:b,gating:m,bundleSizeBytes:a?.bundleSizeBytes,maxLatencyMs:a?.maxLatencyMs});S.streamClient=j;const P=$(w,j,n.taskId,{releaseStream:Ee});if(A(P),E(),S.setupPromise=null,"outbound"===k||"bidirectional"===k){const e=a?.subscribeGraceMs??1e3;e>0&&await new Promise(t=>setTimeout(t,e))}return I&&(S.activated=!0,S.activatePromise=K(w,P,I,xe)),P}};let b;try{b=t.handler?await t.handler(n,I):void 0}finally{Te.delete(n.taskId),h&&(clearTimeout(h),h=null),y()}const w=z?{baseUrl:z,authProvider:l?new S(l):void 0,agentAuth:O}:void 0;if(b?.artifacts&&b.artifacts.length>0)for(const e of b.artifacts){const t="string"==typeof e.data?Buffer.from(e.data,"utf-8"):e.data;await B(r,n.taskId,d,u,t,e.mimeType,e.fileName,e.outputId,w)}const v=c.signal.aborted;if(m){const e=be.has(n.taskId),t=we.has(n.taskId);if(v||e||t){await Ae(n.taskId);const a=v&&!e?"canceled":"completed";await U(r,n.taskId,d,u,{type:"terminal",taskId:n.taskId,state:a,...e?{completionReason:"duration_expired"}:{},...t?{reason:"terminated"}:{}}),pe.remove(n.taskId),q("info",`Task ${n.taskId} ${a}`,{taskId:n.taskId,agentName:u,owner:i})}else q("info",`Task ${n.taskId} handler returned (pipe, no auto-terminal)`,{taskId:n.taskId,agentName:u,owner:i})}else{const e=we.has(n.taskId);await Ae(n.taskId);const t=v?"canceled":"completed";await U(r,n.taskId,d,u,{type:"terminal",taskId:n.taskId,state:t,...e?{reason:"terminated"}:{}}),pe.remove(n.taskId),q("info",`Task ${n.taskId} ${t}`,{taskId:n.taskId,agentName:u,owner:i})}},Re=(()=>{let e=!1;return(t,a)=>{const s=h(t),n="number"==typeof t.statusCode?t.statusCode:null;if(q("debug","access-denied handler invoked",{event:"access_denied_handler_invoked",category:s,operation:y(a),statusCode:n,alreadyHandled:e,instanceId:c}),I(t)&&!e){e=!0,q("error","access denied — destroying control client (agent will go silent)",{event:"access_denied_destroy",category:s,operation:y(a),statusCode:n,instanceId:c,controlChannel:$e}),q("error","access token expired or revoked — agent is no longer receiving tasks. Re-register the agent to resume.",{event:"access_denied_user_message",instanceId:c});try{te.destroy()}catch{}}}})();let je=Q();const Pe={status:e=>{const t=e,a=String(t.category??""),s=String(t.operation??"");"PNConnectedCategory"===a&&q("debug","control client connected",{event:"control_connected",controlChannel:$e}),Re(t,s)},message:e=>{const s=e.message;if(!s)return;if("SwitchEnvironment"===s.type){const e=s,t=e.environment;return void(t&&t!==V&&t in f&&((e,t)=>{if(q("info",`SwitchEnvironment requested: ${V} -> ${e}`,{pamToken:t?"present":"absent"}),!t)return void q("error","SwitchEnvironment rejected: pamToken is required but was absent. The backend must include a pamToken in SwitchEnvironment messages.");const a=te,s=ae;try{a.removeListener(Pe),a.removeListener(je),$e&&a.unsubscribe({channels:[$e]})}catch{}ee(a);const r=f[e];if(te=n({publishKey:r.publishKey,subscribeKey:r.subscribeKey,userId:c,presenceTimeout:20,announceSuccessfulHeartbeats:k()>=g.debug,subscribeRetryUnbounded:!0}),ae=!0,Ce=void 0,te.setToken(t),0!==de&&"function"==typeof te.setFilterExpression&&te.setFilterExpression(`meta.instance == '${c}' || meta.broadcast == "true"`),je=Q(),te.addListener(Pe),te.addListener(je),X("control",te),$e&&te.subscribe({channels:[$e]}),s)try{a.destroy()}catch{}V=e,se=r.subscribeKey,ne=r.publishKey,oe.updateKeys(r.subscribeKey,r.publishKey),q("info",`Switched to ${e} environment`),_e().catch(()=>{})})(t,e.pamToken))}(async(e,s)=>{if("StartTask"===e.type){const r=V,o=((e,t)=>{if("string"==typeof e&&e.length>0)return e;const a=t?.sub;return"string"==typeof a&&a.length>0?a:"anonymous"})(e.ownerId,e.callerClaims),i=e.orgId??o;if(Se.has(e.taskId))return void q("warn",`Ignoring duplicate StartTask for in-flight task ${e.taskId}`,{taskId:e.taskId});const d=Boolean(s?.broadcast);if(e.protocolVersion&&!P(e.protocolVersion))return d?void q("debug",`Ignoring broadcast StartTask ${e.taskId} with unsupported protocolVersion ${e.protocolVersion}`,{taskId:e.taskId}):(await U(te,e.taskId,i,a,{type:"terminal",taskId:e.taskId,state:"failed",error:"unsupported_protocol_version"},e.protocolVersion),void q("warn",`Task ${e.taskId} rejected: unsupported protocolVersion ${e.protocolVersion}`,{taskId:e.taskId,instanceId:c}));if("pipe"===(e.taskKind||"request")){const t=e.duration,s=e.durationExpiresAtMs;if(null==t||!Number.isInteger(t)||t<1||t>43200||"number"!=typeof s||s<=0)return q("error",`Task ${e.taskId} rejected: pipe StartTask with invalid duration (duration=${t}, durationExpiresAtMs=${s})`,{taskId:e.taskId,instanceId:c}),void await U(te,e.taskId,i,a,{type:"terminal",taskId:e.taskId,state:"failed",error:"invalid_start_task"},e.protocolVersion)}const l=me+fe.activeStreamCount;if(ie>0&&l>=ie)return d?void q("debug",`Task ${e.taskId} skipped (broadcast, at capacity ${l}/${ie})`,{taskId:e.taskId,instanceId:c}):(await U(te,e.taskId,i,a,{type:"terminal",taskId:e.taskId,state:"failed",error:"agent_at_capacity"}),void q("warn",`Task ${e.taskId} rejected: agent at capacity`,{taskId:e.taskId,instanceId:c,activeTasks:me}));ge.set(e.taskId,o),he.set(e.taskId,i),Se.add(e.taskId),me++,e.controlToken&&(Ce=e.controlToken);const u=new AbortController;Ie.set(e.taskId,u);let m=null;try{const s=f[r];if(!s.publishKey||!s.subscribeKey)throw new Error("Per-task PubNub client requires valid pub/sub keys. When using an injected PubNub client, ensure it was created with publishKey and subscribeKey.");m=n({publishKey:s.publishKey,subscribeKey:s.subscribeKey,userId:c,subscribeRetryUnbounded:!1}),e.writeToken&&m.setToken(e.writeToken),X(`task-sub:${e.taskId}`,m),pe.set(e.taskId,{ownerId:o,orgId:i,writeToken:e.writeToken??"",agentName:a,environment:r}),await _e();const{writeToken:d,controlToken:l,...p}=e;t.onStartTask?await t.onStartTask(e,m):await Ne(p,m,o,i,u)}catch(s){const n=s?.message??"Agent instance error",r=m??te;try{await Ae(e.taskId)}catch{}await U(r,e.taskId,i,a,{type:"terminal",taskId:e.taskId,state:"failed",error:n}),t.onError&&await t.onError(e.taskId,s),q("error",n,{taskId:e.taskId}),pe.remove(e.taskId)}finally{const t=ve.get(e.taskId);if(t&&(clearTimeout(t),ve.delete(e.taskId)),Se.delete(e.taskId),ge.delete(e.taskId),he.delete(e.taskId),ye.delete(e.taskId),Ie.delete(e.taskId),be.delete(e.taskId),we.delete(e.taskId),m){ee(m);try{m.destroy()}catch{}}me--,0===me&&Ce&&te.setToken(Ce),await _e()}}else if("CancelTask"===e.type){const t=Ie.get(e.taskId);t?(t.abort(),q("info",`Task ${e.taskId} cancel requested (cooperative)`,{taskId:e.taskId,instanceId:c})):pe.get(e.taskId)&&await Me(e.taskId,{state:"canceled"})}else if("ExpireTask"===e.type)Ie.has(e.taskId)?(be.add(e.taskId),Ie.get(e.taskId)?.abort(),q("info",`Task ${e.taskId} expired`,{taskId:e.taskId,instanceId:c})):pe.get(e.taskId)&&await Me(e.taskId,{state:"completed",completionReason:"duration_expired"});else if("TerminateTask"===e.type)Ie.has(e.taskId)?(we.add(e.taskId),Ie.get(e.taskId)?.abort()):pe.get(e.taskId)&&await Me(e.taskId,{state:"canceled",reason:"terminated"});else if("PauseTask"===e.type){if(Se.has(e.taskId)){const t=he.get(e.taskId)??"anonymous";await U(te,e.taskId,t,a,{type:"system",taskId:e.taskId,status:"paused"})}}else if("ResumeTask"===e.type){if(Se.has(e.taskId)){const t=he.get(e.taskId)??"anonymous";await U(te,e.taskId,t,a,{type:"system",taskId:e.taskId,status:"resumed"})}}else"RetryTask"===e.type&&q("info","RetryTask received (no-op)",{taskId:e.taskId})})(s,e.userMetadata&&"object"==typeof e.userMetadata?e.userMetadata:void 0).catch(e=>{q("error","unhandled error in message handler",{event:"message_handler_error",error:e instanceof Error?e.message:String(e),stack:e instanceof Error?e.stack:void 0})})}};te.addListener(Pe),te.addListener(je),X("control",te),J&&null===Z&&(Z=setInterval(()=>{const e=Date.now();let t=!1;const a=W.map(a=>{const s=null!==a.lastStatusAt?e-a.lastStatusAt:null,n=_isDiagEntryStale({lastStatusAt:a.lastStatusAt,lastCategory:a.lastCategory,now:e,thresholdMs:6e4});n&&(t=!0);const r="function"==typeof a.pn.getSubscribedChannels?a.pn.getSubscribedChannels().length:null;return{client:a.label,lastCategory:a.lastCategory,lastOperation:a.lastOperation,lastStatusCode:a.lastStatusCode,msSinceStatus:s,msSinceConnected:null!==a.lastConnectedAt?e-a.lastConnectedAt:null,msSinceMessage:null!==a.lastMessageAt?e-a.lastMessageAt:null,subscribedChannels:r,stale:n}}),s={event:"transport_alive_snapshot",instanceId:c,clients:a};t?q("info","transport alive snapshot — STALE clients present",s):q("debug","transport alive snapshot",s)},1e4),"function"==typeof Z.unref&&Z.unref()),J&&q("info","transport diagnostics armed",{event:"transport_diagnostics_armed",snapshotIntervalMs:1e4,staleThresholdMs:6e4,instanceId:c}),i(a,{instanceId:c,billingMode:Y,description:t.description,scaling:{expectedInstances:de,concurrency:ie,maxPendingBacklog:ce,maxRunningTimeSec:le},card:t.card,cardRef:t.cardRef,cardSummary:t.cardSummary,listing:G??t.listing,actor:`agent-instance:${c}`,baseUrl:t.baseUrl??v?.api.baseUrl,agentAuth:O}).then(e=>{if(q("info",`registered agent: ${a} (instance: ${c})`,{event:"agent_registered",agentName:a,instanceId:c}),!e.controlChannel)throw new Error("Connect response missing controlChannel — server may be outdated");$e=e.controlChannel,e.pamToken&&te.setToken(e.pamToken),0!==de&&"function"==typeof te.setFilterExpression&&te.setFilterExpression(`meta.instance == '${c}' || meta.broadcast == "true"`),te.subscribe({channels:[$e]}),_e().catch(()=>{})}).catch(e=>{const t=e instanceof Error?e.message:String(e);q("error",`failed to register agent: ${a} — ${t}`,{event:"agent_registration_failed",agentName:a,instanceId:c,error:t}),e instanceof N&&process.exit(1)}),q("info",`Agent instance ${c} started (agent name: ${a})`,{agentName:a,event:"agent_instance_started",instanceId:c});return{stop:()=>{try{null!==Z&&(clearInterval(Z),Z=null);for(const e of W)try{e.pn.removeListener(e.listener)}catch{}W.length=0,J&&q("info","transport diagnostics disarmed",{event:"transport_diagnostics_disarmed",instanceId:c}),re?.destroy(),oe.destroy(),te.removeListener(Pe),te.removeListener(je),$e&&te.unsubscribe({channels:[$e]}),ae&&te.destroy();for(const e of fe.streamIds()){const t=fe.get(e);if(t?.streamClient)try{t.streamClient.end()}catch{}}fe.clear(),ke.clear(),pe.clear()}catch{}},agentName:a,instanceId:c,get controlChannel(){return $e},get pubnub(){return te},get subscribeKey(){return f[V].subscribeKey},taskClient:oe,publishTerminal:Me,failStream:xe,cdmConfig:v}};export const artifactUtils={buildArtifactRef:t,shouldInlineArtifact:a};
@@ -1 +1 @@
1
- import{CURRENT_PROTOCOL_VERSION as e,PROTOCOL_VERSION_HEADER as t}from"./protocol-version.js";import{captureAffinity as i,injectAffinity as s}from"./write-affinity.js";const r="API_KEY_INVALID";export class ConsumerAuth{constructor(e){this._token=null,this._refreshToken=null,this._userId=null,this._expiresIn=0,this._destroyed=!1,this._refreshTimer=null,this._refreshPromise=null,this._initialized=!1,this._initPromise=null,this._apiKey=e.apiKey,this._tokenEndpoint=e.tokenEndpoint,this._tokenProvider=e.tokenProvider,this._baseUrl=(e.baseUrl??"").replace(/\/+$/,""),this._onAuthError=e.onAuthError}async init(){if(this._apiKey)await this._initApiKey();else if(this._tokenEndpoint)await this._initTokenEndpoint();else{if(!this._tokenProvider)throw new Error("ConsumerAuth requires one of: apiKey, tokenEndpoint, or tokenProvider");await this._initTokenProvider()}this._scheduleRefresh(),this._initialized=!0}getAuthHeader(){return this._token?`Bearer ${this._token}`:null}getUserId(){return this._userId}async onAuthFailure(){return!this._destroyed&&this._doRefreshDedup()}destroy(){this._destroyed=!0,this._refreshTimer&&(clearTimeout(this._refreshTimer),this._refreshTimer=null)}async ensureReady(){this._initialized||(this._initPromise||(this._initPromise=this.init().then(()=>{this._initialized=!0},e=>{throw this._initPromise=null,e})),await this._initPromise)}async _initApiKey(){const r=`${this._baseUrl}/api/v1/auth/agent/consumer-token`,n={"Content-Type":"application/json",[t]:e};s(n);const o=await fetch(r,{method:"POST",headers:n,body:JSON.stringify({apiKey:this._apiKey})});if(i(o.headers),!o.ok){const e=await o.text().catch(()=>"");throw new Error(`consumer-token failed: HTTP ${o.status}${e?` ${e}`:""}`)}const h=await o.json();this._token=h.accessToken,this._refreshToken=h.refreshToken,this._expiresIn=h.expiresIn,this._userId=h.userId}async _refreshApiKey(){const e=`${this._baseUrl}/api/v1/auth/agent/refresh`,t={Authorization:`Bearer ${this._apiKey}`,"Content-Type":"application/json"};s(t);const n=await fetch(e,{method:"POST",headers:t,body:JSON.stringify({refreshToken:this._refreshToken})});if(i(n.headers),!n.ok){const e=await n.json().catch(()=>({}));if("REFRESH_TOKEN_INVALID"===e.code)return void await this._initApiKey();if(e.code===r)throw new Error(`API key invalid or revoked: ${e.error??r}`);throw new Error(`token refresh failed: ${e.error??`HTTP ${n.status}`}`)}const o=await n.json();this._token=o.accessToken,this._refreshToken=o.refreshToken,"number"==typeof o.expiresIn&&(this._expiresIn=o.expiresIn)}async _initTokenEndpoint(){const e=await this._fetchTokenEndpoint();this._applyTokenResult(e)}async _fetchTokenEndpoint(){const e=this._tokenEndpoint,t="string"==typeof e,i=t?e:e.url;if("string"!=typeof i||!i)throw new Error("TokenEndpointConfig.url is required and must be a non-empty string");const s=t?void 0:e.credentials,r=t?{}:e.headers??{},n=t?{}:e.body??{},o={method:"POST",headers:{"Content-Type":"application/json",...r},body:JSON.stringify(n)};void 0!==s&&(o.credentials=s);const h=await fetch(i,o);if(!h.ok){const e=await h.text().catch(()=>"");throw new Error(`token endpoint failed: HTTP ${h.status}${e?` ${e}`:""}`)}return h.json()}async _initTokenProvider(){const e=await this._tokenProvider();this._applyTokenResult(e)}_applyTokenResult(e){this._token=e.token,this._expiresIn=e.expiresIn,void 0!==e.userId&&(this._userId=e.userId)}async _refreshOnce(){if(this._apiKey)await this._refreshApiKey();else if(this._tokenEndpoint){const e=await this._fetchTokenEndpoint();this._applyTokenResult(e)}else if(this._tokenProvider){const e=await this._tokenProvider();this._applyTokenResult(e)}}_doRefreshDedup(){return this._refreshPromise||(this._refreshPromise=this._refreshOnce().then(()=>(this._scheduleRefresh(),!0)).catch(()=>!1).finally(()=>{this._refreshPromise=null})),this._refreshPromise}_scheduleRefresh(){if(this._destroyed||this._expiresIn<=0)return;this._refreshTimer&&clearTimeout(this._refreshTimer);const e=1e3*this._expiresIn*.8;this._refreshTimer=setTimeout(()=>{this._refreshTimer=null,this._proactiveRefreshWithRetry(0)},e)}async _proactiveRefreshWithRetry(e){if(!this._destroyed)try{await this._refreshOnce(),this._scheduleRefresh()}catch(t){if(e<2){const t=Math.min(5e3*Math.pow(2,e),3e4);this._destroyed||(this._refreshTimer=setTimeout(()=>{this._refreshTimer=null,this._proactiveRefreshWithRetry(e+1)},t))}else{const e=t instanceof Error?t:new Error(String(t));this._onAuthError?this._onAuthError(e):console.warn("[ConsumerAuth] proactive refresh permanently failed:",e.message)}}}}
1
+ import{log as e}from"./logger.js";import{CURRENT_PROTOCOL_VERSION as t,PROTOCOL_VERSION_HEADER as i}from"./protocol-version.js";import{captureAffinity as r,injectAffinity as s}from"./write-affinity.js";const n="API_KEY_INVALID";export class ConsumerAuth{constructor(e){this._token=null,this._refreshToken=null,this._userId=null,this._expiresIn=0,this._destroyed=!1,this._refreshTimer=null,this._refreshPromise=null,this._initialized=!1,this._initPromise=null,this._apiKey=e.apiKey,this._tokenEndpoint=e.tokenEndpoint,this._tokenProvider=e.tokenProvider,this._baseUrl=(e.baseUrl??"").replace(/\/+$/,""),this._onAuthError=e.onAuthError}async init(){if(this._apiKey)await this._initApiKey();else if(this._tokenEndpoint)await this._initTokenEndpoint();else{if(!this._tokenProvider)throw new Error("ConsumerAuth requires one of: apiKey, tokenEndpoint, or tokenProvider");await this._initTokenProvider()}this._scheduleRefresh(),this._initialized=!0}getAuthHeader(){return this._token?`Bearer ${this._token}`:null}getUserId(){return this._userId}async onAuthFailure(){return!this._destroyed&&this._doRefreshDedup()}destroy(){this._destroyed=!0,this._refreshTimer&&(clearTimeout(this._refreshTimer),this._refreshTimer=null)}async ensureReady(){this._initialized||(this._initPromise||(this._initPromise=this.init().then(()=>{this._initialized=!0},e=>{throw this._initPromise=null,e})),await this._initPromise)}async _initApiKey(){const e=`${this._baseUrl}/api/v1/auth/agent/consumer-token`,n={"Content-Type":"application/json",[i]:t};s(n);const o=await fetch(e,{method:"POST",headers:n,body:JSON.stringify({apiKey:this._apiKey})});if(r(o.headers),!o.ok){const e=await o.text().catch(()=>"");throw new Error(`consumer-token failed: HTTP ${o.status}${e?` ${e}`:""}`)}const h=await o.json();this._token=h.accessToken,this._refreshToken=h.refreshToken,this._expiresIn=h.expiresIn,this._userId=h.userId}async _refreshApiKey(){const e=`${this._baseUrl}/api/v1/auth/agent/refresh`,t={Authorization:`Bearer ${this._apiKey}`,"Content-Type":"application/json"};s(t);const i=await fetch(e,{method:"POST",headers:t,body:JSON.stringify({refreshToken:this._refreshToken})});if(r(i.headers),!i.ok){const e=await i.json().catch(()=>({}));if("REFRESH_TOKEN_INVALID"===e.code)return void await this._initApiKey();if(e.code===n)throw new Error(`API key invalid or revoked: ${e.error??n}`);throw new Error(`token refresh failed: ${e.error??`HTTP ${i.status}`}`)}const o=await i.json();this._token=o.accessToken,this._refreshToken=o.refreshToken,"number"==typeof o.expiresIn&&(this._expiresIn=o.expiresIn)}async _initTokenEndpoint(){const e=await this._fetchTokenEndpoint();this._applyTokenResult(e)}async _fetchTokenEndpoint(){const e=this._tokenEndpoint,t="string"==typeof e,i=t?e:e.url;if("string"!=typeof i||!i)throw new Error("TokenEndpointConfig.url is required and must be a non-empty string");const r=t?void 0:e.credentials,s=t?{}:e.headers??{},n=t?{}:e.body??{},o={method:"POST",headers:{"Content-Type":"application/json",...s},body:JSON.stringify(n)};void 0!==r&&(o.credentials=r);const h=await fetch(i,o);if(!h.ok){const e=await h.text().catch(()=>"");throw new Error(`token endpoint failed: HTTP ${h.status}${e?` ${e}`:""}`)}return h.json()}async _initTokenProvider(){const e=await this._tokenProvider();this._applyTokenResult(e)}_applyTokenResult(e){this._token=e.token,this._expiresIn=e.expiresIn,void 0!==e.userId&&(this._userId=e.userId)}async _refreshOnce(){if(this._apiKey)await this._refreshApiKey();else if(this._tokenEndpoint){const e=await this._fetchTokenEndpoint();this._applyTokenResult(e)}else if(this._tokenProvider){const e=await this._tokenProvider();this._applyTokenResult(e)}}_doRefreshDedup(){return this._refreshPromise||(this._refreshPromise=this._refreshOnce().then(()=>(this._scheduleRefresh(),!0)).catch(()=>!1).finally(()=>{this._refreshPromise=null})),this._refreshPromise}_scheduleRefresh(){if(this._destroyed||this._expiresIn<=0)return;this._refreshTimer&&clearTimeout(this._refreshTimer);const e=1e3*this._expiresIn*.8;this._refreshTimer=setTimeout(()=>{this._refreshTimer=null,this._proactiveRefreshWithRetry(0)},e)}async _proactiveRefreshWithRetry(t){var i,r,s;if(!this._destroyed)try{await this._refreshOnce(),this._scheduleRefresh()}catch(n){if(t<2){const e=Math.min(5e3*Math.pow(2,t),3e4);this._destroyed||(this._refreshTimer=setTimeout(()=>{this._refreshTimer=null,this._proactiveRefreshWithRetry(t+1)},e))}else{const t=n instanceof Error?n:new Error(String(n));this._onAuthError?this._onAuthError(t):(i="warn",r="proactive refresh permanently failed",s={event:"consumer_auth_proactive_refresh_failed",error:t.message},e("[ConsumerAuth]",i,r,s))}}}}
@@ -0,0 +1,29 @@
1
+ /** Numeric precedence for LOG_LEVEL threshold filtering. */
2
+ export declare const _LOG_LEVEL_ORDER: Record<string, number>;
3
+ /** Resolve the effective LOG_LEVEL from env (default: "info"). */
4
+ export declare function _resolveLogLevel(): number;
5
+ /**
6
+ * Named subsystems that can be activated via BLOCKS_DEBUG_INTERNAL.
7
+ * diagnostics — Blocks-authored transport-connectivity diagnostics
8
+ * (per-client status + alive-snapshot timer).
9
+ * forward_transport — route the underlying realtime transport's own log
10
+ * output through the Blocks logger under the
11
+ * [Transport] tag.
12
+ *
13
+ * Usage: BLOCKS_DEBUG_INTERNAL=diagnostics,forward_transport
14
+ * Neither subsystem is implied by LOG_LEVEL=debug.
15
+ */
16
+ export type DebugSubsystem = 'diagnostics' | 'forward_transport';
17
+ export declare function isDebugSubsystemEnabled(subsystem: DebugSubsystem): boolean;
18
+ export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
19
+ /**
20
+ * Route a tagged, LOG_LEVEL-filtered log entry to console.error / warn / log.
21
+ *
22
+ * Tag is required (e.g. `'[AgentInstance]'`, `'[StreamClient]'`) so call
23
+ * sites identify which subsystem emitted the line without relying on
24
+ * filename + line number. Entries below the configured LOG_LEVEL threshold
25
+ * are dropped silently.
26
+ */
27
+ export declare function log(tag: string, level: LogLevel, message: string, meta?: Record<string, unknown>): void;
28
+ /** Returns a partial-applied log() bound to a fixed source tag. */
29
+ export declare function createLogger(source: string): (level: LogLevel, message: string, meta?: Record<string, unknown>) => void;
@@ -0,0 +1 @@
1
+ import{getEnv as e}from"../env.js";export const _LOG_LEVEL_ORDER={error:0,warn:1,info:2,debug:3};export function _resolveLogLevel(){const n=(e("LOG_LEVEL")??"info").toLowerCase();return _LOG_LEVEL_ORDER[n]??_LOG_LEVEL_ORDER.info}const n=["diagnostics","forward_transport"];let o=!1;export function isDebugSubsystemEnabled(t){const r=e("BLOCKS_DEBUG_INTERNAL")??"";return r.length>0&&function(e){if(o)return;const t=e.split(",").map(e=>e.trim()).filter(e=>e.length>0).filter(e=>!n.includes(e));0!==t.length&&(o=!0,log("[Blocks]","warn",`BLOCKS_DEBUG_INTERNAL contains unrecognized token(s): ${t.map(e=>JSON.stringify(e)).join(", ")}. Supported values: ${n.join(", ")}. Legacy truthy/falsy values (1, 0, true, false) are not recognized.`,{event:"debug_internal_unknown_token",tokens:t}))}(r),r.split(",").some(e=>e.trim()===t)}export function log(e,n,o,t){const r=_resolveLogLevel();if((_LOG_LEVEL_ORDER[n]??_LOG_LEVEL_ORDER.info)>r)return;const s={level:n,message:o,ts:Date.now(),...t};"error"===n?console.error(e,s):"warn"===n?console.warn(e,s):console.log(e,s)}export function createLogger(e){return(n,o,t)=>log(`[${e}]`,n,o,t)}
@@ -29,16 +29,39 @@ export interface PubNubClientConfig {
29
29
  * §Cross-SDK retry-budget defaults for the contract.
30
30
  */
31
31
  subscribeRetryUnbounded?: boolean;
32
- /**
33
- * BLOCKS-129 Phase 2B: surfaces the SDK's transport-layer retry
34
- * activity during a network outage. PubNub's middleware logs
35
- * `'HTTP request retry #N in Nms.'` at warn level whenever the
36
- * transport schedules a retry; with subscribeRetryUnbounded on these
37
- * fire continuously through an outage but never bubble up to the
38
- * Event Engine listener. Wiring this callback turns those warnings
39
- * into a visible signal so a human watching the log can tell the
40
- * difference between "agent retrying" and "agent dead".
41
- */
42
- onRetry?: (message: string) => void;
43
32
  }
33
+ /** Exported for tests. Resolves eagerly so the value can be compared with `toBe`. */
34
+ export declare const _PUBNUB_LOG_LEVEL_TRACE: number;
35
+ /** Exported for tests. */
36
+ export declare const _PUBNUB_LOG_LEVEL_NONE: number;
37
+ interface PubNubLogMessage {
38
+ location?: string;
39
+ messageType?: string;
40
+ message?: unknown;
41
+ }
42
+ /**
43
+ * Forwarder logger that routes every underlying-transport log entry
44
+ * through the Blocks log() helper at the matching level (trace/debug ->
45
+ * debug, info -> info, warn -> warn, error -> error). Used when
46
+ * BLOCKS_DEBUG_INTERNAL=forward_transport is enabled. The Blocks logger
47
+ * then filters via LOG_LEVEL.
48
+ */
49
+ declare const buildPubNubForwarder: () => {
50
+ trace: (entry: PubNubLogMessage) => void;
51
+ debug: (entry: PubNubLogMessage) => void;
52
+ info: (entry: PubNubLogMessage) => void;
53
+ warn: (entry: PubNubLogMessage) => void;
54
+ error: (entry: PubNubLogMessage) => void;
55
+ };
56
+ /**
57
+ * Returns the { logLevel, loggers } pair to spread into every
58
+ * realtime-client construction site. Forwarding the underlying transport's
59
+ * own log output requires an explicit BLOCKS_DEBUG_INTERNAL=forward_transport;
60
+ * LOG_LEVEL=debug alone does not enable it.
61
+ */
62
+ export declare function buildPubNubLogConfig(): {
63
+ logLevel: number;
64
+ loggers: ReturnType<typeof buildPubNubForwarder>[];
65
+ };
44
66
  export declare const createPubNubClient: (config: PubNubClientConfig) => PubNub;
67
+ export {};
@@ -1 +1 @@
1
- import e from"pubnub";const n=()=>{const n=e.LogLevel;return"number"==typeof n?.Warn?n.Warn:3};export const createPubNubClient=r=>{if(!r.subscribeKey||!r.publishKey)throw new Error("PUBNUB keys not configured");const t=r.subscribeRetryUnbounded??!0?(()=>{const n=e.ExponentialRetryPolicy;if("function"!=typeof n)return null;const r=n({minimumDelay:2,maximumDelay:60,maximumRetry:6});try{r.maximumRetry=43200,r.validate=()=>{}}catch{return null}return 43200!==r.maximumRetry?null:r})():null,u=r.onRetry?(s=r.onRetry,{trace:()=>{},debug:()=>{},info:()=>{},error:()=>{},warn:e=>{"PubNubMiddleware"===e.location&&"text"===e.messageType&&"string"==typeof e.message&&e.message.includes("HTTP request retry")&&s(e.message)}}):null;var s;return new e({publishKey:r.publishKey,subscribeKey:r.subscribeKey,userId:r.userId??"blocks-agent",enableEventEngine:!0,...void 0!==r.presenceTimeout?{presenceTimeout:r.presenceTimeout}:{},...!0===r.announceSuccessfulHeartbeats?{announceSuccessfulHeartbeats:!0}:{},...null!==t?{retryConfiguration:t}:{},...null!==u?{logLevel:n(),loggers:[u]}:{}})};
1
+ import e from"pubnub";import{createLogger as r,isDebugSubsystemEnabled as o}from"./logger.js";const n=(r,o)=>{const n=e.LogLevel;return"number"==typeof n?.[r]?n[r]:o};export const _PUBNUB_LOG_LEVEL_TRACE=n("Trace",0);export const _PUBNUB_LOG_LEVEL_NONE=n("None",5);const t=r("Transport"),u=e=>{if("string"==typeof e)return e;try{return JSON.stringify(e)}catch{return String(e)}};export function buildPubNubLogConfig(){return o("forward_transport")?{logLevel:_PUBNUB_LOG_LEVEL_TRACE,loggers:[{trace:e=>t("debug",u(e.message),{location:e.location}),debug:e=>t("debug",u(e.message),{location:e.location}),info:e=>t("info",u(e.message),{location:e.location}),warn:e=>t("warn",u(e.message),{location:e.location}),error:e=>t("error",u(e.message),{location:e.location})}]}:{logLevel:_PUBNUB_LOG_LEVEL_NONE,loggers:[{trace:()=>{},debug:()=>{},info:()=>{},warn:()=>{},error:()=>{}}]}}export const createPubNubClient=r=>{if(!r.subscribeKey||!r.publishKey)throw new Error("PUBNUB keys not configured");const o=r.subscribeRetryUnbounded??!0?(()=>{const r=e.ExponentialRetryPolicy;if("function"!=typeof r)return null;const o=r({minimumDelay:2,maximumDelay:60,maximumRetry:6});try{o.maximumRetry=43200,o.validate=()=>{}}catch{return null}return 43200!==o.maximumRetry?null:o})():null,{logLevel:n,loggers:t}=buildPubNubLogConfig();return new e({publishKey:r.publishKey,subscribeKey:r.subscribeKey,userId:r.userId??"blocks-agent",enableEventEngine:!0,...void 0!==r.presenceTimeout?{presenceTimeout:r.presenceTimeout}:{},...!0===r.announceSuccessfulHeartbeats?{announceSuccessfulHeartbeats:!0}:{},...null!==o?{retryConfiguration:o}:{},logLevel:n,loggers:t})};
@@ -1 +1 @@
1
- export function createStreamObject(r,e,t,n){return{get streamId(){return r},get channel(){return e.channel},get isActive(){return e.isActive},get external(){return!1},get uuid(){return e.uuid},write(r){e.write(r)},async end(){n&&t?await n.releaseStream(r,t):await e.end()},get inbound(){return e.inbound},bytes:()=>e.bytes(),events:()=>e.events(),readable:()=>e.readable(),onEnd(r){e.onEnd(r)},onError(r){e.onError(r)}}}export function createExternalStreamObject(r,e,t,n){let a=!1;const o=[];return{get streamId(){return r},get channel(){return e},get isActive(){return!a},get external(){return!0},get uuid(){throw new Error("Cannot read uuid on an external stream")},write(){throw new Error("Cannot write to an external stream -- use Stream SDK directly")},async end(){a=!0;for(const r of o)r();o.length=0},get inbound(){throw new Error("Cannot read from an external stream")},bytes(){throw new Error("Cannot read from an external stream")},events(){throw new Error("Cannot read from an external stream")},readable(){throw new Error("Cannot read from an external stream")},onEnd(r){o.push(r)},onError(r){throw new Error("Cannot subscribe to errors on an external stream")},get token(){return t},activate:n}}export function runOnActivate(r,e,t,n){return Promise.resolve().then(()=>t(e)).catch(async e=>{const t=e instanceof Error?e.message:String(e);console.error(`[StreamContext] onActivate error for stream "${r}":`,t);try{await n(r,"stream_crashed")}catch(e){console.error(`[StreamContext] failStream error for "${r}":`,e)}})}
1
+ import{log as r}from"./logger.js";const e=(e,t,n)=>r("[StreamContext]",e,t,n);export function createStreamObject(r,e,t,n){return{get streamId(){return r},get channel(){return e.channel},get isActive(){return e.isActive},get external(){return!1},get uuid(){return e.uuid},write(r){e.write(r)},async end(){n&&t?await n.releaseStream(r,t):await e.end()},get inbound(){return e.inbound},bytes:()=>e.bytes(),events:()=>e.events(),readable:()=>e.readable(),onEnd(r){e.onEnd(r)},onError(r){e.onError(r)}}}export function createExternalStreamObject(r,e,t,n){let a=!1;const o=[];return{get streamId(){return r},get channel(){return e},get isActive(){return!a},get external(){return!0},get uuid(){throw new Error("Cannot read uuid on an external stream")},write(){throw new Error("Cannot write to an external stream -- use Stream SDK directly")},async end(){a=!0;for(const r of o)r();o.length=0},get inbound(){throw new Error("Cannot read from an external stream")},bytes(){throw new Error("Cannot read from an external stream")},events(){throw new Error("Cannot read from an external stream")},readable(){throw new Error("Cannot read from an external stream")},onEnd(r){o.push(r)},onError(r){throw new Error("Cannot subscribe to errors on an external stream")},get token(){return t},activate:n}}export function runOnActivate(r,t,n,a){return Promise.resolve().then(()=>n(t)).catch(async t=>{const n=t instanceof Error?t.message:String(t);e("error",`onActivate error for stream "${r}"`,{event:"stream_context_on_activate_error",streamId:r,error:n});try{await a(r,"stream_crashed")}catch(t){e("error",`failStream error for "${r}"`,{event:"stream_context_fail_stream_error",streamId:r,error:t instanceof Error?t.message:String(t)})}})}
@@ -1 +1 @@
1
- import e from"pubnub";import{callRpc as t}from"./rpc-client.js";import{taskChannel as n}from"./channel-manager.js";import{TaskSession as s}from"./task-session.js";import{ConsumerAuth as i}from"./consumer-auth.js";import{shouldInlineArtifact as r,buildArtifactRef as o}from"./artifacts.js";import{uploadFile as a}from"./file-upload.js";import{normalizeFileInput as c}from"./file-input.js";import{fetchCdmConfig as u}from"./cdm-config.js";import{captureAffinity as d,injectAffinity as l}from"./write-affinity.js";import{getEnv as b}from"../env.js";import{StreamRef as h}from"./stream-ref.js";import{invertDirection as p}from"../stream/index.js";import{asPubNubFetcher as f}from"./pubnub-types.js";import{CURRENT_PROTOCOL_VERSION as m,PROTOCOL_VERSION_HEADER as g}from"./protocol-version.js";import{getAgent as k}from"./agent-registry.js";const y=new Set(["completed","failed","canceled"]);export class AnonTaskAccessDeniedError extends Error{constructor(e="anon-task-read-token denied: HTTP 403 Forbidden"){super(e),this.name="AnonTaskAccessDeniedError"}}function w(e,t,n,s){const i=new Map,r=[],o=[];let a,c="0",u="0";for(const d of e){d.timetoken&&d.timetoken>c&&(c=d.timetoken);const e=d.message;if(e&&"object"==typeof e&&e.type){if(o.push(e),"terminal"===e.type){const t=String(d.timetoken??"0");if(t>=u){const n=e.state;"completed"!==n&&"failed"!==n&&"canceled"!==n||(a=n,u=t)}}if("progress"===e.type&&"stream_started"===e.streamEvent&&e.streams){const r=e.declaredStream,o=e.streams;for(const[e,a]of Object.entries(o)){if(!a||"object"!=typeof a||i.has(e))continue;const o=a.direction,c=p(o),u=a.format;if(!u||"bytes"!==u&&"events"!==u)continue;const d=a.affinity;if("dedicated"!==d&&"shared"!==d){console.warn(`[TaskClient] history-preload: dropping stream "${e}" for task "${t}" — invalid or missing affinity (got ${JSON.stringify(a.affinity)})`);continue}const l={taskId:t,streamId:e,agentName:n,channel:a.channel,token:a.token,agentDirection:o,localDirection:c,format:u,affinity:d,metadata:a.metadata,declaredStream:r};i.set(e,new h(l,s))}}if("artifact"===e.type&&e.artifactRef){const t=e.artifactRef;t&&"object"==typeof t&&t.kind&&r.push(t)}}}return{streams:i,artifacts:r,events:o,highWaterMark:c,terminalState:a}}async function T(e,t){const n=[];let s;for(;;){const i={channels:[t],count:100};s&&(i.start=s);const r=await e.fetchMessages(i),o=r.channels?.[t]??[];if(0===o.length)break;if(n.push(...o),o.length<100)break;s=o[0].timetoken}return n.sort((e,t)=>e.timetoken<t.timetoken?-1:e.timetoken>t.timetoken?1:0),n}export class TaskClient{constructor(e){if(this._ownsPubNub=!1,"free"!==e.billingMode&&"paid"!==e.billingMode)throw new Error("TaskClient requires a billingMode option ('free' or 'paid')");this._billingMode=e.billingMode,this._anonFingerprint=e.anonFingerprint??null,this.config={subscribeKey:e.subscribeKey,authProvider:e.authProvider,baseUrl:e.baseUrl,agentAuth:e.agentAuth},this._pubnub=e.pubnub,this._createPubNub=e.createPubNub,this._createSessionPubNub=e.createSessionPubNub,this.defaultOwnerId=e.defaultOwnerId,this._subscribeKey=e.subscribeKey,this._publishKey=e.publishKey??"",this._ownsPubNub=!e.pubnub&&!!e.createPubNub}static async create(t){const n=t??{},s=n.billingMode;if("free"!==s&&"paid"!==s)throw new Error("TaskClient.create() requires a billingMode option ('free' or 'paid')");const r=!!(n.apiKey||n.tokenEndpoint||n.tokenProvider),o=!!n.anonFingerprint;if([n.apiKey,n.tokenEndpoint,n.tokenProvider,n.anonFingerprint].filter(Boolean).length>1)throw new Error("Only one token provider mode may be specified");if(o&&"free"!==s)throw new Error("TaskClient.create() with anonFingerprint requires billingMode: 'free'");const a=n.cdmUrl??b("BLOCKS_CDM_URL"),c=await u(a),d="paid"===s?c.network:c.playground,l=n.subscribeKey??b("BLOCKS_SUBSCRIBE_KEY")??d.subscribeKey,h=n.publishKey??b("BLOCKS_PUBLISH_KEY")??d.publishKey,p=n.baseUrl??b("BLOCKS_BACKEND_URL")??c.api.baseUrl;if(!l)throw new Error("TaskClient.create() could not resolve subscribeKey from options, env, or CDM");if(!p)throw new Error("TaskClient.create() could not resolve baseUrl. Set baseUrl option, BLOCKS_BACKEND_URL env var, or ensure CDM config has api.baseUrl.");const f=()=>{const t=`blocks-task-${Date.now().toString(36)}-${Math.random().toString(36).slice(2,6)}`;return new e({subscribeKey:l,publishKey:h||void 0,userId:t,enableEventEngine:!0})};if(r){const e=new i({apiKey:n.apiKey,tokenEndpoint:n.tokenEndpoint,tokenProvider:n.tokenProvider,baseUrl:p,onAuthError:n.onAuthError});await e.init();const t=new TaskClient({billingMode:s,subscribeKey:l,publishKey:h,baseUrl:p,createSessionPubNub:f,defaultOwnerId:e.getUserId()??void 0});return t.config.authProvider=e,t._consumerAuth=e,t}return new TaskClient({billingMode:s,subscribeKey:l,publishKey:h,baseUrl:p,createSessionPubNub:f,anonFingerprint:n.anonFingerprint})}getUserId(){return this._consumerAuth?.getUserId()??null}async getAgentCard(e){const t=await k(e,{baseUrl:this.config.baseUrl});return t?.card??null}updateKeys(e,t){this.config.subscribeKey=e,this._subscribeKey=e,this._publishKey=t??""}getPubNub(){if(!this._pubnub&&this._createPubNub&&(this._pubnub=this._createPubNub()),!this._pubnub)throw new Error("TaskClient requires a pubnub instance for subscribe. Pass pubnub or createPubNub in TaskClientOptions.");return this._pubnub}destroy(){this._pubnub&&this._ownsPubNub&&(this._pubnub.destroy(),this._pubnub=void 0),this._consumerAuth&&this._consumerAuth.destroy()}[Symbol.dispose](){this.destroy()}createPerSessionPubNub(t,n,s){let i;const r=n??this._subscribeKey,o=s??this._publishKey;if(this._createSessionPubNub&&r===this._subscribeKey)i=this._createSessionPubNub();else{const t=`blocks-task-${Date.now().toString(36)}-${Math.random().toString(36).slice(2,6)}`;i=new e({subscribeKey:r,publishKey:o||void 0,userId:t,enableEventEngine:!0})}return t&&i.setToken(t),i}async fetchConsumerReadToken(e,t="consumer"){if(!this.config.baseUrl)throw new Error("connect() requires a backend baseUrl. Set baseUrl in TaskClientOptions.");const n=`${this.config.baseUrl.replace(/\/+$/,"")}/api/v1/auth/task-read-token`,s=async()=>{const s={"Content-Type":"application/json",[g]:m},i=this.config.authProvider?.getAuthHeader();return i&&(s.Authorization=i),l(s),fetch(n,{method:"POST",headers:s,body:JSON.stringify({taskId:e,role:t})})};let i=await s();if(d(i.headers),401===i.status&&this.config.authProvider){await this.config.authProvider.onAuthFailure()&&(i=await s(),d(i.headers))}if(!i.ok){const e=await i.text().catch(()=>"");throw new Error(`task-read-token failed: HTTP ${i.status}${e?` ${e}`:""}`)}return i.json()}async fetchAnonConsumerReadToken(e){if(!this.config.baseUrl)throw new Error("connect() requires a backend baseUrl. Set baseUrl in TaskClientOptions.");if(!this._anonFingerprint)throw new Error("fetchAnonConsumerReadToken called without anonFingerprint");const t=`${this.config.baseUrl.replace(/\/+$/,"")}/api/v1/auth/anon-task-read-token`,n={"Content-Type":"application/json",[g]:m};l(n);const s=await fetch(t,{method:"POST",headers:n,body:JSON.stringify({taskId:e,fingerprint:this._anonFingerprint})});if(d(s.headers),403===s.status){const e=await s.text().catch(()=>"");throw new AnonTaskAccessDeniedError("anon-task-read-token denied: HTTP 403"+(e?` ${e}`:""))}if(!s.ok){const e=await s.text().catch(()=>"");throw new Error(`anon-task-read-token failed: HTTP ${s.status}${e?` ${e}`:""}`)}return s.json()}async sendMessage(e){if(this._anonFingerprint)throw new Error("anon-mode TaskClient does not support sendMessage()");this.config.authProvider?.ensureReady&&await this.config.authProvider.ensureReady();const i=e.ownerId||this.defaultOwnerId||"",u=e.taskKind,d=e.duration;if("pipe"===u){if(null==d||!Number.isInteger(d)||d<1||d>43200)throw new Error("Pipe tasks require a duration between 1 and 43200 minutes")}else if(null!=d)throw new Error("Request tasks must not include a duration. Duration is only valid for pipe tasks.");let l;const b=[];for(const t of e.requestParts)if(t.file){if(!t.partId)throw new Error("partId is required for file-bearing request parts");const n=c(t.file),s=t.fileName??"unnamed",i=t.contentType??"application/octet-stream";if(r(n.size)){const e=await n.getBytes(),r=o({data:e,mimeType:i,fileName:s}),{file:a,fileName:c,text:u,...d}=t;b.push({...d,artifactRef:r})}else{if(!this.config.baseUrl)throw new Error("File upload requires a backend baseUrl. Set baseUrl in TaskClientOptions.");const r={baseUrl:this.config.baseUrl,authProvider:this.config.authProvider,agentAuth:this.config.agentAuth},o={role:"consumer-input",agentName:e.agentName,fileName:s,fileSize:n.size,mimeType:i,partId:t.partId,uploadSessionId:l},c=await a(r,o,n.uploadBody);c.uploadSessionId&&(l=c.uploadSessionId);const{file:u,fileName:d,...h}=t;b.push({partId:h.partId,...h.contentType?{contentType:h.contentType}:{}})}}else{const{file:e,fileName:n,...s}=t;b.push(s)}const h={agentName:e.agentName,billingMode:this._billingMode,requestParts:b};l&&(h.uploadSessionId=l),e.idempotencyKey&&(h.idempotencyKey=e.idempotencyKey),h.ownerId=i;const p={};u&&(p.taskKind=u),null!=d&&(p.duration=d),e.consumerPublicKey&&(p.consumerPublicKey=e.consumerPublicKey),Object.keys(p).length>0&&(h.extensions={blocks:p}),e.pushNotificationConfig&&(h.pushNotificationConfig=e.pushNotificationConfig),e.retryPolicy&&(h.retryPolicy=e.retryPolicy);const m=await t(this.config,"SendMessage",h),g=m.extensions?.blocks,k=g?.readToken??null,S=g?.streamChannels?.status??void 0,I=g?.subscribeKey??this._subscribeKey,C=g?.publishKey??this._publishKey;if(!0===m.idempotent&&!!m.state&&y.has(m.state))return new s({taskId:m.taskId,ownerId:i,orgId:m.orgId??i,readToken:k,statusChannel:S,agentName:e.agentName,pubnub:null,ownsSubscribeClient:!1,sdkOptions:{subscribeKey:I,publishKey:C},rpcConfig:this.config,idempotent:m.idempotent,queued:m.queued,pushConfigId:m.pushConfigId,preClosed:!0,state:m.state});const P=this.createPerSessionPubNub(k,I,C),K={subscribeKey:I,publishKey:C},N=S??n(m.taskId,m.orgId??i);try{const t=await P.time(),n=String(t.timetoken),r=f(P);let o,a=new Map,c=[],u="0";if(r?.fetchMessages){const t=w(await T(r,N),m.taskId,e.agentName,K);a=t.streams,c=t.artifacts,u=t.highWaterMark,o=t.terminalState}if(o&&y.has(o))return new s({taskId:m.taskId,ownerId:i,orgId:m.orgId??i,readToken:k,statusChannel:N,agentName:e.agentName,pubnub:P,ownsSubscribeClient:!0,sdkOptions:K,rpcConfig:this.config,idempotent:m.idempotent,queued:m.queued,pushConfigId:m.pushConfigId,autoDrain:e.autoDrain,drainWindowMs:e.drainWindowMs,state:o,skipSubscription:!0,preloadedStreams:a,preloadedArtifacts:c});const d="0"!==u?u:n,l=[];let b,h=!1;const p={message:e=>{if(e.channel!==N)return;const t=e.message;if(!t||"object"!=typeof t||!t.type)return;const n=String(e.timetoken??"0");h&&b?b(t,n):l.push({message:t,timetoken:n})}};P.addListener(p),P.subscribe({channels:[N],timetoken:d});const g=new s({taskId:m.taskId,ownerId:i,orgId:m.orgId??i,readToken:k,statusChannel:N,agentName:e.agentName,pubnub:P,ownsSubscribeClient:!0,sdkOptions:K,rpcConfig:this.config,idempotent:m.idempotent,queued:m.queued,pushConfigId:m.pushConfigId,autoDrain:e.autoDrain,drainWindowMs:e.drainWindowMs,preloadedStreams:a,preloadedArtifacts:c,externalSubscription:{listener:p,channel:N,onReady:e=>{b=e}}});if(b){const e=b;for(const t of l)t.timetoken>d&&e(t.message,t.timetoken)}return l.length=0,h=!0,g}catch{return new s({taskId:m.taskId,ownerId:i,orgId:m.orgId??i,readToken:k,statusChannel:N,agentName:e.agentName,pubnub:P,ownsSubscribeClient:!0,sdkOptions:K,rpcConfig:this.config,idempotent:m.idempotent,queued:m.queued,pushConfigId:m.pushConfigId,autoDrain:e.autoDrain,drainWindowMs:e.drainWindowMs})}}async connect(e){const{taskId:t}=e;if(this._anonFingerprint){const n=await this.getTask(t);if(!n)throw new Error(`Task not found: ${t}`);const s=n.agentName??"",i=n.state??"",r=await this.fetchAnonConsumerReadToken(t);return this._openSessionFromToken({task:n,agentName:s,taskState:i,tokenResult:r,autoDrain:e.autoDrain,drainWindowMs:e.drainWindowMs})}if(!this.config.authProvider?.getAuthHeader())throw new Error("connect() requires an authenticated TaskClient. Use apiKey, tokenEndpoint, or tokenProvider. AgentAuth is not supported for consumer task connections.");const n=await this.getTask(t);if(!n)throw new Error(`Task not found: ${t}`);const s=n.agentName??"",i=n.state??"",r=await this.fetchConsumerReadToken(t,e.role);return this._openSessionFromToken({task:n,agentName:s,taskState:i,tokenResult:r,autoDrain:e.autoDrain,drainWindowMs:e.drainWindowMs})}async _openSessionFromToken(e){const{task:t,agentName:n,taskState:i,tokenResult:r,autoDrain:o,drainWindowMs:a}=e,c=t.taskId,{pamToken:u,channel:d}=r,l={subscribeKey:this._subscribeKey,publishKey:this._publishKey},b=this.createPerSessionPubNub(u);try{const e=await b.time(),r=String(e.timetoken),h=f(b);let p,m=new Map,g=[],k=[],S="0";if(h?.fetchMessages){const e=w(await T(h,d),c,n,l);m=e.streams,g=e.artifacts,k=e.events,S=e.highWaterMark,p=e.terminalState}const I=p&&y.has(p)?p:i;if(y.has(I))return new s({taskId:c,ownerId:t.owner??"",readToken:u,statusChannel:d,agentName:n,pubnub:b,ownsSubscribeClient:!0,sdkOptions:l,rpcConfig:this.config,autoDrain:o,drainWindowMs:a,state:I,skipSubscription:!0,preloadedStreams:m,preloadedArtifacts:g,preloadedEvents:k});const C="0"!==S?S:r,P=[];let K,N=!1;const E={message:e=>{if(e.channel!==d)return;const t=e.message;if(!t||"object"!=typeof t||!t.type)return;const n=String(e.timetoken??"0");N&&K?K(t,n):P.push({message:t,timetoken:n})}};b.addListener(E),b.subscribe({channels:[d],timetoken:C});const _=new s({taskId:c,ownerId:t.owner??"",readToken:u,statusChannel:d,agentName:n,pubnub:b,ownsSubscribeClient:!0,sdkOptions:l,rpcConfig:this.config,autoDrain:o,drainWindowMs:a,state:i,preloadedStreams:m,preloadedArtifacts:g,preloadedEvents:k,externalSubscription:{listener:E,channel:d,onReady:e=>{K=e}}});if(K){const e=K;for(const t of P)t.timetoken>C&&(_._appendHistoryEvent(t.message,t.timetoken),e(t.message,t.timetoken))}return P.length=0,N=!0,_}catch(e){throw b.destroy(),e}}async getTask(e){return(await t(this.config,"GetTask",{taskId:e})).task}async listTasks(e){return t(this.config,"ListTasks",{...e})}async cancelTask(e){await t(this.config,"CancelTask",{taskId:e})}async pauseTask(e){await t(this.config,"PauseTask",{taskId:e})}async resumeTask(e){await t(this.config,"ResumeTask",{taskId:e})}async retryTask(e){await t(this.config,"RetryTask",{taskId:e})}async terminateTask(e){await t(this.config,"TerminateTask",{taskId:e})}subscribeToTask(e,t,s){return function(e,t,s,i){const r=n(t,s),o={message:e=>{if(e.channel!==r)return;const t=e.message;if(t&&"object"==typeof t&&t.type){if(i.onEvent)try{i.onEvent(t)}catch(e){S(e,"onEvent",t,i.onError)}switch(t.type){case"progress":if(i.onProgress)try{i.onProgress(t)}catch(e){S(e,"onProgress",t,i.onError)}break;case"artifact":if(i.onArtifact)try{i.onArtifact(t)}catch(e){S(e,"onArtifact",t,i.onError)}break;case"terminal":if(i.onTerminal)try{i.onTerminal(t)}catch(e){S(e,"onTerminal",t,i.onError)}break;case"system":if(i.onSystem)try{i.onSystem(t)}catch(e){S(e,"onSystem",t,i.onError)}}}}};return e.addListener(o),e.subscribe({channels:[r],timetoken:1e3}),{unsubscribe(){e.removeListener(o),e.unsubscribe({channels:[r]})}}}(this.getPubNub(),e,t,s)}}function S(e,t,n,s){const i=e instanceof Error?e:new Error(String(e));if(s)try{s(i,{entryPoint:"subscribeToTask",callbackType:t,event:n})}catch{}else console.warn(`[subscribeToTask] callback error in ${t}:`,i.message)}
1
+ import e from"pubnub";import{buildPubNubLogConfig as t}from"./pubnub-client.js";import{callRpc as n}from"./rpc-client.js";import{taskChannel as s}from"./channel-manager.js";import{TaskSession as i}from"./task-session.js";import{ConsumerAuth as r}from"./consumer-auth.js";import{shouldInlineArtifact as o,buildArtifactRef as a}from"./artifacts.js";import{uploadFile as c}from"./file-upload.js";import{normalizeFileInput as u}from"./file-input.js";import{fetchCdmConfig as d}from"./cdm-config.js";import{captureAffinity as l,injectAffinity as b}from"./write-affinity.js";import{getEnv as h}from"../env.js";import{log as p}from"./logger.js";import{StreamRef as f}from"./stream-ref.js";import{invertDirection as m}from"../stream/index.js";import{asPubNubFetcher as g}from"./pubnub-types.js";import{CURRENT_PROTOCOL_VERSION as k,PROTOCOL_VERSION_HEADER as y}from"./protocol-version.js";import{getAgent as w}from"./agent-registry.js";const T=(e,t,n)=>p("[TaskClient]",e,t,n),I=new Set(["completed","failed","canceled"]);export class AnonTaskAccessDeniedError extends Error{constructor(e="anon-task-read-token denied: HTTP 403 Forbidden"){super(e),this.name="AnonTaskAccessDeniedError"}}function S(e,t,n,s){const i=new Map,r=[],o=[];let a,c="0",u="0";for(const d of e){d.timetoken&&d.timetoken>c&&(c=d.timetoken);const e=d.message;if(e&&"object"==typeof e&&e.type){if(o.push(e),"terminal"===e.type){const t=String(d.timetoken??"0");if(t>=u){const n=e.state;"completed"!==n&&"failed"!==n&&"canceled"!==n||(a=n,u=t)}}if("progress"===e.type&&"stream_started"===e.streamEvent&&e.streams){const r=e.declaredStream,o=e.streams;for(const[e,a]of Object.entries(o)){if(!a||"object"!=typeof a||i.has(e))continue;const o=a.direction,c=m(o),u=a.format;if(!u||"bytes"!==u&&"events"!==u)continue;const d=a.affinity;if("dedicated"!==d&&"shared"!==d){T("warn",`history-preload: dropping stream "${e}" for task "${t}" — invalid or missing affinity`,{event:"history_preload_invalid_affinity",streamId:e,taskId:t,receivedAffinity:a.affinity});continue}const l={taskId:t,streamId:e,agentName:n,channel:a.channel,token:a.token,agentDirection:o,localDirection:c,format:u,affinity:d,metadata:a.metadata,declaredStream:r};i.set(e,new f(l,s))}}if("artifact"===e.type&&e.artifactRef){const t=e.artifactRef;t&&"object"==typeof t&&t.kind&&r.push(t)}}}return{streams:i,artifacts:r,events:o,highWaterMark:c,terminalState:a}}async function C(e,t){const n=[];let s;for(;;){const i={channels:[t],count:100};s&&(i.start=s);const r=await e.fetchMessages(i),o=r.channels?.[t]??[];if(0===o.length)break;if(n.push(...o),o.length<100)break;s=o[0].timetoken}return n.sort((e,t)=>e.timetoken<t.timetoken?-1:e.timetoken>t.timetoken?1:0),n}export class TaskClient{constructor(e){if(this._ownsPubNub=!1,"free"!==e.billingMode&&"paid"!==e.billingMode)throw new Error("TaskClient requires a billingMode option ('free' or 'paid')");this._billingMode=e.billingMode,this._anonFingerprint=e.anonFingerprint??null,this.config={subscribeKey:e.subscribeKey,authProvider:e.authProvider,baseUrl:e.baseUrl,agentAuth:e.agentAuth},this._pubnub=e.pubnub,this._createPubNub=e.createPubNub,this._createSessionPubNub=e.createSessionPubNub,this.defaultOwnerId=e.defaultOwnerId,this._subscribeKey=e.subscribeKey,this._publishKey=e.publishKey??"",this._ownsPubNub=!e.pubnub&&!!e.createPubNub}static async create(n){const s=n??{},i=s.billingMode;if("free"!==i&&"paid"!==i)throw new Error("TaskClient.create() requires a billingMode option ('free' or 'paid')");const o=!!(s.apiKey||s.tokenEndpoint||s.tokenProvider),a=!!s.anonFingerprint;if([s.apiKey,s.tokenEndpoint,s.tokenProvider,s.anonFingerprint].filter(Boolean).length>1)throw new Error("Only one token provider mode may be specified");if(a&&"free"!==i)throw new Error("TaskClient.create() with anonFingerprint requires billingMode: 'free'");const c=s.cdmUrl??h("BLOCKS_CDM_URL"),u=await d(c),l="paid"===i?u.network:u.playground,b=s.subscribeKey??h("BLOCKS_SUBSCRIBE_KEY")??l.subscribeKey,p=s.publishKey??h("BLOCKS_PUBLISH_KEY")??l.publishKey,f=s.baseUrl??h("BLOCKS_BACKEND_URL")??u.api.baseUrl;if(!b)throw new Error("TaskClient.create() could not resolve subscribeKey from options, env, or CDM");if(!f)throw new Error("TaskClient.create() could not resolve baseUrl. Set baseUrl option, BLOCKS_BACKEND_URL env var, or ensure CDM config has api.baseUrl.");const m=()=>{const n=`blocks-task-${Date.now().toString(36)}-${Math.random().toString(36).slice(2,6)}`;return new e({subscribeKey:b,publishKey:p||void 0,userId:n,enableEventEngine:!0,...t()})};if(o){const e=new r({apiKey:s.apiKey,tokenEndpoint:s.tokenEndpoint,tokenProvider:s.tokenProvider,baseUrl:f,onAuthError:s.onAuthError});await e.init();const t=new TaskClient({billingMode:i,subscribeKey:b,publishKey:p,baseUrl:f,createSessionPubNub:m,defaultOwnerId:e.getUserId()??void 0});return t.config.authProvider=e,t._consumerAuth=e,t}return new TaskClient({billingMode:i,subscribeKey:b,publishKey:p,baseUrl:f,createSessionPubNub:m,anonFingerprint:s.anonFingerprint})}getUserId(){return this._consumerAuth?.getUserId()??null}async getAgentCard(e){const t=await w(e,{baseUrl:this.config.baseUrl});return t?.card??null}updateKeys(e,t){this.config.subscribeKey=e,this._subscribeKey=e,this._publishKey=t??""}getPubNub(){if(!this._pubnub&&this._createPubNub&&(this._pubnub=this._createPubNub()),!this._pubnub)throw new Error("TaskClient requires a pubnub instance for subscribe. Pass pubnub or createPubNub in TaskClientOptions.");return this._pubnub}destroy(){this._pubnub&&this._ownsPubNub&&(this._pubnub.destroy(),this._pubnub=void 0),this._consumerAuth&&this._consumerAuth.destroy()}[Symbol.dispose](){this.destroy()}createPerSessionPubNub(n,s,i){let r;const o=s??this._subscribeKey,a=i??this._publishKey;if(this._createSessionPubNub&&o===this._subscribeKey)r=this._createSessionPubNub();else{const n=`blocks-task-${Date.now().toString(36)}-${Math.random().toString(36).slice(2,6)}`;r=new e({subscribeKey:o,publishKey:a||void 0,userId:n,enableEventEngine:!0,...t()})}return n&&r.setToken(n),r}async fetchConsumerReadToken(e,t="consumer"){if(!this.config.baseUrl)throw new Error("connect() requires a backend baseUrl. Set baseUrl in TaskClientOptions.");const n=`${this.config.baseUrl.replace(/\/+$/,"")}/api/v1/auth/task-read-token`,s=async()=>{const s={"Content-Type":"application/json",[y]:k},i=this.config.authProvider?.getAuthHeader();return i&&(s.Authorization=i),b(s),fetch(n,{method:"POST",headers:s,body:JSON.stringify({taskId:e,role:t})})};let i=await s();if(l(i.headers),401===i.status&&this.config.authProvider){await this.config.authProvider.onAuthFailure()&&(i=await s(),l(i.headers))}if(!i.ok){const e=await i.text().catch(()=>"");throw new Error(`task-read-token failed: HTTP ${i.status}${e?` ${e}`:""}`)}return i.json()}async fetchAnonConsumerReadToken(e){if(!this.config.baseUrl)throw new Error("connect() requires a backend baseUrl. Set baseUrl in TaskClientOptions.");if(!this._anonFingerprint)throw new Error("fetchAnonConsumerReadToken called without anonFingerprint");const t=`${this.config.baseUrl.replace(/\/+$/,"")}/api/v1/auth/anon-task-read-token`,n={"Content-Type":"application/json",[y]:k};b(n);const s=await fetch(t,{method:"POST",headers:n,body:JSON.stringify({taskId:e,fingerprint:this._anonFingerprint})});if(l(s.headers),403===s.status){const e=await s.text().catch(()=>"");throw new AnonTaskAccessDeniedError("anon-task-read-token denied: HTTP 403"+(e?` ${e}`:""))}if(!s.ok){const e=await s.text().catch(()=>"");throw new Error(`anon-task-read-token failed: HTTP ${s.status}${e?` ${e}`:""}`)}return s.json()}async sendMessage(e){if(this._anonFingerprint)throw new Error("anon-mode TaskClient does not support sendMessage()");this.config.authProvider?.ensureReady&&await this.config.authProvider.ensureReady();const t=e.ownerId||this.defaultOwnerId||"",r=e.taskKind,d=e.duration;if("pipe"===r){if(null==d||!Number.isInteger(d)||d<1||d>43200)throw new Error("Pipe tasks require a duration between 1 and 43200 minutes")}else if(null!=d)throw new Error("Request tasks must not include a duration. Duration is only valid for pipe tasks.");let l;const b=[];for(const t of e.requestParts)if(t.file){if(!t.partId)throw new Error("partId is required for file-bearing request parts");const n=u(t.file),s=t.fileName??"unnamed",i=t.contentType??"application/octet-stream";if(o(n.size)){const e=await n.getBytes(),r=a({data:e,mimeType:i,fileName:s}),{file:o,fileName:c,text:u,...d}=t;b.push({...d,artifactRef:r})}else{if(!this.config.baseUrl)throw new Error("File upload requires a backend baseUrl. Set baseUrl in TaskClientOptions.");const r={baseUrl:this.config.baseUrl,authProvider:this.config.authProvider,agentAuth:this.config.agentAuth},o={role:"consumer-input",agentName:e.agentName,fileName:s,fileSize:n.size,mimeType:i,partId:t.partId,uploadSessionId:l},a=await c(r,o,n.uploadBody);a.uploadSessionId&&(l=a.uploadSessionId);const{file:u,fileName:d,...h}=t;b.push({partId:h.partId,...h.contentType?{contentType:h.contentType}:{}})}}else{const{file:e,fileName:n,...s}=t;b.push(s)}const h={agentName:e.agentName,billingMode:this._billingMode,requestParts:b};l&&(h.uploadSessionId=l),e.idempotencyKey&&(h.idempotencyKey=e.idempotencyKey),h.ownerId=t;const p={};r&&(p.taskKind=r),null!=d&&(p.duration=d),e.consumerPublicKey&&(p.consumerPublicKey=e.consumerPublicKey),Object.keys(p).length>0&&(h.extensions={blocks:p}),e.pushNotificationConfig&&(h.pushNotificationConfig=e.pushNotificationConfig),e.retryPolicy&&(h.retryPolicy=e.retryPolicy);const f=await n(this.config,"SendMessage",h),m=f.extensions?.blocks,k=m?.readToken??null,y=m?.streamChannels?.status??void 0,w=m?.subscribeKey??this._subscribeKey,T=m?.publishKey??this._publishKey;if(!0===f.idempotent&&!!f.state&&I.has(f.state))return new i({taskId:f.taskId,ownerId:t,orgId:f.orgId??t,readToken:k,statusChannel:y,agentName:e.agentName,pubnub:null,ownsSubscribeClient:!1,sdkOptions:{subscribeKey:w,publishKey:T},rpcConfig:this.config,idempotent:f.idempotent,queued:f.queued,pushConfigId:f.pushConfigId,preClosed:!0,state:f.state});const P=this.createPerSessionPubNub(k,w,T),K={subscribeKey:w,publishKey:T},_=y??s(f.taskId,f.orgId??t);try{const n=await P.time(),s=String(n.timetoken),r=g(P);let o,a=new Map,c=[],u="0";if(r?.fetchMessages){const t=S(await C(r,_),f.taskId,e.agentName,K);a=t.streams,c=t.artifacts,u=t.highWaterMark,o=t.terminalState}if(o&&I.has(o))return new i({taskId:f.taskId,ownerId:t,orgId:f.orgId??t,readToken:k,statusChannel:_,agentName:e.agentName,pubnub:P,ownsSubscribeClient:!0,sdkOptions:K,rpcConfig:this.config,idempotent:f.idempotent,queued:f.queued,pushConfigId:f.pushConfigId,autoDrain:e.autoDrain,drainWindowMs:e.drainWindowMs,state:o,skipSubscription:!0,preloadedStreams:a,preloadedArtifacts:c});const d="0"!==u?u:s,l=[];let b,h=!1;const p={message:e=>{if(e.channel!==_)return;const t=e.message;if(!t||"object"!=typeof t||!t.type)return;const n=String(e.timetoken??"0");h&&b?b(t,n):l.push({message:t,timetoken:n})}};P.addListener(p),P.subscribe({channels:[_],timetoken:d});const m=new i({taskId:f.taskId,ownerId:t,orgId:f.orgId??t,readToken:k,statusChannel:_,agentName:e.agentName,pubnub:P,ownsSubscribeClient:!0,sdkOptions:K,rpcConfig:this.config,idempotent:f.idempotent,queued:f.queued,pushConfigId:f.pushConfigId,autoDrain:e.autoDrain,drainWindowMs:e.drainWindowMs,preloadedStreams:a,preloadedArtifacts:c,externalSubscription:{listener:p,channel:_,onReady:e=>{b=e}}});if(b){const e=b;for(const t of l)t.timetoken>d&&e(t.message,t.timetoken)}return l.length=0,h=!0,m}catch{return new i({taskId:f.taskId,ownerId:t,orgId:f.orgId??t,readToken:k,statusChannel:_,agentName:e.agentName,pubnub:P,ownsSubscribeClient:!0,sdkOptions:K,rpcConfig:this.config,idempotent:f.idempotent,queued:f.queued,pushConfigId:f.pushConfigId,autoDrain:e.autoDrain,drainWindowMs:e.drainWindowMs})}}async connect(e){const{taskId:t}=e;if(this._anonFingerprint){const n=await this.getTask(t);if(!n)throw new Error(`Task not found: ${t}`);const s=n.agentName??"",i=n.state??"",r=await this.fetchAnonConsumerReadToken(t);return this._openSessionFromToken({task:n,agentName:s,taskState:i,tokenResult:r,autoDrain:e.autoDrain,drainWindowMs:e.drainWindowMs})}if(!this.config.authProvider?.getAuthHeader())throw new Error("connect() requires an authenticated TaskClient. Use apiKey, tokenEndpoint, or tokenProvider. AgentAuth is not supported for consumer task connections.");const n=await this.getTask(t);if(!n)throw new Error(`Task not found: ${t}`);const s=n.agentName??"",i=n.state??"",r=await this.fetchConsumerReadToken(t,e.role);return this._openSessionFromToken({task:n,agentName:s,taskState:i,tokenResult:r,autoDrain:e.autoDrain,drainWindowMs:e.drainWindowMs})}async _openSessionFromToken(e){const{task:t,agentName:n,taskState:s,tokenResult:r,autoDrain:o,drainWindowMs:a}=e,c=t.taskId,{pamToken:u,channel:d}=r,l={subscribeKey:this._subscribeKey,publishKey:this._publishKey},b=this.createPerSessionPubNub(u);try{const e=await b.time(),r=String(e.timetoken),h=g(b);let p,f=new Map,m=[],k=[],y="0";if(h?.fetchMessages){const e=S(await C(h,d),c,n,l);f=e.streams,m=e.artifacts,k=e.events,y=e.highWaterMark,p=e.terminalState}const w=p&&I.has(p)?p:s;if(I.has(w))return new i({taskId:c,ownerId:t.owner??"",readToken:u,statusChannel:d,agentName:n,pubnub:b,ownsSubscribeClient:!0,sdkOptions:l,rpcConfig:this.config,autoDrain:o,drainWindowMs:a,state:w,skipSubscription:!0,preloadedStreams:f,preloadedArtifacts:m,preloadedEvents:k});const T="0"!==y?y:r,P=[];let K,_=!1;const N={message:e=>{if(e.channel!==d)return;const t=e.message;if(!t||"object"!=typeof t||!t.type)return;const n=String(e.timetoken??"0");_&&K?K(t,n):P.push({message:t,timetoken:n})}};b.addListener(N),b.subscribe({channels:[d],timetoken:T});const E=new i({taskId:c,ownerId:t.owner??"",readToken:u,statusChannel:d,agentName:n,pubnub:b,ownsSubscribeClient:!0,sdkOptions:l,rpcConfig:this.config,autoDrain:o,drainWindowMs:a,state:s,preloadedStreams:f,preloadedArtifacts:m,preloadedEvents:k,externalSubscription:{listener:N,channel:d,onReady:e=>{K=e}}});if(K){const e=K;for(const t of P)t.timetoken>T&&(E._appendHistoryEvent(t.message,t.timetoken),e(t.message,t.timetoken))}return P.length=0,_=!0,E}catch(e){throw b.destroy(),e}}async getTask(e){return(await n(this.config,"GetTask",{taskId:e})).task}async listTasks(e){return n(this.config,"ListTasks",{...e})}async cancelTask(e){await n(this.config,"CancelTask",{taskId:e})}async pauseTask(e){await n(this.config,"PauseTask",{taskId:e})}async resumeTask(e){await n(this.config,"ResumeTask",{taskId:e})}async retryTask(e){await n(this.config,"RetryTask",{taskId:e})}async terminateTask(e){await n(this.config,"TerminateTask",{taskId:e})}subscribeToTask(e,t,n){return function(e,t,n,i){const r=s(t,n),o={message:e=>{if(e.channel!==r)return;const t=e.message;if(t&&"object"==typeof t&&t.type){if(i.onEvent)try{i.onEvent(t)}catch(e){P(e,"onEvent",t,i.onError)}switch(t.type){case"progress":if(i.onProgress)try{i.onProgress(t)}catch(e){P(e,"onProgress",t,i.onError)}break;case"artifact":if(i.onArtifact)try{i.onArtifact(t)}catch(e){P(e,"onArtifact",t,i.onError)}break;case"terminal":if(i.onTerminal)try{i.onTerminal(t)}catch(e){P(e,"onTerminal",t,i.onError)}break;case"system":if(i.onSystem)try{i.onSystem(t)}catch(e){P(e,"onSystem",t,i.onError)}}}}};return e.addListener(o),e.subscribe({channels:[r],timetoken:1e3}),{unsubscribe(){e.removeListener(o),e.unsubscribe({channels:[r]})}}}(this.getPubNub(),e,t,n)}}function P(e,t,n,s){const i=e instanceof Error?e:new Error(String(e));if(s)try{s(i,{entryPoint:"subscribeToTask",callbackType:t,event:n})}catch{}else T("warn",`subscribeToTask callback error in ${t}`,{event:"subscribe_callback_error",callbackType:t,error:i.message})}
@@ -163,7 +163,7 @@ export declare class TaskSession {
163
163
  onEvent(cb: (event: TaskEvent) => void): Unsubscribe;
164
164
  /**
165
165
  * Register an error handler for callback exceptions.
166
- * If registered, callback errors are routed here instead of console.warn.
166
+ * If registered, callback errors are routed here instead of the SDK logger.
167
167
  * Returns an unsubscribe function.
168
168
  */
169
169
  onError(cb: (error: Error, context: CallbackErrorContext) => void): Unsubscribe;
@@ -1 +1 @@
1
- import t from"pubnub";import{invertDirection as e}from"../stream/index.js";import{StreamRef as s}from"./stream-ref.js";import{taskChannel as i}from"./channel-manager.js";import{callRpc as r}from"./rpc-client.js";import{downloadArtifact as a}from"./artifacts.js";export const TERMINAL_STATES=new Set(["completed","failed","canceled"]);export class TaskSession{constructor(t){if(this.progressCallbacks=[],this.artifactCallbacks=[],this.terminalCallbacks=[],this.eventCallbacks=[],this.streamCallbacks=[],this.errorCallbacks=[],this.streams=new Map,this.artifacts=[],this.historyEvents=[],this.historyTimetokens=new Set,this.streamWaiters=[],this.terminalWaiters=[],this.closed=!1,this.listener=null,this.terminalReceived=!1,this.drainTimer=null,this.openStreamClients=new Set,this.seenTimetokens=new Set,this.taskId=t.taskId,this.ownerId=t.ownerId,this.orgId=t.orgId??t.ownerId,this.readToken=t.readToken,this.agentName=t.agentName,this.pubnub=t.pubnub,this.ownsSubscribeClient=t.ownsSubscribeClient??!1,this.sdkOptions=t.sdkOptions,this.rpcConfig=t.rpcConfig??null,this.statusChannel=t.statusChannel??i(t.taskId,this.orgId),this.idempotent=t.idempotent,this.queued=t.queued,this.pushConfigId=t.pushConfigId,this.autoDrain=t.autoDrain??!0,this.drainWindowMs=t.drainWindowMs??3e4,this.state=t.state,this.skipSubscriptionMode=t.skipSubscription??!1,this.subscribeKey=t.sdkOptions.subscribeKey,this.publishKey=t.sdkOptions.publishKey??"",t.preloadedStreams)for(const[e,i]of t.preloadedStreams){const t=new s(i.descriptor,this.sdkOptions,{onOpen:t=>this.trackStreamClient(t),sessionState:()=>this.state});this.streams.set(e,t)}t.preloadedArtifacts&&this.artifacts.push(...t.preloadedArtifacts),t.preloadedEvents&&this.historyEvents.push(...t.preloadedEvents),t.preClosed?this.closed=!0:t.skipSubscription||(t.externalSubscription?(this.listener=t.externalSubscription.listener,t.externalSubscription.onReady(this.handleEvent.bind(this))):this.setupSubscription())}setupSubscription(){const t=this.pubnub,e=this.statusChannel;this.listener={message:t=>{if(t.channel!==e)return;const s=t.message;if(!s||"object"!=typeof s||!s.type)return;const i=null!=t.timetoken?String(t.timetoken):void 0;this.handleEvent(s,i)}},t.addListener(this.listener),t.subscribe({channels:[e],timetoken:1e3})}routeCallbackError(t,e,s){const i=t instanceof Error?t:new Error(String(t));if(this.errorCallbacks.length>0)for(const t of this.errorCallbacks)try{t(i,{entryPoint:"taskSession",callbackType:e,event:s})}catch{}else console.warn(`[TaskSession] callback error in ${e}:`,i.message)}handleEvent(t,e){if(!this.closed){if(e){if(this.seenTimetokens.has(e))return;if(this.seenTimetokens.add(e),this.seenTimetokens.size>TaskSession.SEEN_TIMETOKENS_MAX){const t=this.seenTimetokens.values().next().value;void 0!==t&&this.seenTimetokens.delete(t)}}for(const e of this.eventCallbacks)try{e(t)}catch(e){this.routeCallbackError(e,"onEvent",t)}switch(t.type){case"progress":for(const e of this.progressCallbacks)try{e(t)}catch(e){this.routeCallbackError(e,"onProgress",t)}"stream_started"===t.streamEvent&&t.streams&&this.handleStreamStarted(t);break;case"artifact":this.accumulateArtifact(t);for(const e of this.artifactCallbacks)try{e(t)}catch(e){this.routeCallbackError(e,"onArtifact",t)}break;case"terminal":this.state=t.state;for(const e of this.terminalCallbacks)try{e(t)}catch(e){this.routeCallbackError(e,"onTerminal",t)}for(const e of this.terminalWaiters)e.resolve(t);this.terminalWaiters=[],this.autoDrain?this.startAutoDrain():this.close()}}}trackStreamClient(t){this.openStreamClients.add(t),t.onInboundDone(()=>{this.openStreamClients.delete(t),t.isActive&&t.end().catch(()=>{}),this.terminalReceived&&0===this.openStreamClients.size&&(this.drainTimer&&(clearTimeout(this.drainTimer),this.drainTimer=null),this.close())})}accumulateArtifact(t){const e=t.artifactRef;e&&"object"==typeof e&&e.kind&&this.artifacts.push(e)}handleStreamStarted(t){const i=t.streams;if(!i||"object"!=typeof i)return;const r=t.declaredStream;for(const[t,a]of Object.entries(i)){if(!a||"object"!=typeof a)continue;if(this.streams.has(t))continue;const i=a.direction,n=e(i),o=a.format;if(!o||"bytes"!==o&&"events"!==o)continue;const c=a.affinity;if("dedicated"!==c&&"shared"!==c){console.warn(`[TaskSession] live stream_started: dropping stream "${t}" for task "${this.taskId}" — invalid or missing affinity (got ${JSON.stringify(a.affinity)})`);continue}const l={taskId:this.taskId,streamId:t,agentName:this.agentName,channel:a.channel,token:a.token,agentDirection:i,localDirection:n,format:o,affinity:c,metadata:a.metadata,declaredStream:r},h=new s(l,this.sdkOptions,{onOpen:t=>this.trackStreamClient(t),sessionState:()=>this.state});this.streams.set(t,h);for(const t of this.streamCallbacks)try{t(h)}catch(t){this.routeCallbackError(t,"onStream",h)}this.resolveWaiters(h)}}startAutoDrain(){this.closed||(this.terminalReceived=!0,0!==this.openStreamClients.size?this.drainTimer=setTimeout(()=>{this.drainTimer=null;for(const t of this.openStreamClients)t.isActive&&t.end().catch(()=>{});this.close()},this.drainWindowMs):this.close())}findStreamByIdOrDeclared(t){const e=this.streams.get(t);if(e)return e;for(const e of this.streams.values())if(e.descriptor.declaredStream===t)return e}resolveWaiters(t){const e=[];for(const s of this.streamWaiters){let i=!1;if(void 0!==s.streamId)i=t.descriptor.declaredStream===s.streamId||t.descriptor.streamId===s.streamId;else if(s.predicate)try{i=s.predicate(t)}catch(e){this.routeCallbackError(e,"streamPredicate",t)}else i=!0;i?s.resolve(t):e.push(s)}this.streamWaiters=e}onProgress(t){return this.progressCallbacks.push(t),()=>{this.progressCallbacks=this.progressCallbacks.filter(e=>e!==t)}}onArtifact(t){this.artifactCallbacks.push(t);for(const e of[...this.artifacts]){const s={type:"artifact",taskId:this.taskId,artifactRef:e};try{t(s)}catch(t){this.routeCallbackError(t,"onArtifact",s)}}return()=>{this.artifactCallbacks=this.artifactCallbacks.filter(e=>e!==t)}}onTerminal(t){if(this.terminalCallbacks.push(t),this.state&&TERMINAL_STATES.has(this.state)){const e={type:"terminal",taskId:this.taskId,state:this.state};try{t(e)}catch(t){this.routeCallbackError(t,"onTerminal",e)}}return()=>{this.terminalCallbacks=this.terminalCallbacks.filter(e=>e!==t)}}onEvent(t){return this.eventCallbacks.push(t),()=>{this.eventCallbacks=this.eventCallbacks.filter(e=>e!==t)}}onError(t){return this.errorCallbacks.push(t),()=>{this.errorCallbacks=this.errorCallbacks.filter(e=>e!==t)}}listArtifacts(){return[...this.artifacts]}listEvents(){return[...this.historyEvents]}_appendHistoryEvent(t,e){if(e){if(this.historyTimetokens.has(e))return;this.historyTimetokens.add(e)}this.historyEvents.push(t)}async downloadArtifact(e){if(this.pubnub)return a(e,this.pubnub);const s=`blocks-dl-${Date.now().toString(36)}-${Math.random().toString(36).slice(2,6)}`,i=new t({subscribeKey:this.subscribeKey,publishKey:this.publishKey||void 0,userId:s});this.readToken&&i.setToken(this.readToken);try{return await a(e,i)}finally{i.destroy()}}onStream(t){this.streamCallbacks.push(t);for(const e of this.streams.values())try{t(e)}catch(t){this.routeCallbackError(t,"onStream",e)}return()=>{this.streamCallbacks=this.streamCallbacks.filter(e=>e!==t)}}listStreams(){return[...this.streams.values()]}openAllStreams(t){const e=[];for(const s of this.streams.values()){const i=s.descriptor.localDirection;if("inbound"===i||"bidirectional"===i)try{e.push(s.open(t))}catch{}}return e}waitForStream(t){if(this.closed)return Promise.reject(new Error("TaskSession is closed"));if(this.skipSubscriptionMode){if(void 0!==t){const e=this.findStreamByIdOrDeclared(t);if(e)return Promise.resolve(e)}else{if(1===this.streams.size)return Promise.resolve(this.streams.values().next().value);if(this.streams.size>1)return Promise.reject(new Error(`Multiple streams exist (${this.streams.size}). Use waitForStream(streamId) or waitForStreamWhere(predicate) to select one.`))}return Promise.reject(new Error("No matching stream found. This is a terminal task session with no live subscription -- no future stream announcements will arrive."))}if(void 0!==t){const e=this.findStreamByIdOrDeclared(t);if(e)return Promise.resolve(e)}else{if(1===this.streams.size)return Promise.resolve(this.streams.values().next().value);if(this.streams.size>1)return Promise.reject(new Error(`Multiple streams exist (${this.streams.size}). Use waitForStream(streamId) or waitForStreamWhere(predicate) to select one.`))}return new Promise((e,s)=>{this.streamWaiters.push({resolve:e,reject:s,streamId:t})})}waitForStreamWhere(t){if(this.closed)return Promise.reject(new Error("TaskSession is closed"));for(const e of this.streams.values())try{if(t(e))return Promise.resolve(e)}catch(t){this.routeCallbackError(t,"streamPredicate",e)}return this.skipSubscriptionMode?Promise.reject(new Error("No matching stream found. This is a terminal task session with no live subscription -- no future stream announcements will arrive.")):new Promise((e,s)=>{this.streamWaiters.push({resolve:e,reject:s,predicate:t})})}async waitForTerminal(t){if(this.state&&TERMINAL_STATES.has(this.state))return{type:"terminal",taskId:this.taskId,state:this.state};if(this.closed)throw new Error("TaskSession closed");return new Promise((e,s)=>{const i={resolve:e,reject:s};if(this.terminalWaiters.push(i),void 0!==t){const e=setTimeout(()=>{const e=this.terminalWaiters.indexOf(i);-1!==e&&this.terminalWaiters.splice(e,1),s(new Error(`waitForTerminal timed out after ${t}ms`))},t),r=i.resolve,a=i.reject;i.resolve=t=>{clearTimeout(e),r(t)},i.reject=t=>{clearTimeout(e),a(t)}}})}async saveArtifacts(t){const{mkdirSync:e,writeFileSync:s}=await import("node:fs"),{join:i,resolve:r,basename:a}=await import("node:path"),n=r(t);e(n,{recursive:!0});const o=[],c=this.listArtifacts();for(let t=0;t<c.length;t++){const e=await this.downloadArtifact(c[t]),l=e.fileName??`artifact-${t}`,h=r(i(n,a(l)||`artifact-${t}`));if(!h.startsWith(n+"/")&&h!==n)throw new Error(`Artifact filename "${l}" resolves outside target directory`);s(h,e.data),o.push(h)}return o}async cancel(){if(!this.rpcConfig)throw new Error("TaskSession was not created with RPC config; use TaskClient.cancelTask() directly");await r(this.rpcConfig,"CancelTask",{taskId:this.taskId})}async terminate(){if(!this.rpcConfig)throw new Error("TaskSession was not created with RPC config; use TaskClient.terminateTask() directly");await r(this.rpcConfig,"TerminateTask",{taskId:this.taskId})}close(){if(this.closed)return;this.closed=!0,this.drainTimer&&(clearTimeout(this.drainTimer),this.drainTimer=null);for(const t of this.openStreamClients)t.isActive&&t.end().catch(()=>{});this.openStreamClients.clear();const t=new Error("TaskSession closed");for(const e of this.streamWaiters)e.reject(t);this.streamWaiters=[];for(const e of this.terminalWaiters)e.reject(t);this.terminalWaiters=[],this.pubnub&&(this.listener&&(this.pubnub.removeListener(this.listener),this.listener=null),this.pubnub.unsubscribe({channels:[this.statusChannel]}),this.ownsSubscribeClient&&this.pubnub.destroy()),this.progressCallbacks=[],this.artifactCallbacks=[],this.terminalCallbacks=[],this.eventCallbacks=[],this.streamCallbacks=[],this.errorCallbacks=[]}async asyncClose(){if(this.closed)return;const t=[...this.openStreamClients].filter(t=>t.isActive).map(t=>t.end().catch(()=>{}));await Promise.all(t),this.openStreamClients.clear(),this.close()}async[Symbol.asyncDispose](){await this.asyncClose()}get isClosed(){return this.closed}}TaskSession.SEEN_TIMETOKENS_MAX=200;
1
+ import t from"pubnub";import{buildPubNubLogConfig as e}from"./pubnub-client.js";import{invertDirection as s}from"../stream/index.js";import{StreamRef as i}from"./stream-ref.js";import{taskChannel as r}from"./channel-manager.js";import{callRpc as a}from"./rpc-client.js";import{downloadArtifact as n}from"./artifacts.js";import{log as o}from"./logger.js";const c=(t,e,s)=>o("[TaskSession]",t,e,s);export const TERMINAL_STATES=new Set(["completed","failed","canceled"]);export class TaskSession{constructor(t){if(this.progressCallbacks=[],this.artifactCallbacks=[],this.terminalCallbacks=[],this.eventCallbacks=[],this.streamCallbacks=[],this.errorCallbacks=[],this.streams=new Map,this.artifacts=[],this.historyEvents=[],this.historyTimetokens=new Set,this.streamWaiters=[],this.terminalWaiters=[],this.closed=!1,this.listener=null,this.terminalReceived=!1,this.drainTimer=null,this.openStreamClients=new Set,this.seenTimetokens=new Set,this.taskId=t.taskId,this.ownerId=t.ownerId,this.orgId=t.orgId??t.ownerId,this.readToken=t.readToken,this.agentName=t.agentName,this.pubnub=t.pubnub,this.ownsSubscribeClient=t.ownsSubscribeClient??!1,this.sdkOptions={...t.sdkOptions,consumerUserId:t.ownerId},this.rpcConfig=t.rpcConfig??null,this.statusChannel=t.statusChannel??r(t.taskId,this.orgId),this.idempotent=t.idempotent,this.queued=t.queued,this.pushConfigId=t.pushConfigId,this.autoDrain=t.autoDrain??!0,this.drainWindowMs=t.drainWindowMs??3e4,this.state=t.state,this.skipSubscriptionMode=t.skipSubscription??!1,this.subscribeKey=t.sdkOptions.subscribeKey,this.publishKey=t.sdkOptions.publishKey??"",t.preloadedStreams)for(const[e,s]of t.preloadedStreams){const t=new i(s.descriptor,this.sdkOptions,{onOpen:t=>this.trackStreamClient(t),sessionState:()=>this.state});this.streams.set(e,t)}t.preloadedArtifacts&&this.artifacts.push(...t.preloadedArtifacts),t.preloadedEvents&&this.historyEvents.push(...t.preloadedEvents),t.preClosed?this.closed=!0:t.skipSubscription||(t.externalSubscription?(this.listener=t.externalSubscription.listener,t.externalSubscription.onReady(this.handleEvent.bind(this))):this.setupSubscription())}setupSubscription(){const t=this.pubnub,e=this.statusChannel;this.listener={message:t=>{if(t.channel!==e)return;const s=t.message;if(!s||"object"!=typeof s||!s.type)return;const i=null!=t.timetoken?String(t.timetoken):void 0;this.handleEvent(s,i)}},t.addListener(this.listener),t.subscribe({channels:[e],timetoken:1e3})}routeCallbackError(t,e,s){const i=t instanceof Error?t:new Error(String(t));if(this.errorCallbacks.length>0)for(const t of this.errorCallbacks)try{t(i,{entryPoint:"taskSession",callbackType:e,event:s})}catch{}else c("warn",`callback error in ${e}`,{event:"task_session_callback_error",callbackType:e,error:i.message})}handleEvent(t,e){if(!this.closed){if(e){if(this.seenTimetokens.has(e))return;if(this.seenTimetokens.add(e),this.seenTimetokens.size>TaskSession.SEEN_TIMETOKENS_MAX){const t=this.seenTimetokens.values().next().value;void 0!==t&&this.seenTimetokens.delete(t)}}for(const e of this.eventCallbacks)try{e(t)}catch(e){this.routeCallbackError(e,"onEvent",t)}switch(t.type){case"progress":for(const e of this.progressCallbacks)try{e(t)}catch(e){this.routeCallbackError(e,"onProgress",t)}"stream_started"===t.streamEvent&&t.streams&&this.handleStreamStarted(t);break;case"artifact":this.accumulateArtifact(t);for(const e of this.artifactCallbacks)try{e(t)}catch(e){this.routeCallbackError(e,"onArtifact",t)}break;case"terminal":this.state=t.state;for(const e of this.terminalCallbacks)try{e(t)}catch(e){this.routeCallbackError(e,"onTerminal",t)}for(const e of this.terminalWaiters)e.resolve(t);this.terminalWaiters=[],this.autoDrain?this.startAutoDrain():this.close()}}}trackStreamClient(t){this.openStreamClients.add(t),t.onInboundDone(()=>{this.openStreamClients.delete(t),t.isActive&&t.end().catch(()=>{}),this.terminalReceived&&0===this.openStreamClients.size&&(this.drainTimer&&(clearTimeout(this.drainTimer),this.drainTimer=null),this.close())})}accumulateArtifact(t){const e=t.artifactRef;e&&"object"==typeof e&&e.kind&&this.artifacts.push(e)}handleStreamStarted(t){const e=t.streams;if(!e||"object"!=typeof e)return;const r=t.declaredStream;for(const[t,a]of Object.entries(e)){if(!a||"object"!=typeof a)continue;if(this.streams.has(t))continue;const e=a.direction,n=s(e),o=a.format;if(!o||"bytes"!==o&&"events"!==o)continue;const l=a.affinity;if("dedicated"!==l&&"shared"!==l){c("warn",`live stream_started: dropping stream "${t}" for task "${this.taskId}" — invalid or missing affinity`,{event:"task_session_live_invalid_affinity",streamId:t,taskId:this.taskId,receivedAffinity:a.affinity});continue}const h={taskId:this.taskId,streamId:t,agentName:this.agentName,channel:a.channel,token:a.token,agentDirection:e,localDirection:n,format:o,affinity:l,metadata:a.metadata,declaredStream:r},m=new i(h,this.sdkOptions,{onOpen:t=>this.trackStreamClient(t),sessionState:()=>this.state});this.streams.set(t,m);for(const t of this.streamCallbacks)try{t(m)}catch(t){this.routeCallbackError(t,"onStream",m)}this.resolveWaiters(m)}}startAutoDrain(){this.closed||(this.terminalReceived=!0,0!==this.openStreamClients.size?this.drainTimer=setTimeout(()=>{this.drainTimer=null;for(const t of this.openStreamClients)t.isActive&&t.end().catch(()=>{});this.close()},this.drainWindowMs):this.close())}findStreamByIdOrDeclared(t){const e=this.streams.get(t);if(e)return e;for(const e of this.streams.values())if(e.descriptor.declaredStream===t)return e}resolveWaiters(t){const e=[];for(const s of this.streamWaiters){let i=!1;if(void 0!==s.streamId)i=t.descriptor.declaredStream===s.streamId||t.descriptor.streamId===s.streamId;else if(s.predicate)try{i=s.predicate(t)}catch(e){this.routeCallbackError(e,"streamPredicate",t)}else i=!0;i?s.resolve(t):e.push(s)}this.streamWaiters=e}onProgress(t){return this.progressCallbacks.push(t),()=>{this.progressCallbacks=this.progressCallbacks.filter(e=>e!==t)}}onArtifact(t){this.artifactCallbacks.push(t);for(const e of[...this.artifacts]){const s={type:"artifact",taskId:this.taskId,artifactRef:e};try{t(s)}catch(t){this.routeCallbackError(t,"onArtifact",s)}}return()=>{this.artifactCallbacks=this.artifactCallbacks.filter(e=>e!==t)}}onTerminal(t){if(this.terminalCallbacks.push(t),this.state&&TERMINAL_STATES.has(this.state)){const e={type:"terminal",taskId:this.taskId,state:this.state};try{t(e)}catch(t){this.routeCallbackError(t,"onTerminal",e)}}return()=>{this.terminalCallbacks=this.terminalCallbacks.filter(e=>e!==t)}}onEvent(t){return this.eventCallbacks.push(t),()=>{this.eventCallbacks=this.eventCallbacks.filter(e=>e!==t)}}onError(t){return this.errorCallbacks.push(t),()=>{this.errorCallbacks=this.errorCallbacks.filter(e=>e!==t)}}listArtifacts(){return[...this.artifacts]}listEvents(){return[...this.historyEvents]}_appendHistoryEvent(t,e){if(e){if(this.historyTimetokens.has(e))return;this.historyTimetokens.add(e)}this.historyEvents.push(t)}async downloadArtifact(s){if(this.pubnub)return n(s,this.pubnub);const i=`blocks-dl-${Date.now().toString(36)}-${Math.random().toString(36).slice(2,6)}`,r=new t({subscribeKey:this.subscribeKey,publishKey:this.publishKey||void 0,userId:i,...e()});this.readToken&&r.setToken(this.readToken);try{return await n(s,r)}finally{r.destroy()}}onStream(t){this.streamCallbacks.push(t);for(const e of this.streams.values())try{t(e)}catch(t){this.routeCallbackError(t,"onStream",e)}return()=>{this.streamCallbacks=this.streamCallbacks.filter(e=>e!==t)}}listStreams(){return[...this.streams.values()]}openAllStreams(t){const e=[];for(const s of this.streams.values()){const i=s.descriptor.localDirection;if("inbound"===i||"bidirectional"===i)try{e.push(s.open(t))}catch{}}return e}waitForStream(t){if(this.closed)return Promise.reject(new Error("TaskSession is closed"));if(this.skipSubscriptionMode){if(void 0!==t){const e=this.findStreamByIdOrDeclared(t);if(e)return Promise.resolve(e)}else{if(1===this.streams.size)return Promise.resolve(this.streams.values().next().value);if(this.streams.size>1)return Promise.reject(new Error(`Multiple streams exist (${this.streams.size}). Use waitForStream(streamId) or waitForStreamWhere(predicate) to select one.`))}return Promise.reject(new Error("No matching stream found. This is a terminal task session with no live subscription -- no future stream announcements will arrive."))}if(void 0!==t){const e=this.findStreamByIdOrDeclared(t);if(e)return Promise.resolve(e)}else{if(1===this.streams.size)return Promise.resolve(this.streams.values().next().value);if(this.streams.size>1)return Promise.reject(new Error(`Multiple streams exist (${this.streams.size}). Use waitForStream(streamId) or waitForStreamWhere(predicate) to select one.`))}return new Promise((e,s)=>{this.streamWaiters.push({resolve:e,reject:s,streamId:t})})}waitForStreamWhere(t){if(this.closed)return Promise.reject(new Error("TaskSession is closed"));for(const e of this.streams.values())try{if(t(e))return Promise.resolve(e)}catch(t){this.routeCallbackError(t,"streamPredicate",e)}return this.skipSubscriptionMode?Promise.reject(new Error("No matching stream found. This is a terminal task session with no live subscription -- no future stream announcements will arrive.")):new Promise((e,s)=>{this.streamWaiters.push({resolve:e,reject:s,predicate:t})})}async waitForTerminal(t){if(this.state&&TERMINAL_STATES.has(this.state))return{type:"terminal",taskId:this.taskId,state:this.state};if(this.closed)throw new Error("TaskSession closed");return new Promise((e,s)=>{const i={resolve:e,reject:s};if(this.terminalWaiters.push(i),void 0!==t){const e=setTimeout(()=>{const e=this.terminalWaiters.indexOf(i);-1!==e&&this.terminalWaiters.splice(e,1),s(new Error(`waitForTerminal timed out after ${t}ms`))},t),r=i.resolve,a=i.reject;i.resolve=t=>{clearTimeout(e),r(t)},i.reject=t=>{clearTimeout(e),a(t)}}})}async saveArtifacts(t){const{mkdirSync:e,writeFileSync:s}=await import("node:fs"),{join:i,resolve:r,basename:a}=await import("node:path"),n=r(t);e(n,{recursive:!0});const o=[],c=this.listArtifacts();for(let t=0;t<c.length;t++){const e=await this.downloadArtifact(c[t]),l=e.fileName??`artifact-${t}`,h=r(i(n,a(l)||`artifact-${t}`));if(!h.startsWith(n+"/")&&h!==n)throw new Error(`Artifact filename "${l}" resolves outside target directory`);s(h,e.data),o.push(h)}return o}async cancel(){if(!this.rpcConfig)throw new Error("TaskSession was not created with RPC config; use TaskClient.cancelTask() directly");await a(this.rpcConfig,"CancelTask",{taskId:this.taskId})}async terminate(){if(!this.rpcConfig)throw new Error("TaskSession was not created with RPC config; use TaskClient.terminateTask() directly");await a(this.rpcConfig,"TerminateTask",{taskId:this.taskId})}close(){if(this.closed)return;this.closed=!0,this.drainTimer&&(clearTimeout(this.drainTimer),this.drainTimer=null);for(const t of this.openStreamClients)t.isActive&&t.end().catch(()=>{});this.openStreamClients.clear();const t=new Error("TaskSession closed");for(const e of this.streamWaiters)e.reject(t);this.streamWaiters=[];for(const e of this.terminalWaiters)e.reject(t);this.terminalWaiters=[],this.pubnub&&(this.listener&&(this.pubnub.removeListener(this.listener),this.listener=null),this.pubnub.unsubscribe({channels:[this.statusChannel]}),this.ownsSubscribeClient&&this.pubnub.destroy()),this.progressCallbacks=[],this.artifactCallbacks=[],this.terminalCallbacks=[],this.eventCallbacks=[],this.streamCallbacks=[],this.errorCallbacks=[]}async asyncClose(){if(this.closed)return;const t=[...this.openStreamClients].filter(t=>t.isActive).map(t=>t.end().catch(()=>{}));await Promise.all(t),this.openStreamClients.clear(),this.close()}async[Symbol.asyncDispose](){await this.asyncClose()}get isClosed(){return this.closed}}TaskSession.SEEN_TIMETOKENS_MAX=200;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Neutral category labels surfaced through user-facing logs and the public
3
+ * `StreamError.category` field. The SDK maps the underlying realtime
4
+ * transport's internal category strings into this small enum so the
5
+ * user-visible surface stays implementation-agnostic.
6
+ *
7
+ * "other" is the catch-all for transport categories the SDK does not
8
+ * specifically classify. Devs investigating an unrecognised category
9
+ * should opt into BLOCKS_DEBUG_INTERNAL=forward_transport to see the
10
+ * underlying entries directly.
11
+ */
12
+ export type TransportCategory = 'connected' | 'reconnected' | 'network_down' | 'network_issues' | 'timeout' | 'malformed_response' | 'access_denied' | 'bad_request' | 'other';
13
+ /** Status payload shape consumed by the mapper. Mirrors the fields the
14
+ * PubNub JS SDK populates on `Status` / `StatusEvent`. */
15
+ export interface TransportStatusPayload {
16
+ category?: unknown;
17
+ error?: unknown;
18
+ statusCode?: unknown;
19
+ }
20
+ /**
21
+ * Map the underlying transport's status payload to a neutral category.
22
+ *
23
+ * Resolution order:
24
+ * 1. `statusCode === 403` -> `access_denied` (REST-grade override; wins
25
+ * even over an unknown wrapper category, because PAM revocation is
26
+ * the load-bearing case for `accessDeniedHandler`).
27
+ * 2. `statusCode === 400` -> `bad_request` (same rationale).
28
+ * 3. If the outer category is one of the Event Engine wrappers
29
+ * (`PNConnectionErrorCategory` / `PNDisconnectedUnexpectedlyCategory`),
30
+ * look up the nested `error` string in CATEGORY_MAP.
31
+ * 4. Look up the outer category in CATEGORY_MAP.
32
+ * 5. Fallback: `'other'`.
33
+ *
34
+ * Accepts either the full payload `{ category, error, statusCode }` or
35
+ * a bare string (the legacy form). String form is preserved so test
36
+ * fixtures and pure-classifier callers don't have to wrap.
37
+ */
38
+ export declare function mapTransportCategory(input: TransportStatusPayload | string | null | undefined): TransportCategory;
39
+ /**
40
+ * Convenience predicate for the access-denied path. True when the
41
+ * payload resolves to `access_denied` via `mapTransportCategory` (which
42
+ * already honors statusCode 403, the wrapper unwrap, and the leaf
43
+ * category).
44
+ */
45
+ export declare function isAccessDeniedStatus(input: TransportStatusPayload): boolean;
46
+ /** Categories that warrant a "connectivity degraded" warn-level log line. */
47
+ export declare const DEGRADED_TRANSPORT_CATEGORIES: ReadonlySet<TransportCategory>;
48
+ /** Categories that warrant a "connectivity restored" info-level log line. */
49
+ export declare const RESTORED_TRANSPORT_CATEGORIES: ReadonlySet<TransportCategory>;
50
+ /** Categories that force-terminate a stream subscription. */
51
+ export declare const FATAL_TRANSPORT_CATEGORIES: ReadonlySet<TransportCategory>;
52
+ /**
53
+ * Neutral operation labels surfaced through user-facing logs. The SDK
54
+ * maps the underlying realtime transport's internal operation strings
55
+ * (e.g. `PNSubscribeOperation`) into this small enum so the user-visible
56
+ * surface stays implementation-agnostic. "other" is the catch-all for
57
+ * operations the SDK does not specifically classify.
58
+ */
59
+ export type TransportOperation = 'subscribe' | 'heartbeat' | 'publish' | 'history' | 'presence' | 'other';
60
+ export declare function mapTransportOperation(raw: string | undefined): TransportOperation;
@@ -0,0 +1 @@
1
+ const e={PNConnectedCategory:"connected",PNReconnectedCategory:"reconnected",PNNetworkDownCategory:"network_down",PNNetworkIssuesCategory:"network_issues",PNTimeoutCategory:"timeout",PNMalformedResponseCategory:"malformed_response",PNAccessDeniedCategory:"access_denied",PNBadRequestCategory:"bad_request"},t=new Set(["PNConnectionErrorCategory","PNDisconnectedUnexpectedlyCategory"]);export function mapTransportCategory(r){if(null==r)return"other";if("string"==typeof r)return e[r]??"other";const{category:o,error:n,statusCode:s}=r;if(403===s)return"access_denied";if(400===s)return"bad_request";const a="string"==typeof o?o:"";if(a&&t.has(a)){return e["string"==typeof n?n:""]??"other"}return e[a]??"other"}export function isAccessDeniedStatus(e){return"access_denied"===mapTransportCategory(e)}export const DEGRADED_TRANSPORT_CATEGORIES=new Set(["network_down","network_issues","timeout","malformed_response"]);export const RESTORED_TRANSPORT_CATEGORIES=new Set(["reconnected"]);export const FATAL_TRANSPORT_CATEGORIES=new Set(["access_denied","bad_request"]);const r={PNSubscribeOperation:"subscribe",PNHeartbeatOperation:"heartbeat",PNPublishOperation:"publish",PNSignalOperation:"publish",PNHistoryOperation:"history",PNFetchMessagesOperation:"history",PNMessageCountsOperation:"history",PNHereNowOperation:"presence",PNWhereNowOperation:"presence",PNGetStateOperation:"presence",PNSetStateOperation:"presence"};export function mapTransportOperation(e){return void 0===e?"other":r[e]??"other"}
@@ -1 +1 @@
1
- import{utf8ByteLength as t,utf8Encode as e,bytesToBase64 as s}from"./bytes.js";import{CURRENT_PROTOCOL_VERSION as i}from"../runtime/protocol-version.js";export class StreamBundle{constructor(t,e,s,i,n,h){if(this.occupancy=0,this.presenceListener=null,this.buffer=[],this.bufferBytes=0,this.currentBatchHasBinary=!1,this.eventBuffer=[],this.eventBufferSize=0,this.closed=!1,this.flushTimer=null,this.inflightFlushes=[],this.pubnub=t,this.channel=e,this.streamId=s,this.format=i,n.maxMessageSize<=512)throw new Error(`maxMessageSize (${n.maxMessageSize}) must be greater than ENVELOPE_RESERVE (512)`);this.maxMessageSize=n.maxMessageSize,this.bundleSizeBytes=n.bundleSizeBytes,this.maxLatencyMs=n.maxLatencyMs,this.uuid=n.uuid,this.gated=h,this.seq="events"===i?1:0}get isActive(){return!this.closed}get consumerCount(){return this.gated?this.occupancy:0}write(t){if(this.closed)throw new Error("Cannot write to a closed stream");"events"===this.format?this.writeEvent(t):this.writeBytes(t)}async publishEndMarker(){const t={type:"stream_end",streamId:this.streamId,seq:this.seq++,ts:Date.now(),protocolVersion:i};await this.publishMessage(t).catch(()=>{})}async end(){this.closed||(this.closed=!0,this.clearTimer(),this.inflightFlushes.length>0&&(await Promise.all(this.inflightFlushes),this.inflightFlushes=[]),"events"===this.format?await this.flushEvents():await this.flush(),this.teardownPresenceGating(),await(this.onEnd?.()))}setupPresenceGating(){const t=this.channel+"-pnpres";this.presenceListener={message:e=>{e.channel===t&&"number"==typeof e.message?.occupancy&&(this.occupancy=e.message.occupancy)}},this.pubnub.addListener(this.presenceListener),this.pubnub.subscribe({channels:[t]}),this.pubnub.hereNow({channels:[this.channel]}).then(t=>{const e=t.channels?.[this.channel];e&&"number"==typeof e.occupancy&&(this.occupancy=e.occupancy)}).catch(()=>{})}teardownPresenceGating(){this.presenceListener&&(this.pubnub.removeListener(this.presenceListener),this.presenceListener=null),this.gated&&this.pubnub.unsubscribe({channels:[this.channel+"-pnpres"]})}writeBytes(e){let i;"string"==typeof e?i=e:e instanceof Uint8Array?(i=s(e),this.currentBatchHasBinary=!0):i=String(e);const n=t(i);this.buffer.push(i),this.bufferBytes+=n,this.bufferBytes>=this.bundleSizeBytes?this.trackFlush(this.flush()):null===this.flushTimer&&(this.flushTimer=setTimeout(()=>{this.flushTimer=null,this.trackFlush(this.flush())},this.maxLatencyMs))}async flush(){if(0===this.buffer.length)return;this.clearTimer();const e=this.buffer,s=this.currentBatchHasBinary;this.buffer=[],this.bufferBytes=0,this.currentBatchHasBinary=!1;const n={type:"stream_data",streamId:this.streamId,seq:this.seq++,ts:Date.now(),encoding:s?"base64":"utf8",chunks:e,protocolVersion:i},h=JSON.stringify(n);t(h)>this.maxMessageSize?await this.publishMultipart(n):await this.publishMessage(n)}writeEvent(e){if("string"==typeof e)throw new Error('write() does not accept raw strings in format: "events". Pass an object (e.g., { text: "..." }) or use format: "bytes".');e instanceof Uint8Array?(this.eventBuffer.push({$binary:s(e)}),this.eventBufferSize+=Math.ceil(4*e.length/3)+20):(this.eventBuffer.push(e),this.eventBufferSize+=t(JSON.stringify(e))),this.eventBufferSize>=this.bundleSizeBytes?this.trackFlush(this.flushEvents()):null===this.flushTimer&&(this.flushTimer=setTimeout(()=>{this.flushTimer=null,this.trackFlush(this.flushEvents())},this.maxLatencyMs))}async flushEvents(){if(0===this.eventBuffer.length)return;this.clearTimer();const e=this.eventBuffer;this.eventBuffer=[],this.eventBufferSize=0;const s={type:"stream_events",streamId:this.streamId,seq:this.seq++,ts:Date.now(),encoding:"utf8",events:e,protocolVersion:i},n=JSON.stringify(s);t(n)>this.maxMessageSize?await this.publishMultipart(s):await this.publishMessage(s)}trackFlush(t){this.inflightFlushes.push(t),t.then(()=>{const e=this.inflightFlushes.indexOf(t);e>=0&&this.inflightFlushes.splice(e,1)})}async publishMessage(t){let e=null;for(let s=1;s<=3;s++)try{return void await this.pubnub.publish({channel:this.channel,message:t,meta:{sender:this.uuid,protocolVersion:i},storeInHistory:!1,sendByPost:!0})}catch(t){if(e=t,s<3){const t=100*Math.pow(2,s-1);await new Promise(e=>setTimeout(e,t))}}console.error("[StreamBundle] Failed to publish stream message after 3 attempts:",e)}async publishMultipart(t){const n=JSON.stringify(t),h=e(n),r=Math.floor(3*(this.maxMessageSize-512)/4),a=Math.ceil(h.length/r),u=`mp-${Date.now()}-${t.seq}`,l=[];for(let e=0;e<a;e++){const n=h.subarray(e*r,(e+1)*r),c={type:t.type,streamId:this.streamId,seq:t.seq,ts:t.ts,multipart:{id:u,part:e+1,total:a},data:s(n),protocolVersion:i};l.push(()=>this.publishMessage(c))}await async function(t,e){const s=new Array(t.length);let i=0;const n=Math.min(e,t.length),h=Array.from({length:n},async()=>{for(;;){const e=i++;if(e>=t.length)return;s[e]=await t[e]()}});return await Promise.all(h),s}(l,4)}clearTimer(){null!==this.flushTimer&&(clearTimeout(this.flushTimer),this.flushTimer=null)}}
1
+ import{utf8ByteLength as t,utf8Encode as e,bytesToBase64 as s}from"./bytes.js";import{CURRENT_PROTOCOL_VERSION as i}from"../runtime/protocol-version.js";import{log as n}from"../runtime/logger.js";export class StreamBundle{constructor(t,e,s,i,n,r){if(this.occupancy=0,this.presenceListener=null,this.buffer=[],this.bufferBytes=0,this.currentBatchHasBinary=!1,this.eventBuffer=[],this.eventBufferSize=0,this.closed=!1,this.flushTimer=null,this.inflightFlushes=[],this.pubnub=t,this.channel=e,this.streamId=s,this.format=i,n.maxMessageSize<=512)throw new Error(`maxMessageSize (${n.maxMessageSize}) must be greater than ENVELOPE_RESERVE (512)`);this.maxMessageSize=n.maxMessageSize,this.bundleSizeBytes=n.bundleSizeBytes,this.maxLatencyMs=n.maxLatencyMs,this.uuid=n.uuid,this.gated=r,this.seq="events"===i?1:0}get isActive(){return!this.closed}get consumerCount(){return this.gated?this.occupancy:0}write(t){if(this.closed)throw new Error("Cannot write to a closed stream");"events"===this.format?this.writeEvent(t):this.writeBytes(t)}async publishEndMarker(){const t={type:"stream_end",streamId:this.streamId,seq:this.seq++,ts:Date.now(),protocolVersion:i};await this.publishMessage(t).catch(()=>{})}async end(){this.closed||(this.closed=!0,this.clearTimer(),this.inflightFlushes.length>0&&(await Promise.all(this.inflightFlushes),this.inflightFlushes=[]),"events"===this.format?await this.flushEvents():await this.flush(),this.teardownPresenceGating(),await(this.onEnd?.()))}setupPresenceGating(){const t=this.channel+"-pnpres";this.presenceListener={message:e=>{e.channel===t&&"number"==typeof e.message?.occupancy&&(this.occupancy=e.message.occupancy)}},this.pubnub.addListener(this.presenceListener),this.pubnub.subscribe({channels:[t]}),this.pubnub.hereNow({channels:[this.channel]}).then(t=>{const e=t.channels?.[this.channel];e&&"number"==typeof e.occupancy&&(this.occupancy=e.occupancy)}).catch(()=>{})}teardownPresenceGating(){this.presenceListener&&(this.pubnub.removeListener(this.presenceListener),this.presenceListener=null),this.gated&&this.pubnub.unsubscribe({channels:[this.channel+"-pnpres"]})}writeBytes(e){let i;"string"==typeof e?i=e:e instanceof Uint8Array?(i=s(e),this.currentBatchHasBinary=!0):i=String(e);const n=t(i);this.buffer.push(i),this.bufferBytes+=n,this.bufferBytes>=this.bundleSizeBytes?this.trackFlush(this.flush()):null===this.flushTimer&&(this.flushTimer=setTimeout(()=>{this.flushTimer=null,this.trackFlush(this.flush())},this.maxLatencyMs))}async flush(){if(0===this.buffer.length)return;this.clearTimer();const e=this.buffer,s=this.currentBatchHasBinary;this.buffer=[],this.bufferBytes=0,this.currentBatchHasBinary=!1;const n={type:"stream_data",streamId:this.streamId,seq:this.seq++,ts:Date.now(),encoding:s?"base64":"utf8",chunks:e,protocolVersion:i},r=JSON.stringify(n);t(r)>this.maxMessageSize?await this.publishMultipart(n):await this.publishMessage(n)}writeEvent(e){if("string"==typeof e)throw new Error('write() does not accept raw strings in format: "events". Pass an object (e.g., { text: "..." }) or use format: "bytes".');e instanceof Uint8Array?(this.eventBuffer.push({$binary:s(e)}),this.eventBufferSize+=Math.ceil(4*e.length/3)+20):(this.eventBuffer.push(e),this.eventBufferSize+=t(JSON.stringify(e))),this.eventBufferSize>=this.bundleSizeBytes?this.trackFlush(this.flushEvents()):null===this.flushTimer&&(this.flushTimer=setTimeout(()=>{this.flushTimer=null,this.trackFlush(this.flushEvents())},this.maxLatencyMs))}async flushEvents(){if(0===this.eventBuffer.length)return;this.clearTimer();const e=this.eventBuffer;this.eventBuffer=[],this.eventBufferSize=0;const s={type:"stream_events",streamId:this.streamId,seq:this.seq++,ts:Date.now(),encoding:"utf8",events:e,protocolVersion:i},n=JSON.stringify(s);t(n)>this.maxMessageSize?await this.publishMultipart(s):await this.publishMessage(s)}trackFlush(t){this.inflightFlushes.push(t),t.then(()=>{const e=this.inflightFlushes.indexOf(t);e>=0&&this.inflightFlushes.splice(e,1)})}async publishMessage(t){let e=null;for(let s=1;s<=3;s++)try{return void await this.pubnub.publish({channel:this.channel,message:t,meta:{sender:this.uuid,protocolVersion:i},storeInHistory:!1,sendByPost:!0})}catch(t){if(e=t,s<3){const t=100*Math.pow(2,s-1);await new Promise(e=>setTimeout(e,t))}}((t,e,s)=>{n("[StreamBundle]",t,e,s)})("error","failed to publish stream message after 3 attempts",{event:"stream_bundle_publish_failed_after_retries",maxAttempts:3,error:e instanceof Error?e.message:String(e)})}async publishMultipart(t){const n=JSON.stringify(t),r=e(n),h=Math.floor(3*(this.maxMessageSize-512)/4),a=Math.ceil(r.length/h),u=`mp-${Date.now()}-${t.seq}`,l=[];for(let e=0;e<a;e++){const n=r.subarray(e*h,(e+1)*h),c={type:t.type,streamId:this.streamId,seq:t.seq,ts:t.ts,multipart:{id:u,part:e+1,total:a},data:s(n),protocolVersion:i};l.push(()=>this.publishMessage(c))}await async function(t,e){const s=new Array(t.length);let i=0;const n=Math.min(e,t.length),r=Array.from({length:n},async()=>{for(;;){const e=i++;if(e>=t.length)return;s[e]=await t[e]()}});return await Promise.all(r),s}(l,4)}clearTimer(){null!==this.flushTimer&&(clearTimeout(this.flushTimer),this.flushTimer=null)}}
@@ -7,15 +7,15 @@
7
7
  *
8
8
  * StreamClient is the public API. StreamBundle is the internal engine.
9
9
  */
10
+ import { type TransportCategory, type TransportStatusPayload } from '../runtime/transport-categories.js';
10
11
  import type { StreamDescriptor } from './descriptor.js';
11
12
  import type { StreamClientOptions, StreamClientFromDescriptorOptions, InboundMessage } from './types.js';
12
13
  /** Reset the UUID counter (for testing). */
13
14
  export declare function _resetUuidCounter(): void;
14
- export declare const FATAL_STREAM_ERROR_CATEGORIES: ReadonlySet<string>;
15
+ export { FATAL_TRANSPORT_CATEGORIES } from '../runtime/transport-categories.js';
15
16
  /**
16
- * Compatibility helper that detects a PubNub status error across the two
17
- * shapes the pinned `pubnub@10.2.x` JS SDK exposes on the subscribe
18
- * listener:
17
+ * Detect a transport status payload that represents an error. The
18
+ * pinned realtime SDK exposes two shapes on the subscribe listener:
19
19
  *
20
20
  * - `Status` — has a required `error: boolean` and `statusCode: number`.
21
21
  * - `StatusEvent` — has an optional polymorphic
@@ -25,34 +25,29 @@ export declare const FATAL_STREAM_ERROR_CATEGORIES: ReadonlySet<string>;
25
25
  * Priority:
26
26
  * 1. Truthy `status.error` (covers `true`, non-empty string, or
27
27
  * category-name string surfaced by `StatusEvent`).
28
- * 2. Numeric `statusCode >= 400` (REST-style errors surfaced via
29
- * `Status`).
30
- * 3. Fatal-category membership as a last resort when the payload is
31
- * sparse (defensive; keeps PAM detection working even if neither of
32
- * the above fields is populated by a future SDK minor release).
28
+ * 2. Numeric `statusCode >= 400` (REST-style errors).
29
+ * 3. Mapped-category membership in FATAL_TRANSPORT_CATEGORIES as a
30
+ * last resort when the payload is sparse (defensive; keeps
31
+ * access-denied detection working even if neither of the above
32
+ * fields is populated by a future SDK minor release).
33
33
  *
34
34
  * Exported so classifier behavior can be unit-tested directly without
35
35
  * instantiating a full `StreamClient`.
36
36
  */
37
- export declare function isStreamStatusError(status: {
38
- error?: unknown;
39
- statusCode?: unknown;
40
- category?: unknown;
41
- } | null | undefined): boolean;
42
- /** True if this category is in the fatal allowlist. */
43
- export declare function isFatalStreamCategory(category: string | undefined | null): boolean;
37
+ export declare function isStreamStatusError(status: TransportStatusPayload | null | undefined): boolean;
38
+ /** True if this neutral category is in the fatal allowlist. */
39
+ export declare function isFatalTransportCategory(category: TransportCategory | undefined | null): boolean;
44
40
  /**
45
41
  * Typed error payload fired to `StreamClient.onError(...)` subscribers.
46
42
  *
47
- * Fires for every PubNub status event classified as an error by
43
+ * Fires for every transport status event classified as an error by
48
44
  * `isStreamStatusError`. Consumers branch on `fatal` for
49
- * must-terminate conditions (PAM revocation, bad grant) and on
50
- * `category` for finer-grained UX.
45
+ * must-terminate conditions and on `category` for finer-grained UX.
51
46
  */
52
47
  export interface StreamError {
53
- /** PubNub status category (e.g., `PNAccessDeniedCategory`). */
54
- category: string;
55
- /** Raw PubNub error data (`errorData` or `error` payload if present). */
48
+ /** Neutral transport category (e.g., `'access_denied'`, `'timeout'`). */
49
+ category: TransportCategory;
50
+ /** Raw error data from the transport (`errorData` or `error` payload if present). */
56
51
  error: any;
57
52
  /** The stream channel the error applies to. */
58
53
  channel: string;
@@ -1 +1 @@
1
- import e from"pubnub";import{StreamBundle as t}from"./stream-bundle.js";import{validateStreamId as r}from"./validate.js";import{getEnv as i}from"../env.js";import{base64ToBytes as n,concatBytes as s,utf8Decode as o,utf8Encode as a}from"./bytes.js";let u=0;export function _resetUuidCounter(){u=0}function d(e,t,r,n){if(void 0!==e)return e;const s=i(t);return void 0!==s&&""!==s?n(s):r}function h(e){return Number(e)}function l(e){const t=e.toLowerCase();return"true"===t||"on"===t||"1"===t}export const FATAL_STREAM_ERROR_CATEGORIES=new Set(["PNAccessDeniedCategory","PNBadRequestCategory"]);export function isStreamStatusError(e){if(!e)return!1;if(e.error)return!0;const t=e.statusCode;if("number"==typeof t&&t>=400)return!0;const r="string"==typeof e.category?e.category:"";return""!==r&&FATAL_STREAM_ERROR_CATEGORIES.has(r)}export function isFatalStreamCategory(e){return"string"==typeof e&&FATAL_STREAM_ERROR_CATEGORIES.has(e)}export class StreamClient{constructor(i){this._isActive=!0,this.endCallbacks=[],this.inboundDoneCallbacks=[],this.inboundDoneFired=!1,this.errorCallbacks=[],this.inboundQueue=[],this.inboundResolve=null,this.inboundDone=!1,this.messageListener=null,this.multipartBuffers=new Map,this.reorderBuffer=new Map,this.reorderTimer=null,this.endSeq=null;const n=i.agentName;if(!n)throw new Error("agentName is required (pass in options)");if(r(i.streamId),this.format=i.format??"bytes","bytes"!==this.format&&"events"!==this.format)throw new Error(`Invalid stream format: "${this.format}". Must be "bytes" or "events".`);this.direction=i.direction??"outbound";const s=i.affinity;null==s?this._affinity="dedicated":"dedicated"===s||"shared"===s?this._affinity=s:(console.warn("[StreamClient] stream_client_invalid_affinity_fallback",{streamId:i.streamId,receivedAffinity:s,fallbackAffinity:"dedicated"}),this._affinity="dedicated"),this.nextExpectedSeq="events"===this.format?1:0,this.reorderTimeoutMs=i.reorderTimeoutMs??750,u++,this._uuid=`${n}-stream-${String(u).padStart(4,"0")}`,this._channel=i.channel??`stream.${n}.${i.streamId}`;const o=d(i.maxMessageSize,"STREAM_MAX_MESSAGE_SIZE",16384,h),a=d(i.bundleSizeBytes,"STREAM_BUNDLE_SIZE",4096,h),c=d(i.maxLatencyMs,"STREAM_MAX_LATENCY_MS",250,h),f=d(i.gating,"STREAM_GATING",!0,l);this.pubnub=new e({subscribeKey:i.subscribeKey,publishKey:i.publishKey,userId:this._uuid,enableEventEngine:!0}),this.pubnub.setToken(i.token),"bidirectional"===this.direction&&this.pubnub.setFilterExpression(`meta.sender != '${this._uuid}'`);if("outbound"===this.direction||"bidirectional"===this.direction){const e={maxMessageSize:o,bundleSizeBytes:a,maxLatencyMs:c,uuid:this._uuid};this.bundle=new t(this.pubnub,this._channel,i.streamId,this.format,e,f)}else this.bundle=null;("inbound"===this.direction||"bidirectional"===this.direction)&&this.setupInbound()}get isActive(){return this._isActive}get channel(){return this._channel}get uuid(){return this._uuid}write(e){if(!this._isActive)throw new Error("Cannot write to an ended stream");if(!this.bundle)throw new Error("Cannot write to an inbound-only stream");this.bundle.write(e)}async end(){if(this._isActive){if(this._isActive=!1,this.bundle)try{await this.bundle.end()}catch(e){console.warn("[StreamClient] bundle.end() failed during end() (continuing teardown):",e)}if("bidirectional"!==this.direction&&this.bundle&&"shared"!==this._affinity)try{await this.bundle.publishEndMarker()}catch(e){console.warn("[StreamClient] publishEndMarker() failed during end() (continuing teardown):",e)}this.inboundDone=!0,this.inboundResolve&&(this.inboundResolve({value:void 0,done:!0}),this.inboundResolve=null),this.fireInboundDone(),this.multipartBuffers.clear(),this.reorderBuffer.clear(),this.reorderTimer&&(clearTimeout(this.reorderTimer),this.reorderTimer=null),this.messageListener&&(this.pubnub.removeListener(this.messageListener),this.messageListener=null),this.pubnub.unsubscribeAll(),this.pubnub.destroy();for(const e of this.endCallbacks)e();this.endCallbacks=[]}}onEnd(e){this.endCallbacks.push(e)}onInboundDone(e){this.inboundDoneFired?e():this.inboundDoneCallbacks.push(e)}fireInboundDone(){if(!this.inboundDoneFired){this.inboundDoneFired=!0;for(const e of this.inboundDoneCallbacks)try{e()}catch{}this.inboundDoneCallbacks=[]}}onError(e){this.errorCallbacks.push(e)}fireError(e){for(const t of[...this.errorCallbacks])try{t(e)}catch(e){console.error("[StreamClient] onError callback raised:",e)}}get inbound(){if("outbound"===this.direction)throw new Error("Cannot read from an outbound-only stream");const e=this;return{[Symbol.asyncIterator]:()=>({next:()=>e.inboundQueue.length>0?Promise.resolve({value:e.inboundQueue.shift(),done:!1}):e.inboundDone?Promise.resolve({value:void 0,done:!0}):new Promise(t=>{e.inboundResolve=t})})}}setupInbound(){this.messageListener={message:e=>{e.channel===this._channel&&this.handleInboundMessage(e.message)},status:e=>{this.handleStatusEvent(e)}},this.pubnub.addListener(this.messageListener),this.pubnub.subscribe({channels:[this._channel],timetoken:1e3})}handleStatusEvent(e){try{if(!isStreamStatusError(e))return;const t="string"==typeof e?.category?e.category:"",r=isFatalStreamCategory(t),i=e?.errorData??e?.error??null,n={category:t,error:i,channel:this._channel,timestamp:Date.now(),fatal:r};console.warn(`[StreamClient] status error: category=${t} fatal=${r} channel=${this._channel}`,i),this.fireError(n),r&&this._isActive&&this.end().catch(e=>{console.error("[StreamClient] forced end() after fatal error raised:",e)})}catch(e){console.error("[StreamClient] status handler raised:",e)}}handleInboundMessage(e){if(!e||"object"!=typeof e)return;const t=e;if("stream_end"===t.type)return this.reorderTimeoutMs<=0?(this.inboundDone=!0,this.inboundResolve&&(this.inboundResolve({value:void 0,done:!0}),this.inboundResolve=null),void this.fireInboundDone()):"number"==typeof t.seq&&Number.isInteger(t.seq)?(this.endSeq=t.seq,void(this.nextExpectedSeq>=this.endSeq?this.checkEndReached():this.startReorderTimer())):void console.warn("[StreamClient] stream_end missing numeric seq field; ignoring in reorder mode");if(t.multipart&&"object"==typeof t.multipart){const e=t.multipart;return void this.handleMultipartPart(t,e)}const r=this.normalizeMessage(t);r&&this.enqueueReordered(r)}handleMultipartPart(e,t){if(!function(e){return!("string"!=typeof e.id||""===e.id||"number"!=typeof e.part||!Number.isInteger(e.part)||e.part<1||"number"!=typeof e.total||!Number.isInteger(e.total)||e.total<2||e.part>e.total)}(t))return;if("string"!=typeof e.data)return;this.evictStaleGroups();const r=e.seq,i=e.type,a=e.streamId,u=e.data;let d=this.multipartBuffers.get(t.id);if(d){if(d.total!==t.total||d.seq!==r||d.type!==i||d.streamId!==a)return void this.dropMultipartGroup(t.id);const e=d.parts.get(t.part);if(void 0!==e){if(e===u)return;return void this.dropMultipartGroup(t.id)}}else d={total:t.total,parts:new Map,seq:r,ts:e.ts,type:i,streamId:a,createdAt:Date.now()},this.multipartBuffers.set(t.id,d);if(d.parts.set(t.part,u),d.parts.size===d.total){this.dropMultipartGroup(t.id);for(let e=1;e<=d.total;e++)if(!d.parts.has(e))return;try{const e=[];for(let t=1;t<=d.total;t++)e.push(n(d.parts.get(t)));const t=s(e),r=JSON.parse(o(t)),i=this.normalizeMessage(r);i&&this.enqueueReordered(i)}catch{}}}dropMultipartGroup(e){this.multipartBuffers.delete(e)}evictStaleGroups(){const e=Date.now();for(const[t,r]of this.multipartBuffers)e-r.createdAt>3e4&&this.multipartBuffers.delete(t);if(this.multipartBuffers.size>64){const e=[...this.multipartBuffers.entries()].sort((e,t)=>e[1].createdAt-t[1].createdAt),t=e.length-64;for(let r=0;r<t;r++)this.multipartBuffers.delete(e[r][0])}}normalizeMessage(e){const t=e.type;if("stream_data"===t){if("number"!=typeof e.seq||!Number.isInteger(e.seq))return null;const t=e.chunks;if(!Array.isArray(t)||0===t.length||!t.every(e=>"string"==typeof e))return null;const r=e.encoding;return void 0!==r&&"utf8"!==r&&"base64"!==r?null:{data:t,seq:e.seq,ts:e.ts,format:"bytes",encoding:r??"utf8"}}if("stream_events"===t){if("number"!=typeof e.seq||!Number.isInteger(e.seq))return null;const t=e.events;return Array.isArray(t)&&0!==t.length?{data:t,seq:e.seq,ts:e.ts,format:"events",encoding:"utf8"}:null}return"number"==typeof e.seq&&Number.isInteger(e.seq)?{data:e,seq:e.seq,ts:e.ts??Date.now(),format:"raw",encoding:"utf8"}:null}enqueueInbound(e){if(this.inboundResolve){const t=this.inboundResolve;this.inboundResolve=null,t({value:e,done:!1})}else this.inboundQueue.push(e)}enqueueReordered(e){if(this.reorderTimeoutMs<=0)return void this.enqueueInbound(e);const t=e.seq;null!==this.endSeq&&t>=this.endSeq||t<this.nextExpectedSeq||this.reorderBuffer.has(t)||(t===this.nextExpectedSeq?(this.enqueueInbound(e),this.nextExpectedSeq++,this.flushConsecutive(),this.cancelAndRestartTimerIfNeeded(),this.checkEndReached()):(this.reorderBuffer.set(t,e),this.startReorderTimer()))}flushConsecutive(){for(;this.reorderBuffer.has(this.nextExpectedSeq);){const e=this.reorderBuffer.get(this.nextExpectedSeq);this.reorderBuffer.delete(this.nextExpectedSeq),this.enqueueInbound(e),this.nextExpectedSeq++}}startReorderTimer(){this.reorderTimer||(this.reorderTimer=setTimeout(()=>{if(this.reorderTimer=null,this.reorderBuffer.size>0){let e=1/0;for(const t of this.reorderBuffer.keys())t<e&&(e=t);this.nextExpectedSeq=e,this.flushConsecutive(),this.cancelAndRestartTimerIfNeeded(),this.checkEndReached()}else null!==this.endSeq&&this.nextExpectedSeq<this.endSeq&&(this.nextExpectedSeq=this.endSeq,this.checkEndReached())},this.reorderTimeoutMs))}cancelAndRestartTimerIfNeeded(){this.reorderTimer&&(clearTimeout(this.reorderTimer),this.reorderTimer=null),this.reorderBuffer.size>0&&this.startReorderTimer()}checkEndReached(){null!==this.endSeq&&this.nextExpectedSeq>=this.endSeq&&(this.reorderBuffer.size>0&&(console.warn(`[StreamClient] reorder buffer not empty at stream end (${this.reorderBuffer.size} unexpected messages with seq >= ${this.endSeq})`),this.reorderBuffer.clear()),this.reorderTimer&&(clearTimeout(this.reorderTimer),this.reorderTimer=null),this.inboundDone=!0,this.inboundResolve&&(this.inboundResolve({value:void 0,done:!0}),this.inboundResolve=null),this.fireInboundDone())}async*bytes(){for await(const e of this.inbound){const t=Array.isArray(e.data)?e.data:[e.data];for(const r of t)yield"base64"===e.encoding?n(r):a(r)}}async*events(){for await(const e of this.inbound){const t=Array.isArray(e.data)?e.data:[e.data];for(const e of t)yield e}}async readable(){const{Readable:e}=await import("node:stream"),t=this.bytes()[Symbol.asyncIterator]();return new e({async read(){try{const{value:e,done:r}=await t.next();r?this.push(null):this.push(e)}catch{this.push(null)}}})}static fromDescriptor(e,t){const r=e.localDirection,i="outbound"===r||"bidirectional"===r,n=void 0!==t.gating?t.gating:!i;return new StreamClient({subscribeKey:t.subscribeKey,publishKey:t.publishKey,token:e.token,agentName:e.agentName,streamId:e.streamId,channel:e.channel,direction:e.localDirection,format:e.format,affinity:e.affinity,maxMessageSize:t.maxMessageSize,bundleSizeBytes:t.bundleSizeBytes,maxLatencyMs:t.maxLatencyMs,gating:n,reorderTimeoutMs:t.reorderTimeoutMs})}}
1
+ import e from"pubnub";import{buildPubNubLogConfig as t}from"../runtime/pubnub-client.js";import{StreamBundle as r}from"./stream-bundle.js";import{validateStreamId as i}from"./validate.js";import{getEnv as n}from"../env.js";import{log as s}from"../runtime/logger.js";import{mapTransportCategory as o,FATAL_TRANSPORT_CATEGORIES as a}from"../runtime/transport-categories.js";import{base64ToBytes as d,concatBytes as u,utf8Decode as h,utf8Encode as l}from"./bytes.js";const c=(e,t,r)=>s("[StreamClient]",e,t,r);let f=0;export function _resetUuidCounter(){f=0}function m(e,t,r,i){if(void 0!==e)return e;const s=n(t);return void 0!==s&&""!==s?i(s):r}function b(e){return Number(e)}function p(e){const t=e.toLowerCase();return"true"===t||"on"===t||"1"===t}export{FATAL_TRANSPORT_CATEGORIES}from"../runtime/transport-categories.js";export function isStreamStatusError(e){if(!e)return!1;if(e.error)return!0;const t=e.statusCode;return"number"==typeof t&&t>=400||!("string"!=typeof e.category||!e.category)&&a.has(o(e))}export function isFatalTransportCategory(e){return null!=e&&a.has(e)}export class StreamClient{constructor(n){this._isActive=!0,this.endCallbacks=[],this.inboundDoneCallbacks=[],this.inboundDoneFired=!1,this.errorCallbacks=[],this.inboundQueue=[],this.inboundResolve=null,this.inboundDone=!1,this.messageListener=null,this.multipartBuffers=new Map,this.reorderBuffer=new Map,this.reorderTimer=null,this.endSeq=null;const s=n.agentName;if(!s)throw new Error("agentName is required (pass in options)");if(i(n.streamId),this.format=n.format??"bytes","bytes"!==this.format&&"events"!==this.format)throw new Error(`Invalid stream format: "${this.format}". Must be "bytes" or "events".`);this.direction=n.direction??"outbound";const o=n.affinity;null==o?this._affinity="dedicated":"dedicated"===o||"shared"===o?this._affinity=o:(c("warn","invalid affinity in stream options — falling back to dedicated",{event:"stream_client_invalid_affinity_fallback",streamId:n.streamId,receivedAffinity:o,fallbackAffinity:"dedicated"}),this._affinity="dedicated"),this.nextExpectedSeq="events"===this.format?1:0,this.reorderTimeoutMs=n.reorderTimeoutMs??750,f++,this._uuid=`${s}-stream-${String(f).padStart(4,"0")}`,this._channel=n.channel??`stream.${s}.${n.streamId}`;const a=m(n.maxMessageSize,"STREAM_MAX_MESSAGE_SIZE",16384,b),d=m(n.bundleSizeBytes,"STREAM_BUNDLE_SIZE",4096,b),u=m(n.maxLatencyMs,"STREAM_MAX_LATENCY_MS",250,b),h=m(n.gating,"STREAM_GATING",!0,p);this.pubnub=new e({subscribeKey:n.subscribeKey,publishKey:n.publishKey,userId:this._uuid,enableEventEngine:!0,...t()}),this.pubnub.setToken(n.token),"bidirectional"===this.direction&&this.pubnub.setFilterExpression(`meta.sender != '${this._uuid}'`);if("outbound"===this.direction||"bidirectional"===this.direction){const e={maxMessageSize:a,bundleSizeBytes:d,maxLatencyMs:u,uuid:this._uuid};this.bundle=new r(this.pubnub,this._channel,n.streamId,this.format,e,h)}else this.bundle=null;("inbound"===this.direction||"bidirectional"===this.direction)&&this.setupInbound()}get isActive(){return this._isActive}get channel(){return this._channel}get uuid(){return this._uuid}write(e){if(!this._isActive)throw new Error("Cannot write to an ended stream");if(!this.bundle)throw new Error("Cannot write to an inbound-only stream");this.bundle.write(e)}async end(){if(this._isActive){if(this._isActive=!1,this.bundle)try{await this.bundle.end()}catch(e){c("warn","bundle.end() failed during end() — continuing teardown",{event:"stream_client_bundle_end_failed",error:e instanceof Error?e.message:String(e)})}if("bidirectional"!==this.direction&&this.bundle&&"shared"!==this._affinity)try{await this.bundle.publishEndMarker()}catch(e){c("warn","publishEndMarker() failed during end() — continuing teardown",{event:"stream_client_publish_end_marker_failed",error:e instanceof Error?e.message:String(e)})}this.inboundDone=!0,this.inboundResolve&&(this.inboundResolve({value:void 0,done:!0}),this.inboundResolve=null),this.fireInboundDone(),this.multipartBuffers.clear(),this.reorderBuffer.clear(),this.reorderTimer&&(clearTimeout(this.reorderTimer),this.reorderTimer=null),this.messageListener&&(this.pubnub.removeListener(this.messageListener),this.messageListener=null),this.pubnub.unsubscribeAll(),this.pubnub.destroy();for(const e of this.endCallbacks)e();this.endCallbacks=[]}}onEnd(e){this.endCallbacks.push(e)}onInboundDone(e){this.inboundDoneFired?e():this.inboundDoneCallbacks.push(e)}fireInboundDone(){if(!this.inboundDoneFired){this.inboundDoneFired=!0;for(const e of this.inboundDoneCallbacks)try{e()}catch{}this.inboundDoneCallbacks=[]}}onError(e){this.errorCallbacks.push(e)}fireError(e){for(const t of[...this.errorCallbacks])try{t(e)}catch(e){c("error","onError callback raised",{event:"stream_client_on_error_callback_raised",error:e instanceof Error?e.message:String(e)})}}get inbound(){if("outbound"===this.direction)throw new Error("Cannot read from an outbound-only stream");const e=this;return{[Symbol.asyncIterator]:()=>({next:()=>e.inboundQueue.length>0?Promise.resolve({value:e.inboundQueue.shift(),done:!1}):e.inboundDone?Promise.resolve({value:void 0,done:!0}):new Promise(t=>{e.inboundResolve=t})})}}setupInbound(){this.messageListener={message:e=>{e.channel===this._channel&&this.handleInboundMessage(e.message)},status:e=>{this.handleStatusEvent(e)}},this.pubnub.addListener(this.messageListener),this.pubnub.subscribe({channels:[this._channel],timetoken:1e3})}handleStatusEvent(e){try{if(!isStreamStatusError(e))return;const t=o(e),r=isFatalTransportCategory(t),i=e?.errorData??e?.error??null,n={category:t,error:i,channel:this._channel,timestamp:Date.now(),fatal:r};c("warn",`stream status error: category=${t} fatal=${r} channel=${this._channel}`,{event:"stream_client_status_error",category:t,fatal:r,channel:this._channel,rawError:i}),this.fireError(n),r&&this._isActive&&this.end().catch(e=>{c("error","forced end() after fatal error raised",{event:"stream_client_forced_end_after_fatal_raised",error:e instanceof Error?e.message:String(e)})})}catch(e){c("error","stream status handler raised",{event:"stream_client_status_handler_raised",error:e instanceof Error?e.message:String(e)})}}handleInboundMessage(e){if(!e||"object"!=typeof e)return;const t=e;if("stream_end"===t.type)return this.reorderTimeoutMs<=0?(this.inboundDone=!0,this.inboundResolve&&(this.inboundResolve({value:void 0,done:!0}),this.inboundResolve=null),void this.fireInboundDone()):"number"==typeof t.seq&&Number.isInteger(t.seq)?(this.endSeq=t.seq,void(this.nextExpectedSeq>=this.endSeq?this.checkEndReached():this.startReorderTimer())):void c("warn","stream_end missing numeric seq field — ignoring in reorder mode",{event:"stream_client_stream_end_missing_seq"});if(t.multipart&&"object"==typeof t.multipart){const e=t.multipart;return void this.handleMultipartPart(t,e)}const r=this.normalizeMessage(t);r&&this.enqueueReordered(r)}handleMultipartPart(e,t){if(!function(e){return!("string"!=typeof e.id||""===e.id||"number"!=typeof e.part||!Number.isInteger(e.part)||e.part<1||"number"!=typeof e.total||!Number.isInteger(e.total)||e.total<2||e.part>e.total)}(t))return;if("string"!=typeof e.data)return;this.evictStaleGroups();const r=e.seq,i=e.type,n=e.streamId,s=e.data;let o=this.multipartBuffers.get(t.id);if(o){if(o.total!==t.total||o.seq!==r||o.type!==i||o.streamId!==n)return void this.dropMultipartGroup(t.id);const e=o.parts.get(t.part);if(void 0!==e){if(e===s)return;return void this.dropMultipartGroup(t.id)}}else o={total:t.total,parts:new Map,seq:r,ts:e.ts,type:i,streamId:n,createdAt:Date.now()},this.multipartBuffers.set(t.id,o);if(o.parts.set(t.part,s),o.parts.size===o.total){this.dropMultipartGroup(t.id);for(let e=1;e<=o.total;e++)if(!o.parts.has(e))return;try{const e=[];for(let t=1;t<=o.total;t++)e.push(d(o.parts.get(t)));const t=u(e),r=JSON.parse(h(t)),i=this.normalizeMessage(r);i&&this.enqueueReordered(i)}catch{}}}dropMultipartGroup(e){this.multipartBuffers.delete(e)}evictStaleGroups(){const e=Date.now();for(const[t,r]of this.multipartBuffers)e-r.createdAt>3e4&&this.multipartBuffers.delete(t);if(this.multipartBuffers.size>64){const e=[...this.multipartBuffers.entries()].sort((e,t)=>e[1].createdAt-t[1].createdAt),t=e.length-64;for(let r=0;r<t;r++)this.multipartBuffers.delete(e[r][0])}}normalizeMessage(e){const t=e.type;if("stream_data"===t){if("number"!=typeof e.seq||!Number.isInteger(e.seq))return null;const t=e.chunks;if(!Array.isArray(t)||0===t.length||!t.every(e=>"string"==typeof e))return null;const r=e.encoding;return void 0!==r&&"utf8"!==r&&"base64"!==r?null:{data:t,seq:e.seq,ts:e.ts,format:"bytes",encoding:r??"utf8"}}if("stream_events"===t){if("number"!=typeof e.seq||!Number.isInteger(e.seq))return null;const t=e.events;return Array.isArray(t)&&0!==t.length?{data:t,seq:e.seq,ts:e.ts,format:"events",encoding:"utf8"}:null}return"number"==typeof e.seq&&Number.isInteger(e.seq)?{data:e,seq:e.seq,ts:e.ts??Date.now(),format:"raw",encoding:"utf8"}:null}enqueueInbound(e){if(this.inboundResolve){const t=this.inboundResolve;this.inboundResolve=null,t({value:e,done:!1})}else this.inboundQueue.push(e)}enqueueReordered(e){if(this.reorderTimeoutMs<=0)return void this.enqueueInbound(e);const t=e.seq;null!==this.endSeq&&t>=this.endSeq||t<this.nextExpectedSeq||this.reorderBuffer.has(t)||(t===this.nextExpectedSeq?(this.enqueueInbound(e),this.nextExpectedSeq++,this.flushConsecutive(),this.cancelAndRestartTimerIfNeeded(),this.checkEndReached()):(this.reorderBuffer.set(t,e),this.startReorderTimer()))}flushConsecutive(){for(;this.reorderBuffer.has(this.nextExpectedSeq);){const e=this.reorderBuffer.get(this.nextExpectedSeq);this.reorderBuffer.delete(this.nextExpectedSeq),this.enqueueInbound(e),this.nextExpectedSeq++}}startReorderTimer(){this.reorderTimer||(this.reorderTimer=setTimeout(()=>{if(this.reorderTimer=null,this.reorderBuffer.size>0){let e=1/0;for(const t of this.reorderBuffer.keys())t<e&&(e=t);this.nextExpectedSeq=e,this.flushConsecutive(),this.cancelAndRestartTimerIfNeeded(),this.checkEndReached()}else null!==this.endSeq&&this.nextExpectedSeq<this.endSeq&&(this.nextExpectedSeq=this.endSeq,this.checkEndReached())},this.reorderTimeoutMs))}cancelAndRestartTimerIfNeeded(){this.reorderTimer&&(clearTimeout(this.reorderTimer),this.reorderTimer=null),this.reorderBuffer.size>0&&this.startReorderTimer()}checkEndReached(){null!==this.endSeq&&this.nextExpectedSeq>=this.endSeq&&(this.reorderBuffer.size>0&&(c("warn",`reorder buffer not empty at stream end (${this.reorderBuffer.size} unexpected messages with seq >= ${this.endSeq})`,{event:"stream_client_reorder_buffer_not_empty_at_end",unexpectedCount:this.reorderBuffer.size,endSeq:this.endSeq}),this.reorderBuffer.clear()),this.reorderTimer&&(clearTimeout(this.reorderTimer),this.reorderTimer=null),this.inboundDone=!0,this.inboundResolve&&(this.inboundResolve({value:void 0,done:!0}),this.inboundResolve=null),this.fireInboundDone())}async*bytes(){for await(const e of this.inbound){const t=Array.isArray(e.data)?e.data:[e.data];for(const r of t)yield"base64"===e.encoding?d(r):l(r)}}async*events(){for await(const e of this.inbound){const t=Array.isArray(e.data)?e.data:[e.data];for(const e of t)yield e}}async readable(){const{Readable:e}=await import("node:stream"),t=this.bytes()[Symbol.asyncIterator]();return new e({async read(){try{const{value:e,done:r}=await t.next();r?this.push(null):this.push(e)}catch{this.push(null)}}})}static fromDescriptor(e,t){const r=e.localDirection,i="outbound"===r||"bidirectional"===r,n=void 0!==t.gating?t.gating:!i;return new StreamClient({subscribeKey:t.subscribeKey,publishKey:t.publishKey,token:e.token,agentName:t.consumerUserId||e.agentName,streamId:e.streamId,channel:e.channel,direction:e.localDirection,format:e.format,affinity:e.affinity,maxMessageSize:t.maxMessageSize,bundleSizeBytes:t.bundleSizeBytes,maxLatencyMs:t.maxLatencyMs,gating:n,reorderTimeoutMs:t.reorderTimeoutMs})}}
@@ -64,6 +64,16 @@ export interface StreamClientFromDescriptorOptions {
64
64
  gating?: boolean;
65
65
  /** Reorder buffer timeout in ms. Default: 750. Set to 0 to disable. */
66
66
  reorderTimeoutMs?: number;
67
+ /**
68
+ * Consumer's user ID. When set, used as the publisher-identity prefix in
69
+ * the StreamClient UUID so that consumer-side publishes carry an identity
70
+ * derived from the consumer rather than the provider's agentName. Required
71
+ * for correct self-echo filtering on bidirectional streams where the
72
+ * consumer and provider share the same agentName. Omitting it falls back
73
+ * to descriptor.agentName (legacy behavior — safe only for unidirectional
74
+ * streams).
75
+ */
76
+ consumerUserId?: string;
67
77
  }
68
78
  /** Normalized inbound message yielded by the inbound async iterator. */
69
79
  export interface InboundMessage {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blocks-network/sdk",
3
- "version": "0.1.60",
3
+ "version": "0.1.62",
4
4
  "type": "module",
5
5
  "description": "Blocks Network SDK",
6
6
  "exports": {
@@ -31,7 +31,7 @@
31
31
  "LICENSE"
32
32
  ],
33
33
  "engines": {
34
- "node": ">=20.0.0"
34
+ "node": ">=22.0.0"
35
35
  },
36
36
  "scripts": {
37
37
  "build": "tsc",