@cortexkit/subc-client 0.5.0 → 0.8.1

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/src/provider.ts CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  type ReconnectBackoff,
8
8
  type RequestOptions,
9
9
  type RouteTarget,
10
+ type SubcCallErrorKind,
10
11
  } from "./client.js";
11
12
  import { ConnectionFileError, readConnectionFile, type ConnectionInfo } from "./connection-file.js";
12
13
  import {
@@ -33,6 +34,7 @@ import {
33
34
  SocketWriteNotQueuedError,
34
35
  SocketWriteQueuedError,
35
36
  SubcSocket,
37
+ writeBorrowed,
36
38
  } from "./socket.js";
37
39
 
38
40
  const DEFAULT_HANDSHAKE_TIMEOUT_MS = 10_000;
@@ -53,7 +55,6 @@ export type ObservabilityKind = "snapshot" | "stream";
53
55
  export type InternalTransport = "bulk";
54
56
  export type StorageKind = "sqlite";
55
57
  export type StorageScope = "project";
56
- export type LeaseScope = "project";
57
58
  export type HealthStatus = "ok" | "degraded" | "failing";
58
59
 
59
60
  export interface HealthReport {
@@ -69,7 +70,6 @@ export interface ManifestInput {
69
70
  trust_tier: TrustTier;
70
71
  provides: ProviderRoleInput[];
71
72
  consumes: ConsumerRoleInput[];
72
- scheduled_tasks: ScheduledTaskInput[];
73
73
  bindings: BindingsInput;
74
74
  }
75
75
 
@@ -97,6 +97,7 @@ export type ProviderRoleInput =
97
97
  config_schema: unknown;
98
98
  observability: ObservabilitySurfaceInput[];
99
99
  identity_scope: IdentityScope[];
100
+ concurrency: Concurrency;
100
101
  }
101
102
  | {
102
103
  role: "internal_service";
@@ -133,31 +134,6 @@ export type ConsumerRoleInput =
133
134
  | { role: "llm_client"; via: string; auth: string }
134
135
  | { role: "service_client"; of: string[] };
135
136
 
136
- export interface ScheduledTaskInput {
137
- task_id: string;
138
- eligibility: TaskEligibilityInput;
139
- lease_scope: LeaseScope;
140
- renews_during_calls: boolean;
141
- toolset: string[];
142
- model_policy: ModelPolicyInput;
143
- step_cap: number;
144
- circuit_breaker: CircuitBreakerInput;
145
- }
146
-
147
- export interface TaskEligibilityInput {
148
- cooldown: string;
149
- window: string;
150
- }
151
-
152
- export interface ModelPolicyInput {
153
- tier: string;
154
- fallback_chain: string[];
155
- }
156
-
157
- export interface CircuitBreakerInput {
158
- identical_failures: number;
159
- }
160
-
161
137
  export interface BindingsInput {
162
138
  storage: StorageBindingInput;
163
139
  vault_grants: VaultGrantInput[];
@@ -185,6 +161,7 @@ export interface ManagementSurfaceManifestOptions {
185
161
  moduleId: string;
186
162
  operations: Array<string | ManagementOperationInput>;
187
163
  moduleVersion?: string;
164
+ concurrency?: Concurrency;
188
165
  }
189
166
 
190
167
  /**
@@ -374,6 +351,7 @@ export class SubcProviderError extends Error {
374
351
  constructor(
375
352
  message: string,
376
353
  readonly code?: string,
354
+ readonly kind: SubcCallErrorKind = "terminal",
377
355
  ) {
378
356
  super(message);
379
357
  }
@@ -398,10 +376,10 @@ export function managementSurfaceManifest(opts: ManagementSurfaceManifestOptions
398
376
  config_schema: { type: "object" },
399
377
  observability: [],
400
378
  identity_scope: [],
379
+ concurrency: opts.concurrency ?? "module_managed",
401
380
  },
402
381
  ],
403
382
  consumes: [],
404
- scheduled_tasks: [],
405
383
  bindings: {
406
384
  storage: {
407
385
  kind: "sqlite",
@@ -545,22 +523,22 @@ export class SubcProvider {
545
523
 
546
524
  cancel(handle: RouteHandle, corr: bigint): void {
547
525
  this.assertLiveHandle(handle);
548
- void this.sendOn(
526
+ this.sendOn(
549
527
  this.sock,
550
528
  this.generation,
551
529
  buildFrame(FrameType.Cancel, controlFlags(), handle.channel, handle.epoch, corr, new Uint8Array(0)),
552
- );
530
+ ).catch(() => undefined);
553
531
  }
554
532
 
555
533
  closeRoute(handle: RouteHandle): void {
556
534
  this.assertLiveHandle(handle);
557
535
  this.liveRoutes.delete(handle.channel);
558
536
  this.abortHandle(handle);
559
- void this.sendOn(
537
+ this.sendOn(
560
538
  this.sock,
561
539
  this.generation,
562
540
  buildFrame(FrameType.Goodbye, controlFlags(), handle.channel, handle.epoch, 0n, new Uint8Array(0)),
563
- );
541
+ ).catch(() => undefined);
564
542
  }
565
543
 
566
544
  /** Read the connection file, authenticate as a client, register the manifest with HELLO, and serve frames. */
@@ -888,8 +866,15 @@ export class SubcProvider {
888
866
  const releasePermit = await (this.requestGate ?? new AsyncPermitPool(DEFAULT_PROVIDER_HANDLER_CAPACITY)).acquire();
889
867
  const dataFlags = buildFlags(false, Priority.Interactive, false);
890
868
  try {
869
+ if (controller.signal.aborted) {
870
+ await this.sendError(frame, "cancelled", "request cancelled", dataFlags, sock, generation);
871
+ return;
872
+ }
891
873
  const body = await this.opts.handler(handle, frame.body, context);
892
- if (controller.signal.aborted) return;
874
+ if (controller.signal.aborted) {
875
+ await this.sendError(frame, "cancelled", "request cancelled", dataFlags, sock, generation);
876
+ return;
877
+ }
893
878
  this.assertLiveHandle(handle);
894
879
  if (body === undefined) {
895
880
  await this.sendOn(
@@ -923,7 +908,11 @@ export class SubcProvider {
923
908
  throw new SubcProviderError("provider handler must return a Uint8Array or void", "invalid_handler_response");
924
909
  }
925
910
  } catch (error) {
926
- if (error instanceof StaleRouteHandleError || controller.signal.aborted) return;
911
+ if (error instanceof StaleRouteHandleError) return;
912
+ if (controller.signal.aborted) {
913
+ await this.sendError(frame, "cancelled", "request cancelled", dataFlags, sock, generation);
914
+ return;
915
+ }
927
916
  await this.sendError(
928
917
  frame,
929
918
  error instanceof SubcProviderError && error.code ? error.code : "handler_error",
@@ -988,6 +977,10 @@ export class SubcProvider {
988
977
  sock: SubcSocket,
989
978
  generation: number,
990
979
  ): Promise<void> {
980
+ if (frame.header.channel !== 0) {
981
+ const handle = this.liveRoutes.get(frame.header.channel);
982
+ if (!handle || handle.epoch !== frame.header.epoch || !this.isLiveHandle(handle)) return;
983
+ }
991
984
  await this.sendOn(
992
985
  sock,
993
986
  generation,
@@ -1230,7 +1223,11 @@ function routeKey(handle: RouteHandle, corr: bigint): string {
1230
1223
  function providerErrorFromFrame(frame: Frame): SubcProviderError {
1231
1224
  try {
1232
1225
  const body = parseJson(frame.body) as { code?: string; message?: string };
1233
- return new SubcProviderError(body.message ?? "subc error", body.code);
1226
+ return new SubcProviderError(
1227
+ body.message ?? "subc error",
1228
+ body.code,
1229
+ body.code === "stale_route_epoch" || body.code === "unknown_channel" ? "not_sent" : "terminal",
1230
+ );
1234
1231
  } catch {
1235
1232
  return new SubcProviderError(Buffer.from(frame.body).toString("utf8") || "subc error");
1236
1233
  }
@@ -1324,7 +1321,7 @@ function controlFlags(): number {
1324
1321
  }
1325
1322
 
1326
1323
  async function sendFrame(sock: SubcSocket, frame: Frame): Promise<void> {
1327
- await sock.write(encodeFrame(frame), Date.now() + WRITE_TIMEOUT_MS);
1324
+ await writeBorrowed(sock, encodeFrame(frame), Date.now() + WRITE_TIMEOUT_MS);
1328
1325
  }
1329
1326
 
1330
1327
  async function expectHelloAck(sock: SubcSocket, deadline: number): Promise<ModuleHelloAckBody> {
@@ -1387,7 +1384,6 @@ function normalizeManifest(manifest: ManifestInput): ManifestInput {
1387
1384
  trust_tier: manifest.trust_tier,
1388
1385
  provides: manifest.provides.map(normalizeProviderRole),
1389
1386
  consumes: manifest.consumes.map(normalizeConsumerRole),
1390
- scheduled_tasks: manifest.scheduled_tasks.map(normalizeScheduledTask),
1391
1387
  bindings: {
1392
1388
  storage: {
1393
1389
  kind: manifest.bindings.storage.kind,
@@ -1448,6 +1444,7 @@ function normalizeProviderRole(role: ProviderRoleInput): ProviderRoleInput {
1448
1444
  kind: surface.kind,
1449
1445
  })),
1450
1446
  identity_scope: [...role.identity_scope],
1447
+ concurrency: role.concurrency,
1451
1448
  };
1452
1449
  case "internal_service":
1453
1450
  return {
@@ -1471,23 +1468,3 @@ function normalizeConsumerRole(role: ConsumerRoleInput): ConsumerRoleInput {
1471
1468
  }
1472
1469
  }
1473
1470
 
1474
- function normalizeScheduledTask(task: ScheduledTaskInput): ScheduledTaskInput {
1475
- return {
1476
- task_id: task.task_id,
1477
- eligibility: {
1478
- cooldown: task.eligibility.cooldown,
1479
- window: task.eligibility.window,
1480
- },
1481
- lease_scope: task.lease_scope,
1482
- renews_during_calls: task.renews_during_calls,
1483
- toolset: [...task.toolset],
1484
- model_policy: {
1485
- tier: task.model_policy.tier,
1486
- fallback_chain: [...task.model_policy.fallback_chain],
1487
- },
1488
- step_cap: task.step_cap,
1489
- circuit_breaker: {
1490
- identical_failures: task.circuit_breaker.identical_failures,
1491
- },
1492
- };
1493
- }
package/src/socket.ts CHANGED
@@ -44,6 +44,28 @@ export interface SocketWriteResult {
44
44
  completed: Promise<void>;
45
45
  }
46
46
 
47
+ export function toWriteBuffer(bytes: Uint8Array): Buffer {
48
+ // Internal frame writes pass encodeFrame's fresh, single-use Uint8Array.
49
+ // Authentication writes likewise use fresh prefix/JSON storage and await each
50
+ // write without mutating it, so this view remains stable while Node drains it.
51
+ return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
52
+ }
53
+
54
+ const WRITE_BORROWED = Symbol("subc.socket.writeBorrowed");
55
+ const WRITE_TRACKED_BORROWED = Symbol("subc.socket.writeTrackedBorrowed");
56
+
57
+ /** @internal Write fresh, single-use internal storage without copying it. */
58
+ export function writeBorrowed(socket: SubcSocket, bytes: Uint8Array, deadlineMs: number): Promise<void> {
59
+ const borrowed = socket[WRITE_BORROWED];
60
+ return borrowed ? borrowed.call(socket, bytes, deadlineMs) : socket.write(bytes, deadlineMs);
61
+ }
62
+
63
+ /** @internal Track a write of fresh, single-use internal storage without copying it. */
64
+ export function writeTrackedBorrowed(socket: SubcSocket, bytes: Uint8Array, deadlineMs: number): SocketWriteResult {
65
+ const borrowed = socket[WRITE_TRACKED_BORROWED];
66
+ return borrowed ? borrowed.call(socket, bytes, deadlineMs) : socket.writeTracked(bytes, deadlineMs);
67
+ }
68
+
47
69
  interface Waiter {
48
70
  need: number;
49
71
  resolve: (bytes: Uint8Array) => void;
@@ -174,8 +196,24 @@ export class SubcSocket {
174
196
  }
175
197
 
176
198
  async write(bytes: Uint8Array, deadlineMs: number): Promise<void> {
199
+ await this.writeBuffer(Buffer.from(bytes), deadlineMs);
200
+ }
201
+
202
+ writeTracked(bytes: Uint8Array, deadlineMs: number): SocketWriteResult {
203
+ return this.writeTrackedBuffer(Buffer.from(bytes), deadlineMs);
204
+ }
205
+
206
+ async [WRITE_BORROWED](bytes: Uint8Array, deadlineMs: number): Promise<void> {
207
+ await this.writeBuffer(toWriteBuffer(bytes), deadlineMs);
208
+ }
209
+
210
+ [WRITE_TRACKED_BORROWED](bytes: Uint8Array, deadlineMs: number): SocketWriteResult {
211
+ return this.writeTrackedBuffer(toWriteBuffer(bytes), deadlineMs);
212
+ }
213
+
214
+ private async writeBuffer(buffer: Buffer, deadlineMs: number): Promise<void> {
177
215
  try {
178
- await this.writeTracked(bytes, deadlineMs).completed;
216
+ await this.writeTrackedBuffer(buffer, deadlineMs).completed;
179
217
  } catch (err) {
180
218
  if (err instanceof SocketWriteNotQueuedError || err instanceof SocketWriteQueuedError) {
181
219
  throw err.cause ?? err;
@@ -184,7 +222,7 @@ export class SubcSocket {
184
222
  }
185
223
  }
186
224
 
187
- writeTracked(bytes: Uint8Array, deadlineMs: number): SocketWriteResult {
225
+ private writeTrackedBuffer(buffer: Buffer, deadlineMs: number): SocketWriteResult {
188
226
  if (this.closedErr) {
189
227
  return {
190
228
  queued: false,
@@ -230,7 +268,7 @@ export class SubcSocket {
230
268
  }, remaining);
231
269
 
232
270
  try {
233
- this.sock.write(Buffer.from(bytes), (err) => {
271
+ this.sock.write(buffer, (err) => {
234
272
  settle(() => {
235
273
  if (err) {
236
274
  reject(