@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.
Files changed (59) hide show
  1. package/dist/bg-notifications.d.ts +35 -0
  2. package/dist/bg-notifications.d.ts.map +1 -1
  3. package/dist/config.d.ts +7 -3
  4. package/dist/config.d.ts.map +1 -1
  5. package/dist/configure-warnings.d.ts +3 -3
  6. package/dist/configure-warnings.d.ts.map +1 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +6092 -9528
  9. package/dist/notifications.d.ts +2 -2
  10. package/dist/notifications.d.ts.map +1 -1
  11. package/dist/shared/rpc-client.d.ts +6 -0
  12. package/dist/shared/rpc-client.d.ts.map +1 -1
  13. package/dist/shared/rpc-notifications.d.ts +29 -15
  14. package/dist/shared/rpc-notifications.d.ts.map +1 -1
  15. package/dist/shared/rpc-server.d.ts +10 -1
  16. package/dist/shared/rpc-server.d.ts.map +1 -1
  17. package/dist/subc-tool-schemas.d.ts +1 -1
  18. package/dist/subc-tool-schemas.d.ts.map +1 -1
  19. package/dist/tools/_shared.d.ts +12 -2
  20. package/dist/tools/_shared.d.ts.map +1 -1
  21. package/dist/tools/ast.d.ts.map +1 -1
  22. package/dist/tools/bash.d.ts.map +1 -1
  23. package/dist/tools/bash_watch.d.ts.map +1 -1
  24. package/dist/tools/hoisted.d.ts.map +1 -1
  25. package/dist/tools/imports.d.ts.map +1 -1
  26. package/dist/tools/inspect.d.ts +0 -1
  27. package/dist/tools/inspect.d.ts.map +1 -1
  28. package/dist/tools/navigation.d.ts.map +1 -1
  29. package/dist/tools/permissions.d.ts +23 -13
  30. package/dist/tools/permissions.d.ts.map +1 -1
  31. package/dist/tools/reading.d.ts +0 -1
  32. package/dist/tools/reading.d.ts.map +1 -1
  33. package/dist/tools/refactoring.d.ts.map +1 -1
  34. package/dist/tools/safety.d.ts.map +1 -1
  35. package/dist/tools/search.d.ts.map +1 -1
  36. package/dist/tools/semantic.d.ts.map +1 -1
  37. package/dist/tui/notification-socket.d.ts +41 -0
  38. package/dist/tui/notification-socket.d.ts.map +1 -0
  39. package/dist/tui.js +2460 -187
  40. package/dist/types.d.ts +4 -2
  41. package/dist/types.d.ts.map +1 -1
  42. package/package.json +8 -9
  43. package/src/shared/rpc-client.ts +9 -0
  44. package/src/shared/rpc-notifications.ts +97 -41
  45. package/src/shared/rpc-server.ts +286 -67
  46. package/src/tui/index.tsx +77 -125
  47. package/src/tui/notification-socket.ts +421 -0
  48. package/src/tui/sidebar.tsx +38 -54
  49. package/dist/patch-parser.d.ts +0 -33
  50. package/dist/patch-parser.d.ts.map +0 -1
  51. package/dist/shared/opencode-config-dir.d.ts +0 -16
  52. package/dist/shared/opencode-config-dir.d.ts.map +0 -1
  53. package/dist/shared/pty-cache.d.ts +0 -18
  54. package/dist/shared/pty-cache.d.ts.map +0 -1
  55. package/dist/shared/tui-config.d.ts +0 -2
  56. package/dist/shared/tui-config.d.ts.map +0 -1
  57. package/src/shared/opencode-config-dir.ts +0 -46
  58. package/src/shared/pty-cache.ts +0 -113
  59. 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.42.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 shutdown() {
8955
- if (this.cleanupTimer) {
8956
- clearInterval(this.cleanupTimer);
8957
- this.cleanupTimer = null;
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
- const shutdowns = [
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
- async replaceBinary(newPath) {
8968
- this.binaryPath = newPath;
8969
- for (const entry of this.bridges.values()) {
8970
- this.staleBridges.add(entry.bridge);
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
- log(message, meta) {
8977
- const logger = this.logger ?? getActiveLogger();
8978
- if (logger) {
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
- logger.log(message, meta);
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
- console.error(`[aft-bridge] ERROR: pool logger log threw: ${err instanceof Error ? err.message : String(err)}`);
8983
- console.error(`[aft-bridge] ${message}`);
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
- logger.error(message, meta);
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
- console.error(`[aft-bridge] ERROR: pool logger error threw: ${err instanceof Error ? err.message : String(err)}`);
8995
- console.error(`[aft-bridge] ERROR: ${message}`);
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
- } else
8998
- error(message, meta);
10887
+ } finally {
10888
+ record.inflight -= 1;
10889
+ this.deleteSessionIfEmpty(key, record);
10890
+ }
8999
10891
  }
9000
- setConfigureOverride(key, value) {
9001
- if (value === undefined) {
9002
- delete this.configOverrides[key];
9003
- } else {
9004
- this.configOverrides[key] = value;
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
- get size() {
9008
- return this.bridges.size;
10982
+ setConfigureOverride(_key, _value) {}
10983
+ async replaceBinary(path) {
10984
+ return path;
9009
10985
  }
9010
- _testGetConfigOverrides() {
9011
- return { ...this.configOverrides };
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
- _testGetBridgeOptions() {
9014
- return { ...this.bridgeOptions };
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 isRecord(value) {
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 isRecord(root) ? root : {};
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 (!isRecord(entry))
11863
+ if (!isRecord2(entry))
9535
11864
  return structuredClone(DEFAULT_PREFS);
9536
11865
  const d = DEFAULT_PREFS;
9537
- const header = isRecord(entry.header) ? entry.header : {};
9538
- const sections = isRecord(entry.sections) ? entry.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 (!isRecord(entry))
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 (!isRecord(next)) {
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 = isRecord(parsed) ? parsed : {};
11936
+ root = isRecord2(parsed) ? parsed : {};
9608
11937
  } catch {
9609
11938
  root = {};
9610
11939
  }
9611
- if (!isRecord(root[pluginKey])) {
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 generation = 0;
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
- generation++;
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
- generation++;
12233
+ generation2++;
9908
12234
  abortInflight();
9909
12235
  }
9910
- const requestGeneration = ++generation;
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 !== generation)
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 !== generation)
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 scheduleRefresh = () => {
9949
- if (debounceTimer)
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
- generation++;
12292
+ generation2++;
9970
12293
  abortInflight();
9971
- if (debounceTimer)
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 unsubs = [
9983
- props.api.event.on("message.updated", (event) => {
9984
- if (event.properties?.info?.sessionID !== sessionID)
9985
- return;
9986
- scheduleRefresh();
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
- for (const unsub of unsubs) {
10001
- try {
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 pollGeneration = 0;
10459
- let pollController = null;
10460
- const pollStatus = async () => {
10461
- if (pollController)
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 = ++pollGeneration;
10465
- pollController = controller;
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 !== pollGeneration)
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 !== pollGeneration)
12757
+ if (controller.signal.aborted || requestGeneration !== refreshGeneration)
10481
12758
  return;
10482
12759
  } finally {
10483
- if (pollController === controller)
10484
- pollController = null;
12760
+ if (refreshController === controller)
12761
+ refreshController = null;
10485
12762
  }
10486
12763
  };
10487
- const timer = setInterval(() => {
10488
- pollStatus();
10489
- }, POLL_INTERVAL_MS2);
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
- clearInterval(timer);
10492
- pollGeneration++;
10493
- if (pollController) {
10494
- pollController.abort();
10495
- pollController = null;
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
- let pollInFlight = false;
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
- const directory = api.state.path.directory ?? "";
10993
- if (!directory)
10994
- return;
10995
- pollInFlight = true;
10996
- const client = getRpcClient(directory);
10997
- consumeTuiMessages(client, requestedSessionId).then(async (messages) => {
10998
- if (getSessionId(api) !== requestedSessionId)
10999
- return;
11000
- const orderedMessages = [...messages].sort((a, b) => a.id - b.id);
11001
- for (const message of orderedMessages) {
11002
- if (getSessionId(api) !== requestedSessionId)
11003
- return;
11004
- if (message.sessionId && message.sessionId !== requestedSessionId)
11005
- continue;
11006
- if (message.type !== "action")
11007
- continue;
11008
- if (message.payload?.action !== "show-status-dialog")
11009
- continue;
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
- clearInterval(messagePoller);
11021
- lastReceivedNotificationIdBySession.clear();
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();