@cortexkit/subc-client 0.4.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +17 -5
- package/dist/auth.js.map +1 -1
- package/dist/client.d.ts +57 -3
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +168 -14
- package/dist/client.js.map +1 -1
- package/dist/connection-file.d.ts.map +1 -1
- package/dist/connection-file.js +11 -1
- package/dist/connection-file.js.map +1 -1
- package/dist/envelope.d.ts.map +1 -1
- package/dist/envelope.js +36 -1
- package/dist/envelope.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/provider.d.ts +8 -25
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +123 -53
- package/dist/provider.js.map +1 -1
- package/dist/socket.d.ts +12 -0
- package/dist/socket.d.ts.map +1 -1
- package/dist/socket.js +33 -3
- package/dist/socket.js.map +1 -1
- package/package.json +1 -1
- package/src/auth.ts +21 -9
- package/src/client.ts +210 -20
- package/src/connection-file.ts +15 -1
- package/src/envelope.ts +54 -1
- package/src/index.ts +1 -5
- package/src/provider.ts +153 -86
- package/src/socket.ts +41 -3
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
|
/**
|
|
@@ -312,6 +289,13 @@ interface OpenedProviderConnection {
|
|
|
312
289
|
ack: ModuleHelloAckBody;
|
|
313
290
|
}
|
|
314
291
|
|
|
292
|
+
interface ReconnectCycle {
|
|
293
|
+
readonly generation: number;
|
|
294
|
+
socket: SubcSocket | null;
|
|
295
|
+
socketDied: boolean;
|
|
296
|
+
superseded: boolean;
|
|
297
|
+
}
|
|
298
|
+
|
|
315
299
|
export interface ModuleHelloAckBody {
|
|
316
300
|
negotiated_ver: number;
|
|
317
301
|
subc_ops: string[];
|
|
@@ -367,6 +351,7 @@ export class SubcProviderError extends Error {
|
|
|
367
351
|
constructor(
|
|
368
352
|
message: string,
|
|
369
353
|
readonly code?: string,
|
|
354
|
+
readonly kind: SubcCallErrorKind = "terminal",
|
|
370
355
|
) {
|
|
371
356
|
super(message);
|
|
372
357
|
}
|
|
@@ -391,10 +376,10 @@ export function managementSurfaceManifest(opts: ManagementSurfaceManifestOptions
|
|
|
391
376
|
config_schema: { type: "object" },
|
|
392
377
|
observability: [],
|
|
393
378
|
identity_scope: [],
|
|
379
|
+
concurrency: opts.concurrency ?? "module_managed",
|
|
394
380
|
},
|
|
395
381
|
],
|
|
396
382
|
consumes: [],
|
|
397
|
-
scheduled_tasks: [],
|
|
398
383
|
bindings: {
|
|
399
384
|
storage: {
|
|
400
385
|
kind: "sqlite",
|
|
@@ -435,10 +420,10 @@ export class SubcProvider {
|
|
|
435
420
|
private nextCorr = 1n;
|
|
436
421
|
private ingressEpochDropCount = 0;
|
|
437
422
|
private readonly requestGate = new AsyncPermitPool(DEFAULT_PROVIDER_HANDLER_CAPACITY);
|
|
438
|
-
private reconnecting:
|
|
423
|
+
private reconnecting: ReconnectCycle | null = null;
|
|
439
424
|
private generation = 1;
|
|
440
425
|
private connectionEpoch = 1;
|
|
441
|
-
private stateQueue: ProviderConnectionState
|
|
426
|
+
private stateQueue: Array<{ event: ProviderConnectionState; generation?: number; reconnect?: ReconnectCycle }> = [];
|
|
442
427
|
private drainingStateQueue = false;
|
|
443
428
|
private restoredDebounceToken = 0;
|
|
444
429
|
|
|
@@ -538,22 +523,22 @@ export class SubcProvider {
|
|
|
538
523
|
|
|
539
524
|
cancel(handle: RouteHandle, corr: bigint): void {
|
|
540
525
|
this.assertLiveHandle(handle);
|
|
541
|
-
|
|
526
|
+
this.sendOn(
|
|
542
527
|
this.sock,
|
|
543
528
|
this.generation,
|
|
544
529
|
buildFrame(FrameType.Cancel, controlFlags(), handle.channel, handle.epoch, corr, new Uint8Array(0)),
|
|
545
|
-
);
|
|
530
|
+
).catch(() => undefined);
|
|
546
531
|
}
|
|
547
532
|
|
|
548
533
|
closeRoute(handle: RouteHandle): void {
|
|
549
534
|
this.assertLiveHandle(handle);
|
|
550
535
|
this.liveRoutes.delete(handle.channel);
|
|
551
536
|
this.abortHandle(handle);
|
|
552
|
-
|
|
537
|
+
this.sendOn(
|
|
553
538
|
this.sock,
|
|
554
539
|
this.generation,
|
|
555
540
|
buildFrame(FrameType.Goodbye, controlFlags(), handle.channel, handle.epoch, 0n, new Uint8Array(0)),
|
|
556
|
-
);
|
|
541
|
+
).catch(() => undefined);
|
|
557
542
|
}
|
|
558
543
|
|
|
559
544
|
/** Read the connection file, authenticate as a client, register the manifest with HELLO, and serve frames. */
|
|
@@ -587,12 +572,16 @@ export class SubcProvider {
|
|
|
587
572
|
await this.closed;
|
|
588
573
|
}
|
|
589
574
|
|
|
590
|
-
private static async openConnection(
|
|
575
|
+
private static async openConnection(
|
|
576
|
+
opts: NormalizedSubcProviderConnectOptions,
|
|
577
|
+
onSocket?: (sock: SubcSocket) => void,
|
|
578
|
+
): Promise<OpenedProviderConnection> {
|
|
591
579
|
const conn = await readConnectionFile(opts.connectionFile);
|
|
592
580
|
const deadline = Date.now() + (opts.handshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT_MS);
|
|
593
581
|
const endpoint = conn.endpoints[0]!;
|
|
594
582
|
const sock = await SubcSocket.connect(endpoint.host, endpoint.port, deadline);
|
|
595
583
|
try {
|
|
584
|
+
onSocket?.(sock);
|
|
596
585
|
await authenticateClient(sock, conn, deadline);
|
|
597
586
|
await sendFrame(sock, buildHelloFrame(opts));
|
|
598
587
|
const ack = await expectHelloAck(sock, deadline);
|
|
@@ -748,11 +737,32 @@ export class SubcProvider {
|
|
|
748
737
|
throw new SubcProviderError(`unsupported module control request ${request.op ?? "<missing op>"}`);
|
|
749
738
|
}
|
|
750
739
|
|
|
751
|
-
const
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
740
|
+
const boundChannel = numberField(request.route_channel, "route_channel");
|
|
741
|
+
const boundEpoch = numberField(request.epoch, "epoch");
|
|
742
|
+
// Implicit-replace rule (wire spec 3.3.0): the daemon never rebinds a live
|
|
743
|
+
// channel, but its route-gone GOODBYE to modules is best-effort, so a bind can
|
|
744
|
+
// arrive for a channel this endpoint still believes installed. A strictly
|
|
745
|
+
// higher epoch proves the daemon freed the old binding: tear the stale install
|
|
746
|
+
// down locally and proceed. Equal or lower epoch is a protocol violation the
|
|
747
|
+
// daemon cannot produce: reject the bind.
|
|
748
|
+
const stale = this.liveRoutes.get(boundChannel);
|
|
749
|
+
if (stale) {
|
|
750
|
+
if (boundEpoch <= stale.epoch) {
|
|
751
|
+
await this.sendError(
|
|
752
|
+
frame,
|
|
753
|
+
"route_rejected",
|
|
754
|
+
`route.bind epoch ${boundEpoch} does not supersede installed epoch ${stale.epoch} on channel ${boundChannel}`,
|
|
755
|
+
controlFlags(),
|
|
756
|
+
sock,
|
|
757
|
+
generation,
|
|
758
|
+
);
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
this.liveRoutes.delete(stale.channel);
|
|
762
|
+
this.abortHandle(stale);
|
|
763
|
+
await this.opts.onRouteGone?.(stale);
|
|
764
|
+
}
|
|
765
|
+
const tentative = createRouteHandle(boundChannel, boundEpoch, this.connectionToken);
|
|
756
766
|
const bindRequest: RouteBindRequest = {
|
|
757
767
|
handle: tentative,
|
|
758
768
|
target: request.target as RouteTarget,
|
|
@@ -856,8 +866,15 @@ export class SubcProvider {
|
|
|
856
866
|
const releasePermit = await (this.requestGate ?? new AsyncPermitPool(DEFAULT_PROVIDER_HANDLER_CAPACITY)).acquire();
|
|
857
867
|
const dataFlags = buildFlags(false, Priority.Interactive, false);
|
|
858
868
|
try {
|
|
869
|
+
if (controller.signal.aborted) {
|
|
870
|
+
await this.sendError(frame, "cancelled", "request cancelled", dataFlags, sock, generation);
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
859
873
|
const body = await this.opts.handler(handle, frame.body, context);
|
|
860
|
-
if (controller.signal.aborted)
|
|
874
|
+
if (controller.signal.aborted) {
|
|
875
|
+
await this.sendError(frame, "cancelled", "request cancelled", dataFlags, sock, generation);
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
861
878
|
this.assertLiveHandle(handle);
|
|
862
879
|
if (body === undefined) {
|
|
863
880
|
await this.sendOn(
|
|
@@ -891,7 +908,11 @@ export class SubcProvider {
|
|
|
891
908
|
throw new SubcProviderError("provider handler must return a Uint8Array or void", "invalid_handler_response");
|
|
892
909
|
}
|
|
893
910
|
} catch (error) {
|
|
894
|
-
if (error instanceof StaleRouteHandleError
|
|
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
|
+
}
|
|
895
916
|
await this.sendError(
|
|
896
917
|
frame,
|
|
897
918
|
error instanceof SubcProviderError && error.code ? error.code : "handler_error",
|
|
@@ -956,6 +977,10 @@ export class SubcProvider {
|
|
|
956
977
|
sock: SubcSocket,
|
|
957
978
|
generation: number,
|
|
958
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
|
+
}
|
|
959
984
|
await this.sendOn(
|
|
960
985
|
sock,
|
|
961
986
|
generation,
|
|
@@ -977,43 +1002,79 @@ export class SubcProvider {
|
|
|
977
1002
|
}
|
|
978
1003
|
|
|
979
1004
|
private handleUnexpectedDrop(sock: SubcSocket, generation: number, cause: Error): void {
|
|
1005
|
+
if (this.closeStarted || this.sock !== sock || this.generation !== generation) return;
|
|
980
1006
|
this.cancelRestoredDebounce();
|
|
981
1007
|
this.abortGeneration(generation);
|
|
982
1008
|
this.generation += 1;
|
|
983
1009
|
sock.close();
|
|
984
|
-
this.
|
|
985
|
-
this.scheduleReconnectAfterDrop(cause);
|
|
1010
|
+
this.scheduleReconnectAfterDrop(cause, this.generation, sock);
|
|
986
1011
|
}
|
|
987
1012
|
|
|
988
|
-
private scheduleReconnectAfterDrop(
|
|
989
|
-
if (this.closeStarted
|
|
990
|
-
|
|
1013
|
+
private scheduleReconnectAfterDrop(cause: Error, generation: number, droppedSocket: SubcSocket): void {
|
|
1014
|
+
if (this.closeStarted) return;
|
|
1015
|
+
|
|
1016
|
+
const previous = this.reconnecting;
|
|
1017
|
+
if (previous) {
|
|
1018
|
+
if (!this.shouldSupersedeReconnect(previous, generation, droppedSocket)) return;
|
|
1019
|
+
previous.superseded = true;
|
|
1020
|
+
previous.socket?.close();
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
const cycle: ReconnectCycle = {
|
|
1024
|
+
generation,
|
|
1025
|
+
socket: null,
|
|
1026
|
+
socketDied: false,
|
|
1027
|
+
superseded: false,
|
|
1028
|
+
};
|
|
1029
|
+
this.reconnecting = cycle;
|
|
1030
|
+
this.enqueueConnectionState({ state: "down", cause });
|
|
1031
|
+
void this.reconnectWithRetry(cycle)
|
|
991
1032
|
.catch((err) => {
|
|
992
|
-
if (
|
|
1033
|
+
if (this.isCurrentReconnect(cycle) && !this.closeStarted) {
|
|
1034
|
+
this.failFatal(err instanceof Error ? err : new SubcProviderError(String(err)));
|
|
1035
|
+
}
|
|
993
1036
|
})
|
|
994
1037
|
.finally(() => {
|
|
995
|
-
if (this.
|
|
1038
|
+
if (this.isCurrentReconnect(cycle)) this.reconnecting = null;
|
|
996
1039
|
});
|
|
997
|
-
this.reconnecting = promise;
|
|
998
1040
|
}
|
|
999
1041
|
|
|
1000
|
-
private async reconnectWithRetry(
|
|
1042
|
+
private async reconnectWithRetry(cycle: ReconnectCycle): Promise<void> {
|
|
1001
1043
|
let attempt = 0;
|
|
1002
1044
|
let delay = this.opts.reconnectBackoff.baseMs;
|
|
1003
1045
|
|
|
1004
1046
|
for (;;) {
|
|
1047
|
+
if (!this.isCurrentReconnect(cycle)) return;
|
|
1005
1048
|
if (this.closeStarted) throw new SubcProviderError("provider closed");
|
|
1049
|
+
|
|
1050
|
+
cycle.socket = null;
|
|
1051
|
+
cycle.socketDied = false;
|
|
1006
1052
|
attempt += 1;
|
|
1007
|
-
this.
|
|
1053
|
+
this.enqueueReconnectState(cycle, attempt);
|
|
1008
1054
|
try {
|
|
1009
|
-
const opened = await SubcProvider.openConnection(this.opts)
|
|
1010
|
-
|
|
1055
|
+
const opened = await SubcProvider.openConnection(this.opts, (sock) => {
|
|
1056
|
+
if (!this.isCurrentReconnect(cycle)) {
|
|
1057
|
+
sock.close();
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
cycle.socket = sock;
|
|
1061
|
+
});
|
|
1062
|
+
if (!this.isCurrentReconnect(cycle) || this.closeStarted) {
|
|
1011
1063
|
opened.sock.close();
|
|
1012
|
-
throw new SubcProviderError("provider closed");
|
|
1064
|
+
if (this.closeStarted) throw new SubcProviderError("provider closed");
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
const epoch = this.replaceConnection(opened, cycle.generation);
|
|
1069
|
+
this.reconnecting = null;
|
|
1070
|
+
if (this.reconnecting !== null) {
|
|
1071
|
+
throw new SubcProviderError("reconnect state must clear before restored", "reconnect_state");
|
|
1013
1072
|
}
|
|
1014
|
-
this.
|
|
1073
|
+
this.scheduleRestored(cycle.generation, epoch);
|
|
1015
1074
|
return;
|
|
1016
1075
|
} catch (err) {
|
|
1076
|
+
if (!this.isCurrentReconnect(cycle)) return;
|
|
1077
|
+
if (cycle.socket) cycle.socketDied = true;
|
|
1017
1078
|
if (this.closeStarted) throw err;
|
|
1018
1079
|
if (!isProviderReconnectTransient(err)) throw err;
|
|
1019
1080
|
// Providers retry transient failures indefinitely by design (a module
|
|
@@ -1026,7 +1087,20 @@ export class SubcProvider {
|
|
|
1026
1087
|
}
|
|
1027
1088
|
}
|
|
1028
1089
|
|
|
1029
|
-
|
|
1090
|
+
private isCurrentReconnect(cycle: ReconnectCycle): boolean {
|
|
1091
|
+
return !cycle.superseded && this.reconnecting === cycle && this.generation === cycle.generation;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
private shouldSupersedeReconnect(cycle: ReconnectCycle, generation: number, droppedSocket: SubcSocket): boolean {
|
|
1095
|
+
return cycle.generation < generation || cycle.socketDied || cycle.socket === droppedSocket;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
private enqueueReconnectState(cycle: ReconnectCycle, attempt: number): void {
|
|
1099
|
+
if (!this.isCurrentReconnect(cycle)) return;
|
|
1100
|
+
this.enqueueConnectionState({ state: "reconnecting", attempt }, cycle.generation, cycle);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
private replaceConnection(opened: OpenedProviderConnection, generation: number): number {
|
|
1030
1104
|
this.sock.close();
|
|
1031
1105
|
this.sock = opened.sock;
|
|
1032
1106
|
this.currentConn = opened.conn;
|
|
@@ -1036,9 +1110,8 @@ export class SubcProvider {
|
|
|
1036
1110
|
this.connectionToken = newConnectionToken();
|
|
1037
1111
|
this.liveRoutes.clear();
|
|
1038
1112
|
this.nextCorr = 1n;
|
|
1039
|
-
const generation = this.generation;
|
|
1040
1113
|
void this.readLoop(opened.sock, generation);
|
|
1041
|
-
|
|
1114
|
+
return this.connectionEpoch;
|
|
1042
1115
|
}
|
|
1043
1116
|
|
|
1044
1117
|
private scheduleRestored(generation: number, epoch: number): void {
|
|
@@ -1054,7 +1127,7 @@ export class SubcProvider {
|
|
|
1054
1127
|
this.generation === generation &&
|
|
1055
1128
|
this.connectionEpoch === epoch
|
|
1056
1129
|
) {
|
|
1057
|
-
this.enqueueConnectionState({ state: "restored", epoch });
|
|
1130
|
+
this.enqueueConnectionState({ state: "restored", epoch }, generation);
|
|
1058
1131
|
}
|
|
1059
1132
|
})
|
|
1060
1133
|
.catch((err) => {
|
|
@@ -1068,9 +1141,9 @@ export class SubcProvider {
|
|
|
1068
1141
|
this.restoredDebounceToken += 1;
|
|
1069
1142
|
}
|
|
1070
1143
|
|
|
1071
|
-
private enqueueConnectionState(event: ProviderConnectionState): void {
|
|
1144
|
+
private enqueueConnectionState(event: ProviderConnectionState, generation?: number, reconnect?: ReconnectCycle): void {
|
|
1072
1145
|
if (!this.opts.onConnectionState) return;
|
|
1073
|
-
this.stateQueue.push(event);
|
|
1146
|
+
this.stateQueue.push({ event, generation, reconnect });
|
|
1074
1147
|
if (!this.drainingStateQueue) void this.drainConnectionStateQueue();
|
|
1075
1148
|
}
|
|
1076
1149
|
|
|
@@ -1079,7 +1152,17 @@ export class SubcProvider {
|
|
|
1079
1152
|
this.drainingStateQueue = true;
|
|
1080
1153
|
try {
|
|
1081
1154
|
while (this.stateQueue.length > 0) {
|
|
1082
|
-
const
|
|
1155
|
+
const queued = this.stateQueue[0]!;
|
|
1156
|
+
if (
|
|
1157
|
+
this.closeStarted ||
|
|
1158
|
+
(queued.generation !== undefined && queued.generation !== this.generation) ||
|
|
1159
|
+
queued.reconnect?.superseded
|
|
1160
|
+
) {
|
|
1161
|
+
this.stateQueue.shift();
|
|
1162
|
+
continue;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
const { event } = queued;
|
|
1083
1166
|
try {
|
|
1084
1167
|
await this.opts.onConnectionState?.(event);
|
|
1085
1168
|
this.stateQueue.shift();
|
|
@@ -1140,7 +1223,11 @@ function routeKey(handle: RouteHandle, corr: bigint): string {
|
|
|
1140
1223
|
function providerErrorFromFrame(frame: Frame): SubcProviderError {
|
|
1141
1224
|
try {
|
|
1142
1225
|
const body = parseJson(frame.body) as { code?: string; message?: string };
|
|
1143
|
-
return new SubcProviderError(
|
|
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
|
+
);
|
|
1144
1231
|
} catch {
|
|
1145
1232
|
return new SubcProviderError(Buffer.from(frame.body).toString("utf8") || "subc error");
|
|
1146
1233
|
}
|
|
@@ -1234,7 +1321,7 @@ function controlFlags(): number {
|
|
|
1234
1321
|
}
|
|
1235
1322
|
|
|
1236
1323
|
async function sendFrame(sock: SubcSocket, frame: Frame): Promise<void> {
|
|
1237
|
-
await sock
|
|
1324
|
+
await writeBorrowed(sock, encodeFrame(frame), Date.now() + WRITE_TIMEOUT_MS);
|
|
1238
1325
|
}
|
|
1239
1326
|
|
|
1240
1327
|
async function expectHelloAck(sock: SubcSocket, deadline: number): Promise<ModuleHelloAckBody> {
|
|
@@ -1297,7 +1384,6 @@ function normalizeManifest(manifest: ManifestInput): ManifestInput {
|
|
|
1297
1384
|
trust_tier: manifest.trust_tier,
|
|
1298
1385
|
provides: manifest.provides.map(normalizeProviderRole),
|
|
1299
1386
|
consumes: manifest.consumes.map(normalizeConsumerRole),
|
|
1300
|
-
scheduled_tasks: manifest.scheduled_tasks.map(normalizeScheduledTask),
|
|
1301
1387
|
bindings: {
|
|
1302
1388
|
storage: {
|
|
1303
1389
|
kind: manifest.bindings.storage.kind,
|
|
@@ -1358,6 +1444,7 @@ function normalizeProviderRole(role: ProviderRoleInput): ProviderRoleInput {
|
|
|
1358
1444
|
kind: surface.kind,
|
|
1359
1445
|
})),
|
|
1360
1446
|
identity_scope: [...role.identity_scope],
|
|
1447
|
+
concurrency: role.concurrency,
|
|
1361
1448
|
};
|
|
1362
1449
|
case "internal_service":
|
|
1363
1450
|
return {
|
|
@@ -1381,23 +1468,3 @@ function normalizeConsumerRole(role: ConsumerRoleInput): ConsumerRoleInput {
|
|
|
1381
1468
|
}
|
|
1382
1469
|
}
|
|
1383
1470
|
|
|
1384
|
-
function normalizeScheduledTask(task: ScheduledTaskInput): ScheduledTaskInput {
|
|
1385
|
-
return {
|
|
1386
|
-
task_id: task.task_id,
|
|
1387
|
-
eligibility: {
|
|
1388
|
-
cooldown: task.eligibility.cooldown,
|
|
1389
|
-
window: task.eligibility.window,
|
|
1390
|
-
},
|
|
1391
|
-
lease_scope: task.lease_scope,
|
|
1392
|
-
renews_during_calls: task.renews_during_calls,
|
|
1393
|
-
toolset: [...task.toolset],
|
|
1394
|
-
model_policy: {
|
|
1395
|
-
tier: task.model_policy.tier,
|
|
1396
|
-
fallback_chain: [...task.model_policy.fallback_chain],
|
|
1397
|
-
},
|
|
1398
|
-
step_cap: task.step_cap,
|
|
1399
|
-
circuit_breaker: {
|
|
1400
|
-
identical_failures: task.circuit_breaker.identical_failures,
|
|
1401
|
-
},
|
|
1402
|
-
};
|
|
1403
|
-
}
|
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.
|
|
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
|
-
|
|
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(
|
|
271
|
+
this.sock.write(buffer, (err) => {
|
|
234
272
|
settle(() => {
|
|
235
273
|
if (err) {
|
|
236
274
|
reject(
|