pi-freeflow 1.44.0 → 1.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +11 -13
  3. package/package.json +1 -1
  4. package/src/catalog.ts +38 -14
  5. package/src/client-recovery.ts +238 -0
  6. package/src/client-staging.ts +316 -0
  7. package/src/client.ts +117 -472
  8. package/src/cline-accounts.ts +26 -1223
  9. package/src/cline-device-auth.ts +5 -3
  10. package/src/cline-pool.ts +691 -0
  11. package/src/cline-roll.ts +642 -0
  12. package/src/commands-cline.ts +283 -0
  13. package/src/commands-relay.ts +808 -0
  14. package/src/commands-startup.ts +264 -0
  15. package/src/commands.ts +119 -1202
  16. package/src/config-paths.ts +254 -0
  17. package/src/config.ts +6 -687
  18. package/src/daemon.ts +16 -1
  19. package/src/deploy.ts +3 -1
  20. package/src/fingerprint-env.ts +334 -0
  21. package/src/fingerprint-tools.ts +348 -0
  22. package/src/index.ts +10 -6
  23. package/src/lane-translation.ts +263 -0
  24. package/src/models.ts +70 -112
  25. package/src/opencode-fingerprint.ts +29 -1310
  26. package/src/opencode-identity.ts +464 -0
  27. package/src/proxy-lanes.ts +1125 -0
  28. package/src/proxy-ports.ts +317 -0
  29. package/src/proxy.ts +74 -1385
  30. package/src/relay-codec.ts +365 -0
  31. package/src/relay-state.ts +10 -367
  32. package/src/relay.ts +78 -31
  33. package/src/sse-aggregator.ts +659 -0
  34. package/src/stream-cloak.ts +882 -0
  35. package/src/stream-pipe.ts +22 -871
  36. package/src/system-ca-fetch.ts +15 -4
  37. package/src/token.ts +7 -2
  38. package/src/tool-args.ts +10 -3
  39. package/src/tool-translation.ts +1 -236
  40. package/src/toolcall-audit.ts +20 -7
  41. package/src/update-checker.ts +17 -1
  42. package/src/update-core.ts +5 -2
  43. package/src/updater.ts +8 -3
  44. package/src/upstream-health.ts +22 -2
  45. package/src/zen-sidecar.ts +1 -0
  46. package/src/zen-transport.ts +48 -18
@@ -46,6 +46,7 @@ function isPrivateRelayHostname(hostname: string): boolean {
46
46
  return false;
47
47
  }
