@cortexkit/aft-opencode 0.42.0 → 0.43.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/dist/bg-notifications.d.ts +35 -0
- package/dist/bg-notifications.d.ts.map +1 -1
- package/dist/config.d.ts +7 -3
- package/dist/config.d.ts.map +1 -1
- package/dist/configure-warnings.d.ts +3 -3
- package/dist/configure-warnings.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6092 -9528
- package/dist/notifications.d.ts +2 -2
- package/dist/notifications.d.ts.map +1 -1
- package/dist/shared/rpc-client.d.ts +6 -0
- package/dist/shared/rpc-client.d.ts.map +1 -1
- package/dist/shared/rpc-notifications.d.ts +29 -15
- package/dist/shared/rpc-notifications.d.ts.map +1 -1
- package/dist/shared/rpc-server.d.ts +10 -1
- package/dist/shared/rpc-server.d.ts.map +1 -1
- package/dist/subc-tool-schemas.d.ts +1 -1
- package/dist/subc-tool-schemas.d.ts.map +1 -1
- package/dist/tools/_shared.d.ts +12 -2
- package/dist/tools/_shared.d.ts.map +1 -1
- package/dist/tools/ast.d.ts.map +1 -1
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash_watch.d.ts.map +1 -1
- package/dist/tools/hoisted.d.ts.map +1 -1
- package/dist/tools/imports.d.ts.map +1 -1
- package/dist/tools/inspect.d.ts +0 -1
- package/dist/tools/inspect.d.ts.map +1 -1
- package/dist/tools/navigation.d.ts.map +1 -1
- package/dist/tools/permissions.d.ts +23 -13
- package/dist/tools/permissions.d.ts.map +1 -1
- package/dist/tools/reading.d.ts +0 -1
- package/dist/tools/reading.d.ts.map +1 -1
- package/dist/tools/refactoring.d.ts.map +1 -1
- package/dist/tools/safety.d.ts.map +1 -1
- package/dist/tools/search.d.ts.map +1 -1
- package/dist/tools/semantic.d.ts.map +1 -1
- package/dist/tui/notification-socket.d.ts +41 -0
- package/dist/tui/notification-socket.d.ts.map +1 -0
- package/dist/tui.js +2460 -187
- package/dist/types.d.ts +4 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +8 -9
- package/src/shared/rpc-client.ts +9 -0
- package/src/shared/rpc-notifications.ts +97 -41
- package/src/shared/rpc-server.ts +286 -67
- package/src/tui/index.tsx +77 -125
- package/src/tui/notification-socket.ts +421 -0
- package/src/tui/sidebar.tsx +38 -54
- package/dist/patch-parser.d.ts +0 -33
- package/dist/patch-parser.d.ts.map +0 -1
- package/dist/shared/opencode-config-dir.d.ts +0 -16
- package/dist/shared/opencode-config-dir.d.ts.map +0 -1
- package/dist/shared/pty-cache.d.ts +0 -18
- package/dist/shared/pty-cache.d.ts.map +0 -1
- package/dist/shared/tui-config.d.ts +0 -2
- package/dist/shared/tui-config.d.ts.map +0 -1
- package/src/shared/opencode-config-dir.ts +0 -46
- package/src/shared/pty-cache.ts +0 -113
- package/src/shared/tui-config.ts +0 -58
package/dist/tui.js
CHANGED
|
@@ -7790,7 +7790,7 @@ var require_src2 = __commonJS((exports, module) => {
|
|
|
7790
7790
|
// src/tui/index.tsx
|
|
7791
7791
|
import { createMemo as createMemo2, createSignal as createSignal2, onCleanup as onCleanup2 } from "solid-js";
|
|
7792
7792
|
// package.json
|
|
7793
|
-
var version = "0.
|
|
7793
|
+
var version = "0.43.1";
|
|
7794
7794
|
|
|
7795
7795
|
// src/shared/rpc-client.ts
|
|
7796
7796
|
import { existsSync, readdirSync, readFileSync, unlinkSync } from "node:fs";
|
|
@@ -8143,6 +8143,15 @@ class BinaryBridge {
|
|
|
8143
8143
|
async send(command, params = {}, options) {
|
|
8144
8144
|
return this.sendWithVersionMismatchRetry(command, params, options, true);
|
|
8145
8145
|
}
|
|
8146
|
+
async toolCall(sessionId, name, rawArgs = {}, options) {
|
|
8147
|
+
const params = { name, arguments: rawArgs };
|
|
8148
|
+
if (sessionId)
|
|
8149
|
+
params.session_id = sessionId;
|
|
8150
|
+
const { preview, ...sendOptions } = options ?? {};
|
|
8151
|
+
if (preview === true)
|
|
8152
|
+
params.preview = true;
|
|
8153
|
+
return await this.send("tool_call", params, Object.keys(sendOptions).length > 0 ? sendOptions : undefined);
|
|
8154
|
+
}
|
|
8146
8155
|
async sendWithVersionMismatchRetry(command, params, options, canRetryAfterVersionSwap) {
|
|
8147
8156
|
try {
|
|
8148
8157
|
if (this._shuttingDown) {
|
|
@@ -8917,6 +8926,9 @@ class BridgePool {
|
|
|
8917
8926
|
this.bridges.set(key, { bridge, lastUsed: Date.now() });
|
|
8918
8927
|
return bridge;
|
|
8919
8928
|
}
|
|
8929
|
+
async toolCall(projectRoot, runtime, name, rawArgs = {}, options) {
|
|
8930
|
+
return this.getBridge(projectRoot).toolCall(runtime.sessionID, name, rawArgs, options);
|
|
8931
|
+
}
|
|
8920
8932
|
cleanup() {
|
|
8921
8933
|
const now = Date.now();
|
|
8922
8934
|
for (const [dir, entry] of this.bridges) {
|
|
@@ -8951,72 +8963,2088 @@ class BridgePool {
|
|
|
8951
8963
|
this.bridges.delete(oldestDir);
|
|
8952
8964
|
}
|
|
8953
8965
|
}
|
|
8954
|
-
async
|
|
8955
|
-
|
|
8956
|
-
|
|
8957
|
-
this.cleanupTimer
|
|
8966
|
+
async closeSession(_projectRoot, _session) {}
|
|
8967
|
+
async shutdown() {
|
|
8968
|
+
if (this.cleanupTimer) {
|
|
8969
|
+
clearInterval(this.cleanupTimer);
|
|
8970
|
+
this.cleanupTimer = null;
|
|
8971
|
+
}
|
|
8972
|
+
const shutdowns = [
|
|
8973
|
+
...Array.from(this.bridges.values(), (e) => e.bridge.shutdown()),
|
|
8974
|
+
...Array.from(this.staleBridges.values(), (bridge) => bridge.shutdown())
|
|
8975
|
+
];
|
|
8976
|
+
this.bridges.clear();
|
|
8977
|
+
this.staleBridges.clear();
|
|
8978
|
+
await Promise.allSettled(shutdowns);
|
|
8979
|
+
}
|
|
8980
|
+
async replaceBinary(newPath) {
|
|
8981
|
+
this.binaryPath = newPath;
|
|
8982
|
+
for (const entry of this.bridges.values()) {
|
|
8983
|
+
this.staleBridges.add(entry.bridge);
|
|
8984
|
+
}
|
|
8985
|
+
this.bridges.clear();
|
|
8986
|
+
this.log(`Binary path updated to ${newPath}. Active bridges marked stale — next calls will use the new binary.`);
|
|
8987
|
+
return newPath;
|
|
8988
|
+
}
|
|
8989
|
+
log(message, meta) {
|
|
8990
|
+
const logger = this.logger ?? getActiveLogger();
|
|
8991
|
+
if (logger) {
|
|
8992
|
+
try {
|
|
8993
|
+
logger.log(message, meta);
|
|
8994
|
+
} catch (err) {
|
|
8995
|
+
console.error(`[aft-bridge] ERROR: pool logger log threw: ${err instanceof Error ? err.message : String(err)}`);
|
|
8996
|
+
console.error(`[aft-bridge] ${message}`);
|
|
8997
|
+
}
|
|
8998
|
+
} else
|
|
8999
|
+
log(message, meta);
|
|
9000
|
+
}
|
|
9001
|
+
error(message, meta) {
|
|
9002
|
+
const logger = this.logger ?? getActiveLogger();
|
|
9003
|
+
if (logger) {
|
|
9004
|
+
try {
|
|
9005
|
+
logger.error(message, meta);
|
|
9006
|
+
} catch (err) {
|
|
9007
|
+
console.error(`[aft-bridge] ERROR: pool logger error threw: ${err instanceof Error ? err.message : String(err)}`);
|
|
9008
|
+
console.error(`[aft-bridge] ERROR: ${message}`);
|
|
9009
|
+
}
|
|
9010
|
+
} else
|
|
9011
|
+
error(message, meta);
|
|
9012
|
+
}
|
|
9013
|
+
setConfigureOverride(key, value) {
|
|
9014
|
+
if (value === undefined) {
|
|
9015
|
+
delete this.configOverrides[key];
|
|
9016
|
+
} else {
|
|
9017
|
+
this.configOverrides[key] = value;
|
|
9018
|
+
}
|
|
9019
|
+
}
|
|
9020
|
+
get size() {
|
|
9021
|
+
return this.bridges.size;
|
|
9022
|
+
}
|
|
9023
|
+
_testGetConfigOverrides() {
|
|
9024
|
+
return { ...this.configOverrides };
|
|
9025
|
+
}
|
|
9026
|
+
_testGetBridgeOptions() {
|
|
9027
|
+
return { ...this.bridgeOptions };
|
|
9028
|
+
}
|
|
9029
|
+
}
|
|
9030
|
+
function normalizeKey(projectRoot) {
|
|
9031
|
+
return canonicalizeProjectRoot(projectRoot);
|
|
9032
|
+
}
|
|
9033
|
+
// ../../node_modules/.bun/@cortexkit+subc-client@0.2.0/node_modules/@cortexkit/subc-client/src/client.ts
|
|
9034
|
+
import { promises as fs2 } from "node:fs";
|
|
9035
|
+
|
|
9036
|
+
// ../../node_modules/.bun/@cortexkit+subc-client@0.2.0/node_modules/@cortexkit/subc-client/src/auth.ts
|
|
9037
|
+
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
9038
|
+
var NONCE_LEN = 32;
|
|
9039
|
+
var MAX_AUTH_MESSAGE_LEN = 4096;
|
|
9040
|
+
var SERVER_PROOF_DOMAIN = "subc-server-v1";
|
|
9041
|
+
var CLIENT_AUTH_DOMAIN = "subc-client-v1";
|
|
9042
|
+
var DEFAULT_CLIENT_ROLE = "client";
|
|
9043
|
+
|
|
9044
|
+
class AuthError extends Error {
|
|
9045
|
+
}
|
|
9046
|
+
function computeProof(key, domain, clientNonce, serverNonce, daemonId) {
|
|
9047
|
+
const mac = createHmac("sha256", Buffer.from(key));
|
|
9048
|
+
mac.update(Buffer.from(domain, "utf8"));
|
|
9049
|
+
mac.update(Buffer.from(clientNonce));
|
|
9050
|
+
mac.update(Buffer.from(serverNonce));
|
|
9051
|
+
mac.update(Buffer.from(daemonId));
|
|
9052
|
+
return new Uint8Array(mac.digest());
|
|
9053
|
+
}
|
|
9054
|
+
function constantTimeEq(a, b) {
|
|
9055
|
+
if (a.length !== b.length)
|
|
9056
|
+
return false;
|
|
9057
|
+
return timingSafeEqual(Buffer.from(a), Buffer.from(b));
|
|
9058
|
+
}
|
|
9059
|
+
async function writeMessage(sock, value, deadlineMs) {
|
|
9060
|
+
const json = Buffer.from(JSON.stringify(value), "utf8");
|
|
9061
|
+
if (json.length > MAX_AUTH_MESSAGE_LEN) {
|
|
9062
|
+
throw new AuthError(`auth message too large: ${json.length} > ${MAX_AUTH_MESSAGE_LEN}`);
|
|
9063
|
+
}
|
|
9064
|
+
const lenPrefix = new Uint8Array(4);
|
|
9065
|
+
new DataView(lenPrefix.buffer).setUint32(0, json.length, true);
|
|
9066
|
+
await sock.write(lenPrefix, deadlineMs);
|
|
9067
|
+
await sock.write(json, deadlineMs);
|
|
9068
|
+
}
|
|
9069
|
+
async function readMessage(sock, deadlineMs) {
|
|
9070
|
+
const lenBytes = await sock.readExact(4, deadlineMs);
|
|
9071
|
+
const len = new DataView(lenBytes.buffer, lenBytes.byteOffset, 4).getUint32(0, true);
|
|
9072
|
+
if (len > MAX_AUTH_MESSAGE_LEN) {
|
|
9073
|
+
throw new AuthError(`auth message too large: ${len} > ${MAX_AUTH_MESSAGE_LEN}`);
|
|
9074
|
+
}
|
|
9075
|
+
const body = len === 0 ? new Uint8Array(0) : await sock.readExact(len, deadlineMs);
|
|
9076
|
+
try {
|
|
9077
|
+
return JSON.parse(Buffer.from(body).toString("utf8"));
|
|
9078
|
+
} catch (err) {
|
|
9079
|
+
throw new AuthError(`auth message JSON decode failed: ${String(err)}`);
|
|
9080
|
+
}
|
|
9081
|
+
}
|
|
9082
|
+
async function authenticateClient(sock, conn, deadlineMs) {
|
|
9083
|
+
const clientNonce = new Uint8Array(randomBytes(NONCE_LEN));
|
|
9084
|
+
await writeMessage(sock, { client_nonce: Array.from(clientNonce), role: DEFAULT_CLIENT_ROLE }, deadlineMs);
|
|
9085
|
+
const proof = await readMessage(sock, deadlineMs);
|
|
9086
|
+
const serverNonce = Uint8Array.from(proof.server_nonce);
|
|
9087
|
+
const daemonId = Uint8Array.from(proof.daemon_id);
|
|
9088
|
+
const serverProof = Uint8Array.from(proof.server_proof);
|
|
9089
|
+
const expected = computeProof(conn.key, SERVER_PROOF_DOMAIN, clientNonce, serverNonce, daemonId);
|
|
9090
|
+
if (!constantTimeEq(expected, serverProof)) {
|
|
9091
|
+
throw new AuthError("server proof mismatch — wrong key or impostor daemon");
|
|
9092
|
+
}
|
|
9093
|
+
if (!constantTimeEq(daemonId, conn.daemonId)) {
|
|
9094
|
+
throw new AuthError("daemon id mismatch — connection file points at a different daemon");
|
|
9095
|
+
}
|
|
9096
|
+
const clientAuth = computeProof(conn.key, CLIENT_AUTH_DOMAIN, clientNonce, serverNonce, daemonId);
|
|
9097
|
+
await writeMessage(sock, { client_auth: Array.from(clientAuth) }, deadlineMs);
|
|
9098
|
+
}
|
|
9099
|
+
|
|
9100
|
+
// ../../node_modules/.bun/@cortexkit+subc-client@0.2.0/node_modules/@cortexkit/subc-client/src/connection-file.ts
|
|
9101
|
+
import { promises as fs } from "node:fs";
|
|
9102
|
+
var SCHEMA_VERSION = 1;
|
|
9103
|
+
var MIN_KEY_LEN = 32;
|
|
9104
|
+
var DAEMON_ID_LEN = 16;
|
|
9105
|
+
|
|
9106
|
+
class ConnectionFileError extends Error {
|
|
9107
|
+
}
|
|
9108
|
+
function toBytes(value, field) {
|
|
9109
|
+
if (!Array.isArray(value) || value.some((n) => typeof n !== "number")) {
|
|
9110
|
+
throw new ConnectionFileError(`connection file field '${field}' must be a JSON array of bytes`);
|
|
9111
|
+
}
|
|
9112
|
+
return Uint8Array.from(value);
|
|
9113
|
+
}
|
|
9114
|
+
function validate(info) {
|
|
9115
|
+
if (info.schema !== SCHEMA_VERSION) {
|
|
9116
|
+
throw new ConnectionFileError(`unsupported connection file schema ${info.schema}; expected ${SCHEMA_VERSION}`);
|
|
9117
|
+
}
|
|
9118
|
+
if (info.endpoints.length === 0) {
|
|
9119
|
+
throw new ConnectionFileError("connection file must include at least one endpoint");
|
|
9120
|
+
}
|
|
9121
|
+
if (info.key.length < MIN_KEY_LEN) {
|
|
9122
|
+
throw new ConnectionFileError(`connection file key is too short: ${info.key.length} bytes, need at least ${MIN_KEY_LEN}`);
|
|
9123
|
+
}
|
|
9124
|
+
if (info.daemonId.length !== DAEMON_ID_LEN) {
|
|
9125
|
+
throw new ConnectionFileError(`connection file daemon_id must be ${DAEMON_ID_LEN} bytes, got ${info.daemonId.length}`);
|
|
9126
|
+
}
|
|
9127
|
+
}
|
|
9128
|
+
async function verifyOwnerOnly(path) {
|
|
9129
|
+
if (process.platform === "win32")
|
|
9130
|
+
return;
|
|
9131
|
+
const stat = await fs.stat(path);
|
|
9132
|
+
const mode = stat.mode & 511;
|
|
9133
|
+
if ((mode & 63) !== 0) {
|
|
9134
|
+
throw new ConnectionFileError(`connection file ${path} has insecure permissions 0o${mode.toString(8)}; expected owner-only 0600`);
|
|
9135
|
+
}
|
|
9136
|
+
}
|
|
9137
|
+
async function readConnectionFile(path) {
|
|
9138
|
+
await verifyOwnerOnly(path);
|
|
9139
|
+
const raw = await fs.readFile(path, "utf8");
|
|
9140
|
+
let parsed;
|
|
9141
|
+
try {
|
|
9142
|
+
parsed = JSON.parse(raw);
|
|
9143
|
+
} catch (err) {
|
|
9144
|
+
throw new ConnectionFileError(`connection file JSON read failed for ${path}: ${String(err)}`);
|
|
9145
|
+
}
|
|
9146
|
+
const endpointsRaw = parsed.endpoints;
|
|
9147
|
+
if (!Array.isArray(endpointsRaw)) {
|
|
9148
|
+
throw new ConnectionFileError("connection file 'endpoints' must be an array");
|
|
9149
|
+
}
|
|
9150
|
+
const endpoints = endpointsRaw.map((e) => {
|
|
9151
|
+
const ep = e;
|
|
9152
|
+
if (typeof ep.host !== "string" || typeof ep.port !== "number") {
|
|
9153
|
+
throw new ConnectionFileError("connection file endpoint must be { host: string, port: number }");
|
|
9154
|
+
}
|
|
9155
|
+
return { host: ep.host, port: ep.port };
|
|
9156
|
+
});
|
|
9157
|
+
const info = {
|
|
9158
|
+
schema: parsed.schema,
|
|
9159
|
+
endpoints,
|
|
9160
|
+
key: toBytes(parsed.key, "key"),
|
|
9161
|
+
daemonId: toBytes(parsed.daemon_id, "daemon_id"),
|
|
9162
|
+
pid: parsed.pid,
|
|
9163
|
+
daemonVer: parsed.daemon_ver ?? ""
|
|
9164
|
+
};
|
|
9165
|
+
validate(info);
|
|
9166
|
+
return info;
|
|
9167
|
+
}
|
|
9168
|
+
|
|
9169
|
+
// ../../node_modules/.bun/@cortexkit+subc-client@0.2.0/node_modules/@cortexkit/subc-client/src/envelope.ts
|
|
9170
|
+
var PROTOCOL_VERSION = 1;
|
|
9171
|
+
var HEADER_LEN = 17;
|
|
9172
|
+
var FROZEN_PREFIX_LEN = 5;
|
|
9173
|
+
var MAX_FRAME_BODY_LEN = 64 * 1024 * 1024;
|
|
9174
|
+
var FrameType;
|
|
9175
|
+
((FrameType2) => {
|
|
9176
|
+
FrameType2[FrameType2["Request"] = 0] = "Request";
|
|
9177
|
+
FrameType2[FrameType2["Response"] = 1] = "Response";
|
|
9178
|
+
FrameType2[FrameType2["Push"] = 2] = "Push";
|
|
9179
|
+
FrameType2[FrameType2["StreamData"] = 3] = "StreamData";
|
|
9180
|
+
FrameType2[FrameType2["StreamEnd"] = 4] = "StreamEnd";
|
|
9181
|
+
FrameType2[FrameType2["Error"] = 5] = "Error";
|
|
9182
|
+
FrameType2[FrameType2["Cancel"] = 6] = "Cancel";
|
|
9183
|
+
FrameType2[FrameType2["Ping"] = 7] = "Ping";
|
|
9184
|
+
FrameType2[FrameType2["Pong"] = 8] = "Pong";
|
|
9185
|
+
FrameType2[FrameType2["Hello"] = 9] = "Hello";
|
|
9186
|
+
FrameType2[FrameType2["HelloAck"] = 10] = "HelloAck";
|
|
9187
|
+
FrameType2[FrameType2["Goodbye"] = 11] = "Goodbye";
|
|
9188
|
+
})(FrameType ||= {});
|
|
9189
|
+
var FRAME_TYPE_MAX = 11 /* Goodbye */;
|
|
9190
|
+
function isPureHeader(ty) {
|
|
9191
|
+
return ty === 6 /* Cancel */ || ty === 7 /* Ping */ || ty === 8 /* Pong */ || ty === 11 /* Goodbye */;
|
|
9192
|
+
}
|
|
9193
|
+
var FLAG_BINARY = 1;
|
|
9194
|
+
var FLAG_PRIORITY_MASK = 6;
|
|
9195
|
+
var FLAG_PRIORITY_SHIFT = 1;
|
|
9196
|
+
var FLAG_LAST = 8;
|
|
9197
|
+
var FLAG_RESERVED_MASK = 240;
|
|
9198
|
+
function buildFlags(binary, priority, last) {
|
|
9199
|
+
let b = 0;
|
|
9200
|
+
if (binary)
|
|
9201
|
+
b |= FLAG_BINARY;
|
|
9202
|
+
b |= priority << FLAG_PRIORITY_SHIFT;
|
|
9203
|
+
if (last)
|
|
9204
|
+
b |= FLAG_LAST;
|
|
9205
|
+
return b;
|
|
9206
|
+
}
|
|
9207
|
+
function encodeHeader(h) {
|
|
9208
|
+
const buf = new Uint8Array(HEADER_LEN);
|
|
9209
|
+
const view = new DataView(buf.buffer);
|
|
9210
|
+
view.setUint32(0, h.len, true);
|
|
9211
|
+
buf[4] = h.ver;
|
|
9212
|
+
buf[5] = h.ty;
|
|
9213
|
+
buf[6] = h.flags;
|
|
9214
|
+
view.setUint16(7, h.channel, true);
|
|
9215
|
+
view.setBigUint64(9, h.corr, true);
|
|
9216
|
+
return buf;
|
|
9217
|
+
}
|
|
9218
|
+
|
|
9219
|
+
class DecodeError extends Error {
|
|
9220
|
+
}
|
|
9221
|
+
function decodeHeader(bytes) {
|
|
9222
|
+
if (bytes.length < FROZEN_PREFIX_LEN) {
|
|
9223
|
+
throw new DecodeError(`header shorter than frozen prefix: have ${bytes.length} bytes`);
|
|
9224
|
+
}
|
|
9225
|
+
const ver = bytes[4];
|
|
9226
|
+
if (ver !== PROTOCOL_VERSION) {
|
|
9227
|
+
throw new DecodeError(`unsupported envelope version ${ver}`);
|
|
9228
|
+
}
|
|
9229
|
+
if (bytes.length < HEADER_LEN) {
|
|
9230
|
+
throw new DecodeError(`header too short for version: have ${bytes.length} bytes, need ${HEADER_LEN}`);
|
|
9231
|
+
}
|
|
9232
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
9233
|
+
const len = view.getUint32(0, true);
|
|
9234
|
+
const tyByte = bytes[5];
|
|
9235
|
+
if (tyByte > FRAME_TYPE_MAX) {
|
|
9236
|
+
throw new DecodeError(`unknown frame type byte ${tyByte}`);
|
|
9237
|
+
}
|
|
9238
|
+
const ty = tyByte;
|
|
9239
|
+
const flags = bytes[6];
|
|
9240
|
+
if ((flags & FLAG_RESERVED_MASK) !== 0) {
|
|
9241
|
+
throw new DecodeError(`reserved flag bits set in flags 0b${flags.toString(2)}`);
|
|
9242
|
+
}
|
|
9243
|
+
if ((flags & FLAG_PRIORITY_MASK) >> FLAG_PRIORITY_SHIFT === 3) {
|
|
9244
|
+
throw new DecodeError(`reserved priority bits set in flags 0b${flags.toString(2)}`);
|
|
9245
|
+
}
|
|
9246
|
+
if (isPureHeader(ty) && len !== 0) {
|
|
9247
|
+
throw new DecodeError(`pure-header frame ${FrameType[ty]} declared non-zero body length ${len}`);
|
|
9248
|
+
}
|
|
9249
|
+
const channel = view.getUint16(7, true);
|
|
9250
|
+
const corr = view.getBigUint64(9, true);
|
|
9251
|
+
return { len, ver, ty, flags, channel, corr };
|
|
9252
|
+
}
|
|
9253
|
+
function buildFrame(ty, flags, channel, corr, body) {
|
|
9254
|
+
return buildFrameWithVersion(PROTOCOL_VERSION, ty, flags, channel, corr, body);
|
|
9255
|
+
}
|
|
9256
|
+
function buildFrameWithVersion(ver, ty, flags, channel, corr, body) {
|
|
9257
|
+
if (body.length > MAX_FRAME_BODY_LEN) {
|
|
9258
|
+
throw new DecodeError(`frame body ${body.length} exceeds max ${MAX_FRAME_BODY_LEN}`);
|
|
9259
|
+
}
|
|
9260
|
+
if (isPureHeader(ty) && body.length !== 0) {
|
|
9261
|
+
throw new DecodeError(`pure-header frame ${FrameType[ty]} cannot carry a body`);
|
|
9262
|
+
}
|
|
9263
|
+
return {
|
|
9264
|
+
header: { len: body.length, ver, ty, flags, channel, corr },
|
|
9265
|
+
body
|
|
9266
|
+
};
|
|
9267
|
+
}
|
|
9268
|
+
function encodeFrame(frame) {
|
|
9269
|
+
const header = encodeHeader(frame.header);
|
|
9270
|
+
const out = new Uint8Array(header.length + frame.body.length);
|
|
9271
|
+
out.set(header, 0);
|
|
9272
|
+
out.set(frame.body, header.length);
|
|
9273
|
+
return out;
|
|
9274
|
+
}
|
|
9275
|
+
|
|
9276
|
+
// ../../node_modules/.bun/@cortexkit+subc-client@0.2.0/node_modules/@cortexkit/subc-client/src/socket.ts
|
|
9277
|
+
import net from "node:net";
|
|
9278
|
+
|
|
9279
|
+
class SocketClosedError extends Error {
|
|
9280
|
+
}
|
|
9281
|
+
|
|
9282
|
+
class SocketTimeoutError extends Error {
|
|
9283
|
+
}
|
|
9284
|
+
|
|
9285
|
+
class SocketWriteNotQueuedError extends Error {
|
|
9286
|
+
cause;
|
|
9287
|
+
constructor(message, cause) {
|
|
9288
|
+
super(message);
|
|
9289
|
+
this.cause = cause;
|
|
9290
|
+
}
|
|
9291
|
+
}
|
|
9292
|
+
|
|
9293
|
+
class SocketWriteQueuedError extends Error {
|
|
9294
|
+
cause;
|
|
9295
|
+
constructor(message, cause) {
|
|
9296
|
+
super(message);
|
|
9297
|
+
this.cause = cause;
|
|
9298
|
+
}
|
|
9299
|
+
}
|
|
9300
|
+
|
|
9301
|
+
class SubcSocket {
|
|
9302
|
+
sock;
|
|
9303
|
+
chunks = [];
|
|
9304
|
+
buffered = 0;
|
|
9305
|
+
waiter = null;
|
|
9306
|
+
closedErr = null;
|
|
9307
|
+
constructor(sock) {
|
|
9308
|
+
this.sock = sock;
|
|
9309
|
+
sock.on("data", (chunk) => {
|
|
9310
|
+
this.chunks.push(chunk);
|
|
9311
|
+
this.buffered += chunk.length;
|
|
9312
|
+
this.tryServe();
|
|
9313
|
+
});
|
|
9314
|
+
const fail = (err) => {
|
|
9315
|
+
if (!this.closedErr)
|
|
9316
|
+
this.closedErr = err;
|
|
9317
|
+
this.tryServe();
|
|
9318
|
+
};
|
|
9319
|
+
sock.on("error", (err) => fail(err instanceof Error ? err : new Error(String(err))));
|
|
9320
|
+
sock.on("end", () => fail(new SocketClosedError("subc closed the connection")));
|
|
9321
|
+
sock.on("close", () => fail(new SocketClosedError("subc connection closed")));
|
|
9322
|
+
}
|
|
9323
|
+
static connect(host, port, deadlineMs) {
|
|
9324
|
+
return new Promise((resolve2, reject) => {
|
|
9325
|
+
const sock = net.connect({ host, port });
|
|
9326
|
+
sock.setNoDelay(true);
|
|
9327
|
+
const timer = setTimeout(() => {
|
|
9328
|
+
sock.destroy();
|
|
9329
|
+
reject(new SocketTimeoutError(`timed out connecting to ${host}:${port}`));
|
|
9330
|
+
}, Math.max(0, deadlineMs - Date.now()));
|
|
9331
|
+
sock.once("connect", () => {
|
|
9332
|
+
clearTimeout(timer);
|
|
9333
|
+
resolve2(new SubcSocket(sock));
|
|
9334
|
+
});
|
|
9335
|
+
sock.once("error", (err) => {
|
|
9336
|
+
clearTimeout(timer);
|
|
9337
|
+
reject(err);
|
|
9338
|
+
});
|
|
9339
|
+
});
|
|
9340
|
+
}
|
|
9341
|
+
readExact(n, deadlineMs) {
|
|
9342
|
+
if (this.waiter) {
|
|
9343
|
+
return Promise.reject(new Error("concurrent readExact is not supported"));
|
|
9344
|
+
}
|
|
9345
|
+
if (n === 0)
|
|
9346
|
+
return Promise.resolve(new Uint8Array(0));
|
|
9347
|
+
return new Promise((resolve2, reject) => {
|
|
9348
|
+
let timer = null;
|
|
9349
|
+
if (Number.isFinite(deadlineMs)) {
|
|
9350
|
+
const remaining = deadlineMs - Date.now();
|
|
9351
|
+
if (remaining <= 0) {
|
|
9352
|
+
reject(new SocketTimeoutError(`timed out waiting for ${n} bytes`));
|
|
9353
|
+
return;
|
|
9354
|
+
}
|
|
9355
|
+
timer = setTimeout(() => {
|
|
9356
|
+
this.waiter = null;
|
|
9357
|
+
reject(new SocketTimeoutError(`timed out waiting for ${n} bytes`));
|
|
9358
|
+
}, remaining);
|
|
9359
|
+
}
|
|
9360
|
+
this.waiter = { need: n, resolve: resolve2, reject, timer };
|
|
9361
|
+
this.tryServe();
|
|
9362
|
+
});
|
|
9363
|
+
}
|
|
9364
|
+
async write(bytes, deadlineMs) {
|
|
9365
|
+
try {
|
|
9366
|
+
await this.writeTracked(bytes, deadlineMs).completed;
|
|
9367
|
+
} catch (err) {
|
|
9368
|
+
if (err instanceof SocketWriteNotQueuedError || err instanceof SocketWriteQueuedError) {
|
|
9369
|
+
throw err.cause ?? err;
|
|
9370
|
+
}
|
|
9371
|
+
throw err;
|
|
9372
|
+
}
|
|
9373
|
+
}
|
|
9374
|
+
writeTracked(bytes, deadlineMs) {
|
|
9375
|
+
if (this.closedErr) {
|
|
9376
|
+
return {
|
|
9377
|
+
queued: false,
|
|
9378
|
+
completed: Promise.reject(new SocketWriteNotQueuedError("subc socket was closed before bytes could be queued", this.closedErr))
|
|
9379
|
+
};
|
|
9380
|
+
}
|
|
9381
|
+
let queued = false;
|
|
9382
|
+
let settled = false;
|
|
9383
|
+
let timer = null;
|
|
9384
|
+
const completed = new Promise((resolve2, reject) => {
|
|
9385
|
+
const settle = (run) => {
|
|
9386
|
+
if (settled)
|
|
9387
|
+
return;
|
|
9388
|
+
settled = true;
|
|
9389
|
+
if (timer)
|
|
9390
|
+
clearTimeout(timer);
|
|
9391
|
+
run();
|
|
9392
|
+
};
|
|
9393
|
+
const remaining = deadlineMs - Date.now();
|
|
9394
|
+
if (remaining <= 0) {
|
|
9395
|
+
settle(() => reject(new SocketWriteNotQueuedError("timed out before bytes could be queued to subc", new SocketTimeoutError("timed out writing to subc"))));
|
|
9396
|
+
return;
|
|
9397
|
+
}
|
|
9398
|
+
timer = setTimeout(() => {
|
|
9399
|
+
const timeout = new SocketTimeoutError("timed out writing to subc");
|
|
9400
|
+
settle(() => reject(queued ? new SocketWriteQueuedError("timed out after bytes were handed to the subc socket", timeout) : new SocketWriteNotQueuedError("timed out before bytes could be queued to subc", timeout)));
|
|
9401
|
+
}, remaining);
|
|
9402
|
+
try {
|
|
9403
|
+
this.sock.write(Buffer.from(bytes), (err) => {
|
|
9404
|
+
settle(() => {
|
|
9405
|
+
if (err) {
|
|
9406
|
+
reject(new SocketWriteQueuedError("subc socket reported a write error after bytes were handed to the socket", err instanceof Error ? err : new Error(String(err))));
|
|
9407
|
+
} else {
|
|
9408
|
+
resolve2();
|
|
9409
|
+
}
|
|
9410
|
+
});
|
|
9411
|
+
});
|
|
9412
|
+
queued = true;
|
|
9413
|
+
} catch (err) {
|
|
9414
|
+
settle(() => reject(new SocketWriteNotQueuedError("subc socket write threw before bytes could be queued", err instanceof Error ? err : new Error(String(err)))));
|
|
9415
|
+
}
|
|
9416
|
+
});
|
|
9417
|
+
return { queued, completed };
|
|
9418
|
+
}
|
|
9419
|
+
close() {
|
|
9420
|
+
this.sock.destroy();
|
|
9421
|
+
}
|
|
9422
|
+
tryServe() {
|
|
9423
|
+
const w = this.waiter;
|
|
9424
|
+
if (!w)
|
|
9425
|
+
return;
|
|
9426
|
+
if (this.buffered >= w.need) {
|
|
9427
|
+
const out = this.take(w.need);
|
|
9428
|
+
this.waiter = null;
|
|
9429
|
+
if (w.timer)
|
|
9430
|
+
clearTimeout(w.timer);
|
|
9431
|
+
w.resolve(out);
|
|
9432
|
+
return;
|
|
9433
|
+
}
|
|
9434
|
+
if (this.closedErr) {
|
|
9435
|
+
this.waiter = null;
|
|
9436
|
+
if (w.timer)
|
|
9437
|
+
clearTimeout(w.timer);
|
|
9438
|
+
w.reject(this.closedErr);
|
|
9439
|
+
}
|
|
9440
|
+
}
|
|
9441
|
+
take(n) {
|
|
9442
|
+
const out = Buffer.allocUnsafe(n);
|
|
9443
|
+
let off = 0;
|
|
9444
|
+
while (off < n) {
|
|
9445
|
+
const head = this.chunks[0];
|
|
9446
|
+
const want = n - off;
|
|
9447
|
+
if (head.length <= want) {
|
|
9448
|
+
head.copy(out, off);
|
|
9449
|
+
off += head.length;
|
|
9450
|
+
this.chunks.shift();
|
|
9451
|
+
} else {
|
|
9452
|
+
head.copy(out, off, 0, want);
|
|
9453
|
+
this.chunks[0] = head.subarray(want);
|
|
9454
|
+
off += want;
|
|
9455
|
+
}
|
|
9456
|
+
}
|
|
9457
|
+
this.buffered -= n;
|
|
9458
|
+
return out;
|
|
9459
|
+
}
|
|
9460
|
+
}
|
|
9461
|
+
|
|
9462
|
+
// ../../node_modules/.bun/@cortexkit+subc-client@0.2.0/node_modules/@cortexkit/subc-client/src/client.ts
|
|
9463
|
+
var DEFAULT_HANDSHAKE_TIMEOUT_MS = 1e4;
|
|
9464
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 30000;
|
|
9465
|
+
var BODY_READ_TIMEOUT_MS = 30000;
|
|
9466
|
+
var EMPTY_BODY = new Uint8Array(0);
|
|
9467
|
+
var DEFAULT_MANAGED_TARGET_KIND = "management_surface";
|
|
9468
|
+
var SUBC_MODULE_ID_ENV = "SUBC_MODULE_ID";
|
|
9469
|
+
var SUBC_LAUNCH_NONCE_ENV = "SUBC_LAUNCH_NONCE";
|
|
9470
|
+
var DEFAULT_RECONNECT_BACKOFF = {
|
|
9471
|
+
baseMs: 100,
|
|
9472
|
+
capMs: 2000,
|
|
9473
|
+
maxAttempts: 6
|
|
9474
|
+
};
|
|
9475
|
+
|
|
9476
|
+
class SubcCallError extends Error {
|
|
9477
|
+
kind;
|
|
9478
|
+
code;
|
|
9479
|
+
cause;
|
|
9480
|
+
constructor(kind, message, code, cause) {
|
|
9481
|
+
super(message);
|
|
9482
|
+
this.kind = kind;
|
|
9483
|
+
this.code = code;
|
|
9484
|
+
this.cause = cause;
|
|
9485
|
+
this.name = "SubcCallError";
|
|
9486
|
+
}
|
|
9487
|
+
}
|
|
9488
|
+
|
|
9489
|
+
class SubcError extends Error {
|
|
9490
|
+
code;
|
|
9491
|
+
constructor(message, code) {
|
|
9492
|
+
super(message);
|
|
9493
|
+
this.code = code;
|
|
9494
|
+
}
|
|
9495
|
+
}
|
|
9496
|
+
|
|
9497
|
+
class SubcClient {
|
|
9498
|
+
sock;
|
|
9499
|
+
currentConn;
|
|
9500
|
+
opts;
|
|
9501
|
+
nextCorr = 1n;
|
|
9502
|
+
pending = new Map;
|
|
9503
|
+
routes = new Map;
|
|
9504
|
+
closedErr = null;
|
|
9505
|
+
closeStarted = false;
|
|
9506
|
+
reconnecting = null;
|
|
9507
|
+
generation = 1;
|
|
9508
|
+
constructor(sock, currentConn, opts) {
|
|
9509
|
+
this.sock = sock;
|
|
9510
|
+
this.currentConn = currentConn;
|
|
9511
|
+
this.opts = opts;
|
|
9512
|
+
this.readLoop(sock, this.generation);
|
|
9513
|
+
}
|
|
9514
|
+
get conn() {
|
|
9515
|
+
return this.currentConn;
|
|
9516
|
+
}
|
|
9517
|
+
static async connect(opts) {
|
|
9518
|
+
const normalized = normalizeConnectOptions(opts);
|
|
9519
|
+
const opened = await SubcClient.openConnection(normalized);
|
|
9520
|
+
return new SubcClient(opened.sock, opened.conn, normalized);
|
|
9521
|
+
}
|
|
9522
|
+
async catalogList(moduleId) {
|
|
9523
|
+
const body = this.encode(moduleId === undefined ? { op: "catalog.list" } : { op: "catalog.list", module_id: moduleId });
|
|
9524
|
+
const reply = await this.controlRpc(body);
|
|
9525
|
+
const parsed = this.parseJson(reply);
|
|
9526
|
+
return parsed.modules ?? [];
|
|
9527
|
+
}
|
|
9528
|
+
async routeOpen(target, identity, opts = {}) {
|
|
9529
|
+
const consumerIdentity = routeOpenConsumerIdentity(opts);
|
|
9530
|
+
const body = this.encode({
|
|
9531
|
+
op: "route.open",
|
|
9532
|
+
target,
|
|
9533
|
+
identity,
|
|
9534
|
+
...consumerIdentity ? { consumer_identity: consumerIdentity } : {}
|
|
9535
|
+
});
|
|
9536
|
+
const reply = await this.controlRpc(body);
|
|
9537
|
+
const parsed = this.parseJson(reply);
|
|
9538
|
+
if (typeof parsed.route_channel !== "number") {
|
|
9539
|
+
throw new SubcError(`route.open returned no route_channel: ${JSON.stringify(parsed)}`);
|
|
9540
|
+
}
|
|
9541
|
+
return parsed.route_channel;
|
|
9542
|
+
}
|
|
9543
|
+
async request(routeChannel, body, opts = {}) {
|
|
9544
|
+
const bytes = body instanceof Uint8Array ? body : this.encode(body);
|
|
9545
|
+
const priority = opts.priority ?? 1 /* Interactive */;
|
|
9546
|
+
const reply = await this.send(routeChannel, bytes, priority, opts.timeoutMs, opts.onProgress);
|
|
9547
|
+
return this.parseJson(reply);
|
|
9548
|
+
}
|
|
9549
|
+
async call(moduleId, method, params, opts = {}) {
|
|
9550
|
+
const body = params === undefined ? { method } : { method, params };
|
|
9551
|
+
for (;; ) {
|
|
9552
|
+
const routeChannel = await this.cachedRouteChannel(moduleId, opts);
|
|
9553
|
+
try {
|
|
9554
|
+
return await this.managedRequest(routeChannel, body, opts);
|
|
9555
|
+
} catch (err) {
|
|
9556
|
+
if (!(err instanceof SubcCallError))
|
|
9557
|
+
throw this.terminalCallError("managed call failed", err);
|
|
9558
|
+
if (err.kind === "not_sent") {
|
|
9559
|
+
try {
|
|
9560
|
+
await this.reconnectAfterDrop(err);
|
|
9561
|
+
} catch (reconnectErr) {
|
|
9562
|
+
throw this.notSentRecoveryError("managed call was not sent", reconnectErr);
|
|
9563
|
+
}
|
|
9564
|
+
continue;
|
|
9565
|
+
}
|
|
9566
|
+
if (err.kind === "outcome_unknown") {
|
|
9567
|
+
this.scheduleReconnectAfterDrop(err);
|
|
9568
|
+
}
|
|
9569
|
+
throw err;
|
|
9570
|
+
}
|
|
9571
|
+
}
|
|
9572
|
+
}
|
|
9573
|
+
subscribe(routeChannel, body, onEvent, opts = {}) {
|
|
9574
|
+
const bytes = body instanceof Uint8Array ? body : this.encode(body);
|
|
9575
|
+
const priority = opts.priority ?? 1 /* Interactive */;
|
|
9576
|
+
const corr = this.nextCorr++;
|
|
9577
|
+
const key = `${routeChannel}:${corr}`;
|
|
9578
|
+
const closed = new Promise((resolve2, reject) => {
|
|
9579
|
+
if (this.closedErr) {
|
|
9580
|
+
reject(this.closedErr);
|
|
9581
|
+
return;
|
|
9582
|
+
}
|
|
9583
|
+
this.pending.set(key, {
|
|
9584
|
+
channel: routeChannel,
|
|
9585
|
+
resolve: () => resolve2(),
|
|
9586
|
+
reject,
|
|
9587
|
+
onProgress: onEvent,
|
|
9588
|
+
timer: null,
|
|
9589
|
+
subscription: true
|
|
9590
|
+
});
|
|
9591
|
+
const frame = buildFrame(0 /* Request */, buildFlags(false, priority, false), routeChannel, corr, bytes);
|
|
9592
|
+
this.sock.write(encodeFrame(frame), Date.now() + DEFAULT_REQUEST_TIMEOUT_MS).catch((err) => {
|
|
9593
|
+
const p = this.pending.get(key);
|
|
9594
|
+
if (p)
|
|
9595
|
+
this.rejectPending(key, p, err instanceof Error ? err : new SubcError(String(err)));
|
|
9596
|
+
});
|
|
9597
|
+
});
|
|
9598
|
+
let cancelled = false;
|
|
9599
|
+
const unsubscribe = () => {
|
|
9600
|
+
if (cancelled)
|
|
9601
|
+
return;
|
|
9602
|
+
cancelled = true;
|
|
9603
|
+
const cancel = buildFrame(6 /* Cancel */, buildFlags(false, priority, false), routeChannel, corr, EMPTY_BODY);
|
|
9604
|
+
this.sock.write(encodeFrame(cancel), Date.now() + DEFAULT_REQUEST_TIMEOUT_MS).catch(() => {});
|
|
9605
|
+
};
|
|
9606
|
+
return { unsubscribe, closed };
|
|
9607
|
+
}
|
|
9608
|
+
async closeRoute(target, identity, opts = {}) {
|
|
9609
|
+
const key = routeCacheKey(target, identity, routeOpenConsumerIdentity(opts));
|
|
9610
|
+
const cached = this.routes.get(key);
|
|
9611
|
+
if (!cached)
|
|
9612
|
+
return;
|
|
9613
|
+
cached.closed = true;
|
|
9614
|
+
this.routes.delete(key);
|
|
9615
|
+
const channel = cached.channel;
|
|
9616
|
+
cached.channel = null;
|
|
9617
|
+
if (channel !== null)
|
|
9618
|
+
await this.closeRouteChannel(channel, opts);
|
|
9619
|
+
}
|
|
9620
|
+
async closeRouteChannel(channel, opts = {}) {
|
|
9621
|
+
if (channel === 0)
|
|
9622
|
+
return;
|
|
9623
|
+
if (opts.drain) {
|
|
9624
|
+
await this.drainUnaryOnChannel(channel);
|
|
9625
|
+
}
|
|
9626
|
+
this.failChannel(channel, new SubcError("route closed by closeRoute", "route_closed"));
|
|
9627
|
+
this.sendRouteGoodbye(channel);
|
|
9628
|
+
}
|
|
9629
|
+
close() {
|
|
9630
|
+
this.closeStarted = true;
|
|
9631
|
+
this.fail(new SubcError("client closed"));
|
|
9632
|
+
this.sock.close();
|
|
9633
|
+
}
|
|
9634
|
+
drainUnaryOnChannel(channel) {
|
|
9635
|
+
const waiters = [];
|
|
9636
|
+
for (const pending of this.pending.values()) {
|
|
9637
|
+
if (pending.channel === channel && !pending.subscription) {
|
|
9638
|
+
waiters.push(new Promise((resolve2) => {
|
|
9639
|
+
const prev = pending.onSettle;
|
|
9640
|
+
pending.onSettle = () => {
|
|
9641
|
+
prev?.();
|
|
9642
|
+
resolve2();
|
|
9643
|
+
};
|
|
9644
|
+
}));
|
|
9645
|
+
}
|
|
9646
|
+
}
|
|
9647
|
+
return Promise.all(waiters).then(() => {
|
|
9648
|
+
return;
|
|
9649
|
+
});
|
|
9650
|
+
}
|
|
9651
|
+
sendRouteGoodbye(channel) {
|
|
9652
|
+
if (this.closedErr)
|
|
9653
|
+
return;
|
|
9654
|
+
const goodbye = buildFrame(11 /* Goodbye */, buildFlags(false, 1 /* Interactive */, false), channel, 0n, EMPTY_BODY);
|
|
9655
|
+
this.sock.write(encodeFrame(goodbye), Date.now() + DEFAULT_REQUEST_TIMEOUT_MS).catch(() => {});
|
|
9656
|
+
}
|
|
9657
|
+
static async openConnection(opts) {
|
|
9658
|
+
const conn = await readConnectionFile(opts.connectionFile);
|
|
9659
|
+
const deadline = Date.now() + (opts.handshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT_MS);
|
|
9660
|
+
const endpoint = conn.endpoints[0];
|
|
9661
|
+
const sock = await SubcSocket.connect(endpoint.host, endpoint.port, deadline);
|
|
9662
|
+
try {
|
|
9663
|
+
await authenticateClient(sock, conn, deadline);
|
|
9664
|
+
} catch (err) {
|
|
9665
|
+
sock.close();
|
|
9666
|
+
throw err;
|
|
9667
|
+
}
|
|
9668
|
+
return { sock, conn };
|
|
9669
|
+
}
|
|
9670
|
+
async controlRpc(body) {
|
|
9671
|
+
return this.send(0, body, 1 /* Interactive */, undefined, undefined);
|
|
9672
|
+
}
|
|
9673
|
+
send(channel, body, priority, timeoutMs, onProgress) {
|
|
9674
|
+
if (this.closedErr)
|
|
9675
|
+
return Promise.reject(this.closedErr);
|
|
9676
|
+
const corr = this.nextCorr++;
|
|
9677
|
+
const key = `${channel}:${corr}`;
|
|
9678
|
+
const frame = buildFrame(0 /* Request */, buildFlags(false, priority, false), channel, corr, body);
|
|
9679
|
+
return new Promise((resolve2, reject) => {
|
|
9680
|
+
const ms = timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
9681
|
+
const pending = {
|
|
9682
|
+
channel,
|
|
9683
|
+
resolve: resolve2,
|
|
9684
|
+
reject,
|
|
9685
|
+
onProgress,
|
|
9686
|
+
timer: null
|
|
9687
|
+
};
|
|
9688
|
+
pending.timer = setTimeout(() => {
|
|
9689
|
+
this.rejectPending(key, pending, new SubcError(`request on channel ${channel} timed out after ${ms}ms`));
|
|
9690
|
+
}, ms);
|
|
9691
|
+
this.pending.set(key, pending);
|
|
9692
|
+
this.sock.write(encodeFrame(frame), Date.now() + ms).catch((err) => {
|
|
9693
|
+
const p = this.pending.get(key);
|
|
9694
|
+
if (p)
|
|
9695
|
+
this.rejectPending(key, p, err instanceof Error ? err : new SubcError(String(err)));
|
|
9696
|
+
});
|
|
9697
|
+
});
|
|
9698
|
+
}
|
|
9699
|
+
async managedRequest(routeChannel, body, opts) {
|
|
9700
|
+
const bytes = body instanceof Uint8Array ? body : this.encode(body);
|
|
9701
|
+
const priority = opts.priority ?? 1 /* Interactive */;
|
|
9702
|
+
try {
|
|
9703
|
+
const reply = await this.sendManaged(routeChannel, bytes, priority, opts.timeoutMs, opts.onProgress);
|
|
9704
|
+
return this.parseJson(reply);
|
|
9705
|
+
} catch (err) {
|
|
9706
|
+
if (err instanceof SubcCallError)
|
|
9707
|
+
throw err;
|
|
9708
|
+
throw this.terminalCallError("managed call failed", err);
|
|
9709
|
+
}
|
|
9710
|
+
}
|
|
9711
|
+
sendManaged(channel, body, priority, timeoutMs, onProgress) {
|
|
9712
|
+
if (this.closedErr) {
|
|
9713
|
+
return Promise.reject(this.notSentCallError("request was not sent because the subc connection was already closed", this.closedErr));
|
|
9714
|
+
}
|
|
9715
|
+
const corr = this.nextCorr++;
|
|
9716
|
+
const key = `${channel}:${corr}`;
|
|
9717
|
+
const frame = buildFrame(0 /* Request */, buildFlags(false, priority, false), channel, corr, body);
|
|
9718
|
+
let handedToSocket = false;
|
|
9719
|
+
const classifyFailure = (err) => {
|
|
9720
|
+
if (!handedToSocket) {
|
|
9721
|
+
return this.notSentCallError("request bytes were not queued to the subc socket", err);
|
|
9722
|
+
}
|
|
9723
|
+
return this.outcomeUnknownCallError("connection dropped before the managed call returned a response", err);
|
|
9724
|
+
};
|
|
9725
|
+
return new Promise((resolve2, reject) => {
|
|
9726
|
+
const ms = timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
9727
|
+
const pending = {
|
|
9728
|
+
channel,
|
|
9729
|
+
resolve: resolve2,
|
|
9730
|
+
reject,
|
|
9731
|
+
onProgress,
|
|
9732
|
+
timer: null,
|
|
9733
|
+
classifyFailure
|
|
9734
|
+
};
|
|
9735
|
+
pending.timer = setTimeout(() => {
|
|
9736
|
+
this.rejectPending(key, pending, new SubcError(`request on channel ${channel} timed out after ${ms}ms`));
|
|
9737
|
+
}, ms);
|
|
9738
|
+
this.pending.set(key, pending);
|
|
9739
|
+
const write = this.sock.writeTracked(encodeFrame(frame), Date.now() + ms);
|
|
9740
|
+
handedToSocket = write.queued;
|
|
9741
|
+
write.completed.catch((err) => {
|
|
9742
|
+
const p = this.pending.get(key);
|
|
9743
|
+
if (p)
|
|
9744
|
+
this.rejectPending(key, p, err instanceof Error ? err : new SubcError(String(err)));
|
|
9745
|
+
});
|
|
9746
|
+
});
|
|
9747
|
+
}
|
|
9748
|
+
async cachedRouteChannel(moduleId, opts) {
|
|
9749
|
+
const identity = opts.identity ?? this.opts.identity;
|
|
9750
|
+
if (!identity) {
|
|
9751
|
+
throw new SubcCallError("terminal", "managed call requires a BindIdentity in SubcClient.connect({ identity }) or call(..., { identity })", "missing_identity");
|
|
9752
|
+
}
|
|
9753
|
+
const target = { kind: opts.targetKind ?? this.opts.targetKind, module_id: moduleId };
|
|
9754
|
+
const consumerIdentity = routeOpenConsumerIdentity(opts);
|
|
9755
|
+
const key = routeCacheKey(target, identity, consumerIdentity);
|
|
9756
|
+
let cached = this.routes.get(key);
|
|
9757
|
+
if (!cached) {
|
|
9758
|
+
cached = {
|
|
9759
|
+
key,
|
|
9760
|
+
moduleId,
|
|
9761
|
+
target,
|
|
9762
|
+
identity,
|
|
9763
|
+
consumerIdentity,
|
|
9764
|
+
channel: null,
|
|
9765
|
+
generation: 0,
|
|
9766
|
+
opening: null
|
|
9767
|
+
};
|
|
9768
|
+
this.routes.set(key, cached);
|
|
9769
|
+
}
|
|
9770
|
+
if (cached.channel !== null && cached.generation === this.generation && !this.closedErr) {
|
|
9771
|
+
return cached.channel;
|
|
9772
|
+
}
|
|
9773
|
+
if (!cached.opening) {
|
|
9774
|
+
cached.opening = this.openCachedRoute(cached).finally(() => {
|
|
9775
|
+
cached.opening = null;
|
|
9776
|
+
});
|
|
9777
|
+
}
|
|
9778
|
+
return cached.opening;
|
|
9779
|
+
}
|
|
9780
|
+
async openCachedRoute(cached) {
|
|
9781
|
+
for (;; ) {
|
|
9782
|
+
if (cached.closed)
|
|
9783
|
+
throw this.routeClosedDuringOpen();
|
|
9784
|
+
try {
|
|
9785
|
+
await this.ensureConnectedForManaged();
|
|
9786
|
+
} catch (err) {
|
|
9787
|
+
throw this.notSentRecoveryError("route.open could not run because reconnect failed", err);
|
|
9788
|
+
}
|
|
9789
|
+
if (cached.channel !== null && cached.generation === this.generation && !this.closedErr) {
|
|
9790
|
+
return cached.channel;
|
|
9791
|
+
}
|
|
9792
|
+
try {
|
|
9793
|
+
const channel = await this.routeOpen(cached.target, cached.identity, {
|
|
9794
|
+
consumerIdentity: cached.consumerIdentity ?? null
|
|
9795
|
+
});
|
|
9796
|
+
if (cached.closed) {
|
|
9797
|
+
this.sendRouteGoodbye(channel);
|
|
9798
|
+
throw this.routeClosedDuringOpen();
|
|
9799
|
+
}
|
|
9800
|
+
cached.channel = channel;
|
|
9801
|
+
cached.generation = this.generation;
|
|
9802
|
+
return channel;
|
|
9803
|
+
} catch (err) {
|
|
9804
|
+
if (err instanceof SubcCallError && err.code === "route_closed")
|
|
9805
|
+
throw err;
|
|
9806
|
+
if (!this.closeStarted && isConsumerReconnectTransient(err)) {
|
|
9807
|
+
try {
|
|
9808
|
+
await this.reconnectAfterDrop(err);
|
|
9809
|
+
} catch (reconnectErr) {
|
|
9810
|
+
throw this.notSentRecoveryError("route.open was not sent and reconnect failed", reconnectErr);
|
|
9811
|
+
}
|
|
9812
|
+
continue;
|
|
9813
|
+
}
|
|
9814
|
+
throw this.terminalCallError(`route.open failed for module ${cached.moduleId}`, err);
|
|
9815
|
+
}
|
|
9816
|
+
}
|
|
9817
|
+
}
|
|
9818
|
+
async ensureConnectedForManaged() {
|
|
9819
|
+
if (this.closeStarted)
|
|
9820
|
+
throw new SubcError("client closed");
|
|
9821
|
+
if (this.reconnecting)
|
|
9822
|
+
await this.reconnecting;
|
|
9823
|
+
if (this.closedErr)
|
|
9824
|
+
await this.reconnectAfterDrop(this.closedErr);
|
|
9825
|
+
}
|
|
9826
|
+
scheduleReconnectAfterDrop(err) {
|
|
9827
|
+
if (this.closeStarted || this.reconnecting)
|
|
9828
|
+
return;
|
|
9829
|
+
this.reconnectAfterDrop(err).catch(() => {});
|
|
9830
|
+
}
|
|
9831
|
+
reconnectAfterDrop(trigger) {
|
|
9832
|
+
if (this.closeStarted)
|
|
9833
|
+
return Promise.reject(new SubcError("client closed"));
|
|
9834
|
+
if (this.reconnecting)
|
|
9835
|
+
return this.reconnecting;
|
|
9836
|
+
const promise = this.reconnectWithRetry(trigger).finally(() => {
|
|
9837
|
+
if (this.reconnecting === promise)
|
|
9838
|
+
this.reconnecting = null;
|
|
9839
|
+
});
|
|
9840
|
+
this.reconnecting = promise;
|
|
9841
|
+
return promise;
|
|
9842
|
+
}
|
|
9843
|
+
async reconnectWithRetry(_trigger) {
|
|
9844
|
+
let attempt = 0;
|
|
9845
|
+
let delay = this.opts.reconnectBackoff.baseMs;
|
|
9846
|
+
for (;; ) {
|
|
9847
|
+
if (this.closeStarted)
|
|
9848
|
+
throw new SubcError("client closed");
|
|
9849
|
+
attempt += 1;
|
|
9850
|
+
try {
|
|
9851
|
+
const opened = await SubcClient.openConnection(this.opts);
|
|
9852
|
+
if (this.closeStarted) {
|
|
9853
|
+
opened.sock.close();
|
|
9854
|
+
throw new SubcError("client closed");
|
|
9855
|
+
}
|
|
9856
|
+
this.replaceConnection(opened);
|
|
9857
|
+
await this.reopenCachedRoutes();
|
|
9858
|
+
return;
|
|
9859
|
+
} catch (err) {
|
|
9860
|
+
if (!isConsumerReconnectTransient(err) || attempt >= this.opts.reconnectBackoff.maxAttempts) {
|
|
9861
|
+
throw err;
|
|
9862
|
+
}
|
|
9863
|
+
await this.opts.sleep(delay);
|
|
9864
|
+
delay = Math.min(delay * 2, this.opts.reconnectBackoff.capMs);
|
|
9865
|
+
}
|
|
9866
|
+
}
|
|
9867
|
+
}
|
|
9868
|
+
replaceConnection(opened) {
|
|
9869
|
+
this.sock.close();
|
|
9870
|
+
this.sock = opened.sock;
|
|
9871
|
+
this.currentConn = opened.conn;
|
|
9872
|
+
this.closedErr = null;
|
|
9873
|
+
this.generation += 1;
|
|
9874
|
+
this.readLoop(opened.sock, this.generation);
|
|
9875
|
+
}
|
|
9876
|
+
async reopenCachedRoutes() {
|
|
9877
|
+
for (const cached of this.routes.values()) {
|
|
9878
|
+
cached.channel = null;
|
|
9879
|
+
cached.generation = 0;
|
|
9880
|
+
}
|
|
9881
|
+
for (const cached of this.routes.values()) {
|
|
9882
|
+
if (cached.closed)
|
|
9883
|
+
continue;
|
|
9884
|
+
const channel = await this.routeOpen(cached.target, cached.identity);
|
|
9885
|
+
if (cached.closed) {
|
|
9886
|
+
this.sendRouteGoodbye(channel);
|
|
9887
|
+
continue;
|
|
9888
|
+
}
|
|
9889
|
+
cached.channel = channel;
|
|
9890
|
+
cached.generation = this.generation;
|
|
9891
|
+
}
|
|
9892
|
+
}
|
|
9893
|
+
routeClosedDuringOpen() {
|
|
9894
|
+
return new SubcCallError("not_sent", "route was closed before route.open completed", "route_closed");
|
|
9895
|
+
}
|
|
9896
|
+
async readLoop(sock, generation) {
|
|
9897
|
+
try {
|
|
9898
|
+
for (;; ) {
|
|
9899
|
+
const headerBytes = await sock.readExact(HEADER_LEN, Number.POSITIVE_INFINITY);
|
|
9900
|
+
const header = decodeHeader(headerBytes);
|
|
9901
|
+
const body = header.len === 0 ? new Uint8Array(0) : await sock.readExact(header.len, Date.now() + BODY_READ_TIMEOUT_MS);
|
|
9902
|
+
this.dispatch({ header, body });
|
|
9903
|
+
}
|
|
9904
|
+
} catch (err) {
|
|
9905
|
+
if (this.sock === sock && this.generation === generation) {
|
|
9906
|
+
this.fail(err instanceof Error ? err : new SubcError(String(err)));
|
|
9907
|
+
}
|
|
9908
|
+
}
|
|
9909
|
+
}
|
|
9910
|
+
dispatch(frame) {
|
|
9911
|
+
const key = `${frame.header.channel}:${frame.header.corr}`;
|
|
9912
|
+
const pending = this.pending.get(key);
|
|
9913
|
+
if (pending) {
|
|
9914
|
+
switch (frame.header.ty) {
|
|
9915
|
+
case 2 /* Push */:
|
|
9916
|
+
case 3 /* StreamData */:
|
|
9917
|
+
pending.onProgress?.(frame.body);
|
|
9918
|
+
return;
|
|
9919
|
+
case 1 /* Response */:
|
|
9920
|
+
case 4 /* StreamEnd */:
|
|
9921
|
+
this.settle(key, pending, () => pending.resolve(frame));
|
|
9922
|
+
return;
|
|
9923
|
+
case 5 /* Error */:
|
|
9924
|
+
this.settle(key, pending, () => pending.reject(this.errorFromFrame(frame)));
|
|
9925
|
+
return;
|
|
9926
|
+
default:
|
|
9927
|
+
return;
|
|
9928
|
+
}
|
|
9929
|
+
}
|
|
9930
|
+
if (frame.header.ty === 11 /* Goodbye */) {
|
|
9931
|
+
this.failChannel(frame.header.channel, new SubcError("route closed by subc (GOODBYE)"));
|
|
9932
|
+
return;
|
|
9933
|
+
}
|
|
9934
|
+
}
|
|
9935
|
+
settle(key, pending, run) {
|
|
9936
|
+
this.pending.delete(key);
|
|
9937
|
+
if (pending.timer)
|
|
9938
|
+
clearTimeout(pending.timer);
|
|
9939
|
+
run();
|
|
9940
|
+
pending.onSettle?.();
|
|
9941
|
+
}
|
|
9942
|
+
rejectPending(key, pending, err) {
|
|
9943
|
+
this.settle(key, pending, () => pending.reject(pending.classifyFailure?.(err) ?? err));
|
|
9944
|
+
}
|
|
9945
|
+
errorFromFrame(frame) {
|
|
9946
|
+
try {
|
|
9947
|
+
const parsed = JSON.parse(Buffer.from(frame.body).toString("utf8"));
|
|
9948
|
+
return new SubcError(parsed.message ?? "subc error", parsed.code);
|
|
9949
|
+
} catch {
|
|
9950
|
+
return new SubcError(Buffer.from(frame.body).toString("utf8") || "subc error");
|
|
9951
|
+
}
|
|
9952
|
+
}
|
|
9953
|
+
failChannel(channel, err) {
|
|
9954
|
+
for (const [key, pending] of this.pending) {
|
|
9955
|
+
if (pending.channel === channel) {
|
|
9956
|
+
this.rejectPending(key, pending, err);
|
|
9957
|
+
}
|
|
9958
|
+
}
|
|
9959
|
+
}
|
|
9960
|
+
fail(err) {
|
|
9961
|
+
if (!this.closedErr)
|
|
9962
|
+
this.closedErr = err;
|
|
9963
|
+
for (const [key, pending] of this.pending) {
|
|
9964
|
+
this.rejectPending(key, pending, err);
|
|
9965
|
+
}
|
|
9966
|
+
}
|
|
9967
|
+
notSentCallError(message, cause) {
|
|
9968
|
+
return new SubcCallError("not_sent", `${message}${causeMessage(cause)}`, errorCode(cause), cause);
|
|
9969
|
+
}
|
|
9970
|
+
outcomeUnknownCallError(message, cause) {
|
|
9971
|
+
return new SubcCallError("outcome_unknown", `${message}${causeMessage(cause)}`, errorCode(cause), cause);
|
|
9972
|
+
}
|
|
9973
|
+
terminalCallError(message, cause) {
|
|
9974
|
+
if (cause instanceof SubcCallError)
|
|
9975
|
+
return cause;
|
|
9976
|
+
return new SubcCallError("terminal", `${message}${causeMessage(cause)}`, errorCode(cause), cause);
|
|
9977
|
+
}
|
|
9978
|
+
notSentRecoveryError(message, cause) {
|
|
9979
|
+
if (cause instanceof SubcCallError)
|
|
9980
|
+
return cause;
|
|
9981
|
+
if (isConsumerReconnectTransient(cause))
|
|
9982
|
+
return this.notSentCallError(message, cause);
|
|
9983
|
+
return this.terminalCallError(message, cause);
|
|
9984
|
+
}
|
|
9985
|
+
encode(value) {
|
|
9986
|
+
return new Uint8Array(Buffer.from(JSON.stringify(value), "utf8"));
|
|
9987
|
+
}
|
|
9988
|
+
parseJson(frame) {
|
|
9989
|
+
return JSON.parse(Buffer.from(frame.body).toString("utf8"));
|
|
9990
|
+
}
|
|
9991
|
+
}
|
|
9992
|
+
function isConsumerReconnectTransient(err) {
|
|
9993
|
+
if (err instanceof SocketClosedError || err instanceof SocketTimeoutError)
|
|
9994
|
+
return true;
|
|
9995
|
+
if (err instanceof SocketWriteNotQueuedError || err instanceof SocketWriteQueuedError)
|
|
9996
|
+
return true;
|
|
9997
|
+
if (err instanceof SubcCallError)
|
|
9998
|
+
return err.kind === "not_sent" || err.kind === "outcome_unknown";
|
|
9999
|
+
if (err instanceof SubcError || err instanceof ConnectionFileError || err instanceof AuthError)
|
|
10000
|
+
return false;
|
|
10001
|
+
const code = errorCode(err);
|
|
10002
|
+
return code === "ECONNREFUSED" || code === "ECONNRESET" || code === "EPIPE" || code === "ETIMEDOUT" || code === "ENOENT";
|
|
10003
|
+
}
|
|
10004
|
+
async function connectionFileExists(path) {
|
|
10005
|
+
try {
|
|
10006
|
+
await fs2.access(path);
|
|
10007
|
+
return true;
|
|
10008
|
+
} catch {
|
|
10009
|
+
return false;
|
|
10010
|
+
}
|
|
10011
|
+
}
|
|
10012
|
+
function normalizeConnectOptions(opts) {
|
|
10013
|
+
return {
|
|
10014
|
+
connectionFile: opts.connectionFile,
|
|
10015
|
+
handshakeTimeoutMs: opts.handshakeTimeoutMs,
|
|
10016
|
+
identity: opts.identity,
|
|
10017
|
+
targetKind: opts.targetKind ?? DEFAULT_MANAGED_TARGET_KIND,
|
|
10018
|
+
reconnectBackoff: opts.reconnectBackoff ?? DEFAULT_RECONNECT_BACKOFF,
|
|
10019
|
+
sleep: opts.sleep ?? ((ms) => new Promise((resolve2) => setTimeout(resolve2, ms)))
|
|
10020
|
+
};
|
|
10021
|
+
}
|
|
10022
|
+
function routeCacheKey(target, identity, consumerIdentity) {
|
|
10023
|
+
const consumerPart = consumerIdentity ? `${consumerIdentity.module_id}\x00${consumerIdentity.launch_nonce}` : "";
|
|
10024
|
+
return `${target.kind}\x00${target.module_id}\x00${identity.project_root}\x00${identity.harness}\x00${identity.session}\x00${consumerPart}`;
|
|
10025
|
+
}
|
|
10026
|
+
function routeOpenConsumerIdentity(opts = {}) {
|
|
10027
|
+
if (opts.consumerIdentity !== undefined)
|
|
10028
|
+
return opts.consumerIdentity ?? undefined;
|
|
10029
|
+
const moduleId = process.env[SUBC_MODULE_ID_ENV];
|
|
10030
|
+
const launchNonce = process.env[SUBC_LAUNCH_NONCE_ENV];
|
|
10031
|
+
if (!moduleId || !launchNonce)
|
|
10032
|
+
return;
|
|
10033
|
+
return { module_id: moduleId, launch_nonce: launchNonce };
|
|
10034
|
+
}
|
|
10035
|
+
function errorCode(err) {
|
|
10036
|
+
if (typeof err === "object" && err !== null && "code" in err) {
|
|
10037
|
+
const code = err.code;
|
|
10038
|
+
if (typeof code === "string")
|
|
10039
|
+
return code;
|
|
10040
|
+
}
|
|
10041
|
+
return;
|
|
10042
|
+
}
|
|
10043
|
+
function causeMessage(cause) {
|
|
10044
|
+
if (cause === undefined)
|
|
10045
|
+
return "";
|
|
10046
|
+
return `: ${cause instanceof Error ? cause.message : String(cause)}`;
|
|
10047
|
+
}
|
|
10048
|
+
// ../../node_modules/.bun/@cortexkit+subc-client@0.2.0/node_modules/@cortexkit/subc-client/src/provider.ts
|
|
10049
|
+
import { Buffer as Buffer2 } from "node:buffer";
|
|
10050
|
+
var DEFAULT_HANDSHAKE_TIMEOUT_MS2 = 1e4;
|
|
10051
|
+
var BODY_READ_TIMEOUT_MS2 = 30000;
|
|
10052
|
+
var WRITE_TIMEOUT_MS = 30000;
|
|
10053
|
+
var DEFAULT_RESTORED_DEBOUNCE_MS = 250;
|
|
10054
|
+
var HELLO_CORR = 1n;
|
|
10055
|
+
|
|
10056
|
+
class SubcProviderError extends Error {
|
|
10057
|
+
code;
|
|
10058
|
+
constructor(message, code) {
|
|
10059
|
+
super(message);
|
|
10060
|
+
this.code = code;
|
|
10061
|
+
}
|
|
10062
|
+
}
|
|
10063
|
+
class SubcProvider {
|
|
10064
|
+
sock;
|
|
10065
|
+
currentConn;
|
|
10066
|
+
opts;
|
|
10067
|
+
closed;
|
|
10068
|
+
resolveClosed = () => {
|
|
10069
|
+
return;
|
|
10070
|
+
};
|
|
10071
|
+
closeStarted = false;
|
|
10072
|
+
closedErr = null;
|
|
10073
|
+
inflight = new Map;
|
|
10074
|
+
reconnecting = null;
|
|
10075
|
+
generation = 1;
|
|
10076
|
+
connectionEpoch = 1;
|
|
10077
|
+
stateQueue = [];
|
|
10078
|
+
drainingStateQueue = false;
|
|
10079
|
+
restoredDebounceToken = 0;
|
|
10080
|
+
storage;
|
|
10081
|
+
constructor(sock, currentConn, opts, storage) {
|
|
10082
|
+
this.sock = sock;
|
|
10083
|
+
this.currentConn = currentConn;
|
|
10084
|
+
this.opts = opts;
|
|
10085
|
+
this.storage = storage;
|
|
10086
|
+
this.closed = new Promise((resolve2) => {
|
|
10087
|
+
this.resolveClosed = resolve2;
|
|
10088
|
+
});
|
|
10089
|
+
this.readLoop(sock, this.generation);
|
|
10090
|
+
this.enqueueConnectionState({ state: "connected", epoch: this.connectionEpoch });
|
|
10091
|
+
}
|
|
10092
|
+
get conn() {
|
|
10093
|
+
return this.currentConn;
|
|
10094
|
+
}
|
|
10095
|
+
currentEpoch() {
|
|
10096
|
+
return this.connectionEpoch;
|
|
10097
|
+
}
|
|
10098
|
+
static async connect(opts) {
|
|
10099
|
+
if (opts.manifest.protocol_ver !== PROTOCOL_VERSION) {
|
|
10100
|
+
throw new SubcProviderError(`manifest protocol_ver ${opts.manifest.protocol_ver} does not match client protocol ${PROTOCOL_VERSION}`, "invalid_manifest");
|
|
10101
|
+
}
|
|
10102
|
+
const normalized = normalizeProviderConnectOptions(opts);
|
|
10103
|
+
const opened = await SubcProvider.openConnection(normalized);
|
|
10104
|
+
return new SubcProvider(opened.sock, opened.conn, normalized, opened.ack.storage);
|
|
10105
|
+
}
|
|
10106
|
+
async close() {
|
|
10107
|
+
if (!this.closeStarted) {
|
|
10108
|
+
this.closeStarted = true;
|
|
10109
|
+
this.cancelRestoredDebounce();
|
|
10110
|
+
const sock = this.sock;
|
|
10111
|
+
try {
|
|
10112
|
+
await sendFrame(sock, buildFrame(11 /* Goodbye */, controlFlags(), 0, 0n, new Uint8Array(0)));
|
|
10113
|
+
} catch {} finally {
|
|
10114
|
+
sock.close();
|
|
10115
|
+
this.finishClosed();
|
|
10116
|
+
}
|
|
10117
|
+
}
|
|
10118
|
+
await this.closed;
|
|
10119
|
+
}
|
|
10120
|
+
static async openConnection(opts) {
|
|
10121
|
+
const conn = await readConnectionFile(opts.connectionFile);
|
|
10122
|
+
const deadline = Date.now() + (opts.handshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT_MS2);
|
|
10123
|
+
const endpoint = conn.endpoints[0];
|
|
10124
|
+
const sock = await SubcSocket.connect(endpoint.host, endpoint.port, deadline);
|
|
10125
|
+
try {
|
|
10126
|
+
await authenticateClient(sock, conn, deadline);
|
|
10127
|
+
await sendFrame(sock, buildHelloFrame(opts));
|
|
10128
|
+
const ack = await expectHelloAck(sock, deadline);
|
|
10129
|
+
return { sock, conn, ack };
|
|
10130
|
+
} catch (err) {
|
|
10131
|
+
sock.close();
|
|
10132
|
+
throw err;
|
|
10133
|
+
}
|
|
10134
|
+
}
|
|
10135
|
+
async readLoop(sock, generation) {
|
|
10136
|
+
try {
|
|
10137
|
+
for (;; ) {
|
|
10138
|
+
const headerBytes = await sock.readExact(HEADER_LEN, Number.POSITIVE_INFINITY);
|
|
10139
|
+
const header = decodeHeader(headerBytes);
|
|
10140
|
+
const body = header.len === 0 ? new Uint8Array(0) : await sock.readExact(header.len, Date.now() + BODY_READ_TIMEOUT_MS2);
|
|
10141
|
+
const keepGoing = await this.dispatch({ header, body }, sock, generation);
|
|
10142
|
+
if (!keepGoing) {
|
|
10143
|
+
if (this.sock === sock && this.generation === generation)
|
|
10144
|
+
this.closeStarted = true;
|
|
10145
|
+
break;
|
|
10146
|
+
}
|
|
10147
|
+
}
|
|
10148
|
+
} catch (err) {
|
|
10149
|
+
if (this.sock === sock && this.generation === generation && !this.closeStarted) {
|
|
10150
|
+
this.handleUnexpectedDrop(sock, generation, err instanceof Error ? err : new SubcProviderError(String(err)));
|
|
10151
|
+
return;
|
|
10152
|
+
}
|
|
10153
|
+
} finally {
|
|
10154
|
+
if (this.sock === sock && this.generation === generation) {
|
|
10155
|
+
sock.close();
|
|
10156
|
+
if (this.closeStarted)
|
|
10157
|
+
this.finishClosed();
|
|
10158
|
+
}
|
|
10159
|
+
}
|
|
10160
|
+
}
|
|
10161
|
+
async dispatch(frame, sock, generation) {
|
|
10162
|
+
switch (frame.header.ty) {
|
|
10163
|
+
case 7 /* Ping */:
|
|
10164
|
+
if (frame.header.channel === 0) {
|
|
10165
|
+
await this.sendOn(sock, generation, buildFrameWithVersion(frame.header.ver, 8 /* Pong */, frame.header.flags, 0, frame.header.corr, new Uint8Array(0)));
|
|
10166
|
+
}
|
|
10167
|
+
return true;
|
|
10168
|
+
case 11 /* Goodbye */:
|
|
10169
|
+
if (frame.header.channel === 0)
|
|
10170
|
+
return false;
|
|
10171
|
+
this.abortChannel(generation, frame.header.channel);
|
|
10172
|
+
await this.opts.onRouteGone?.(frame.header.channel);
|
|
10173
|
+
return true;
|
|
10174
|
+
case 6 /* Cancel */:
|
|
10175
|
+
this.inflight.get(routeKey(generation, frame.header.channel, frame.header.corr))?.abort();
|
|
10176
|
+
return true;
|
|
10177
|
+
case 0 /* Request */:
|
|
10178
|
+
if (frame.header.channel === 0) {
|
|
10179
|
+
await this.handleControlRequest(frame, sock, generation);
|
|
10180
|
+
} else {
|
|
10181
|
+
this.handleDataRequest(frame, sock, generation).catch((err) => {
|
|
10182
|
+
if (!this.closeStarted && this.sock === sock && this.generation === generation) {
|
|
10183
|
+
console.warn("SubcProvider handler failed after its request was dispatched", err);
|
|
10184
|
+
}
|
|
10185
|
+
});
|
|
10186
|
+
}
|
|
10187
|
+
return true;
|
|
10188
|
+
default:
|
|
10189
|
+
return true;
|
|
10190
|
+
}
|
|
10191
|
+
}
|
|
10192
|
+
abortChannel(generation, channel) {
|
|
10193
|
+
const prefix = `${generation}:${channel}:`;
|
|
10194
|
+
for (const [key, controller] of this.inflight) {
|
|
10195
|
+
if (key.startsWith(prefix))
|
|
10196
|
+
controller.abort();
|
|
10197
|
+
}
|
|
10198
|
+
}
|
|
10199
|
+
abortGeneration(generation) {
|
|
10200
|
+
const prefix = `${generation}:`;
|
|
10201
|
+
for (const [key, controller] of this.inflight) {
|
|
10202
|
+
if (key.startsWith(prefix))
|
|
10203
|
+
controller.abort();
|
|
10204
|
+
}
|
|
10205
|
+
}
|
|
10206
|
+
abortAllInflight() {
|
|
10207
|
+
for (const controller of this.inflight.values())
|
|
10208
|
+
controller.abort();
|
|
10209
|
+
}
|
|
10210
|
+
async handleControlRequest(frame, sock, generation) {
|
|
10211
|
+
const request = parseJson(frame.body);
|
|
10212
|
+
if (request.op !== "route.bind") {
|
|
10213
|
+
throw new SubcProviderError(`unsupported module control request ${request.op ?? "<missing op>"}`);
|
|
10214
|
+
}
|
|
10215
|
+
const bindRequest = {
|
|
10216
|
+
route_channel: numberField(request.route_channel, "route_channel"),
|
|
10217
|
+
target: request.target,
|
|
10218
|
+
identity: request.identity,
|
|
10219
|
+
principal: request.principal
|
|
10220
|
+
};
|
|
10221
|
+
const decision = await this.opts.onBind?.(bindRequest);
|
|
10222
|
+
const rejection = bindRejection(decision);
|
|
10223
|
+
if (rejection) {
|
|
10224
|
+
await this.sendError(frame, rejection.code, rejection.message, controlFlags(), sock, generation);
|
|
10225
|
+
return;
|
|
10226
|
+
}
|
|
10227
|
+
await this.sendOn(sock, generation, buildFrameWithVersion(frame.header.ver, 1 /* Response */, controlFlags(), 0, frame.header.corr, encodeJson({ op: "route.bind" })));
|
|
10228
|
+
}
|
|
10229
|
+
async handleDataRequest(frame, sock, generation) {
|
|
10230
|
+
const { channel, corr, ver } = frame.header;
|
|
10231
|
+
const key = routeKey(generation, channel, corr);
|
|
10232
|
+
const controller = new AbortController;
|
|
10233
|
+
this.inflight.set(key, controller);
|
|
10234
|
+
const dataFlags = buildFlags(false, 1 /* Interactive */, false);
|
|
10235
|
+
const ctx = {
|
|
10236
|
+
signal: controller.signal,
|
|
10237
|
+
currentEpoch: () => this.connectionEpoch,
|
|
10238
|
+
emit: async (eventBody) => {
|
|
10239
|
+
if (controller.signal.aborted)
|
|
10240
|
+
return;
|
|
10241
|
+
await this.sendOn(sock, generation, buildFrameWithVersion(ver, 3 /* StreamData */, dataFlags, channel, corr, eventBody));
|
|
10242
|
+
}
|
|
10243
|
+
};
|
|
10244
|
+
try {
|
|
10245
|
+
const body = await this.opts.handler(channel, frame.body, ctx);
|
|
10246
|
+
if (body === undefined) {
|
|
10247
|
+
await this.sendOn(sock, generation, buildFrameWithVersion(ver, 4 /* StreamEnd */, dataFlags, channel, corr, new Uint8Array(0)));
|
|
10248
|
+
} else if (body instanceof Uint8Array) {
|
|
10249
|
+
await this.sendOn(sock, generation, buildFrameWithVersion(ver, 1 /* Response */, dataFlags, channel, corr, body));
|
|
10250
|
+
} else {
|
|
10251
|
+
throw new SubcProviderError("provider handler must return a Uint8Array or void", "invalid_handler_response");
|
|
10252
|
+
}
|
|
10253
|
+
} catch (err) {
|
|
10254
|
+
await this.sendError(frame, err instanceof SubcProviderError && err.code ? err.code : "handler_error", err instanceof Error ? err.message : String(err), dataFlags, sock, generation);
|
|
10255
|
+
} finally {
|
|
10256
|
+
if (this.inflight.get(key) === controller)
|
|
10257
|
+
this.inflight.delete(key);
|
|
10258
|
+
}
|
|
10259
|
+
}
|
|
10260
|
+
async sendError(frame, code, message, flags, sock, generation) {
|
|
10261
|
+
await this.sendOn(sock, generation, buildFrameWithVersion(frame.header.ver, 5 /* Error */, flags, frame.header.channel, frame.header.corr, encodeJson({ code, message })));
|
|
10262
|
+
}
|
|
10263
|
+
async sendOn(sock, generation, frame) {
|
|
10264
|
+
if (this.sock !== sock || this.generation !== generation || this.closeStarted || this.closedErr)
|
|
10265
|
+
return;
|
|
10266
|
+
await sendFrame(sock, frame);
|
|
10267
|
+
}
|
|
10268
|
+
handleUnexpectedDrop(sock, generation, cause) {
|
|
10269
|
+
this.cancelRestoredDebounce();
|
|
10270
|
+
this.abortGeneration(generation);
|
|
10271
|
+
this.generation += 1;
|
|
10272
|
+
sock.close();
|
|
10273
|
+
this.enqueueConnectionState({ state: "down", cause });
|
|
10274
|
+
this.scheduleReconnectAfterDrop(cause);
|
|
10275
|
+
}
|
|
10276
|
+
scheduleReconnectAfterDrop(trigger) {
|
|
10277
|
+
if (this.closeStarted || this.reconnecting)
|
|
10278
|
+
return;
|
|
10279
|
+
const promise = this.reconnectWithRetry(trigger).catch((err) => {
|
|
10280
|
+
if (!this.closeStarted)
|
|
10281
|
+
this.failFatal(err instanceof Error ? err : new SubcProviderError(String(err)));
|
|
10282
|
+
}).finally(() => {
|
|
10283
|
+
if (this.reconnecting === promise)
|
|
10284
|
+
this.reconnecting = null;
|
|
10285
|
+
});
|
|
10286
|
+
this.reconnecting = promise;
|
|
10287
|
+
}
|
|
10288
|
+
async reconnectWithRetry(_trigger) {
|
|
10289
|
+
let attempt = 0;
|
|
10290
|
+
let delay = this.opts.reconnectBackoff.baseMs;
|
|
10291
|
+
for (;; ) {
|
|
10292
|
+
if (this.closeStarted)
|
|
10293
|
+
throw new SubcProviderError("provider closed");
|
|
10294
|
+
attempt += 1;
|
|
10295
|
+
this.enqueueConnectionState({ state: "reconnecting", attempt });
|
|
10296
|
+
try {
|
|
10297
|
+
const opened = await SubcProvider.openConnection(this.opts);
|
|
10298
|
+
if (this.closeStarted) {
|
|
10299
|
+
opened.sock.close();
|
|
10300
|
+
throw new SubcProviderError("provider closed");
|
|
10301
|
+
}
|
|
10302
|
+
this.replaceConnection(opened);
|
|
10303
|
+
return;
|
|
10304
|
+
} catch (err) {
|
|
10305
|
+
if (this.closeStarted)
|
|
10306
|
+
throw err;
|
|
10307
|
+
if (!isProviderReconnectTransient(err))
|
|
10308
|
+
throw err;
|
|
10309
|
+
await this.opts.sleep(delay);
|
|
10310
|
+
delay = Math.min(delay * 2, this.opts.reconnectBackoff.capMs);
|
|
10311
|
+
}
|
|
10312
|
+
}
|
|
10313
|
+
}
|
|
10314
|
+
replaceConnection(opened) {
|
|
10315
|
+
this.sock.close();
|
|
10316
|
+
this.sock = opened.sock;
|
|
10317
|
+
this.currentConn = opened.conn;
|
|
10318
|
+
this.storage = opened.ack.storage;
|
|
10319
|
+
this.closedErr = null;
|
|
10320
|
+
this.connectionEpoch += 1;
|
|
10321
|
+
const generation = this.generation;
|
|
10322
|
+
this.readLoop(opened.sock, generation);
|
|
10323
|
+
this.scheduleRestored(generation, this.connectionEpoch);
|
|
10324
|
+
}
|
|
10325
|
+
scheduleRestored(generation, epoch) {
|
|
10326
|
+
if (!this.opts.onConnectionState)
|
|
10327
|
+
return;
|
|
10328
|
+
const token = ++this.restoredDebounceToken;
|
|
10329
|
+
this.opts.sleep(this.opts.restoredDebounceMs).then(() => {
|
|
10330
|
+
if (token === this.restoredDebounceToken && !this.closeStarted && this.sock && this.generation === generation && this.connectionEpoch === epoch) {
|
|
10331
|
+
this.enqueueConnectionState({ state: "restored", epoch });
|
|
10332
|
+
}
|
|
10333
|
+
}).catch((err) => {
|
|
10334
|
+
if (token === this.restoredDebounceToken && !this.closeStarted) {
|
|
10335
|
+
console.warn("SubcProvider restored debounce timer failed", err);
|
|
10336
|
+
}
|
|
10337
|
+
});
|
|
10338
|
+
}
|
|
10339
|
+
cancelRestoredDebounce() {
|
|
10340
|
+
this.restoredDebounceToken += 1;
|
|
10341
|
+
}
|
|
10342
|
+
enqueueConnectionState(event) {
|
|
10343
|
+
if (!this.opts.onConnectionState)
|
|
10344
|
+
return;
|
|
10345
|
+
this.stateQueue.push(event);
|
|
10346
|
+
if (!this.drainingStateQueue)
|
|
10347
|
+
this.drainConnectionStateQueue();
|
|
10348
|
+
}
|
|
10349
|
+
async drainConnectionStateQueue() {
|
|
10350
|
+
if (this.drainingStateQueue)
|
|
10351
|
+
return;
|
|
10352
|
+
this.drainingStateQueue = true;
|
|
10353
|
+
try {
|
|
10354
|
+
while (this.stateQueue.length > 0) {
|
|
10355
|
+
const event = this.stateQueue[0];
|
|
10356
|
+
try {
|
|
10357
|
+
await this.opts.onConnectionState?.(event);
|
|
10358
|
+
this.stateQueue.shift();
|
|
10359
|
+
} catch (err) {
|
|
10360
|
+
if (event.state === "restored") {
|
|
10361
|
+
console.warn("SubcProvider restored callback failed; retrying delivery", err);
|
|
10362
|
+
await pauseBeforeStateRetry();
|
|
10363
|
+
continue;
|
|
10364
|
+
}
|
|
10365
|
+
console.warn("SubcProvider connection-state callback failed", err);
|
|
10366
|
+
this.stateQueue.shift();
|
|
10367
|
+
}
|
|
10368
|
+
}
|
|
10369
|
+
} finally {
|
|
10370
|
+
this.drainingStateQueue = false;
|
|
10371
|
+
if (this.stateQueue.length > 0)
|
|
10372
|
+
this.drainConnectionStateQueue();
|
|
10373
|
+
}
|
|
10374
|
+
}
|
|
10375
|
+
failFatal(err) {
|
|
10376
|
+
if (!this.closedErr)
|
|
10377
|
+
this.closedErr = err;
|
|
10378
|
+
this.closeStarted = true;
|
|
10379
|
+
this.cancelRestoredDebounce();
|
|
10380
|
+
this.abortAllInflight();
|
|
10381
|
+
this.sock.close();
|
|
10382
|
+
this.finishClosed();
|
|
10383
|
+
}
|
|
10384
|
+
finishClosed() {
|
|
10385
|
+
this.resolveClosed();
|
|
10386
|
+
}
|
|
10387
|
+
}
|
|
10388
|
+
function routeKey(generation, channel, corr) {
|
|
10389
|
+
return `${generation}:${channel}:${corr}`;
|
|
10390
|
+
}
|
|
10391
|
+
function launchNonce(opts) {
|
|
10392
|
+
const nonce = opts.launchNonce ?? process.env[SUBC_LAUNCH_NONCE_ENV2];
|
|
10393
|
+
return nonce && nonce.length > 0 ? nonce : undefined;
|
|
10394
|
+
}
|
|
10395
|
+
var SUBC_LAUNCH_NONCE_ENV2 = "SUBC_LAUNCH_NONCE";
|
|
10396
|
+
function normalizeProviderConnectOptions(opts) {
|
|
10397
|
+
return {
|
|
10398
|
+
connectionFile: opts.connectionFile,
|
|
10399
|
+
manifest: opts.manifest,
|
|
10400
|
+
handler: opts.handler,
|
|
10401
|
+
handshakeTimeoutMs: opts.handshakeTimeoutMs,
|
|
10402
|
+
controlOps: opts.controlOps,
|
|
10403
|
+
onBind: opts.onBind,
|
|
10404
|
+
onRouteGone: opts.onRouteGone,
|
|
10405
|
+
reconnectBackoff: opts.reconnectBackoff ?? DEFAULT_RECONNECT_BACKOFF,
|
|
10406
|
+
sleep: opts.sleep ?? ((ms) => new Promise((resolve2) => setTimeout(resolve2, ms))),
|
|
10407
|
+
restoredDebounceMs: opts.restoredDebounceMs ?? DEFAULT_RESTORED_DEBOUNCE_MS,
|
|
10408
|
+
onConnectionState: opts.onConnectionState,
|
|
10409
|
+
launchNonce: opts.launchNonce
|
|
10410
|
+
};
|
|
10411
|
+
}
|
|
10412
|
+
function buildHelloFrame(opts) {
|
|
10413
|
+
const nonce = launchNonce(opts);
|
|
10414
|
+
return buildFrame(9 /* Hello */, controlFlags(), 0, HELLO_CORR, encodeJson({
|
|
10415
|
+
manifest: normalizeManifest(opts.manifest),
|
|
10416
|
+
protocol_ver: PROTOCOL_VERSION,
|
|
10417
|
+
control_ops: opts.controlOps === undefined ? null : opts.controlOps,
|
|
10418
|
+
...nonce ? { launch_nonce: nonce } : {}
|
|
10419
|
+
}));
|
|
10420
|
+
}
|
|
10421
|
+
function isProviderReconnectTransient(err) {
|
|
10422
|
+
if (err instanceof SubcProviderError)
|
|
10423
|
+
return err.code === "duplicate_module_id";
|
|
10424
|
+
if (err instanceof SocketClosedError || err instanceof SocketTimeoutError)
|
|
10425
|
+
return true;
|
|
10426
|
+
if (err instanceof SocketWriteNotQueuedError || err instanceof SocketWriteQueuedError)
|
|
10427
|
+
return true;
|
|
10428
|
+
if (err instanceof ConnectionFileError || err instanceof AuthError)
|
|
10429
|
+
return false;
|
|
10430
|
+
const code = errorCode2(err);
|
|
10431
|
+
return code === "ECONNREFUSED" || code === "ECONNRESET" || code === "EPIPE" || code === "ETIMEDOUT" || code === "ENOENT";
|
|
10432
|
+
}
|
|
10433
|
+
function errorCode2(err) {
|
|
10434
|
+
if (typeof err === "object" && err !== null && "code" in err) {
|
|
10435
|
+
const code = err.code;
|
|
10436
|
+
if (typeof code === "string")
|
|
10437
|
+
return code;
|
|
10438
|
+
}
|
|
10439
|
+
return;
|
|
10440
|
+
}
|
|
10441
|
+
async function pauseBeforeStateRetry() {
|
|
10442
|
+
await new Promise((resolve2) => setTimeout(resolve2, 0));
|
|
10443
|
+
}
|
|
10444
|
+
function controlFlags() {
|
|
10445
|
+
return buildFlags(false, 0 /* Passive */, false);
|
|
10446
|
+
}
|
|
10447
|
+
async function sendFrame(sock, frame) {
|
|
10448
|
+
await sock.write(encodeFrame(frame), Date.now() + WRITE_TIMEOUT_MS);
|
|
10449
|
+
}
|
|
10450
|
+
async function expectHelloAck(sock, deadline) {
|
|
10451
|
+
const header = decodeHeader(await sock.readExact(HEADER_LEN, deadline));
|
|
10452
|
+
const body = header.len === 0 ? new Uint8Array(0) : await sock.readExact(header.len, deadline);
|
|
10453
|
+
const frame = { header, body };
|
|
10454
|
+
switch (header.ty) {
|
|
10455
|
+
case 10 /* HelloAck */:
|
|
10456
|
+
return parseJson(body);
|
|
10457
|
+
case 5 /* Error */: {
|
|
10458
|
+
const error2 = parseJson(body);
|
|
10459
|
+
throw new SubcProviderError(`subc rejected HELLO: ${error2.code ?? "unknown"} — ${error2.message ?? "subc error"}`, error2.code);
|
|
10460
|
+
}
|
|
10461
|
+
default:
|
|
10462
|
+
throw new SubcProviderError(`unexpected frame ${FrameType[frame.header.ty]} awaiting HELLO_ACK`);
|
|
10463
|
+
}
|
|
10464
|
+
}
|
|
10465
|
+
function encodeJson(value) {
|
|
10466
|
+
return new Uint8Array(Buffer2.from(JSON.stringify(value), "utf8"));
|
|
10467
|
+
}
|
|
10468
|
+
function parseJson(bytes) {
|
|
10469
|
+
return JSON.parse(Buffer2.from(bytes).toString("utf8"));
|
|
10470
|
+
}
|
|
10471
|
+
function numberField(value, field) {
|
|
10472
|
+
if (typeof value !== "number" || !Number.isInteger(value)) {
|
|
10473
|
+
throw new SubcProviderError(`route.bind ${field} must be an integer`);
|
|
10474
|
+
}
|
|
10475
|
+
return value;
|
|
10476
|
+
}
|
|
10477
|
+
function bindRejection(decision) {
|
|
10478
|
+
if (decision === undefined || decision === true)
|
|
10479
|
+
return null;
|
|
10480
|
+
if (decision === false) {
|
|
10481
|
+
return { code: "route_rejected", message: "route.bind rejected by provider" };
|
|
10482
|
+
}
|
|
10483
|
+
if (decision.accept)
|
|
10484
|
+
return null;
|
|
10485
|
+
return {
|
|
10486
|
+
code: decision.code ?? "route_rejected",
|
|
10487
|
+
message: decision.message ?? "route.bind rejected by provider"
|
|
10488
|
+
};
|
|
10489
|
+
}
|
|
10490
|
+
function normalizeManifest(manifest) {
|
|
10491
|
+
return {
|
|
10492
|
+
module_id: manifest.module_id,
|
|
10493
|
+
module_version: manifest.module_version,
|
|
10494
|
+
protocol_ver: manifest.protocol_ver,
|
|
10495
|
+
trust_tier: manifest.trust_tier,
|
|
10496
|
+
provides: manifest.provides.map(normalizeProviderRole),
|
|
10497
|
+
consumes: manifest.consumes.map(normalizeConsumerRole),
|
|
10498
|
+
scheduled_tasks: manifest.scheduled_tasks.map(normalizeScheduledTask),
|
|
10499
|
+
bindings: {
|
|
10500
|
+
storage: {
|
|
10501
|
+
kind: manifest.bindings.storage.kind,
|
|
10502
|
+
scope: manifest.bindings.storage.scope,
|
|
10503
|
+
owns_schema: manifest.bindings.storage.owns_schema
|
|
10504
|
+
},
|
|
10505
|
+
vault_grants: manifest.bindings.vault_grants.map((grant) => ({
|
|
10506
|
+
secret: grant.secret,
|
|
10507
|
+
reason: grant.reason
|
|
10508
|
+
})),
|
|
10509
|
+
identity: {
|
|
10510
|
+
requires: [...manifest.bindings.identity.requires],
|
|
10511
|
+
optional: [...manifest.bindings.identity.optional]
|
|
10512
|
+
}
|
|
10513
|
+
}
|
|
10514
|
+
};
|
|
10515
|
+
}
|
|
10516
|
+
function normalizeProviderRole(role) {
|
|
10517
|
+
switch (role.role) {
|
|
10518
|
+
case "tool_provider":
|
|
10519
|
+
return {
|
|
10520
|
+
role: "tool_provider",
|
|
10521
|
+
tools: role.tools.map((tool) => ({
|
|
10522
|
+
name: tool.name,
|
|
10523
|
+
execution_mode: tool.execution_mode,
|
|
10524
|
+
schema: tool.schema
|
|
10525
|
+
})),
|
|
10526
|
+
identity_scope: [...role.identity_scope],
|
|
10527
|
+
concurrency: role.concurrency,
|
|
10528
|
+
emits_push: role.emits_push,
|
|
10529
|
+
sub_supervises: role.sub_supervises
|
|
10530
|
+
};
|
|
10531
|
+
case "pipeline_stage":
|
|
10532
|
+
return {
|
|
10533
|
+
role: "pipeline_stage",
|
|
10534
|
+
stage: role.stage,
|
|
10535
|
+
applies_to: {
|
|
10536
|
+
provider: role.applies_to.provider,
|
|
10537
|
+
model: role.applies_to.model
|
|
10538
|
+
},
|
|
10539
|
+
interface: role.interface,
|
|
10540
|
+
declares_frozen_floor: role.declares_frozen_floor,
|
|
10541
|
+
needs_signals: [...role.needs_signals],
|
|
10542
|
+
conformance_class: role.conformance_class
|
|
10543
|
+
};
|
|
10544
|
+
case "management_surface":
|
|
10545
|
+
return {
|
|
10546
|
+
role: "management_surface",
|
|
10547
|
+
operations: role.operations.map((operation) => ({
|
|
10548
|
+
name: operation.name,
|
|
10549
|
+
kind: operation.kind
|
|
10550
|
+
})),
|
|
10551
|
+
config_schema: role.config_schema,
|
|
10552
|
+
observability: role.observability.map((surface) => ({
|
|
10553
|
+
name: surface.name,
|
|
10554
|
+
kind: surface.kind
|
|
10555
|
+
})),
|
|
10556
|
+
identity_scope: [...role.identity_scope]
|
|
10557
|
+
};
|
|
10558
|
+
case "internal_service":
|
|
10559
|
+
return {
|
|
10560
|
+
role: "internal_service",
|
|
10561
|
+
service_id: role.service_id,
|
|
10562
|
+
transport: role.transport,
|
|
10563
|
+
agent_facing: role.agent_facing,
|
|
10564
|
+
operations: [...role.operations]
|
|
10565
|
+
};
|
|
10566
|
+
}
|
|
10567
|
+
}
|
|
10568
|
+
function normalizeConsumerRole(role) {
|
|
10569
|
+
switch (role.role) {
|
|
10570
|
+
case "tool_client":
|
|
10571
|
+
return { role: "tool_client", of: [...role.of] };
|
|
10572
|
+
case "llm_client":
|
|
10573
|
+
return { role: "llm_client", via: role.via, auth: role.auth };
|
|
10574
|
+
case "service_client":
|
|
10575
|
+
return { role: "service_client", of: [...role.of] };
|
|
10576
|
+
}
|
|
10577
|
+
}
|
|
10578
|
+
function normalizeScheduledTask(task) {
|
|
10579
|
+
return {
|
|
10580
|
+
task_id: task.task_id,
|
|
10581
|
+
eligibility: {
|
|
10582
|
+
cooldown: task.eligibility.cooldown,
|
|
10583
|
+
window: task.eligibility.window
|
|
10584
|
+
},
|
|
10585
|
+
lease_scope: task.lease_scope,
|
|
10586
|
+
renews_during_calls: task.renews_during_calls,
|
|
10587
|
+
toolset: [...task.toolset],
|
|
10588
|
+
model_policy: {
|
|
10589
|
+
tier: task.model_policy.tier,
|
|
10590
|
+
fallback_chain: [...task.model_policy.fallback_chain]
|
|
10591
|
+
},
|
|
10592
|
+
step_cap: task.step_cap,
|
|
10593
|
+
circuit_breaker: {
|
|
10594
|
+
identical_failures: task.circuit_breaker.identical_failures
|
|
10595
|
+
}
|
|
10596
|
+
};
|
|
10597
|
+
}
|
|
10598
|
+
// ../aft-bridge/dist/subc-transport.js
|
|
10599
|
+
var AFT_MODULE_ID = "aft";
|
|
10600
|
+
var MAX_CONSECUTIVE_TRANSPORT_FAILURES = 3;
|
|
10601
|
+
var BG_STABLE_MS = 5000;
|
|
10602
|
+
var DEFAULT_SESSION_ID = "__default__";
|
|
10603
|
+
var LOCALLY_SATISFIED_COMMANDS = new Set(["configure"]);
|
|
10604
|
+
function identityKey(identity) {
|
|
10605
|
+
return `${identity.project_root}\x00${identity.harness}\x00${identity.session}`;
|
|
10606
|
+
}
|
|
10607
|
+
|
|
10608
|
+
class BgSubscription {
|
|
10609
|
+
identity;
|
|
10610
|
+
acquireClient;
|
|
10611
|
+
dropClient;
|
|
10612
|
+
onNudge;
|
|
10613
|
+
sleep;
|
|
10614
|
+
stopped = false;
|
|
10615
|
+
current = null;
|
|
10616
|
+
loop;
|
|
10617
|
+
constructor(identity, acquireClient, dropClient, onNudge, sleep) {
|
|
10618
|
+
this.identity = identity;
|
|
10619
|
+
this.acquireClient = acquireClient;
|
|
10620
|
+
this.dropClient = dropClient;
|
|
10621
|
+
this.onNudge = onNudge;
|
|
10622
|
+
this.sleep = sleep;
|
|
10623
|
+
this.loop = this.run();
|
|
10624
|
+
}
|
|
10625
|
+
async stop() {
|
|
10626
|
+
this.stopped = true;
|
|
10627
|
+
const sub = this.current;
|
|
10628
|
+
if (sub) {
|
|
10629
|
+
try {
|
|
10630
|
+
sub.unsubscribe();
|
|
10631
|
+
} catch {}
|
|
10632
|
+
}
|
|
10633
|
+
await this.loop.catch(() => {
|
|
10634
|
+
return;
|
|
10635
|
+
});
|
|
10636
|
+
}
|
|
10637
|
+
async run() {
|
|
10638
|
+
let attempt = 0;
|
|
10639
|
+
while (!this.stopped) {
|
|
10640
|
+
let client;
|
|
10641
|
+
try {
|
|
10642
|
+
client = await this.acquireClient();
|
|
10643
|
+
} catch {
|
|
10644
|
+
await this.backoff(attempt++);
|
|
10645
|
+
continue;
|
|
10646
|
+
}
|
|
10647
|
+
if (this.stopped)
|
|
10648
|
+
return;
|
|
10649
|
+
let channel;
|
|
10650
|
+
try {
|
|
10651
|
+
channel = await client.routeOpen({ kind: "tool_provider", module_id: AFT_MODULE_ID }, this.identity);
|
|
10652
|
+
} catch (err) {
|
|
10653
|
+
if (isConsumerReconnectTransient(err))
|
|
10654
|
+
this.dropClient(client);
|
|
10655
|
+
await this.backoff(attempt++);
|
|
10656
|
+
continue;
|
|
10657
|
+
}
|
|
10658
|
+
if (this.stopped) {
|
|
10659
|
+
safeCloseRoute(client, channel);
|
|
10660
|
+
return;
|
|
10661
|
+
}
|
|
10662
|
+
const subscribedAt = Date.now();
|
|
10663
|
+
try {
|
|
10664
|
+
const sub = client.subscribe(channel, { op: "bg_events" }, () => {
|
|
10665
|
+
if (!this.stopped)
|
|
10666
|
+
this.onNudge();
|
|
10667
|
+
});
|
|
10668
|
+
this.current = sub;
|
|
10669
|
+
if (this.stopped)
|
|
10670
|
+
sub.unsubscribe();
|
|
10671
|
+
if (!this.stopped)
|
|
10672
|
+
this.onNudge();
|
|
10673
|
+
await sub.closed;
|
|
10674
|
+
return;
|
|
10675
|
+
} catch (err) {
|
|
10676
|
+
if (this.stopped)
|
|
10677
|
+
return;
|
|
10678
|
+
if (isConsumerReconnectTransient(err))
|
|
10679
|
+
this.dropClient(client);
|
|
10680
|
+
if (Date.now() - subscribedAt >= BG_STABLE_MS)
|
|
10681
|
+
attempt = 0;
|
|
10682
|
+
} finally {
|
|
10683
|
+
this.current = null;
|
|
10684
|
+
safeCloseRoute(client, channel);
|
|
10685
|
+
}
|
|
10686
|
+
await this.backoff(attempt++);
|
|
10687
|
+
}
|
|
10688
|
+
}
|
|
10689
|
+
async backoff(attempt) {
|
|
10690
|
+
const ms = Math.min(100 * 2 ** Math.min(attempt, 6), 2000);
|
|
10691
|
+
await this.sleep(ms);
|
|
10692
|
+
}
|
|
10693
|
+
}
|
|
10694
|
+
function isRecord(value) {
|
|
10695
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10696
|
+
}
|
|
10697
|
+
function safeCloseRoute(client, channel) {
|
|
10698
|
+
try {
|
|
10699
|
+
client.closeRouteChannel(channel).catch(() => {
|
|
10700
|
+
return;
|
|
10701
|
+
});
|
|
10702
|
+
} catch {}
|
|
10703
|
+
}
|
|
10704
|
+
|
|
10705
|
+
class RouteTornDownError extends Error {
|
|
10706
|
+
}
|
|
10707
|
+
function reliftReply(reply) {
|
|
10708
|
+
if (!isRecord(reply) || !isRecord(reply.structuredContent)) {
|
|
10709
|
+
throw new Error("subc tool reply is missing the structuredContent envelope (protocol violation)");
|
|
10710
|
+
}
|
|
10711
|
+
const flat = reply.structuredContent;
|
|
10712
|
+
if (typeof flat.success !== "boolean" || typeof flat.text !== "string") {
|
|
10713
|
+
throw new Error("subc tool reply structuredContent lacks a boolean `success` / string `text` (protocol violation)");
|
|
10714
|
+
}
|
|
10715
|
+
return flat;
|
|
10716
|
+
}
|
|
10717
|
+
|
|
10718
|
+
class SubcTransport {
|
|
10719
|
+
pool;
|
|
10720
|
+
projectRoot;
|
|
10721
|
+
lastStatusBar;
|
|
10722
|
+
cachedStatus = null;
|
|
10723
|
+
constructor(pool, projectRoot) {
|
|
10724
|
+
this.pool = pool;
|
|
10725
|
+
this.projectRoot = projectRoot;
|
|
10726
|
+
}
|
|
10727
|
+
getCwd() {
|
|
10728
|
+
return this.projectRoot;
|
|
10729
|
+
}
|
|
10730
|
+
getStatusBar() {
|
|
10731
|
+
return this.lastStatusBar;
|
|
10732
|
+
}
|
|
10733
|
+
getCachedStatus() {
|
|
10734
|
+
return this.cachedStatus;
|
|
10735
|
+
}
|
|
10736
|
+
cacheStatusSnapshot(snapshot) {
|
|
10737
|
+
this.cachedStatus = snapshot;
|
|
10738
|
+
}
|
|
10739
|
+
captureStatusBar(response) {
|
|
10740
|
+
const parsed = parseStatusBarCounts(response.status_bar);
|
|
10741
|
+
if (parsed)
|
|
10742
|
+
this.lastStatusBar = parsed;
|
|
10743
|
+
}
|
|
10744
|
+
identityFor(session) {
|
|
10745
|
+
return {
|
|
10746
|
+
project_root: this.projectRoot,
|
|
10747
|
+
harness: this.pool.harness,
|
|
10748
|
+
session: session && session.length > 0 ? session : DEFAULT_SESSION_ID
|
|
10749
|
+
};
|
|
10750
|
+
}
|
|
10751
|
+
async toolCall(sessionId, name, rawArgs = {}, options) {
|
|
10752
|
+
const { preview, timeoutMs, onProgress } = this.splitOptions(options);
|
|
10753
|
+
const body = { name, arguments: rawArgs };
|
|
10754
|
+
if (preview === true)
|
|
10755
|
+
body.preview = true;
|
|
10756
|
+
const reply = await this.pool.routeRequest(this.identityFor(sessionId), body, timeoutMs, onProgress);
|
|
10757
|
+
const result = reliftReply(reply);
|
|
10758
|
+
this.captureStatusBar(result);
|
|
10759
|
+
return result;
|
|
10760
|
+
}
|
|
10761
|
+
async send(command, params = {}, options) {
|
|
10762
|
+
if (LOCALLY_SATISFIED_COMMANDS.has(command)) {
|
|
10763
|
+
return { success: true, command, subc_local: true };
|
|
10764
|
+
}
|
|
10765
|
+
const { timeoutMs, onProgress } = this.splitOptions(options);
|
|
10766
|
+
const session = typeof params.session_id === "string" ? params.session_id : undefined;
|
|
10767
|
+
const reply = await this.pool.routeRequest(this.identityFor(session), { name: command, arguments: params }, timeoutMs, onProgress);
|
|
10768
|
+
const response = reliftReply(reply);
|
|
10769
|
+
this.captureStatusBar(response);
|
|
10770
|
+
return response;
|
|
10771
|
+
}
|
|
10772
|
+
splitOptions(options) {
|
|
10773
|
+
if (!options)
|
|
10774
|
+
return {};
|
|
10775
|
+
const preview = options.preview;
|
|
10776
|
+
const timeoutMs = options.timeoutMs;
|
|
10777
|
+
const onProgress = options.onProgress;
|
|
10778
|
+
return { preview, timeoutMs, onProgress };
|
|
10779
|
+
}
|
|
10780
|
+
}
|
|
10781
|
+
|
|
10782
|
+
class SubcTransportPool {
|
|
10783
|
+
harness;
|
|
10784
|
+
connectionFile;
|
|
10785
|
+
handshakeTimeoutMs;
|
|
10786
|
+
connectFn;
|
|
10787
|
+
onBgEventsNudge;
|
|
10788
|
+
bgBackoffSleep;
|
|
10789
|
+
client = null;
|
|
10790
|
+
connecting = null;
|
|
10791
|
+
sessions = new Map;
|
|
10792
|
+
transportFailures = 0;
|
|
10793
|
+
transports = new Map;
|
|
10794
|
+
shuttingDown = false;
|
|
10795
|
+
constructor(options) {
|
|
10796
|
+
this.connectionFile = options.connectionFile;
|
|
10797
|
+
this.harness = options.harness;
|
|
10798
|
+
this.handshakeTimeoutMs = options.handshakeTimeoutMs;
|
|
10799
|
+
this.connectFn = options.connect ?? ((opts) => SubcClient.connect(opts));
|
|
10800
|
+
this.onBgEventsNudge = options.onBgEventsNudge;
|
|
10801
|
+
this.bgBackoffSleep = options.bgBackoffSleep ?? ((ms) => new Promise((resolve2) => setTimeout(resolve2, ms)));
|
|
10802
|
+
}
|
|
10803
|
+
static async connectionAvailable(connectionFile) {
|
|
10804
|
+
return connectionFileExists(connectionFile);
|
|
10805
|
+
}
|
|
10806
|
+
getBridge(projectRoot) {
|
|
10807
|
+
const key = canonicalizeProjectRoot(projectRoot);
|
|
10808
|
+
let transport = this.transports.get(key);
|
|
10809
|
+
if (!transport) {
|
|
10810
|
+
transport = new SubcTransport(this, key);
|
|
10811
|
+
this.transports.set(key, transport);
|
|
10812
|
+
}
|
|
10813
|
+
return transport;
|
|
10814
|
+
}
|
|
10815
|
+
getActiveBridgeForRoot(projectRoot) {
|
|
10816
|
+
const key = canonicalizeProjectRoot(projectRoot);
|
|
10817
|
+
if (!this.client)
|
|
10818
|
+
return null;
|
|
10819
|
+
return this.transports.get(key) ?? null;
|
|
10820
|
+
}
|
|
10821
|
+
async toolCall(projectRoot, runtime, name, rawArgs = {}, options) {
|
|
10822
|
+
return this.getBridge(projectRoot).toolCall(runtime.sessionID, name, rawArgs, options);
|
|
10823
|
+
}
|
|
10824
|
+
getOrCreateSession(key) {
|
|
10825
|
+
let record = this.sessions.get(key);
|
|
10826
|
+
if (!record || record.closed) {
|
|
10827
|
+
record = { routeEntry: null, bgSub: null, closed: false, inflight: 0 };
|
|
10828
|
+
this.sessions.set(key, record);
|
|
8958
10829
|
}
|
|
8959
|
-
|
|
8960
|
-
...Array.from(this.bridges.values(), (e) => e.bridge.shutdown()),
|
|
8961
|
-
...Array.from(this.staleBridges.values(), (bridge) => bridge.shutdown())
|
|
8962
|
-
];
|
|
8963
|
-
this.bridges.clear();
|
|
8964
|
-
this.staleBridges.clear();
|
|
8965
|
-
await Promise.allSettled(shutdowns);
|
|
10830
|
+
return record;
|
|
8966
10831
|
}
|
|
8967
|
-
|
|
8968
|
-
this.
|
|
8969
|
-
|
|
8970
|
-
|
|
10832
|
+
isCurrentSession(key, record) {
|
|
10833
|
+
return this.sessions.get(key) === record && !record.closed;
|
|
10834
|
+
}
|
|
10835
|
+
deleteSessionIfEmpty(key, record) {
|
|
10836
|
+
if (this.sessions.get(key) === record && !record.closed && record.inflight === 0 && record.routeEntry === null && record.bgSub === null) {
|
|
10837
|
+
this.sessions.delete(key);
|
|
8971
10838
|
}
|
|
8972
|
-
this.bridges.clear();
|
|
8973
|
-
this.log(`Binary path updated to ${newPath}. Active bridges marked stale — next calls will use the new binary.`);
|
|
8974
|
-
return newPath;
|
|
8975
10839
|
}
|
|
8976
|
-
|
|
8977
|
-
const
|
|
8978
|
-
|
|
10840
|
+
async routeRequest(identity, body, timeoutMs, onProgress) {
|
|
10841
|
+
const key = identityKey(identity);
|
|
10842
|
+
const record = this.getOrCreateSession(key);
|
|
10843
|
+
record.inflight += 1;
|
|
10844
|
+
try {
|
|
10845
|
+
const client = await this.ensureClient();
|
|
10846
|
+
if (!this.isCurrentSession(key, record)) {
|
|
10847
|
+
throw new RouteTornDownError("subc session closed");
|
|
10848
|
+
}
|
|
10849
|
+
let channel;
|
|
10850
|
+
let entry;
|
|
8979
10851
|
try {
|
|
8980
|
-
|
|
10852
|
+
({ channel, entry } = await this.routeChannel(client, identity, record));
|
|
10853
|
+
if (!this.isCurrentSession(key, record)) {
|
|
10854
|
+
throw new RouteTornDownError("subc session closed");
|
|
10855
|
+
}
|
|
8981
10856
|
} catch (err) {
|
|
8982
|
-
|
|
8983
|
-
|
|
10857
|
+
if (err instanceof RouteTornDownError)
|
|
10858
|
+
throw err;
|
|
10859
|
+
if (isConsumerReconnectTransient(err) && this.isCurrentSession(key, record) && this.client === client) {
|
|
10860
|
+
this.dropClient(client);
|
|
10861
|
+
}
|
|
10862
|
+
throw err;
|
|
8984
10863
|
}
|
|
8985
|
-
} else
|
|
8986
|
-
log(message, meta);
|
|
8987
|
-
}
|
|
8988
|
-
error(message, meta) {
|
|
8989
|
-
const logger = this.logger ?? getActiveLogger();
|
|
8990
|
-
if (logger) {
|
|
8991
10864
|
try {
|
|
8992
|
-
|
|
10865
|
+
const reply = await client.request(channel, body, { timeoutMs, onProgress });
|
|
10866
|
+
if (this.isCurrentSession(key, record) && this.client === client) {
|
|
10867
|
+
this.transportFailures = 0;
|
|
10868
|
+
}
|
|
10869
|
+
this.ensureBgSubscription(identity, record);
|
|
10870
|
+
return reply;
|
|
8993
10871
|
} catch (err) {
|
|
8994
|
-
|
|
8995
|
-
|
|
10872
|
+
if (record.routeEntry === entry) {
|
|
10873
|
+
entry.closed = true;
|
|
10874
|
+
record.routeEntry = null;
|
|
10875
|
+
}
|
|
10876
|
+
if (this.isCurrentSession(key, record) && this.client === client) {
|
|
10877
|
+
if (isConsumerReconnectTransient(err)) {
|
|
10878
|
+
this.transportFailures = 0;
|
|
10879
|
+
this.dropClient(client);
|
|
10880
|
+
} else if (++this.transportFailures >= MAX_CONSECUTIVE_TRANSPORT_FAILURES) {
|
|
10881
|
+
this.transportFailures = 0;
|
|
10882
|
+
this.dropClient(client);
|
|
10883
|
+
}
|
|
10884
|
+
}
|
|
10885
|
+
throw err;
|
|
8996
10886
|
}
|
|
8997
|
-
}
|
|
8998
|
-
|
|
10887
|
+
} finally {
|
|
10888
|
+
record.inflight -= 1;
|
|
10889
|
+
this.deleteSessionIfEmpty(key, record);
|
|
10890
|
+
}
|
|
8999
10891
|
}
|
|
9000
|
-
|
|
9001
|
-
if (
|
|
9002
|
-
|
|
9003
|
-
}
|
|
9004
|
-
|
|
10892
|
+
async ensureClient() {
|
|
10893
|
+
if (this.shuttingDown) {
|
|
10894
|
+
throw new SubcCallError("terminal", "subc transport is shutting down");
|
|
10895
|
+
}
|
|
10896
|
+
if (this.client)
|
|
10897
|
+
return this.client;
|
|
10898
|
+
if (this.connecting)
|
|
10899
|
+
return this.connecting;
|
|
10900
|
+
this.connecting = this.connectFn({
|
|
10901
|
+
connectionFile: this.connectionFile,
|
|
10902
|
+
handshakeTimeoutMs: this.handshakeTimeoutMs
|
|
10903
|
+
}).then((client) => {
|
|
10904
|
+
this.connecting = null;
|
|
10905
|
+
if (this.shuttingDown) {
|
|
10906
|
+
try {
|
|
10907
|
+
client.close();
|
|
10908
|
+
} catch {}
|
|
10909
|
+
throw new SubcCallError("terminal", "subc transport is shutting down");
|
|
10910
|
+
}
|
|
10911
|
+
this.client = client;
|
|
10912
|
+
this.transportFailures = 0;
|
|
10913
|
+
return client;
|
|
10914
|
+
}).catch((err) => {
|
|
10915
|
+
this.connecting = null;
|
|
10916
|
+
throw err;
|
|
10917
|
+
});
|
|
10918
|
+
return this.connecting;
|
|
10919
|
+
}
|
|
10920
|
+
async routeChannel(client, identity, record) {
|
|
10921
|
+
const key = identityKey(identity);
|
|
10922
|
+
const existing = record.routeEntry;
|
|
10923
|
+
if (existing?.channel != null)
|
|
10924
|
+
return { channel: existing.channel, entry: existing };
|
|
10925
|
+
if (existing?.opening)
|
|
10926
|
+
return { channel: await existing.opening, entry: existing };
|
|
10927
|
+
const entry = { client, opening: null, channel: null, closed: false };
|
|
10928
|
+
const opening = client.routeOpen({ kind: "tool_provider", module_id: AFT_MODULE_ID }, identity).then((channel) => {
|
|
10929
|
+
if (!this.isCurrentSession(key, record) || record.routeEntry !== entry || entry.closed || this.client !== client) {
|
|
10930
|
+
safeCloseRoute(client, channel);
|
|
10931
|
+
if (record.routeEntry === entry)
|
|
10932
|
+
record.routeEntry = null;
|
|
10933
|
+
throw new RouteTornDownError("subc route opened after teardown");
|
|
10934
|
+
}
|
|
10935
|
+
entry.channel = channel;
|
|
10936
|
+
entry.opening = null;
|
|
10937
|
+
return channel;
|
|
10938
|
+
}).catch((err) => {
|
|
10939
|
+
const current = this.isCurrentSession(key, record);
|
|
10940
|
+
if (record.routeEntry === entry) {
|
|
10941
|
+
entry.closed = true;
|
|
10942
|
+
record.routeEntry = null;
|
|
10943
|
+
}
|
|
10944
|
+
if (!current && !(err instanceof RouteTornDownError)) {
|
|
10945
|
+
throw new RouteTornDownError("subc route opened after session closed");
|
|
10946
|
+
}
|
|
10947
|
+
throw err;
|
|
10948
|
+
});
|
|
10949
|
+
entry.opening = opening;
|
|
10950
|
+
record.routeEntry = entry;
|
|
10951
|
+
return { channel: await opening, entry };
|
|
10952
|
+
}
|
|
10953
|
+
ensureBgSubscription(identity, record) {
|
|
10954
|
+
if (this.shuttingDown || !this.onBgEventsNudge)
|
|
10955
|
+
return;
|
|
10956
|
+
const key = identityKey(identity);
|
|
10957
|
+
if (!this.isCurrentSession(key, record))
|
|
10958
|
+
return;
|
|
10959
|
+
if (record.bgSub)
|
|
10960
|
+
return;
|
|
10961
|
+
const onNudge = () => this.onBgEventsNudge?.(identity.project_root, identity.session);
|
|
10962
|
+
const sub = new BgSubscription(identity, () => this.ensureClient(), (client) => this.dropClient(client), onNudge, this.bgBackoffSleep);
|
|
10963
|
+
record.bgSub = sub;
|
|
10964
|
+
}
|
|
10965
|
+
dropClient(client) {
|
|
10966
|
+
if (this.client === client) {
|
|
10967
|
+
this.client = null;
|
|
10968
|
+
for (const [key, record] of this.sessions) {
|
|
10969
|
+
const entry = record.routeEntry;
|
|
10970
|
+
if (entry?.client === client) {
|
|
10971
|
+
entry.closed = true;
|
|
10972
|
+
record.routeEntry = null;
|
|
10973
|
+
this.deleteSessionIfEmpty(key, record);
|
|
10974
|
+
}
|
|
10975
|
+
}
|
|
10976
|
+
this.transportFailures = 0;
|
|
10977
|
+
try {
|
|
10978
|
+
client.close();
|
|
10979
|
+
} catch {}
|
|
9005
10980
|
}
|
|
9006
10981
|
}
|
|
9007
|
-
|
|
9008
|
-
|
|
10982
|
+
setConfigureOverride(_key, _value) {}
|
|
10983
|
+
async replaceBinary(path) {
|
|
10984
|
+
return path;
|
|
9009
10985
|
}
|
|
9010
|
-
|
|
9011
|
-
|
|
10986
|
+
async shutdown() {
|
|
10987
|
+
this.shuttingDown = true;
|
|
10988
|
+
const subs = [];
|
|
10989
|
+
const entries = [];
|
|
10990
|
+
for (const record of this.sessions.values()) {
|
|
10991
|
+
record.closed = true;
|
|
10992
|
+
const sub = record.bgSub;
|
|
10993
|
+
record.bgSub = null;
|
|
10994
|
+
if (sub)
|
|
10995
|
+
subs.push(sub);
|
|
10996
|
+
const entry = record.routeEntry;
|
|
10997
|
+
record.routeEntry = null;
|
|
10998
|
+
if (entry) {
|
|
10999
|
+
entry.closed = true;
|
|
11000
|
+
entries.push(entry);
|
|
11001
|
+
}
|
|
11002
|
+
}
|
|
11003
|
+
this.sessions.clear();
|
|
11004
|
+
const client = this.client;
|
|
11005
|
+
this.client = null;
|
|
11006
|
+
this.transports.clear();
|
|
11007
|
+
await Promise.allSettled(subs.map((sub) => sub.stop()));
|
|
11008
|
+
await Promise.allSettled(entries.map(async (entry) => {
|
|
11009
|
+
if (entry.channel == null)
|
|
11010
|
+
return;
|
|
11011
|
+
try {
|
|
11012
|
+
await entry.client.closeRouteChannel(entry.channel);
|
|
11013
|
+
} catch {}
|
|
11014
|
+
}));
|
|
11015
|
+
if (client) {
|
|
11016
|
+
try {
|
|
11017
|
+
client.close();
|
|
11018
|
+
} catch {}
|
|
11019
|
+
}
|
|
9012
11020
|
}
|
|
9013
|
-
|
|
9014
|
-
|
|
11021
|
+
async closeSession(projectRoot, session) {
|
|
11022
|
+
const identity = {
|
|
11023
|
+
project_root: canonicalizeProjectRoot(projectRoot),
|
|
11024
|
+
harness: this.harness,
|
|
11025
|
+
session: session && session.length > 0 ? session : DEFAULT_SESSION_ID
|
|
11026
|
+
};
|
|
11027
|
+
const key = identityKey(identity);
|
|
11028
|
+
const record = this.sessions.get(key);
|
|
11029
|
+
if (!record)
|
|
11030
|
+
return;
|
|
11031
|
+
record.closed = true;
|
|
11032
|
+
this.sessions.delete(key);
|
|
11033
|
+
const sub = record.bgSub;
|
|
11034
|
+
record.bgSub = null;
|
|
11035
|
+
const entry = record.routeEntry;
|
|
11036
|
+
record.routeEntry = null;
|
|
11037
|
+
if (entry)
|
|
11038
|
+
entry.closed = true;
|
|
11039
|
+
if (sub)
|
|
11040
|
+
await sub.stop();
|
|
11041
|
+
if (entry?.channel != null) {
|
|
11042
|
+
try {
|
|
11043
|
+
await entry.client.closeRouteChannel(entry.channel);
|
|
11044
|
+
} catch {}
|
|
11045
|
+
}
|
|
9015
11046
|
}
|
|
9016
11047
|
}
|
|
9017
|
-
function normalizeKey(projectRoot) {
|
|
9018
|
-
return canonicalizeProjectRoot(projectRoot);
|
|
9019
|
-
}
|
|
9020
11048
|
// src/shared/rpc-utils.ts
|
|
9021
11049
|
function projectHash(directory) {
|
|
9022
11050
|
return projectRootKeyHash(directory);
|
|
@@ -9163,6 +11191,12 @@ class AftRpcClient {
|
|
|
9163
11191
|
return false;
|
|
9164
11192
|
}
|
|
9165
11193
|
}
|
|
11194
|
+
async resolveEndpoint(signal) {
|
|
11195
|
+
const [info] = await this.resolvePortInfos(signal);
|
|
11196
|
+
if (!info)
|
|
11197
|
+
return null;
|
|
11198
|
+
return { port: info.port, token: info.token };
|
|
11199
|
+
}
|
|
9166
11200
|
async resolvePortInfos(signal) {
|
|
9167
11201
|
for (let attempt = 0;attempt < MAX_RETRIES; attempt++) {
|
|
9168
11202
|
throwIfAborted(signal);
|
|
@@ -9447,9 +11481,6 @@ function coerceAftStatus(response) {
|
|
|
9447
11481
|
};
|
|
9448
11482
|
}
|
|
9449
11483
|
|
|
9450
|
-
// src/tui/sidebar.tsx
|
|
9451
|
-
import { createEffect, createMemo, createSignal, on, onCleanup } from "solid-js";
|
|
9452
|
-
|
|
9453
11484
|
// src/shared/storage-paths.ts
|
|
9454
11485
|
import { homedir as homedir2 } from "node:os";
|
|
9455
11486
|
import { join as join4 } from "node:path";
|
|
@@ -9470,6 +11501,304 @@ function resolveCortexKitStorageRoot2() {
|
|
|
9470
11501
|
return join4(dataHome(), "cortexkit", "aft");
|
|
9471
11502
|
}
|
|
9472
11503
|
|
|
11504
|
+
// src/tui/notification-socket.ts
|
|
11505
|
+
var RECONNECT_BASE_MS = 500;
|
|
11506
|
+
var RECONNECT_MAX_MS = 1e4;
|
|
11507
|
+
var WEB_SOCKET_OPEN = 1;
|
|
11508
|
+
var clients = new Map;
|
|
11509
|
+
var statusInvalidationListeners = new Set;
|
|
11510
|
+
var lastHandledIdBySession = new Map;
|
|
11511
|
+
function defaultWebSocketCtor() {
|
|
11512
|
+
const ctor = globalThis.WebSocket;
|
|
11513
|
+
return ctor ?? null;
|
|
11514
|
+
}
|
|
11515
|
+
function defaultClient(directory) {
|
|
11516
|
+
let client = clients.get(directory);
|
|
11517
|
+
if (!client) {
|
|
11518
|
+
client = new AftRpcClient(resolveCortexKitStorageRoot2(), directory);
|
|
11519
|
+
clients.set(directory, client);
|
|
11520
|
+
}
|
|
11521
|
+
return client;
|
|
11522
|
+
}
|
|
11523
|
+
var deps = {
|
|
11524
|
+
createClient: defaultClient,
|
|
11525
|
+
WebSocketCtor: defaultWebSocketCtor(),
|
|
11526
|
+
setTimeout,
|
|
11527
|
+
clearTimeout
|
|
11528
|
+
};
|
|
11529
|
+
var opts = null;
|
|
11530
|
+
var socket = null;
|
|
11531
|
+
var socketScope = null;
|
|
11532
|
+
var helloedScope = null;
|
|
11533
|
+
var reconnectTimer;
|
|
11534
|
+
var reconnectAttempt = 0;
|
|
11535
|
+
var closed = true;
|
|
11536
|
+
var generation = 0;
|
|
11537
|
+
var connectingGeneration = null;
|
|
11538
|
+
var connectingScope = null;
|
|
11539
|
+
function startAftTuiSocket(options) {
|
|
11540
|
+
opts = options;
|
|
11541
|
+
closed = false;
|
|
11542
|
+
generation += 1;
|
|
11543
|
+
reconcileSocketScope();
|
|
11544
|
+
}
|
|
11545
|
+
function stopAftTuiSocket() {
|
|
11546
|
+
closed = true;
|
|
11547
|
+
generation += 1;
|
|
11548
|
+
connectingGeneration = null;
|
|
11549
|
+
connectingScope = null;
|
|
11550
|
+
if (reconnectTimer) {
|
|
11551
|
+
deps.clearTimeout(reconnectTimer);
|
|
11552
|
+
reconnectTimer = undefined;
|
|
11553
|
+
}
|
|
11554
|
+
closeCurrentSocket(false);
|
|
11555
|
+
for (const client of clients.values())
|
|
11556
|
+
client.reset();
|
|
11557
|
+
clients.clear();
|
|
11558
|
+
reconnectAttempt = 0;
|
|
11559
|
+
}
|
|
11560
|
+
function refreshAftTuiSocketScope() {
|
|
11561
|
+
if (closed)
|
|
11562
|
+
return;
|
|
11563
|
+
reconcileSocketScope();
|
|
11564
|
+
}
|
|
11565
|
+
function subscribeStatusInvalidations(listener) {
|
|
11566
|
+
statusInvalidationListeners.add(listener);
|
|
11567
|
+
return () => {
|
|
11568
|
+
statusInvalidationListeners.delete(listener);
|
|
11569
|
+
};
|
|
11570
|
+
}
|
|
11571
|
+
function createDebouncedStatusRefresh(refresh, delayMs) {
|
|
11572
|
+
let timer;
|
|
11573
|
+
let disposed = false;
|
|
11574
|
+
return {
|
|
11575
|
+
schedule() {
|
|
11576
|
+
if (disposed)
|
|
11577
|
+
return;
|
|
11578
|
+
if (timer)
|
|
11579
|
+
deps.clearTimeout(timer);
|
|
11580
|
+
timer = deps.setTimeout(() => {
|
|
11581
|
+
timer = undefined;
|
|
11582
|
+
refresh();
|
|
11583
|
+
}, delayMs);
|
|
11584
|
+
},
|
|
11585
|
+
dispose() {
|
|
11586
|
+
disposed = true;
|
|
11587
|
+
if (timer) {
|
|
11588
|
+
deps.clearTimeout(timer);
|
|
11589
|
+
timer = undefined;
|
|
11590
|
+
}
|
|
11591
|
+
}
|
|
11592
|
+
};
|
|
11593
|
+
}
|
|
11594
|
+
function currentScope() {
|
|
11595
|
+
const directory = opts?.getDirectory() ?? "";
|
|
11596
|
+
const sessionId = opts?.getSessionId() ?? "";
|
|
11597
|
+
if (!directory || !sessionId)
|
|
11598
|
+
return null;
|
|
11599
|
+
return { directory, sessionId };
|
|
11600
|
+
}
|
|
11601
|
+
function sameScope(a, b) {
|
|
11602
|
+
return a?.directory === b?.directory && a?.sessionId === b?.sessionId;
|
|
11603
|
+
}
|
|
11604
|
+
async function reconcileSocketScope() {
|
|
11605
|
+
if (closed || !opts)
|
|
11606
|
+
return;
|
|
11607
|
+
const scope = currentScope();
|
|
11608
|
+
if (!scope) {
|
|
11609
|
+
generation += 1;
|
|
11610
|
+
connectingGeneration = null;
|
|
11611
|
+
connectingScope = null;
|
|
11612
|
+
closeCurrentSocket(false);
|
|
11613
|
+
return;
|
|
11614
|
+
}
|
|
11615
|
+
if (socket && socketScope?.directory === scope.directory) {
|
|
11616
|
+
if (!sameScope(socketScope, scope) && socket.readyState !== WEB_SOCKET_OPEN) {
|
|
11617
|
+
generation += 1;
|
|
11618
|
+
closeCurrentSocket(false);
|
|
11619
|
+
await connect(scope, generation);
|
|
11620
|
+
return;
|
|
11621
|
+
}
|
|
11622
|
+
if (socket.readyState === WEB_SOCKET_OPEN && !sameScope(helloedScope, scope)) {
|
|
11623
|
+
await sendHelloWithFreshToken(socket, scope, generation);
|
|
11624
|
+
}
|
|
11625
|
+
return;
|
|
11626
|
+
}
|
|
11627
|
+
if (sameScope(connectingScope, scope))
|
|
11628
|
+
return;
|
|
11629
|
+
generation += 1;
|
|
11630
|
+
connectingGeneration = null;
|
|
11631
|
+
connectingScope = null;
|
|
11632
|
+
closeCurrentSocket(false);
|
|
11633
|
+
await connect(scope, generation);
|
|
11634
|
+
}
|
|
11635
|
+
async function connect(scope, connectGeneration) {
|
|
11636
|
+
if (closed || socket || connectingGeneration !== null)
|
|
11637
|
+
return;
|
|
11638
|
+
const WebSocketCtor = deps.WebSocketCtor ?? defaultWebSocketCtor();
|
|
11639
|
+
if (!WebSocketCtor) {
|
|
11640
|
+
scheduleReconnect();
|
|
11641
|
+
return;
|
|
11642
|
+
}
|
|
11643
|
+
connectingGeneration = connectGeneration;
|
|
11644
|
+
connectingScope = scope;
|
|
11645
|
+
let endpoint = null;
|
|
11646
|
+
try {
|
|
11647
|
+
endpoint = await deps.createClient(scope.directory).resolveEndpoint();
|
|
11648
|
+
} catch {
|
|
11649
|
+
endpoint = null;
|
|
11650
|
+
}
|
|
11651
|
+
if (connectingGeneration === connectGeneration) {
|
|
11652
|
+
connectingGeneration = null;
|
|
11653
|
+
connectingScope = null;
|
|
11654
|
+
}
|
|
11655
|
+
if (closed || connectGeneration !== generation || !sameScope(currentScope(), scope))
|
|
11656
|
+
return;
|
|
11657
|
+
if (!endpoint) {
|
|
11658
|
+
scheduleReconnect();
|
|
11659
|
+
return;
|
|
11660
|
+
}
|
|
11661
|
+
let ws;
|
|
11662
|
+
try {
|
|
11663
|
+
ws = new WebSocketCtor(`ws://127.0.0.1:${endpoint.port}/ws`);
|
|
11664
|
+
} catch {
|
|
11665
|
+
scheduleReconnect();
|
|
11666
|
+
return;
|
|
11667
|
+
}
|
|
11668
|
+
socket = ws;
|
|
11669
|
+
socketScope = scope;
|
|
11670
|
+
helloedScope = null;
|
|
11671
|
+
ws.addEventListener("open", () => {
|
|
11672
|
+
if (socket !== ws || connectGeneration !== generation)
|
|
11673
|
+
return;
|
|
11674
|
+
reconnectAttempt = 0;
|
|
11675
|
+
sendHello(ws, scope, endpoint.token);
|
|
11676
|
+
});
|
|
11677
|
+
ws.addEventListener("message", (event) => {
|
|
11678
|
+
if (socket !== ws || connectGeneration !== generation)
|
|
11679
|
+
return;
|
|
11680
|
+
const data = typeof event.data === "string" ? event.data : String(event.data ?? "");
|
|
11681
|
+
handleSocketMessage(ws, data, connectGeneration);
|
|
11682
|
+
});
|
|
11683
|
+
const onDown = () => {
|
|
11684
|
+
if (socket !== ws)
|
|
11685
|
+
return;
|
|
11686
|
+
socket = null;
|
|
11687
|
+
socketScope = null;
|
|
11688
|
+
helloedScope = null;
|
|
11689
|
+
generation += 1;
|
|
11690
|
+
scheduleReconnect();
|
|
11691
|
+
};
|
|
11692
|
+
ws.addEventListener("close", onDown);
|
|
11693
|
+
ws.addEventListener("error", () => {
|
|
11694
|
+
try {
|
|
11695
|
+
ws.close();
|
|
11696
|
+
} catch {}
|
|
11697
|
+
onDown();
|
|
11698
|
+
});
|
|
11699
|
+
}
|
|
11700
|
+
function closeCurrentSocket(schedule) {
|
|
11701
|
+
const ws = socket;
|
|
11702
|
+
socket = null;
|
|
11703
|
+
socketScope = null;
|
|
11704
|
+
helloedScope = null;
|
|
11705
|
+
if (ws) {
|
|
11706
|
+
try {
|
|
11707
|
+
ws.close();
|
|
11708
|
+
} catch {}
|
|
11709
|
+
}
|
|
11710
|
+
if (schedule)
|
|
11711
|
+
scheduleReconnect();
|
|
11712
|
+
}
|
|
11713
|
+
function scheduleReconnect() {
|
|
11714
|
+
if (closed || reconnectTimer)
|
|
11715
|
+
return;
|
|
11716
|
+
const scope = currentScope();
|
|
11717
|
+
if (!scope)
|
|
11718
|
+
return;
|
|
11719
|
+
const delay2 = Math.min(RECONNECT_BASE_MS * 2 ** reconnectAttempt, RECONNECT_MAX_MS);
|
|
11720
|
+
reconnectAttempt += 1;
|
|
11721
|
+
reconnectTimer = deps.setTimeout(() => {
|
|
11722
|
+
reconnectTimer = undefined;
|
|
11723
|
+
const nextScope = currentScope();
|
|
11724
|
+
if (!nextScope)
|
|
11725
|
+
return;
|
|
11726
|
+
connect(nextScope, generation);
|
|
11727
|
+
}, delay2);
|
|
11728
|
+
}
|
|
11729
|
+
async function sendHelloWithFreshToken(ws, scope, expectedGeneration) {
|
|
11730
|
+
let endpoint = null;
|
|
11731
|
+
try {
|
|
11732
|
+
endpoint = await deps.createClient(scope.directory).resolveEndpoint();
|
|
11733
|
+
} catch {
|
|
11734
|
+
endpoint = null;
|
|
11735
|
+
}
|
|
11736
|
+
if (closed || socket !== ws || expectedGeneration !== generation || !sameScope(currentScope(), scope)) {
|
|
11737
|
+
return;
|
|
11738
|
+
}
|
|
11739
|
+
sendHello(ws, scope, endpoint?.token ?? null);
|
|
11740
|
+
}
|
|
11741
|
+
function sendHello(ws, scope, token) {
|
|
11742
|
+
helloedScope = scope;
|
|
11743
|
+
if (socket === ws)
|
|
11744
|
+
socketScope = scope;
|
|
11745
|
+
const lastReceivedId = lastHandledIdBySession.get(scope.sessionId) ?? 0;
|
|
11746
|
+
ws.send(JSON.stringify({
|
|
11747
|
+
type: "hello",
|
|
11748
|
+
token: token ?? "",
|
|
11749
|
+
sessionId: scope.sessionId,
|
|
11750
|
+
lastReceivedId
|
|
11751
|
+
}));
|
|
11752
|
+
}
|
|
11753
|
+
async function handleSocketMessage(ws, raw, messageGeneration) {
|
|
11754
|
+
let msg;
|
|
11755
|
+
try {
|
|
11756
|
+
msg = JSON.parse(raw);
|
|
11757
|
+
} catch {
|
|
11758
|
+
return;
|
|
11759
|
+
}
|
|
11760
|
+
if (msg.type === "status-changed") {
|
|
11761
|
+
for (const listener of statusInvalidationListeners) {
|
|
11762
|
+
try {
|
|
11763
|
+
listener({ sessionId: msg.sessionId });
|
|
11764
|
+
} catch {}
|
|
11765
|
+
}
|
|
11766
|
+
return;
|
|
11767
|
+
}
|
|
11768
|
+
if (msg.type === "notification" && msg.notification) {
|
|
11769
|
+
const notification = msg.notification;
|
|
11770
|
+
const active = opts?.getSessionId() ?? null;
|
|
11771
|
+
if (!active)
|
|
11772
|
+
return;
|
|
11773
|
+
if (notification.sessionId && notification.sessionId !== active)
|
|
11774
|
+
return;
|
|
11775
|
+
let consumed = false;
|
|
11776
|
+
try {
|
|
11777
|
+
consumed = await Promise.resolve(opts?.onNotification(notification) ?? false);
|
|
11778
|
+
} catch {
|
|
11779
|
+
consumed = false;
|
|
11780
|
+
}
|
|
11781
|
+
if (socket !== ws || messageGeneration !== generation || (opts?.getSessionId() ?? null) !== active) {
|
|
11782
|
+
return;
|
|
11783
|
+
}
|
|
11784
|
+
if (consumed && notification.id > (lastHandledIdBySession.get(active) ?? 0)) {
|
|
11785
|
+
lastHandledIdBySession.set(active, notification.id);
|
|
11786
|
+
try {
|
|
11787
|
+
ws.send(JSON.stringify({ type: "ack", lastReceivedId: notification.id }));
|
|
11788
|
+
} catch {}
|
|
11789
|
+
}
|
|
11790
|
+
return;
|
|
11791
|
+
}
|
|
11792
|
+
if (msg.type === "error") {
|
|
11793
|
+
try {
|
|
11794
|
+
ws.close();
|
|
11795
|
+
} catch {}
|
|
11796
|
+
}
|
|
11797
|
+
}
|
|
11798
|
+
|
|
11799
|
+
// src/tui/sidebar.tsx
|
|
11800
|
+
import { createEffect, createMemo, createSignal, on, onCleanup } from "solid-js";
|
|
11801
|
+
|
|
9473
11802
|
// src/tui/preferences.ts
|
|
9474
11803
|
var import_comment_json = __toESM(require_src2(), 1);
|
|
9475
11804
|
import { watch } from "node:fs";
|
|
@@ -9485,14 +11814,14 @@ function getTuiPreferencesFile() {
|
|
|
9485
11814
|
const configDir = process.env.OPENCODE_CONFIG_DIR || join5(process.env.XDG_CONFIG_HOME || join5(homedir3(), ".config"), "opencode");
|
|
9486
11815
|
return join5(configDir, FILE_NAME);
|
|
9487
11816
|
}
|
|
9488
|
-
function
|
|
11817
|
+
function isRecord2(value) {
|
|
9489
11818
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9490
11819
|
}
|
|
9491
11820
|
async function readTuiPreferencesFile() {
|
|
9492
11821
|
try {
|
|
9493
11822
|
const raw = await readFile(getTuiPreferencesFile(), "utf8");
|
|
9494
11823
|
const root = import_comment_json.parse(raw);
|
|
9495
|
-
return
|
|
11824
|
+
return isRecord2(root) ? root : {};
|
|
9496
11825
|
} catch {
|
|
9497
11826
|
return {};
|
|
9498
11827
|
}
|
|
@@ -9531,11 +11860,11 @@ function seedCollapsedFromPrefs(prefs) {
|
|
|
9531
11860
|
}
|
|
9532
11861
|
function resolveAftPrefs(root) {
|
|
9533
11862
|
const entry = root[PLUGIN_KEY];
|
|
9534
|
-
if (!
|
|
11863
|
+
if (!isRecord2(entry))
|
|
9535
11864
|
return structuredClone(DEFAULT_PREFS);
|
|
9536
11865
|
const d = DEFAULT_PREFS;
|
|
9537
|
-
const header =
|
|
9538
|
-
const sections =
|
|
11866
|
+
const header = isRecord2(entry.header) ? entry.header : {};
|
|
11867
|
+
const sections = isRecord2(entry.sections) ? entry.sections : {};
|
|
9539
11868
|
return {
|
|
9540
11869
|
forceToTop: bool(entry.forceToTop, d.forceToTop),
|
|
9541
11870
|
order: int(entry.order, d.order, -1e4, 1e4),
|
|
@@ -9557,7 +11886,7 @@ function resolveAftPrefs(root) {
|
|
|
9557
11886
|
var FORCE_TOP_BASE = -1e5;
|
|
9558
11887
|
function computeEffectiveOrder(root, pluginKey, defaultOrder) {
|
|
9559
11888
|
const entry = root[pluginKey];
|
|
9560
|
-
if (!
|
|
11889
|
+
if (!isRecord2(entry))
|
|
9561
11890
|
return defaultOrder;
|
|
9562
11891
|
if (entry.forceToTop === true) {
|
|
9563
11892
|
return FORCE_TOP_BASE + Object.keys(root).indexOf(pluginKey);
|
|
@@ -9577,7 +11906,7 @@ function setNested(target, path, value) {
|
|
|
9577
11906
|
if (key === undefined)
|
|
9578
11907
|
return;
|
|
9579
11908
|
const next = current[key];
|
|
9580
|
-
if (!
|
|
11909
|
+
if (!isRecord2(next)) {
|
|
9581
11910
|
const created = {};
|
|
9582
11911
|
current[key] = created;
|
|
9583
11912
|
current = created;
|
|
@@ -9604,11 +11933,11 @@ async function writePreference(pluginKey, path, value) {
|
|
|
9604
11933
|
let root;
|
|
9605
11934
|
try {
|
|
9606
11935
|
const parsed = import_comment_json.parse(text);
|
|
9607
|
-
root =
|
|
11936
|
+
root = isRecord2(parsed) ? parsed : {};
|
|
9608
11937
|
} catch {
|
|
9609
11938
|
root = {};
|
|
9610
11939
|
}
|
|
9611
|
-
if (!
|
|
11940
|
+
if (!isRecord2(root[pluginKey])) {
|
|
9612
11941
|
root[pluginKey] = {};
|
|
9613
11942
|
}
|
|
9614
11943
|
const entry = root[pluginKey];
|
|
@@ -9672,7 +12001,6 @@ function persistCollapsedIfEnabled(prefs, collapsed) {
|
|
|
9672
12001
|
import { jsxDEV, Fragment } from "@opentui/solid/jsx-dev-runtime";
|
|
9673
12002
|
var SINGLE_BORDER = { type: "single" };
|
|
9674
12003
|
var REFRESH_DEBOUNCE_MS = 200;
|
|
9675
|
-
var POLL_INTERVAL_MS = 1500;
|
|
9676
12004
|
function formatBytes2(n) {
|
|
9677
12005
|
if (!Number.isFinite(n) || n <= 0)
|
|
9678
12006
|
return "—";
|
|
@@ -9864,9 +12192,7 @@ var SidebarContent = (props) => {
|
|
|
9864
12192
|
const [prefs, setPrefs] = createSignal(structuredClone(DEFAULT_PREFS));
|
|
9865
12193
|
const [collapsed, setCollapsed] = createSignal(seedCollapsedFromPrefs(DEFAULT_PREFS));
|
|
9866
12194
|
let inflight = null;
|
|
9867
|
-
let
|
|
9868
|
-
let debounceTimer;
|
|
9869
|
-
let pollTimer;
|
|
12195
|
+
let generation2 = 0;
|
|
9870
12196
|
const currentDirectory = () => props.api.state.path.directory ?? "";
|
|
9871
12197
|
const requestRender = () => {
|
|
9872
12198
|
try {
|
|
@@ -9892,7 +12218,7 @@ var SidebarContent = (props) => {
|
|
|
9892
12218
|
const sid = props.sessionID();
|
|
9893
12219
|
const directory = currentDirectory();
|
|
9894
12220
|
if (!sid || !directory) {
|
|
9895
|
-
|
|
12221
|
+
generation2++;
|
|
9896
12222
|
abortInflight();
|
|
9897
12223
|
if (status()) {
|
|
9898
12224
|
setStatus(null);
|
|
@@ -9904,10 +12230,10 @@ var SidebarContent = (props) => {
|
|
|
9904
12230
|
if (inflight) {
|
|
9905
12231
|
if (inflight.directory === directory && inflight.sessionID === sid)
|
|
9906
12232
|
return;
|
|
9907
|
-
|
|
12233
|
+
generation2++;
|
|
9908
12234
|
abortInflight();
|
|
9909
12235
|
}
|
|
9910
|
-
const requestGeneration = ++
|
|
12236
|
+
const requestGeneration = ++generation2;
|
|
9911
12237
|
const controller = new AbortController;
|
|
9912
12238
|
inflight = { controller, generation: requestGeneration, directory, sessionID: sid };
|
|
9913
12239
|
try {
|
|
@@ -9921,7 +12247,7 @@ var SidebarContent = (props) => {
|
|
|
9921
12247
|
return isSnapshotForContext(coerceAftStatus(rec), directory, rec?.served_directory);
|
|
9922
12248
|
}
|
|
9923
12249
|
});
|
|
9924
|
-
if (controller.signal.aborted || requestGeneration !==
|
|
12250
|
+
if (controller.signal.aborted || requestGeneration !== generation2)
|
|
9925
12251
|
return;
|
|
9926
12252
|
if (currentDirectory() !== directory || props.sessionID() !== sid)
|
|
9927
12253
|
return;
|
|
@@ -9934,25 +12260,22 @@ var SidebarContent = (props) => {
|
|
|
9934
12260
|
const haveGoodForContext = current !== null && current.directory === directory && current.sessionID === sid && current.snapshot.cache_role !== "not_initialized";
|
|
9935
12261
|
if (shouldSuppressUninitializedDowngrade(snapshot.cache_role, haveGoodForContext))
|
|
9936
12262
|
return;
|
|
12263
|
+
if (current !== null && current.directory === directory && current.sessionID === sid && JSON.stringify(current.snapshot) === JSON.stringify(snapshot)) {
|
|
12264
|
+
return;
|
|
12265
|
+
}
|
|
9937
12266
|
setStatus({ directory, sessionID: sid, snapshot });
|
|
9938
12267
|
requestRender();
|
|
9939
12268
|
}
|
|
9940
12269
|
} catch {
|
|
9941
|
-
if (controller.signal.aborted || requestGeneration !==
|
|
12270
|
+
if (controller.signal.aborted || requestGeneration !== generation2)
|
|
9942
12271
|
return;
|
|
9943
12272
|
} finally {
|
|
9944
12273
|
if (inflight?.generation === requestGeneration)
|
|
9945
12274
|
inflight = null;
|
|
9946
12275
|
}
|
|
9947
12276
|
};
|
|
9948
|
-
const
|
|
9949
|
-
|
|
9950
|
-
clearTimeout(debounceTimer);
|
|
9951
|
-
debounceTimer = setTimeout(() => {
|
|
9952
|
-
debounceTimer = undefined;
|
|
9953
|
-
refresh();
|
|
9954
|
-
}, REFRESH_DEBOUNCE_MS);
|
|
9955
|
-
};
|
|
12277
|
+
const statusDebouncer = createDebouncedStatusRefresh(refresh, REFRESH_DEBOUNCE_MS);
|
|
12278
|
+
const scheduleRefresh = () => statusDebouncer.schedule();
|
|
9956
12279
|
const reloadPrefs = async () => {
|
|
9957
12280
|
const root = await readTuiPreferencesFile();
|
|
9958
12281
|
const next = resolveAftPrefs(root);
|
|
@@ -9966,48 +12289,26 @@ var SidebarContent = (props) => {
|
|
|
9966
12289
|
});
|
|
9967
12290
|
onCleanup(() => {
|
|
9968
12291
|
unwatchPrefs();
|
|
9969
|
-
|
|
12292
|
+
generation2++;
|
|
9970
12293
|
abortInflight();
|
|
9971
|
-
|
|
9972
|
-
clearTimeout(debounceTimer);
|
|
9973
|
-
if (pollTimer)
|
|
9974
|
-
clearInterval(pollTimer);
|
|
12294
|
+
statusDebouncer.dispose();
|
|
9975
12295
|
});
|
|
9976
12296
|
createEffect(on(props.sessionID, () => {
|
|
12297
|
+
refreshAftTuiSocketScope();
|
|
9977
12298
|
refresh();
|
|
9978
12299
|
}));
|
|
9979
12300
|
createEffect(on(props.sessionID, (sessionID) => {
|
|
9980
12301
|
if (!sessionID)
|
|
9981
12302
|
return;
|
|
9982
|
-
const
|
|
9983
|
-
|
|
9984
|
-
|
|
9985
|
-
|
|
9986
|
-
|
|
9987
|
-
}),
|
|
9988
|
-
props.api.event.on("session.updated", (event) => {
|
|
9989
|
-
if (event.properties?.info?.id !== sessionID)
|
|
9990
|
-
return;
|
|
9991
|
-
scheduleRefresh();
|
|
9992
|
-
})
|
|
9993
|
-
];
|
|
9994
|
-
if (!pollTimer) {
|
|
9995
|
-
pollTimer = setInterval(() => {
|
|
9996
|
-
scheduleRefresh();
|
|
9997
|
-
}, POLL_INTERVAL_MS);
|
|
9998
|
-
}
|
|
12303
|
+
const unsubscribe = subscribeStatusInvalidations((event) => {
|
|
12304
|
+
if (event.sessionId && event.sessionId !== props.sessionID())
|
|
12305
|
+
return;
|
|
12306
|
+
scheduleRefresh();
|
|
12307
|
+
});
|
|
9999
12308
|
onCleanup(() => {
|
|
10000
|
-
|
|
10001
|
-
|
|
10002
|
-
unsub();
|
|
10003
|
-
} catch {}
|
|
10004
|
-
}
|
|
10005
|
-
generation++;
|
|
12309
|
+
unsubscribe();
|
|
12310
|
+
generation2++;
|
|
10006
12311
|
abortInflight();
|
|
10007
|
-
if (pollTimer) {
|
|
10008
|
-
clearInterval(pollTimer);
|
|
10009
|
-
pollTimer = undefined;
|
|
10010
|
-
}
|
|
10011
12312
|
});
|
|
10012
12313
|
}, { defer: false }));
|
|
10013
12314
|
const s = () => scopedSidebarSnapshot(status(), currentDirectory(), props.sessionID());
|
|
@@ -10359,30 +12660,6 @@ function getSessionId(api) {
|
|
|
10359
12660
|
} catch {}
|
|
10360
12661
|
return null;
|
|
10361
12662
|
}
|
|
10362
|
-
var lastReceivedNotificationIdBySession = new Map;
|
|
10363
|
-
async function consumeTuiMessages(client, sessionId) {
|
|
10364
|
-
try {
|
|
10365
|
-
const result = await client.call("consume-notifications", {
|
|
10366
|
-
lastReceivedId: lastReceivedNotificationIdBySession.get(sessionId) ?? 0,
|
|
10367
|
-
sessionId
|
|
10368
|
-
});
|
|
10369
|
-
return (result.messages ?? []).map((message) => ({
|
|
10370
|
-
id: message.id,
|
|
10371
|
-
type: message.type,
|
|
10372
|
-
payload: message.payload,
|
|
10373
|
-
sessionId: message.sessionId
|
|
10374
|
-
}));
|
|
10375
|
-
} catch {
|
|
10376
|
-
return [];
|
|
10377
|
-
}
|
|
10378
|
-
}
|
|
10379
|
-
function markTuiMessagesReceived(sessionId, messages) {
|
|
10380
|
-
const previous = lastReceivedNotificationIdBySession.get(sessionId) ?? 0;
|
|
10381
|
-
const next = messages.reduce((max, message) => Math.max(max, message.id), previous);
|
|
10382
|
-
if (next > previous)
|
|
10383
|
-
lastReceivedNotificationIdBySession.set(sessionId, next);
|
|
10384
|
-
}
|
|
10385
|
-
var POLL_INTERVAL_MS2 = 1500;
|
|
10386
12663
|
function formatCountShort(value) {
|
|
10387
12664
|
if (value == null || !Number.isFinite(value))
|
|
10388
12665
|
return "—";
|
|
@@ -10455,17 +12732,17 @@ var StatusDialog = (props) => {
|
|
|
10455
12732
|
const t = () => theme();
|
|
10456
12733
|
const [status, setStatus] = createSignal2(props.initial);
|
|
10457
12734
|
const [error2, setError] = createSignal2(props.initialError);
|
|
10458
|
-
let
|
|
10459
|
-
let
|
|
10460
|
-
const
|
|
10461
|
-
if (
|
|
12735
|
+
let refreshGeneration = 0;
|
|
12736
|
+
let refreshController = null;
|
|
12737
|
+
const refreshStatus = async () => {
|
|
12738
|
+
if (refreshController)
|
|
10462
12739
|
return;
|
|
10463
12740
|
const controller = new AbortController;
|
|
10464
|
-
const requestGeneration = ++
|
|
10465
|
-
|
|
12741
|
+
const requestGeneration = ++refreshGeneration;
|
|
12742
|
+
refreshController = controller;
|
|
10466
12743
|
try {
|
|
10467
12744
|
const response = await props.client.call("status", { sessionID: props.sessionID }, { signal: controller.signal, accept: statusAcceptGate(props.directory) });
|
|
10468
|
-
if (controller.signal.aborted || requestGeneration !==
|
|
12745
|
+
if (controller.signal.aborted || requestGeneration !== refreshGeneration)
|
|
10469
12746
|
return;
|
|
10470
12747
|
if (response.success !== false) {
|
|
10471
12748
|
const snapshot = coerceAftStatus(response);
|
|
@@ -10477,22 +12754,26 @@ var StatusDialog = (props) => {
|
|
|
10477
12754
|
setError(null);
|
|
10478
12755
|
}
|
|
10479
12756
|
} catch {
|
|
10480
|
-
if (controller.signal.aborted || requestGeneration !==
|
|
12757
|
+
if (controller.signal.aborted || requestGeneration !== refreshGeneration)
|
|
10481
12758
|
return;
|
|
10482
12759
|
} finally {
|
|
10483
|
-
if (
|
|
10484
|
-
|
|
12760
|
+
if (refreshController === controller)
|
|
12761
|
+
refreshController = null;
|
|
10485
12762
|
}
|
|
10486
12763
|
};
|
|
10487
|
-
const
|
|
10488
|
-
|
|
10489
|
-
|
|
12764
|
+
const statusDebouncer = createDebouncedStatusRefresh(refreshStatus, 200);
|
|
12765
|
+
const unsubscribeStatusInvalidations = subscribeStatusInvalidations((event) => {
|
|
12766
|
+
if (event.sessionId && event.sessionId !== props.sessionID)
|
|
12767
|
+
return;
|
|
12768
|
+
statusDebouncer.schedule();
|
|
12769
|
+
});
|
|
10490
12770
|
onCleanup2(() => {
|
|
10491
|
-
|
|
10492
|
-
|
|
10493
|
-
|
|
10494
|
-
|
|
10495
|
-
|
|
12771
|
+
unsubscribeStatusInvalidations();
|
|
12772
|
+
statusDebouncer.dispose();
|
|
12773
|
+
refreshGeneration++;
|
|
12774
|
+
if (refreshController) {
|
|
12775
|
+
refreshController.abort();
|
|
12776
|
+
refreshController = null;
|
|
10496
12777
|
}
|
|
10497
12778
|
});
|
|
10498
12779
|
const cacheRoleTone = (role) => role === "main" ? "accent" : role === "worktree" ? "warn" : "muted";
|
|
@@ -10982,43 +13263,35 @@ var tui = async (api) => {
|
|
|
10982
13263
|
api.slots.register(await createAftSidebarSlot(api, version));
|
|
10983
13264
|
} catch {}
|
|
10984
13265
|
registerStatusCommand(api);
|
|
10985
|
-
|
|
10986
|
-
const messagePoller = setInterval(() => {
|
|
10987
|
-
if (pollInFlight)
|
|
10988
|
-
return;
|
|
13266
|
+
const handleNotification = async (message) => {
|
|
10989
13267
|
const requestedSessionId = getSessionId(api);
|
|
10990
13268
|
if (!requestedSessionId)
|
|
10991
|
-
return;
|
|
10992
|
-
|
|
10993
|
-
|
|
10994
|
-
|
|
10995
|
-
|
|
10996
|
-
|
|
10997
|
-
|
|
10998
|
-
|
|
10999
|
-
|
|
11000
|
-
|
|
11001
|
-
|
|
11002
|
-
|
|
11003
|
-
|
|
11004
|
-
|
|
11005
|
-
|
|
11006
|
-
|
|
11007
|
-
|
|
11008
|
-
|
|
11009
|
-
|
|
11010
|
-
await showStatusDialog(api);
|
|
11011
|
-
}
|
|
11012
|
-
if (getSessionId(api) !== requestedSessionId)
|
|
11013
|
-
return;
|
|
11014
|
-
markTuiMessagesReceived(requestedSessionId, orderedMessages);
|
|
11015
|
-
}).catch(() => {}).finally(() => {
|
|
11016
|
-
pollInFlight = false;
|
|
11017
|
-
});
|
|
11018
|
-
}, 500);
|
|
13269
|
+
return false;
|
|
13270
|
+
if (message.sessionId && message.sessionId !== requestedSessionId)
|
|
13271
|
+
return false;
|
|
13272
|
+
if (message.type !== "action")
|
|
13273
|
+
return false;
|
|
13274
|
+
if (message.payload?.action !== "show-status-dialog")
|
|
13275
|
+
return false;
|
|
13276
|
+
await showStatusDialog(api);
|
|
13277
|
+
return true;
|
|
13278
|
+
};
|
|
13279
|
+
startAftTuiSocket({
|
|
13280
|
+
getDirectory: () => api.state.path.directory ?? "",
|
|
13281
|
+
getSessionId: () => getSessionId(api),
|
|
13282
|
+
onNotification: handleNotification
|
|
13283
|
+
});
|
|
13284
|
+
const socketScopeUnsubs = [
|
|
13285
|
+
api.event?.on?.("message.updated", () => refreshAftTuiSocketScope()),
|
|
13286
|
+
api.event?.on?.("session.updated", () => refreshAftTuiSocketScope())
|
|
13287
|
+
].filter(Boolean);
|
|
11019
13288
|
api.lifecycle?.onDispose?.(() => {
|
|
11020
|
-
|
|
11021
|
-
|
|
13289
|
+
stopAftTuiSocket();
|
|
13290
|
+
for (const unsub of socketScopeUnsubs) {
|
|
13291
|
+
try {
|
|
13292
|
+
unsub();
|
|
13293
|
+
} catch {}
|
|
13294
|
+
}
|
|
11022
13295
|
for (const client of rpcClients.values())
|
|
11023
13296
|
client.reset();
|
|
11024
13297
|
rpcClients.clear();
|