@cortexkit/aft-opencode 0.43.1 → 0.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"ast.d.ts","sourceRoot":"","sources":["../../src/tools/ast.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAOH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAiDjD,wBAAgB,QAAQ,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CA0H3E"}
1
+ {"version":3,"file":"ast.d.ts","sourceRoot":"","sources":["../../src/tools/ast.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAOH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAkDjD,wBAAgB,QAAQ,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CA0H3E"}
@@ -1 +1 @@
1
- {"version":3,"file":"bash.d.ts","sourceRoot":"","sources":["../../src/tools/bash.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAe,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAMvE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAuCjD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,mBAAmB,CACjC,mBAAmB,EAAE,OAAO,EAC5B,aAAa,EAAE,OAAO,EACtB,YAAY,EAAE,OAAO,GACpB,MAAM,CAaR;AA6ED,wBAAgB,cAAc,CAC5B,GAAG,EAAE,aAAa,EAClB,2BAA2B,CAAC,EAAE,OAAO,GACpC,cAAc,CAwMhB;AAED,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,aAAa,GAAG,cAAc,CA2BvE;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,aAAa,GAAG,cAAc,CAwBrE"}
1
+ {"version":3,"file":"bash.d.ts","sourceRoot":"","sources":["../../src/tools/bash.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAe,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAMvE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAuCjD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,mBAAmB,CACjC,mBAAmB,EAAE,OAAO,EAC5B,aAAa,EAAE,OAAO,EACtB,YAAY,EAAE,OAAO,GACpB,MAAM,CAaR;AA6ED,wBAAgB,cAAc,CAC5B,GAAG,EAAE,aAAa,EAClB,2BAA2B,CAAC,EAAE,OAAO,GACpC,cAAc,CA2NhB;AAED,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,aAAa,GAAG,cAAc,CA2BvE;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,aAAa,GAAG,cAAc,CAwBrE"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Pick the text color for a sidebar badge label drawn on a theme accent.
3
+ * Keep this logic in sync with Magic Context so both sidebars make the same
4
+ * contrast decision for shared themes.
5
+ */
6
+ type Color = {
7
+ r: number;
8
+ g: number;
9
+ b: number;
10
+ a?: number;
11
+ };
12
+ export declare function readableTextColorOn(bg: Color): string;
13
+ export declare function badgeTextColor<T extends Color>(accent: T, background: T): T | string;
14
+ export {};
15
+ //# sourceMappingURL=badge-contrast.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"badge-contrast.d.ts","sourceRoot":"","sources":["../../src/tui/badge-contrast.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,KAAK,KAAK,GAAG;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AA0B7D,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,KAAK,GAAG,MAAM,CAErD;AAED,wBAAgB,cAAc,CAAC,CAAC,SAAS,KAAK,EAAE,MAAM,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,GAAG,CAAC,GAAG,MAAM,CAMpF"}
package/dist/tui.js CHANGED
@@ -7790,10 +7790,10 @@ 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.43.1";
7793
+ var version = "0.45.0";
7794
7794
 
7795
7795
  // src/shared/rpc-client.ts
7796
- import { existsSync, readdirSync, readFileSync, unlinkSync } from "node:fs";
7796
+ import { existsSync, readdirSync, readFileSync as readFileSync2, unlinkSync } from "node:fs";
7797
7797
  import { join as join3 } from "node:path";
7798
7798
 
7799
7799
  // src/shared/rpc-utils.ts
@@ -7856,6 +7856,8 @@ function error(message, meta) {
7856
7856
  }
7857
7857
  // ../aft-bridge/dist/bridge.js
7858
7858
  import { spawn } from "node:child_process";
7859
+ import { createHash } from "node:crypto";
7860
+ import { readFileSync } from "node:fs";
7859
7861
  import { homedir } from "node:os";
7860
7862
  import { join } from "node:path";
7861
7863
  import { StringDecoder } from "node:string_decoder";
@@ -7912,6 +7914,22 @@ function bashTaskIdFrom(response) {
7912
7914
  return camelCase;
7913
7915
  return;
7914
7916
  }
7917
+ function hashBinaryOnDisk(binaryPath) {
7918
+ try {
7919
+ return createHash("sha256").update(readFileSync(binaryPath)).digest("hex");
7920
+ } catch {
7921
+ return null;
7922
+ }
7923
+ }
7924
+ var binaryFingerprintReader = hashBinaryOnDisk;
7925
+ function readBinaryFingerprint(binaryPath) {
7926
+ try {
7927
+ const fingerprint = binaryFingerprintReader(binaryPath);
7928
+ return typeof fingerprint === "string" && fingerprint.length > 0 ? fingerprint : null;
7929
+ } catch {
7930
+ return null;
7931
+ }
7932
+ }
7915
7933
  function tagStderrLine(line) {
7916
7934
  return /^\[aft(-\w+)?\] /.test(line) ? line : `[aft] ${line}`;
7917
7935
  }
@@ -8003,6 +8021,9 @@ class BinaryBridge {
8003
8021
  binaryPath;
8004
8022
  cwd;
8005
8023
  process = null;
8024
+ spawnedBinaryFingerprint = null;
8025
+ lastBinaryFingerprintCheckAt = 0;
8026
+ _retiringDueToBinaryChange = false;
8006
8027
  pending = new Map;
8007
8028
  outstandingBackgroundTaskIds = new Set;
8008
8029
  nextId = 1;
@@ -8122,6 +8143,24 @@ class BinaryBridge {
8122
8143
  hasOutstandingBackgroundTasks() {
8123
8144
  return this.outstandingBackgroundTaskIds.size > 0;
8124
8145
  }
8146
+ maybeScheduleRespawnForUpdatedBinary(checkIntervalMs, now = Date.now()) {
8147
+ if (this._shuttingDown || this._retiringDueToBinaryChange || !this.isAlive())
8148
+ return false;
8149
+ if (this.pending.size > 0 || this.outstandingBackgroundTaskIds.size > 0)
8150
+ return false;
8151
+ if (now - this.lastBinaryFingerprintCheckAt < checkIntervalMs)
8152
+ return false;
8153
+ this.lastBinaryFingerprintCheckAt = now;
8154
+ const spawnedFingerprint = this.spawnedBinaryFingerprint;
8155
+ if (!spawnedFingerprint)
8156
+ return false;
8157
+ const currentFingerprint = readBinaryFingerprint(this.binaryPath);
8158
+ if (!currentFingerprint || currentFingerprint === spawnedFingerprint)
8159
+ return false;
8160
+ this._retiringDueToBinaryChange = true;
8161
+ this.logVia(`Binary contents changed on disk for ${this.binaryPath}; retiring this bridge so the next tool call respawns onto the updated binary.`);
8162
+ return true;
8163
+ }
8125
8164
  getCwd() {
8126
8165
  return this.cwd;
8127
8166
  }
@@ -8154,6 +8193,9 @@ class BinaryBridge {
8154
8193
  }
8155
8194
  async sendWithVersionMismatchRetry(command, params, options, canRetryAfterVersionSwap) {
8156
8195
  try {
8196
+ if (this._retiringDueToBinaryChange) {
8197
+ throw new Error(`${this.errorPrefix} Bridge is retiring after the on-disk binary changed; retry to respawn on the updated binary`);
8198
+ }
8157
8199
  if (this._shuttingDown) {
8158
8200
  throw new Error(`${this.errorPrefix} Bridge is shutting down, cannot send "${command}"`);
8159
8201
  }
@@ -8352,6 +8394,7 @@ class BinaryBridge {
8352
8394
  }
8353
8395
  async shutdown() {
8354
8396
  this._shuttingDown = true;
8397
+ this.spawnedBinaryFingerprint = null;
8355
8398
  this.clearRestartResetTimer();
8356
8399
  this.configureWarningClients.clear();
8357
8400
  this.rejectAllPending(new Error(`${this.errorPrefix} Bridge shutting down`));
@@ -8405,6 +8448,9 @@ class BinaryBridge {
8405
8448
  async replaceCurrentBinary(newBinaryPath) {
8406
8449
  this.binaryPath = newBinaryPath;
8407
8450
  this.configured = false;
8451
+ this.spawnedBinaryFingerprint = null;
8452
+ this.lastBinaryFingerprintCheckAt = 0;
8453
+ this._retiringDueToBinaryChange = false;
8408
8454
  this.clearRestartResetTimer();
8409
8455
  this.rejectAllPending(new Error(`${this.errorPrefix} Bridge restarting with updated binary: ${newBinaryPath}`));
8410
8456
  if (!this.process)
@@ -8430,6 +8476,7 @@ class BinaryBridge {
8430
8476
  this.spawnProcess(triggeringSessionId);
8431
8477
  }
8432
8478
  spawnProcess(triggeringSessionId) {
8479
+ this._retiringDueToBinaryChange = false;
8433
8480
  this.lastStatusBar = undefined;
8434
8481
  if (triggeringSessionId) {
8435
8482
  this.sessionLogVia(triggeringSessionId, `Spawning binary: ${this.binaryPath} (cwd: ${this.cwd})`);
@@ -8520,6 +8567,8 @@ class BinaryBridge {
8520
8567
  this.handleCrash();
8521
8568
  });
8522
8569
  this.process = child;
8570
+ this.spawnedBinaryFingerprint = readBinaryFingerprint(this.binaryPath);
8571
+ this.lastBinaryFingerprintCheckAt = Date.now();
8523
8572
  this.stdoutBuffer = "";
8524
8573
  this.stdoutReadOffset = 0;
8525
8574
  this.stderrBuffer = "";
@@ -8698,6 +8747,7 @@ class BinaryBridge {
8698
8747
  }
8699
8748
  handleTimeout(triggeringSessionId) {
8700
8749
  this.consecutiveRequestTimeouts = 0;
8750
+ this.spawnedBinaryFingerprint = null;
8701
8751
  this.rejectAllPending(new Error(`${this.errorPrefix} bridge killed during sibling timeout — request aborted`));
8702
8752
  this.outstandingBackgroundTaskIds.clear();
8703
8753
  if (this.process) {
@@ -8724,6 +8774,7 @@ class BinaryBridge {
8724
8774
  handleCrash(cause) {
8725
8775
  const proc = this.process;
8726
8776
  this.process = null;
8777
+ this.spawnedBinaryFingerprint = null;
8727
8778
  if (proc && proc.exitCode === null && !proc.killed) {
8728
8779
  proc.kill("SIGKILL");
8729
8780
  }
@@ -8735,6 +8786,10 @@ class BinaryBridge {
8735
8786
  this.errorVia(`Binary crashed (restarts: ${this._restartCount})${cause ? `: ${cause.message}` : ""}.${tail}`);
8736
8787
  }
8737
8788
  this.rejectAllPending(new Error(`${this.errorPrefix} Binary crashed (restarts: ${this._restartCount})${cause ? `: ${cause.message}` : ""} (see ${this.getLogFilePathVia()})`));
8789
+ if (this._retiringDueToBinaryChange) {
8790
+ this.logVia("Binary exited while retiring after an on-disk update; skipping auto-restart");
8791
+ return;
8792
+ }
8738
8793
  if (this._restartCount < this.maxRestarts) {
8739
8794
  const delay = 100 * 2 ** this._restartCount;
8740
8795
  this._restartCount++;
@@ -8819,7 +8874,7 @@ var ORT_PLATFORM_MAP = {
8819
8874
  }
8820
8875
  };
8821
8876
  // ../aft-bridge/dist/project-identity.js
8822
- import { createHash } from "node:crypto";
8877
+ import { createHash as createHash2 } from "node:crypto";
8823
8878
  import { realpathSync } from "node:fs";
8824
8879
  import { resolve } from "node:path";
8825
8880
  function canonicalizeProjectRoot(dir) {
@@ -8850,7 +8905,7 @@ function normalizeWindowsRoot(p) {
8850
8905
  return s;
8851
8906
  }
8852
8907
  function projectRootKeyHash(dir) {
8853
- return createHash("sha256").update(canonicalizeProjectRoot(dir)).digest("hex").slice(0, 16);
8908
+ return createHash2("sha256").update(canonicalizeProjectRoot(dir)).digest("hex").slice(0, 16);
8854
8909
  }
8855
8910
 
8856
8911
  // ../aft-bridge/dist/pool.js
@@ -8934,6 +8989,11 @@ class BridgePool {
8934
8989
  for (const [dir, entry] of this.bridges) {
8935
8990
  if (entry.bridge.hasPendingRequests() || entry.bridge.hasOutstandingBackgroundTasks())
8936
8991
  continue;
8992
+ if (entry.bridge.maybeScheduleRespawnForUpdatedBinary(CLEANUP_INTERVAL_MS, now)) {
8993
+ this.staleBridges.add(entry.bridge);
8994
+ this.bridges.delete(dir);
8995
+ continue;
8996
+ }
8937
8997
  if (now - entry.lastUsed > this.idleTimeoutMs) {
8938
8998
  entry.bridge.shutdown().catch((err) => this.error("cleanup shutdown failed:", err));
8939
8999
  this.bridges.delete(dir);
@@ -9030,10 +9090,11 @@ class BridgePool {
9030
9090
  function normalizeKey(projectRoot) {
9031
9091
  return canonicalizeProjectRoot(projectRoot);
9032
9092
  }
9033
- // ../../node_modules/.bun/@cortexkit+subc-client@0.2.0/node_modules/@cortexkit/subc-client/src/client.ts
9093
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.3.0/node_modules/@cortexkit/subc-client/src/client.ts
9034
9094
  import { promises as fs2 } from "node:fs";
9095
+ import { debuglog } from "node:util";
9035
9096
 
9036
- // ../../node_modules/.bun/@cortexkit+subc-client@0.2.0/node_modules/@cortexkit/subc-client/src/auth.ts
9097
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.3.0/node_modules/@cortexkit/subc-client/src/auth.ts
9037
9098
  import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
9038
9099
  var NONCE_LEN = 32;
9039
9100
  var MAX_AUTH_MESSAGE_LEN = 4096;
@@ -9097,7 +9158,7 @@ async function authenticateClient(sock, conn, deadlineMs) {
9097
9158
  await writeMessage(sock, { client_auth: Array.from(clientAuth) }, deadlineMs);
9098
9159
  }
9099
9160
 
9100
- // ../../node_modules/.bun/@cortexkit+subc-client@0.2.0/node_modules/@cortexkit/subc-client/src/connection-file.ts
9161
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.3.0/node_modules/@cortexkit/subc-client/src/connection-file.ts
9101
9162
  import { promises as fs } from "node:fs";
9102
9163
  var SCHEMA_VERSION = 1;
9103
9164
  var MIN_KEY_LEN = 32;
@@ -9166,7 +9227,7 @@ async function readConnectionFile(path) {
9166
9227
  return info;
9167
9228
  }
9168
9229
 
9169
- // ../../node_modules/.bun/@cortexkit+subc-client@0.2.0/node_modules/@cortexkit/subc-client/src/envelope.ts
9230
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.3.0/node_modules/@cortexkit/subc-client/src/envelope.ts
9170
9231
  var PROTOCOL_VERSION = 1;
9171
9232
  var HEADER_LEN = 17;
9172
9233
  var FROZEN_PREFIX_LEN = 5;
@@ -9273,7 +9334,7 @@ function encodeFrame(frame) {
9273
9334
  return out;
9274
9335
  }
9275
9336
 
9276
- // ../../node_modules/.bun/@cortexkit+subc-client@0.2.0/node_modules/@cortexkit/subc-client/src/socket.ts
9337
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.3.0/node_modules/@cortexkit/subc-client/src/socket.ts
9277
9338
  import net from "node:net";
9278
9339
 
9279
9340
  class SocketClosedError extends Error {
@@ -9304,6 +9365,9 @@ class SubcSocket {
9304
9365
  buffered = 0;
9305
9366
  waiter = null;
9306
9367
  closedErr = null;
9368
+ bufferedBytes() {
9369
+ return this.buffered;
9370
+ }
9307
9371
  constructor(sock) {
9308
9372
  this.sock = sock;
9309
9373
  sock.on("data", (chunk) => {
@@ -9320,6 +9384,9 @@ class SubcSocket {
9320
9384
  sock.on("end", () => fail(new SocketClosedError("subc closed the connection")));
9321
9385
  sock.on("close", () => fail(new SocketClosedError("subc connection closed")));
9322
9386
  }
9387
+ localPort() {
9388
+ return this.sock.localPort ?? null;
9389
+ }
9323
9390
  static connect(host, port, deadlineMs) {
9324
9391
  return new Promise((resolve2, reject) => {
9325
9392
  const sock = net.connect({ host, port });
@@ -9459,9 +9526,14 @@ class SubcSocket {
9459
9526
  }
9460
9527
  }
9461
9528
 
9462
- // ../../node_modules/.bun/@cortexkit+subc-client@0.2.0/node_modules/@cortexkit/subc-client/src/client.ts
9529
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.3.0/node_modules/@cortexkit/subc-client/src/client.ts
9530
+ var debug = debuglog("subc-client");
9463
9531
  var DEFAULT_HANDSHAKE_TIMEOUT_MS = 1e4;
9464
9532
  var DEFAULT_REQUEST_TIMEOUT_MS = 30000;
9533
+ var TIMEOUT_ARBITRATION_GRACE_MS = 50;
9534
+ var REQUEST_DEADLINE_MARKER = "request_deadline";
9535
+ var DEADLINE_NO_DROP_CODE = "deadline_exceeded_no_drop_observed";
9536
+ var ROUTE_OPEN_RETRY_DEADLINE_MS = 1e4;
9465
9537
  var BODY_READ_TIMEOUT_MS = 30000;
9466
9538
  var EMPTY_BODY = new Uint8Array(0);
9467
9539
  var DEFAULT_MANAGED_TARGET_KIND = "management_surface";
@@ -9505,6 +9577,7 @@ class SubcClient {
9505
9577
  closeStarted = false;
9506
9578
  reconnecting = null;
9507
9579
  generation = 1;
9580
+ readerActive = false;
9508
9581
  constructor(sock, currentConn, opts) {
9509
9582
  this.sock = sock;
9510
9583
  this.currentConn = currentConn;
@@ -9563,7 +9636,7 @@ class SubcClient {
9563
9636
  }
9564
9637
  continue;
9565
9638
  }
9566
- if (err.kind === "outcome_unknown") {
9639
+ if (err.kind === "outcome_unknown" && err.code !== DEADLINE_NO_DROP_CODE) {
9567
9640
  this.scheduleReconnectAfterDrop(err);
9568
9641
  }
9569
9642
  throw err;
@@ -9686,7 +9759,7 @@ class SubcClient {
9686
9759
  timer: null
9687
9760
  };
9688
9761
  pending.timer = setTimeout(() => {
9689
- this.rejectPending(key, pending, new SubcError(`request on channel ${channel} timed out after ${ms}ms`));
9762
+ this.arbitrateTimeout(key, pending, channel, corr, ms);
9690
9763
  }, ms);
9691
9764
  this.pending.set(key, pending);
9692
9765
  this.sock.write(encodeFrame(frame), Date.now() + ms).catch((err) => {
@@ -9696,6 +9769,23 @@ class SubcClient {
9696
9769
  });
9697
9770
  });
9698
9771
  }
9772
+ arbitrateTimeout(key, pending, channel, corr, ms) {
9773
+ const settleAsTimeout = () => {
9774
+ this.rejectPending(key, pending, new SubcError(this.timeoutMessage(channel, corr, ms), REQUEST_DEADLINE_MARKER));
9775
+ };
9776
+ const graceDeadline = Date.now() + this.opts.timeoutArbitrationGraceMs;
9777
+ const arbitrate = () => {
9778
+ if (this.pending.get(key) !== pending)
9779
+ return;
9780
+ const readerDraining = this.readerActive || this.sock.bufferedBytes() > 0;
9781
+ if (readerDraining && Date.now() < graceDeadline) {
9782
+ setImmediate(arbitrate);
9783
+ return;
9784
+ }
9785
+ settleAsTimeout();
9786
+ };
9787
+ setImmediate(arbitrate);
9788
+ }
9699
9789
  async managedRequest(routeChannel, body, opts) {
9700
9790
  const bytes = body instanceof Uint8Array ? body : this.encode(body);
9701
9791
  const priority = opts.priority ?? 1 /* Interactive */;
@@ -9720,6 +9810,9 @@ class SubcClient {
9720
9810
  if (!handedToSocket) {
9721
9811
  return this.notSentCallError("request bytes were not queued to the subc socket", err);
9722
9812
  }
9813
+ if (err instanceof SubcError && err.code === REQUEST_DEADLINE_MARKER) {
9814
+ return new SubcCallError("outcome_unknown", `managed call deadline exceeded after request bytes were queued to the local socket; no terminal response was observed; outcome unknown${causeMessage(err)}`, DEADLINE_NO_DROP_CODE, err);
9815
+ }
9723
9816
  return this.outcomeUnknownCallError("connection dropped before the managed call returned a response", err);
9724
9817
  };
9725
9818
  return new Promise((resolve2, reject) => {
@@ -9733,7 +9826,7 @@ class SubcClient {
9733
9826
  classifyFailure
9734
9827
  };
9735
9828
  pending.timer = setTimeout(() => {
9736
- this.rejectPending(key, pending, new SubcError(`request on channel ${channel} timed out after ${ms}ms`));
9829
+ this.arbitrateTimeout(key, pending, channel, corr, ms);
9737
9830
  }, ms);
9738
9831
  this.pending.set(key, pending);
9739
9832
  const write = this.sock.writeTracked(encodeFrame(frame), Date.now() + ms);
@@ -9778,6 +9871,9 @@ class SubcClient {
9778
9871
  return cached.opening;
9779
9872
  }
9780
9873
  async openCachedRoute(cached) {
9874
+ const routeRetryDeadline = Date.now() + ROUTE_OPEN_RETRY_DEADLINE_MS;
9875
+ let routeRetryDelay = this.opts.reconnectBackoff.baseMs;
9876
+ let routeRetryAttempt = 0;
9781
9877
  for (;; ) {
9782
9878
  if (cached.closed)
9783
9879
  throw this.routeClosedDuringOpen();
@@ -9811,6 +9907,15 @@ class SubcClient {
9811
9907
  }
9812
9908
  continue;
9813
9909
  }
9910
+ if (!this.closeStarted && err instanceof SubcError && isRetryableRouteOpenCode(err.code)) {
9911
+ routeRetryAttempt += 1;
9912
+ if (routeRetryAttempt < this.opts.reconnectBackoff.maxAttempts && Date.now() < routeRetryDeadline) {
9913
+ await this.opts.sleep(routeRetryDelay);
9914
+ routeRetryDelay = Math.min(routeRetryDelay * 2, this.opts.reconnectBackoff.capMs);
9915
+ continue;
9916
+ }
9917
+ throw this.notSentCallError(`route.open failed for module ${cached.moduleId}: ${err.code} (retry budget exhausted)`, err);
9918
+ }
9814
9919
  throw this.terminalCallError(`route.open failed for module ${cached.moduleId}`, err);
9815
9920
  }
9816
9921
  }
@@ -9881,7 +9986,9 @@ class SubcClient {
9881
9986
  for (const cached of this.routes.values()) {
9882
9987
  if (cached.closed)
9883
9988
  continue;
9884
- const channel = await this.routeOpen(cached.target, cached.identity);
9989
+ const channel = await this.routeOpen(cached.target, cached.identity, {
9990
+ consumerIdentity: cached.consumerIdentity ?? null
9991
+ });
9885
9992
  if (cached.closed) {
9886
9993
  this.sendRouteGoodbye(channel);
9887
9994
  continue;
@@ -9890,6 +9997,11 @@ class SubcClient {
9890
9997
  cached.generation = this.generation;
9891
9998
  }
9892
9999
  }
10000
+ timeoutMessage(channel, corr, ms) {
10001
+ const port = this.sock.localPort();
10002
+ const where = port === null ? "channel" : `local_port=${port} channel`;
10003
+ return `request on ${where} ${channel} corr ${corr} timed out after ${ms}ms`;
10004
+ }
9893
10005
  routeClosedDuringOpen() {
9894
10006
  return new SubcCallError("not_sent", "route was closed before route.open completed", "route_closed");
9895
10007
  }
@@ -9897,9 +10009,16 @@ class SubcClient {
9897
10009
  try {
9898
10010
  for (;; ) {
9899
10011
  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 });
10012
+ this.readerActive = true;
10013
+ try {
10014
+ const header = decodeHeader(headerBytes);
10015
+ const body = header.len === 0 ? new Uint8Array(0) : await sock.readExact(header.len, Date.now() + BODY_READ_TIMEOUT_MS);
10016
+ if (this.sock === sock && this.generation === generation) {
10017
+ this.dispatch({ header, body });
10018
+ }
10019
+ } finally {
10020
+ this.readerActive = false;
10021
+ }
9903
10022
  }
9904
10023
  } catch (err) {
9905
10024
  if (this.sock === sock && this.generation === generation) {
@@ -9931,13 +10050,20 @@ class SubcClient {
9931
10050
  this.failChannel(frame.header.channel, new SubcError("route closed by subc (GOODBYE)"));
9932
10051
  return;
9933
10052
  }
10053
+ if (frame.header.ty === 1 /* Response */ || frame.header.ty === 5 /* Error */ || frame.header.ty === 4 /* StreamEnd */) {
10054
+ debug("dropped terminal frame with no waiter: type=%d channel=%d corr=%s port=%s", frame.header.ty, frame.header.channel, frame.header.corr, this.sock.localPort() ?? "?");
10055
+ return;
10056
+ }
9934
10057
  }
9935
10058
  settle(key, pending, run) {
10059
+ if (this.pending.get(key) !== pending)
10060
+ return false;
9936
10061
  this.pending.delete(key);
9937
10062
  if (pending.timer)
9938
10063
  clearTimeout(pending.timer);
9939
10064
  run();
9940
10065
  pending.onSettle?.();
10066
+ return true;
9941
10067
  }
9942
10068
  rejectPending(key, pending, err) {
9943
10069
  this.settle(key, pending, () => pending.reject(pending.classifyFailure?.(err) ?? err));
@@ -10001,6 +10127,9 @@ function isConsumerReconnectTransient(err) {
10001
10127
  const code = errorCode(err);
10002
10128
  return code === "ECONNREFUSED" || code === "ECONNRESET" || code === "EPIPE" || code === "ETIMEDOUT" || code === "ENOENT";
10003
10129
  }
10130
+ function isRetryableRouteOpenCode(code) {
10131
+ return code === "unknown_module" || code === "module_reloading" || code === "target_unavailable" || code === "module_timeout";
10132
+ }
10004
10133
  async function connectionFileExists(path) {
10005
10134
  try {
10006
10135
  await fs2.access(path);
@@ -10016,7 +10145,8 @@ function normalizeConnectOptions(opts) {
10016
10145
  identity: opts.identity,
10017
10146
  targetKind: opts.targetKind ?? DEFAULT_MANAGED_TARGET_KIND,
10018
10147
  reconnectBackoff: opts.reconnectBackoff ?? DEFAULT_RECONNECT_BACKOFF,
10019
- sleep: opts.sleep ?? ((ms) => new Promise((resolve2) => setTimeout(resolve2, ms)))
10148
+ sleep: opts.sleep ?? ((ms) => new Promise((resolve2) => setTimeout(resolve2, ms))),
10149
+ timeoutArbitrationGraceMs: opts.timeoutArbitrationGraceMs ?? TIMEOUT_ARBITRATION_GRACE_MS
10020
10150
  };
10021
10151
  }
10022
10152
  function routeCacheKey(target, identity, consumerIdentity) {
@@ -10045,7 +10175,7 @@ function causeMessage(cause) {
10045
10175
  return "";
10046
10176
  return `: ${cause instanceof Error ? cause.message : String(cause)}`;
10047
10177
  }
10048
- // ../../node_modules/.bun/@cortexkit+subc-client@0.2.0/node_modules/@cortexkit/subc-client/src/provider.ts
10178
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.3.0/node_modules/@cortexkit/subc-client/src/provider.ts
10049
10179
  import { Buffer as Buffer2 } from "node:buffer";
10050
10180
  var DEFAULT_HANDSHAKE_TIMEOUT_MS2 = 1e4;
10051
10181
  var BODY_READ_TIMEOUT_MS2 = 30000;
@@ -11263,7 +11393,7 @@ class AftRpcClient {
11263
11393
  }
11264
11394
  parsePortFile(filePath) {
11265
11395
  try {
11266
- const content = readFileSync(filePath, "utf-8");
11396
+ const content = readFileSync2(filePath, "utf-8");
11267
11397
  return parseRpcPortRecord(content);
11268
11398
  } catch {
11269
11399
  return null;
@@ -11799,6 +11929,30 @@ async function handleSocketMessage(ws, raw, messageGeneration) {
11799
11929
  // src/tui/sidebar.tsx
11800
11930
  import { createEffect, createMemo, createSignal, on, onCleanup } from "solid-js";
11801
11931
 
11932
+ // src/tui/badge-contrast.ts
11933
+ var MIN_OPAQUE_ALPHA = 0.5;
11934
+ var MIN_CHANNEL_DISTANCE = 0.06;
11935
+ var LIGHT_ACCENT_LUMINANCE = 0.5;
11936
+ function srgbChannelToLinear(c) {
11937
+ return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
11938
+ }
11939
+ function relativeLuminance(bg) {
11940
+ return 0.2126 * srgbChannelToLinear(bg.r) + 0.7152 * srgbChannelToLinear(bg.g) + 0.0722 * srgbChannelToLinear(bg.b);
11941
+ }
11942
+ function nearlyEqual(a, b) {
11943
+ return Math.abs(a.r - b.r) < MIN_CHANNEL_DISTANCE && Math.abs(a.g - b.g) < MIN_CHANNEL_DISTANCE && Math.abs(a.b - b.b) < MIN_CHANNEL_DISTANCE;
11944
+ }
11945
+ function readableTextColorOn(bg) {
11946
+ return relativeLuminance(bg) < LIGHT_ACCENT_LUMINANCE ? "#ffffff" : "#000000";
11947
+ }
11948
+ function badgeTextColor(accent, background) {
11949
+ const alpha = background.a ?? 1;
11950
+ if (alpha >= MIN_OPAQUE_ALPHA && !nearlyEqual(accent, background)) {
11951
+ return background;
11952
+ }
11953
+ return readableTextColorOn(accent);
11954
+ }
11955
+
11802
11956
  // src/tui/preferences.ts
11803
11957
  var import_comment_json = __toESM(require_src2(), 1);
11804
11958
  import { watch } from "node:fs";
@@ -12369,7 +12523,7 @@ var SidebarContent = (props) => {
12369
12523
  paddingRight: 1,
12370
12524
  backgroundColor: props.theme.accent,
12371
12525
  children: /* @__PURE__ */ jsxDEV("text", {
12372
- fg: props.theme.background,
12526
+ fg: badgeTextColor(props.theme.accent, props.theme.background),
12373
12527
  children: /* @__PURE__ */ jsxDEV("b", {
12374
12528
  children: [
12375
12529
  notInitialized() ? "" : collapsed() ? "▶ " : "▼ ",
@@ -12384,7 +12538,7 @@ var SidebarContent = (props) => {
12384
12538
  marginLeft: 1,
12385
12539
  backgroundColor: props.theme.warning,
12386
12540
  children: /* @__PURE__ */ jsxDEV("text", {
12387
- fg: props.theme.background,
12541
+ fg: badgeTextColor(props.theme.warning, props.theme.background),
12388
12542
  children: /* @__PURE__ */ jsxDEV("b", {
12389
12543
  children: "DEGRADED"
12390
12544
  }, undefined, false, undefined, this)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/aft-opencode",
3
- "version": "0.43.1",
3
+ "version": "0.45.0",
4
4
  "type": "module",
5
5
  "description": "OpenCode plugin for Agent File Tools (AFT) — tree-sitter and lsp powered code analysis",
6
6
  "main": "dist/index.js",
@@ -31,7 +31,7 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@clack/prompts": "^1.6.0",
34
- "@cortexkit/aft-bridge": "0.43.1",
34
+ "@cortexkit/aft-bridge": "0.45.0",
35
35
  "@opentui/core": "^0.4.2",
36
36
  "@opentui/solid": "^0.4.2",
37
37
  "comment-json": "^4.6.2",
@@ -40,12 +40,12 @@
40
40
  "zod": "^4.4.3"
41
41
  },
42
42
  "optionalDependencies": {
43
- "@cortexkit/aft-darwin-arm64": "0.43.1",
44
- "@cortexkit/aft-darwin-x64": "0.43.1",
45
- "@cortexkit/aft-linux-arm64": "0.43.1",
46
- "@cortexkit/aft-linux-x64": "0.43.1",
47
- "@cortexkit/aft-win32-arm64": "0.43.1",
48
- "@cortexkit/aft-win32-x64": "0.43.1"
43
+ "@cortexkit/aft-darwin-arm64": "0.45.0",
44
+ "@cortexkit/aft-darwin-x64": "0.45.0",
45
+ "@cortexkit/aft-linux-arm64": "0.45.0",
46
+ "@cortexkit/aft-linux-x64": "0.45.0",
47
+ "@cortexkit/aft-win32-arm64": "0.45.0",
48
+ "@cortexkit/aft-win32-x64": "0.45.0"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@opencode-ai/plugin": "^1.17.11",
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Pick the text color for a sidebar badge label drawn on a theme accent.
3
+ * Keep this logic in sync with Magic Context so both sidebars make the same
4
+ * contrast decision for shared themes.
5
+ */
6
+
7
+ type Color = { r: number; g: number; b: number; a?: number };
8
+
9
+ const MIN_OPAQUE_ALPHA = 0.5;
10
+ const MIN_CHANNEL_DISTANCE = 0.06;
11
+ const LIGHT_ACCENT_LUMINANCE = 0.5;
12
+
13
+ function srgbChannelToLinear(c: number): number {
14
+ return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
15
+ }
16
+
17
+ function relativeLuminance(bg: Color): number {
18
+ return (
19
+ 0.2126 * srgbChannelToLinear(bg.r) +
20
+ 0.7152 * srgbChannelToLinear(bg.g) +
21
+ 0.0722 * srgbChannelToLinear(bg.b)
22
+ );
23
+ }
24
+
25
+ function nearlyEqual(a: Color, b: Color): boolean {
26
+ return (
27
+ Math.abs(a.r - b.r) < MIN_CHANNEL_DISTANCE &&
28
+ Math.abs(a.g - b.g) < MIN_CHANNEL_DISTANCE &&
29
+ Math.abs(a.b - b.b) < MIN_CHANNEL_DISTANCE
30
+ );
31
+ }
32
+
33
+ export function readableTextColorOn(bg: Color): string {
34
+ return relativeLuminance(bg) < LIGHT_ACCENT_LUMINANCE ? "#ffffff" : "#000000";
35
+ }
36
+
37
+ export function badgeTextColor<T extends Color>(accent: T, background: T): T | string {
38
+ const alpha = background.a ?? 1;
39
+ if (alpha >= MIN_OPAQUE_ALPHA && !nearlyEqual(accent, background)) {
40
+ return background;
41
+ }
42
+ return readableTextColorOn(accent);
43
+ }
@@ -19,6 +19,7 @@ import {
19
19
  type StatusCompression,
20
20
  } from "../shared/status";
21
21
  import { resolveCortexKitStorageRoot } from "../shared/storage-paths";
22
+ import { badgeTextColor } from "./badge-contrast";
22
23
  import {
23
24
  createDebouncedStatusRefresh,
24
25
  refreshAftTuiSocketScope,
@@ -592,7 +593,7 @@ const SidebarContent = (props: {
592
593
  {/* Triangle lives inside the accent badge so the toggle reads as one
593
594
  unit: "▶ AFT" / "▼ AFT". Hidden pre-init (nothing to collapse). */}
594
595
  <box paddingLeft={1} paddingRight={1} backgroundColor={props.theme.accent}>
595
- <text fg={props.theme.background}>
596
+ <text fg={badgeTextColor(props.theme.accent, props.theme.background)}>
596
597
  <b>
597
598
  {notInitialized() ? "" : collapsed() ? "▶ " : "▼ "}
598
599
  {prefs().header.label}
@@ -606,7 +607,7 @@ const SidebarContent = (props: {
606
607
  marginLeft={1}
607
608
  backgroundColor={props.theme.warning}
608
609
  >
609
- <text fg={props.theme.background}>
610
+ <text fg={badgeTextColor(props.theme.warning, props.theme.background)}>
610
611
  <b>DEGRADED</b>
611
612
  </text>
612
613
  </box>