48
48
  if (host.includes(":")) {
49
+ if (host.startsWith("64:ff9b:") || host.startsWith("2002:")) return true;
49
50
  if (host.startsWith("fc") || host.startsWith("fd")) return true; // ULA fc00::/7
50
51
  if (/^fe[89ab]/.test(host)) return true; // link-local fe80::/10
51
52
  return false;
@@ -611,7 +612,8 @@ const RELAY_STATE_LOCK_FILE = `${RELAY_STATE_FILE}.lock`;
611
612
  /** A lock older than this is presumed abandoned (crashed holder) and taken over. */
612
613
  const LOCK_STALE_MS = 30_000;
613
614
 
614
- function tryAcquireRelayLock(): boolean {
615
+ export function tryAcquireRelayLock(depth = 0): boolean {
616
+ if (depth >= 3) return false;
615
617
  try {
616
618
  fs.writeFileSync(RELAY_STATE_LOCK_FILE, String(process.pid), { flag: "wx" });
617
619
  return true;
@@ -630,9 +632,12 @@ function tryAcquireRelayLock(): boolean {
630
632
  stale = Date.now() - fs.statSync(RELAY_STATE_LOCK_FILE).mtimeMs > LOCK_STALE_MS;
631
633
  } catch {
632
634
  // The lock vanished between the failed create and the stat — retry once.
633
- return tryAcquireRelayLock();
635
+ return tryAcquireRelayLock(depth + 1);
636
+ }
637
+ if (!stale) {
638
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 20);
639
+ return tryAcquireRelayLock(depth + 1);
634
640
  }
635
- if (!stale) return false;
636
641
  try {
637
642
  fs.rmSync(RELAY_STATE_LOCK_FILE, { force: true });
638
643
  } catch {
@@ -640,7 +645,7 @@ function tryAcquireRelayLock(): boolean {
640
645
  // the proxy request path.
641
646
  return false;
642
647
  }
643
- return tryAcquireRelayLock();
648
+ return tryAcquireRelayLock(depth + 1);
644
649
  }
645
650
  }
646
651
 
@@ -964,366 +969,4 @@ export function updateRelayStatusUi(targetUrl?: string): void {
964
969
  activeStatusUi.setStatus("freeflow", label ?? undefined);
965
970
  }
966
971
 
967
- // ── Relay export/import codec (relay-pool portability) ─────────────────────
968
- // Pure in-memory codec for `/freeflow export` + `/freeflow import`. These
969
- // functions never touch disk, never prompt, and never validate the live pool:
970
- // the command layer parses 100% in memory, then commits via one CAS write.
971
-
972
- /** Envelope marker identifying a pi-freeflow relay export file. */
973
- export const EXPORT_KIND = "pi-freeflow/relay-export" as const;
974
- /** Current relay export file version. Bump on any envelope breaking change. */
975
- export const EXPORT_VERSION = 1;
976
- /** Default file name used by `/freeflow export` when no path is given. */
977
- export const EXPORT_DEFAULT_FILENAME = "freeflow-relays.json";
978
- /** Hard cap on accepted import file size (1 MiB). */
979
- export const EXPORT_MAX_BYTES = 1_048_576;
980
-
981
- /** A parsed relay export file envelope. */
982
- export interface RelayExportArtifact {
983
- kind: "pi-freeflow/relay-export";
984
- version: 1;
985
- exportedAt: string;
986
- state: {
987
- mode?: RelayMode;
988
- enabled: boolean;
989
- url: string;
990
- relays: KnownRelay[];
991
- hideWidget?: boolean;
992
- };
993
- }
994
-
995
- /** One import candidate entry that was skipped instead of aborting the file. */
996
- export interface RelayImportSkipped {
997
- index: number;
998
- url: string;
999
- reason: string;
1000
- }
1001
-
1002
- /** RelayState subset carried by an import file (pool scope only). */
1003
- export type RelayImportFragment = Pick<
1004
- RelayState,
1005
- "mode" | "enabled" | "url" | "relays" | "hideWidget"
1006
- >;
1007
-
1008
- /** Result of planning an import against the current pool. */
1009
- export interface RelayImportPlanResult {
1010
- next: RelayState;
1011
- added: number;
1012
- updated: number;
1013
- removed: number;
1014
- skipped: RelayImportSkipped[];
1015
- }
1016
-
1017
- /** Alias kept for the cross-slice contract name. */
1018
- export type PlanResult = RelayImportPlanResult;
1019
-
1020
- /**
1021
- * Build an export artifact from live relay state. Passwords are stripped by
1022
- * default (the key is absent, not null); pass `{ includeSecrets: true }` to
1023
- * carry them verbatim.
1024
- */
1025
- export function buildRelayExport(
1026
- state: RelayState,
1027
- opts?: { includeSecrets?: boolean },
1028
- ): RelayExportArtifact {
1029
- const includeSecrets = opts?.includeSecrets === true;
1030
- return {
1031
- kind: EXPORT_KIND,
1032
- version: EXPORT_VERSION,
1033
- exportedAt: new Date().toISOString(),
1034
- state: {
1035
- mode: state.mode,
1036
- enabled: state.enabled,
1037
- url: state.url,
1038
- relays: state.relays.map((r) => {
1039
- const out: KnownRelay = { url: r.url };
1040
- if (r.label !== undefined) out.label = r.label;
1041
- if (r.addedAt !== undefined) out.addedAt = r.addedAt;
1042
- if (
1043
- includeSecrets &&
1044
- typeof r.auth === "string" &&
1045
- r.auth.length > 0
1046
- ) {
1047
- out.auth = r.auth;
1048
- }
1049
- return out;
1050
- }),
1051
- hideWidget: state.hideWidget,
1052
- },
1053
- };
1054
- }
1055
-
1056
- /**
1057
- * Parse raw import file text into a relay fragment. Throws on envelope
1058
- * failure (oversize, bad JSON, wrong kind/version, missing pool); per-entry
1059
- * problems fail closed to `skipped` so one bad relay never wipes the pool.
1060
- */
1061
- export function parseRelayImport(raw: string): {
1062
- fragment: RelayImportFragment;
1063
- skipped: RelayImportSkipped[];
1064
- warnings: string[];
1065
- } {
1066
- if (raw.length > EXPORT_MAX_BYTES) {
1067
- throw new Error(
1068
- `Relay import file too large (${raw.length} bytes, limit ${EXPORT_MAX_BYTES})`,
1069
- );
1070
- }
1071
- let doc: unknown;
1072
- try {
1073
- doc = JSON.parse(raw);
1074
- } catch {
1075
- throw new Error("Relay import file is not valid JSON");
1076
- }
1077
- const envelope =
1078
- typeof doc === "object" && doc !== null
1079
- ? (doc as Record<string, unknown>)
1080
- : null;
1081
- if (envelope?.kind !== EXPORT_KIND) {
1082
- throw new Error("File is not a relay export (bad kind marker)");
1083
- }
1084
- if (
1085
- typeof envelope.version !== "number" ||
1086
- !Number.isInteger(envelope.version) ||
1087
- envelope.version !== EXPORT_VERSION
1088
- ) {
1089
- throw new Error(
1090
- `Unsupported relay export version (${String(envelope.version)}); this build reads version ${EXPORT_VERSION}`,
1091
- );
1092
- }
1093
- const rawState =
1094
- typeof envelope.state === "object" && envelope.state !== null
1095
- ? (envelope.state as Record<string, unknown>)
1096
- : null;
1097
- if (!rawState || !Array.isArray(rawState.relays)) {
1098
- throw new Error("File is not a relay export (state pool is missing)");
1099
- }
1100
-
1101
- const warnings: string[] = [];
1102
- const skipped: RelayImportSkipped[] = [];
1103
-
1104
- const rawMode = rawState.mode;
1105
- const mode: RelayMode =
1106
- rawMode === "on" || rawMode === "off" || rawMode === "auto" || rawMode === "spread"
1107
- ? rawMode
1108
- : "auto";
1109
- if (rawMode !== "on" && rawMode !== "off" && rawMode !== "auto" && rawMode !== "spread") {
1110
- warnings.push("Unknown relay mode in file; using automatic mode.");
1111
- }
1112
-
1113
- const rawEnabled = rawState.enabled;
1114
- const enabled =
1115
- typeof rawEnabled === "boolean"
1116
- ? rawEnabled
1117
- : mode === "on"
1118
- ? true
1119
- : mode === "off"
1120
- ? false
1121
- : (rawState.relays as unknown[]).length > 0;
1122
-
1123
- const rawUrl = rawState.url;
1124
- let url = "";
1125
- if (typeof rawUrl === "string" && rawUrl.trim()) {
1126
- const trimmed = rawUrl.trim();
1127
- if (validateRelayUrl(trimmed).ok) {
1128
- url = trimmed;
1129
- } else {
1130
- warnings.push("Active relay address in file is not usable; ignoring it.");
1131
- }
1132
- }
1133
-
1134
- const hideWidget = rawState.hideWidget === true;
1135
- const relays: KnownRelay[] = [];
1136
- const seen = new Set<string>();
1137
- const entries = rawState.relays as unknown[];
1138
- for (let index = 0; index < entries.length; index++) {
1139
- const entry = entries[index];
1140
- if (typeof entry !== "object" || entry === null) {
1141
- skipped.push({ index, url: "", reason: "entry is not an object" });
1142
- continue;
1143
- }
1144
- const record = entry as Record<string, unknown>;
1145
- const trimmedUrl =
1146
- typeof record.url === "string" ? record.url.trim() : "";
1147
- if (!trimmedUrl) {
1148
- skipped.push({ index, url: "", reason: "missing relay address" });
1149
- continue;
1150
- }
1151
- const check = validateRelayUrl(trimmedUrl);
1152
- if (!check.ok) {
1153
- skipped.push({ index, url: trimmedUrl, reason: check.reason });
1154
- continue;
1155
- }
1156
- // Exact trimmed-address dedupe (first wins), mirroring ensureRelay.
1157
- if (seen.has(trimmedUrl)) {
1158
- skipped.push({ index, url: trimmedUrl, reason: "duplicate relay address" });
1159
- continue;
1160
- }
1161
- seen.add(trimmedUrl);
1162
- const candidate: KnownRelay = { url: trimmedUrl };
1163
- if (typeof record.label === "string") {
1164
- const cleanLabel = record.label.trim();
1165
- if (cleanLabel && cleanLabel !== "manual") {
1166
- if (cleanLabel.length > 64) {
1167
- candidate.label = cleanLabel.slice(0, 64);
1168
- warnings.push(
1169
- `Short name for ${trimmedUrl} truncated to 64 characters.`,
1170
- );
1171
- } else {
1172
- candidate.label = cleanLabel;
1173
- }
1174
- }
1175
- }
1176
- if (
1177
- typeof record.addedAt === "string" &&
1178
- Number.isNaN(Date.parse(record.addedAt)) === false
1179
- ) {
1180
- candidate.addedAt = record.addedAt;
1181
- }
1182
- if (
1183
- typeof record.auth === "string" &&
1184
- record.auth.length > 0
1185
- ) {
1186
- candidate.auth = record.auth;
1187
- } else if (record.auth !== undefined) {
1188
- warnings.push(`Password for ${trimmedUrl} is not usable; ignoring it.`);
1189
- }
1190
- relays.push(candidate);
1191
- }
1192
-
1193
- return {
1194
- fragment: { mode, enabled, url, relays, hideWidget },
1195
- skipped,
1196
- warnings,
1197
- };
1198
- }
1199
-
1200
- function copyRelayEntry(r: KnownRelay): KnownRelay {
1201
- const out: KnownRelay = { url: r.url };
1202
- if (r.label !== undefined) out.label = r.label;
1203
- if (r.addedAt !== undefined) out.addedAt = r.addedAt;
1204
- if (r.auth !== undefined) out.auth = r.auth;
1205
- return out;
1206
- }
1207
-
1208
- /**
1209
- * Plan an import against the current pool without touching disk or prompting.
1210
- * Merge folds the file pool into the current one via ensureRelay semantics and
1211
- * keeps the current mode/switch/widget; replace installs the file pool fresh.
1212
- */
1213
- export function planRelayImport(
1214
- current: RelayState,
1215
- fragment: RelayImportFragment,
1216
- opts: { mode: "merge" | "replace" },
1217
- ): RelayImportPlanResult {
1218
- if (opts.mode === "replace") {
1219
- const installed: KnownRelay[] = [];
1220
- const seen = new Set<string>();
1221
- const skipped: RelayImportSkipped[] = [];
1222
- const source = fragment.relays ?? [];
1223
- for (let index = 0; index < source.length; index++) {
1224
- const entry = source[index];
1225
- const trimmedUrl =
1226
- typeof entry?.url === "string" ? entry.url.trim() : "";
1227
- if (!trimmedUrl || !validateRelayUrl(trimmedUrl).ok) {
1228
- skipped.push({
1229
- index,
1230
- url: trimmedUrl,
1231
- reason: "relay address is not usable",
1232
- });
1233
- continue;
1234
- }
1235
- if (seen.has(trimmedUrl)) {
1236
- skipped.push({
1237
- index,
1238
- url: trimmedUrl,
1239
- reason: "duplicate relay address",
1240
- });
1241
- continue;
1242
- }
1243
- seen.add(trimmedUrl);
1244
- installed.push(copyRelayEntry({ ...entry, url: trimmedUrl }));
1245
- }
1246
- // Empty file pool installs nothing: fail closed here (not just in the
1247
- // command handler) so no caller can turn zero usable addresses into a wipe.
1248
- if (installed.length === 0) {
1249
- return {
1250
- next: { ...current, relays: current.relays.map(copyRelayEntry) },
1251
- added: 0,
1252
- updated: 0,
1253
- removed: 0,
1254
- skipped,
1255
- };
1256
- }
1257
- const installedUrls = new Set(installed.map((r) => r.url));
1258
- const active =
1259
- fragment.url && installedUrls.has(fragment.url)
1260
- ? fragment.url
1261
- : (installed[0]?.url ?? "");
1262
- return {
1263
- next: {
1264
- mode: fragment.mode ?? "auto",
1265
- enabled: fragment.enabled,
1266
- url: active,
1267
- relays: installed,
1268
- hideWidget: fragment.hideWidget === true,
1269
- },
1270
- added: installed.length,
1271
- updated: 0,
1272
- removed: Math.max(0, current.relays.length - installed.length),
1273
- skipped,
1274
- };
1275
- }
1276
-
1277
- const next: RelayState = {
1278
- ...current,
1279
- relays: current.relays.map(copyRelayEntry),
1280
- };
1281
- let added = 0;
1282
- let updated = 0;
1283
- const skipped: RelayImportSkipped[] = [];
1284
- const source = fragment.relays ?? [];
1285
- for (let index = 0; index < source.length; index++) {
1286
- const entry = source[index];
1287
- const trimmedUrl =
1288
- typeof entry?.url === "string" ? entry.url.trim() : "";
1289
- if (!trimmedUrl) {
1290
- skipped.push({ index, url: "", reason: "missing relay address" });
1291
- continue;
1292
- }
1293
- let relay: KnownRelay | undefined;
1294
- try {
1295
- const already = next.relays.some((r) => r.url === trimmedUrl);
1296
- relay = ensureRelay(next, trimmedUrl, entry.label);
1297
- if (already) {
1298
- updated += 1;
1299
- } else {
1300
- added += 1;
1301
- }
1302
- } catch (err) {
1303
- skipped.push({
1304
- index,
1305
- url: trimmedUrl,
1306
- reason: err instanceof Error ? err.message : "relay address rejected",
1307
- });
1308
- continue;
1309
- }
1310
- // File password overwrites; an absent file password keeps the stored one.
1311
- if (typeof entry.auth === "string" && entry.auth.length > 0) {
1312
- relay.auth = entry.auth;
1313
- }
1314
- if (
1315
- typeof entry.addedAt === "string" &&
1316
- Number.isNaN(Date.parse(entry.addedAt)) === false
1317
- ) {
1318
- relay.addedAt = entry.addedAt;
1319
- }
1320
- }
1321
- const mergedUrls = new Set(next.relays.map((r) => r.url));
1322
- if (!mergedUrls.has(next.url)) {
1323
- next.url =
1324
- fragment.url && mergedUrls.has(fragment.url)
1325
- ? fragment.url
1326
- : (next.relays[0]?.url ?? "");
1327
- }
1328
- return { next, added, updated, removed: 0, skipped };
1329
- }
972
+ export * from "./relay-codec.ts";
package/src/relay.ts CHANGED
@@ -401,50 +401,97 @@ export async function relayFetch(
401
401
  fetchA.catch(() => {});
402
402
  fetchB.catch(() => {});
403
403
  interface HedgedWin { url: string; response: Response; }
404
+ let hedgeBAbortedByWinner = false;
405
+ let bProducedOutcome = false;
406
+ const isCallerAborted = () => Boolean(opts.signal?.aborted);
407
+ const isNonWinning = (status: number): boolean =>
408
+ isRetriableStatus(status) || status === 403 || status === 401 || status === 402;
404
409
  try {
405
- const winner = await new Promise<HedgedWin>((resolve, reject) => {
406
- let failures = 0;
407
- let firstError: unknown = null;
408
- const onError = (err: unknown) => {
409
- failures += 1;
410
- if (firstError === null) firstError = err;
411
- if (failures >= 2) reject(firstError);
412
- };
413
- fetchA.then(
414
- (response) => resolve({ url: primaryUrl, response }),
415
- onError,
416
- );
417
- fetchB.then(
418
- (response) => resolve({ url: nextUrl, response }),
419
- onError,
420
- );
421
- });
410
+ const { promise: winnerPromise, resolve, reject } = Promise.withResolvers<HedgedWin>();
411
+ let firstBad: HedgedWin | null = null;
412
+ let firstError: unknown = null;
413
+ let settledCount = 0;
414
+ const onResponse = (legUrl: string, response: Response) => {
415
+ settledCount += 1;
416
+ if (legUrl === nextUrl) {
417
+ bProducedOutcome = true;
418
+ }
419
+ if (!isNonWinning(response.status)) {
420
+ if (firstBad) {
421
+ firstBad.response.body?.cancel().catch(() => {});
422
+ markRelayFailure(firstBad.url, firstBad.response.status);
423
+ }
424
+ resolve({ url: legUrl, response });
425
+ return;
426
+ }
427
+ if (!firstBad) {
428
+ if (settledCount >= 2) {
429
+ resolve({ url: legUrl, response });
430
+ } else {
431
+ firstBad = { url: legUrl, response };
432
+ }
433
+ return;
434
+ }
435
+ if (legUrl === nextUrl) {
436
+ markRelayFailure(nextUrl, response.status);
437
+ response.body?.cancel().catch(() => {});
438
+ } else {
439
+ markRelayFailure(primaryUrl, response.status);
440
+ response.body?.cancel().catch(() => {});
441
+ }
442
+ resolve(firstBad);
443
+ };
444
+ const onError = (legUrl: string, err: unknown) => {
445
+ settledCount += 1;
446
+ if (legUrl === nextUrl && !hedgeBAbortedByWinner) {
447
+ bProducedOutcome = true;
448
+ }
449
+ if (firstBad) {
450
+ if (legUrl === nextUrl && !hedgeBAbortedByWinner && !isCallerAborted()) {
451
+ markRelayFailure(nextUrl, 0, (err as Error)?.message || String(err));
452
+ }
453
+ resolve(firstBad);
454
+ return;
455
+ }
456
+ if (firstError === null) {
457
+ firstError = err;
458
+ }
459
+ if (settledCount >= 2) {
460
+ reject(firstError);
461
+ }
462
+ };
463
+ fetchA.then(
464
+ (r) => onResponse(primaryUrl, r),
465
+ (e) => onError(primaryUrl, e),
466
+ );
467
+ fetchB.then(
468
+ (r) => onResponse(nextUrl, r),
469
+ (e) => onError(nextUrl, e),
470
+ );
471
+ const winner = await winnerPromise;
422
472
  if (winner.url === nextUrl) {
423
473
  hedgeCtlA.abort();
424
474
  targetUrl = nextUrl;
425
475
  attemptStart = hedgeStart;
426
476
  } else {
477
+ hedgeBAbortedByWinner = true;
427
478
  hedgeCtlB.abort();
428
479
  }
429
480
  res = winner.response;
430
- // Both candidates are consumed by this race: skip the next
431
- // index so the loser is never retried in this request.
432
- hedgeSkipNext = true;
481
+ if (bProducedOutcome) {
482
+ hedgeSkipNext = true;
483
+ }
433
484
  } catch (hedgeErr) {
434
- // Both hedged attempts failed without usable headers. Caller
435
- // abort must propagate unmarked (the outer catch rethrows);
436
- // otherwise record the next relay here and let the outer
437
- // catch record the primary.
438
485
  if ((hedgeErr as Error)?.name === "AbortError" && opts.signal?.aborted) {
439
486
  throw hedgeErr;
440
487
  }
441
- markRelayFailure(nextUrl, 0, (hedgeErr as Error)?.message || String(hedgeErr));
442
- // Both legs of this race were abandoned; the outer catch
443
- // reports the primary one, this reports the hedge leg.
444
- reportAbandoned(
445
- (hedgeErr as Error)?.name === "AbortError" ? "header-timeout" : "network-error",
446
- );
447
- hedgeSkipNext = true;
488
+ if (bProducedOutcome) {
489
+ markRelayFailure(nextUrl, 0, (hedgeErr as Error)?.message || String(hedgeErr));
490
+ reportAbandoned(
491
+ (hedgeErr as Error)?.name === "AbortError" ? "header-timeout" : "network-error",
492
+ );
493
+ hedgeSkipNext = true;
494
+ }
448
495
  throw hedgeErr;
449
496
  }
450
497
  }