@vizuh/sabi 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/mod/sabi.mjs CHANGED
@@ -1,6 +1,260 @@
1
- // @vizuh/sabi 0.1.3 — generated by pack.mjs from packages/adapters/command-code/mod/sabi.ts.
1
+ // @vizuh/sabi 0.1.5 — generated by pack.mjs from packages/adapters/command-code/mod/sabi.ts.
2
2
  // Source and docs: https://github.com/vizuh/sabi
3
3
 
4
+ // packages/core/src/telemetry.ts
5
+ var ALLOWLIST = /* @__PURE__ */ new Set([
6
+ "error-line",
7
+ "python-traceback",
8
+ "panic",
9
+ "exception",
10
+ "typescript-error",
11
+ "fail-marker",
12
+ "command-failed",
13
+ "nonzero-exit",
14
+ "failure-count",
15
+ "command-not-found",
16
+ "permission-denied",
17
+ "missing-file",
18
+ "soft-warning",
19
+ "soft-deprecated",
20
+ "soft-retrying",
21
+ "soft-timeout",
22
+ "tool-error",
23
+ "permission-denial",
24
+ "rate-limited",
25
+ "quota-exceeded",
26
+ "timeout",
27
+ "mutation",
28
+ "verification-receipt",
29
+ "summary-claim",
30
+ "scope-observed",
31
+ "constraint",
32
+ "prior-failure",
33
+ "context-boundary",
34
+ "observation"
35
+ ]);
36
+ function allowlisted(value) {
37
+ const code = value.split(":")[0]?.trim();
38
+ return ALLOWLIST.has(code);
39
+ }
40
+ function telemetryPolicy(config) {
41
+ const captureSnippets = config?.captureSnippets === true;
42
+ const captureChars = config?.captureChars ?? 800;
43
+ return {
44
+ captureSnippets,
45
+ allowlisted: (value) => allowlisted(value),
46
+ snippet: (value) => captureSnippets ? String(value).slice(0, captureChars) : ""
47
+ };
48
+ }
49
+ function sanitizeReason(reason, policy) {
50
+ if (!policy.captureSnippets) {
51
+ if (!reason || allowlisted(reason)) return reason;
52
+ const segments = reason.split(":").map((segment) => segment.trim());
53
+ const last = segments[segments.length - 1] ?? "";
54
+ if (allowlisted(last)) return reason;
55
+ return "reason withheld (telemetry.allowlistOnly)";
56
+ }
57
+ return policy.snippet(reason);
58
+ }
59
+ var KEYWORD_REDACT = /\b(bearer|authorization|api[-_]?\s*key|token|secret|password|passwd)\b["']?\s*[:=\s]\s*["']?[^\s"'{},\]]+/gi;
60
+ var BARE_TOKEN_REDACT = [
61
+ [/\bsk-[A-Za-z0-9_-]{8,}\b/g, "sk-[REDACTED]"],
62
+ [/\bAKIA[0-9A-Z]{16}\b/g, "AKIA[REDACTED]"],
63
+ [/\bAIza[0-9A-Za-z_-]{30,}\b/g, "AIza[REDACTED]"],
64
+ [/\bghp_[A-Za-z0-9]{8,}\b/g, "ghp_[REDACTED]"],
65
+ [/\bghu_[A-Za-z0-9]{8,}\b/g, "ghu_[REDACTED]"],
66
+ [/\bghs_[A-Za-z0-9]{8,}\b/g, "ghs_[REDACTED]"],
67
+ [/\bgithub_pat_[A-Za-z0-9_]{8,}\b/g, "github_pat_[REDACTED]"]
68
+ ];
69
+ var CREDENTIAL_URL_REDACT = /([a-zA-Z][a-zA-Z0-9+.-]*:\/\/)[^:@\s/]+:[^@\s/]+@/g;
70
+ function sanitizeError(error) {
71
+ const first = String(error ?? "").split("\n")[0]?.slice(0, 200) ?? "";
72
+ if (!first.trim()) return "unknown error";
73
+ let redacted = first.replace(KEYWORD_REDACT, "$1=REDACTED");
74
+ for (const [pattern, replacement] of BARE_TOKEN_REDACT) redacted = redacted.replace(pattern, replacement);
75
+ redacted = redacted.replace(CREDENTIAL_URL_REDACT, "$1REDACTED@");
76
+ if (looksLikeCanary(redacted)) return "upstream error redacted (possible secret)";
77
+ return redacted || "unknown error";
78
+ }
79
+ var CANARY_PATTERNS = [
80
+ /\b(BEGIN|END)\s+(RSA|OPENSSH|EC|DSA)\s+PRIVATE\s+KEY/i,
81
+ /\bsk-[A-Za-z0-9_-]{8,}\b/,
82
+ /\bAKIA[0-9A-Z]{16}\b/,
83
+ /\bAIza[0-9A-Za-z_-]{30,}\b/,
84
+ /\bghp_[A-Za-z0-9]{8,}\b/,
85
+ /\bghu_[A-Za-z0-9]{8,}\b/,
86
+ /\bghs_[A-Za-z0-9]{8,}\b/,
87
+ /\bgithub_pat_[A-Za-z0-9_]{8,}\b/,
88
+ /[a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^:@\s/]+:[^@\s/]+@/,
89
+ /(?:password|passwd|pwd|secret)\s*[:=]\s*\S+/i
90
+ ];
91
+ function looksLikeCanary(text) {
92
+ return CANARY_PATTERNS.some((re) => re.test(text));
93
+ }
94
+
95
+ // packages/core/src/evidence.ts
96
+ var MAX_EVIDENCE_ITEMS = 16;
97
+ var MAX_EVIDENCE_DETAIL_CHARS = 240;
98
+ var MAX_SCOPE_ITEMS = 32;
99
+ var MAX_SCOPE_LABEL_CHARS = 120;
100
+ var MAX_RECEIPT_ID_CHARS = 128;
101
+ var EVIDENCE_CODES = /* @__PURE__ */ new Set([
102
+ "error-line",
103
+ "python-traceback",
104
+ "panic",
105
+ "exception",
106
+ "typescript-error",
107
+ "fail-marker",
108
+ "command-failed",
109
+ "nonzero-exit",
110
+ "failure-count",
111
+ "command-not-found",
112
+ "permission-denied",
113
+ "missing-file",
114
+ "soft-warning",
115
+ "soft-deprecated",
116
+ "soft-retrying",
117
+ "soft-timeout",
118
+ "tool-error",
119
+ "permission-denial",
120
+ "rate-limited",
121
+ "quota-exceeded",
122
+ "timeout",
123
+ "mutation",
124
+ "verification-receipt",
125
+ "summary-claim",
126
+ "scope-observed",
127
+ "constraint",
128
+ "prior-failure",
129
+ "context-boundary",
130
+ "observation"
131
+ ]);
132
+ var EVIDENCE_SOURCES = /* @__PURE__ */ new Set(["tool", "user", "harness", "judge", "summary"]);
133
+ var EVIDENCE_STATUSES = /* @__PURE__ */ new Set(["observed", "verified", "unverified", "contradicted"]);
134
+ function boundedText(value, maxChars) {
135
+ if (typeof value !== "string") return void 0;
136
+ const text = value.replace(/[\u0000-\u001f\u007f]/g, " ").trim();
137
+ if (!text || looksLikeCanary(text)) return void 0;
138
+ return text.slice(0, maxChars);
139
+ }
140
+ function generationOf(value) {
141
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : 0;
142
+ }
143
+ function isEvidenceCode(value) {
144
+ return typeof value === "string" && EVIDENCE_CODES.has(value);
145
+ }
146
+ function isEvidenceSource(value) {
147
+ return typeof value === "string" && EVIDENCE_SOURCES.has(value);
148
+ }
149
+ function isEvidenceStatus(value) {
150
+ return typeof value === "string" && EVIDENCE_STATUSES.has(value);
151
+ }
152
+ function normalizeTrajectoryEvidence(value, fallbackGeneration = 0) {
153
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
154
+ const item = value;
155
+ if (!isEvidenceCode(item.code) || !isEvidenceSource(item.source) || !isEvidenceStatus(item.status)) return void 0;
156
+ const detail = boundedText(item.detail, MAX_EVIDENCE_DETAIL_CHARS);
157
+ return {
158
+ code: item.code,
159
+ source: item.source,
160
+ status: item.status,
161
+ contextGeneration: generationOf(item.contextGeneration ?? fallbackGeneration),
162
+ ...detail ? { detail } : {}
163
+ };
164
+ }
165
+ function boundTrajectoryEvidence(values, maxItems = MAX_EVIDENCE_ITEMS) {
166
+ const result = [];
167
+ if (maxItems <= 0) return result;
168
+ const seen = /* @__PURE__ */ new Set();
169
+ for (const value of values ?? []) {
170
+ const item = normalizeTrajectoryEvidence(value);
171
+ if (!item) continue;
172
+ const key = JSON.stringify(item);
173
+ if (seen.has(key)) continue;
174
+ seen.add(key);
175
+ result.push(item);
176
+ if (result.length >= Math.max(0, Math.min(maxItems, MAX_EVIDENCE_ITEMS))) break;
177
+ }
178
+ return result;
179
+ }
180
+ function countScope(value) {
181
+ if (typeof value === "number") return Number.isSafeInteger(value) && value >= 0 ? { count: value } : {};
182
+ if (!Array.isArray(value)) return {};
183
+ const labels = [...new Set(value.map((item) => boundedText(item, MAX_SCOPE_LABEL_CHARS)).filter((item) => Boolean(item)))].slice(0, MAX_SCOPE_ITEMS);
184
+ return { count: labels.length, labels };
185
+ }
186
+ function buildScopeCoverage(input = {}) {
187
+ const expected = countScope(input.expected);
188
+ const observed = countScope(input.observed);
189
+ const explicit = input.source === "explicit" || input.expected !== void 0;
190
+ const source = input.source ?? (explicit ? "explicit" : input.observed !== void 0 ? "inferred" : "unknown");
191
+ const expectedCount = expected.count;
192
+ const observedCount = observed.count;
193
+ const ratio = expectedCount !== void 0 && expectedCount > 0 && observedCount !== void 0 ? Math.min(1, observedCount / expectedCount) : void 0;
194
+ const missing = input.missing ? [...new Set(input.missing.map((item) => boundedText(item, MAX_SCOPE_LABEL_CHARS)).filter((item) => Boolean(item)))].slice(0, MAX_SCOPE_ITEMS) : expected.labels && observed.labels ? expected.labels.filter((item) => !observed.labels.includes(item)).slice(0, MAX_SCOPE_ITEMS) : void 0;
195
+ return {
196
+ ...expectedCount !== void 0 ? { expected: expectedCount } : {},
197
+ ...observedCount !== void 0 ? { observed: observedCount } : {},
198
+ ...ratio !== void 0 ? { ratio } : {},
199
+ source,
200
+ ...missing && missing.length > 0 ? { missing } : {}
201
+ };
202
+ }
203
+ function validReceipt(value, generation) {
204
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
205
+ const item = value;
206
+ const id = boundedText(item.id, MAX_RECEIPT_ID_CHARS);
207
+ const status = item.status === "passed" || item.status === "failed" ? item.status : void 0;
208
+ const source = item.source === "tool" || item.source === "user" || item.source === "harness" ? item.source : void 0;
209
+ const receiptGeneration = generationOf(item.generation);
210
+ if (!id || !status || !source || item.valid === false || receiptGeneration !== generation) return void 0;
211
+ return { id, status, source, generation: receiptGeneration, ...item.valid === true ? { valid: true } : {} };
212
+ }
213
+ function deriveVerificationState(input = {}) {
214
+ const generation = generationOf(input.generation);
215
+ const receiptCandidate = input.receipt;
216
+ if (receiptCandidate !== void 0) {
217
+ const receipt = validReceipt(receiptCandidate, generation);
218
+ if (receipt) return { status: receipt.status, receiptId: receipt.id, generation };
219
+ return { status: "unknown", reason: "invalid-receipt", generation };
220
+ }
221
+ if (input.summaryClaim) return { status: "unknown", reason: "summary-without-receipt", generation };
222
+ if (input.mutation) return { status: "needed", reason: "mutation-without-receipt", generation };
223
+ if (input.verificationAttempted) return { status: "attempted", reason: "missing-receipt", generation };
224
+ return { status: "not-required", generation };
225
+ }
226
+ function evidenceForTrajectory(state, options = {}) {
227
+ const generation = generationOf(state.contextGeneration);
228
+ const evidence = [];
229
+ if (state.roundKind === "implementation") evidence.push({ code: "mutation", source: "tool", status: "observed", contextGeneration: generation });
230
+ for (const code of state.failureEvidence) {
231
+ if (isEvidenceCode(code)) evidence.push({ code, source: "tool", status: "observed", contextGeneration: generation });
232
+ }
233
+ if (options.verificationReceipt) evidence.push({ code: "verification-receipt", source: "harness", status: "verified", contextGeneration: generation });
234
+ if (options.summaryClaim) evidence.push({ code: "summary-claim", source: "summary", status: "unverified", contextGeneration: generation });
235
+ if (generation > 0) evidence.push({ code: "context-boundary", source: "harness", status: "observed", contextGeneration: generation });
236
+ return boundTrajectoryEvidence(evidence);
237
+ }
238
+ function decorateTrajectoryState(state, metadata = {}) {
239
+ const generation = generationOf(metadata.generation ?? state.contextGeneration);
240
+ const receipt = metadata.verificationReceipt;
241
+ const verification = deriveVerificationState({
242
+ mutation: state.roundKind === "implementation",
243
+ verificationAttempted: state.roundKind === "verification",
244
+ summaryClaim: metadata.summaryClaim,
245
+ generation,
246
+ receipt
247
+ });
248
+ const scopeCoverage = metadata.scope ? buildScopeCoverage(metadata.scope) : void 0;
249
+ const evidence = evidenceForTrajectory(state, { summaryClaim: metadata.summaryClaim, verificationReceipt: receipt !== void 0 });
250
+ return {
251
+ ...state,
252
+ ...evidence.length > 0 ? { evidence } : {},
253
+ verification,
254
+ ...scopeCoverage ? { scopeCoverage } : {}
255
+ };
256
+ }
257
+
4
258
  // packages/core/src/state.ts
5
259
  var EXPLORE_TOOLS = /* @__PURE__ */ new Set([
6
260
  "read",
@@ -331,8 +585,8 @@ function servesInputModalities(declared, required) {
331
585
  return required.every((modality) => declared.includes(modality));
332
586
  }
333
587
  function firstServingTier(tiers, required, declared) {
334
- for (const [name, tier] of Object.entries(tiers)) {
335
- if (servesInputModalities(declared(tier), required)) return name;
588
+ for (const name of Object.keys(tiers).sort()) {
589
+ if (servesInputModalities(declared(tiers[name]), required)) return name;
336
590
  }
337
591
  return void 0;
338
592
  }
@@ -441,6 +695,9 @@ function validateModelMetadata(value, label) {
441
695
  if (!Array.isArray(items) || items.some((item) => typeof item !== "string" || !item.trim())) {
442
696
  fail(`capabilities.${field}`, "must be an array of nonempty strings");
443
697
  }
698
+ if ((field === "inputModalities" || field === "outputModalities") && items.length === 0) {
699
+ fail(`capabilities.${field}`, "must not be empty");
700
+ }
444
701
  const strings = items;
445
702
  if (new Set(strings).size !== strings.length) fail(`capabilities.${field}`, "must not contain duplicates");
446
703
  const allowed = field.endsWith("Modalities") ? MODALITIES : field === "structuredOutput" ? ["json_object", "json_schema"] : void 0;
@@ -530,11 +787,30 @@ function validateConfig(value, source = "<inline>") {
530
787
  throw new Error(`Sabi config ${source}: alias '${alias}' targets unknown tier '${target}'`);
531
788
  }
532
789
  }
790
+ const knownRules = new Set(POLICY_ORDER);
533
791
  for (const [condition, tier] of Object.entries(policy)) {
792
+ if (!knownRules.has(condition)) {
793
+ throw new Error(`Sabi config ${source}: policy rule '${condition}' is not a known rule (${POLICY_ORDER.join(", ")})`);
794
+ }
534
795
  if (typeof tier !== "string" || tier !== "off" && !Object.hasOwn(models, tier)) {
535
796
  throw new Error(`Sabi config ${source}: policy rule '${condition}' targets unknown tier '${tier}'`);
536
797
  }
537
798
  }
799
+ if (Object.values(aliases).includes("auto")) {
800
+ const fallbackTier = typeof policy.unclassified === "string" && policy.unclassified !== "off" ? policy.unclassified : "cheap";
801
+ if (!Object.hasOwn(models, fallbackTier)) {
802
+ throw new Error(`Sabi config ${source}: policy.unclassified must resolve to a declared tier (got '${String(policy.unclassified)}')`);
803
+ }
804
+ }
805
+ const transportFallback = config.transportFallback;
806
+ if (transportFallback !== void 0) {
807
+ if (!isObject(transportFallback)) {
808
+ throw new Error(`Sabi config ${source}: transportFallback must be an object`);
809
+ }
810
+ if (transportFallback.enabled !== void 0 && typeof transportFallback.enabled !== "boolean") {
811
+ throw new Error(`Sabi config ${source}: transportFallback.enabled must be a boolean`);
812
+ }
813
+ }
538
814
  const judge = config.judge;
539
815
  if (judge !== void 0) {
540
816
  if (typeof judge !== "object" || judge === null || typeof judge.enabled !== "boolean") {
@@ -556,6 +832,14 @@ function validateConfig(value, source = "<inline>") {
556
832
  if (!Array.isArray(judge.callOn) || judge.callOn.some((rule) => typeof rule !== "string")) {
557
833
  throw new Error(`Sabi config ${source}: judge.callOn must be an array of policy rule names`);
558
834
  }
835
+ for (const rule of judge.callOn) {
836
+ if (!knownRules.has(rule)) {
837
+ throw new Error(`Sabi config ${source}: judge.callOn rule '${rule}' is not a known rule (${POLICY_ORDER.join(", ")})`);
838
+ }
839
+ }
840
+ }
841
+ if (judge.includeSnippets !== void 0 && typeof judge.includeSnippets !== "boolean") {
842
+ throw new Error(`Sabi config ${source}: judge.includeSnippets must be a boolean`);
559
843
  }
560
844
  for (const [name, value2] of Object.entries(judge.thresholds ?? {})) {
561
845
  if (typeof value2 !== "number" || value2 < 0 || value2 > 1) {
@@ -632,25 +916,233 @@ function validateConfig(value, source = "<inline>") {
632
916
  if (tier.minPlan !== void 0 && typeof tier.minPlan !== "string") {
633
917
  throw new Error(`Sabi config ${source}: harness.tiers.${tierName}.minPlan must be a string`);
634
918
  }
919
+ if (tier.inputModalities !== void 0) {
920
+ if (!Array.isArray(tier.inputModalities) || tier.inputModalities.length === 0 || tier.inputModalities.some((modality) => typeof modality !== "string" || !MODALITIES.includes(modality))) {
921
+ throw new Error(`Sabi config ${source}: harness.tiers.${tierName}.inputModalities must be a nonempty array of: ${MODALITIES.join(", ")}`);
922
+ }
923
+ if (new Set(tier.inputModalities).size !== tier.inputModalities.length) {
924
+ throw new Error(`Sabi config ${source}: harness.tiers.${tierName}.inputModalities must not contain duplicates`);
925
+ }
926
+ }
927
+ if (tier.contextWindow !== void 0 && (typeof tier.contextWindow !== "number" || !Number.isSafeInteger(tier.contextWindow) || tier.contextWindow < 1)) {
928
+ throw new Error(`Sabi config ${source}: harness.tiers.${tierName}.contextWindow must be a safe integer >= 1`);
929
+ }
635
930
  }
636
931
  }
637
932
  return { ...config, upstreams, models, aliases, policy };
638
933
  }
639
934
 
935
+ // packages/core/src/recovery-actions.ts
936
+ var RECOVERY_ACTIONS = [
937
+ "continue",
938
+ "retry-same",
939
+ "retry-with-feedback",
940
+ "gather-evidence",
941
+ "escalate-model",
942
+ "fresh-context",
943
+ "rollback-with-reflection",
944
+ "ask-user"
945
+ ];
946
+ function isRecoveryAction(value) {
947
+ return typeof value === "string" && RECOVERY_ACTIONS.includes(value);
948
+ }
949
+ function normalizeRecoveryAction(value, fallback = "ask-user") {
950
+ return isRecoveryAction(value) ? value : fallback;
951
+ }
952
+
640
953
  // packages/core/src/log.ts
641
- import { appendFileSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2 } from "node:fs";
642
- import { createHash, randomUUID } from "node:crypto";
954
+ import { appendFileSync, chmodSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
955
+ import { createHmac, randomBytes, randomUUID } from "node:crypto";
956
+ import os2 from "node:os";
643
957
  import path2 from "node:path";
644
958
  function defaultLogPath() {
645
959
  return process.env.SABI_LOG?.trim() || path2.join(process.cwd(), ".sabi", "decisions.jsonl");
646
960
  }
961
+ function appendPrivateLine(logFile, line) {
962
+ const dir = path2.dirname(logFile);
963
+ mkdirSync(dir, { recursive: true, mode: 448 });
964
+ appendFileSync(logFile, line, { mode: 384 });
965
+ if (process.platform !== "win32") {
966
+ chmodSync(dir, 448);
967
+ chmodSync(logFile, 384);
968
+ }
969
+ }
970
+ function identitySaltPath(env = process.env) {
971
+ const override = env.SABI_ID_SALT_FILE?.trim();
972
+ if (override) return override;
973
+ const base = env.XDG_CONFIG_HOME?.trim() || path2.join(os2.homedir(), ".config");
974
+ return path2.join(base, "sabi", ".identity-salt");
975
+ }
976
+ var cachedSalt;
977
+ var cachedSaltSource;
978
+ var ephemeralSaltWarned = false;
979
+ function loadOrCreateSalt(source) {
980
+ try {
981
+ if (existsSync2(source)) {
982
+ const stored = readFileSync2(source, "utf8").trim();
983
+ if (stored.length >= 16) return stored;
984
+ }
985
+ const fresh = randomBytes(32).toString("hex");
986
+ mkdirSync(path2.dirname(source), { recursive: true });
987
+ writeFileSync(source, `${fresh}
988
+ `, { mode: 384 });
989
+ try {
990
+ chmodSync(source, 384);
991
+ } catch {
992
+ }
993
+ return fresh;
994
+ } catch {
995
+ return void 0;
996
+ }
997
+ }
998
+ function getIdentitySalt(env = process.env) {
999
+ const override = env.SABI_ID_SALT?.trim();
1000
+ if (override) return override;
1001
+ const source = identitySaltPath(env);
1002
+ if (cachedSalt !== void 0 && cachedSaltSource === source) return cachedSalt;
1003
+ const salt = loadOrCreateSalt(source);
1004
+ if (salt === void 0) {
1005
+ if (!ephemeralSaltWarned) {
1006
+ ephemeralSaltWarned = true;
1007
+ console.warn(`Sabi: identity salt at ${source} is unreadable \u2014 using an ephemeral per-process salt`);
1008
+ }
1009
+ cachedSalt = randomBytes(32).toString("hex");
1010
+ cachedSaltSource = source;
1011
+ return cachedSalt;
1012
+ }
1013
+ cachedSalt = salt;
1014
+ cachedSaltSource = source;
1015
+ return salt;
1016
+ }
1017
+ var logWriteFailures = 0;
1018
+ var warnedLogFiles = /* @__PURE__ */ new Set();
1019
+ function logWriteFailurePath(logFile) {
1020
+ return `${logFile}.write-failures.json`;
1021
+ }
1022
+ function recordLogWriteFailure(logFile, error) {
1023
+ logWriteFailures += 1;
1024
+ if (!warnedLogFiles.has(logFile)) {
1025
+ warnedLogFiles.add(logFile);
1026
+ console.warn(`Sabi: could not append to the decision log at ${logFile}: ${error?.message ?? error} (telemetry degraded, will retry next round)`);
1027
+ }
1028
+ try {
1029
+ const sidecar = logWriteFailurePath(logFile);
1030
+ let failures = 0;
1031
+ try {
1032
+ const prior = JSON.parse(readFileSync2(sidecar, "utf8"));
1033
+ if (typeof prior.failures === "number" && Number.isFinite(prior.failures)) failures = Math.floor(prior.failures);
1034
+ } catch {
1035
+ }
1036
+ const state = {
1037
+ failures: failures + 1,
1038
+ lastTs: (/* @__PURE__ */ new Date()).toISOString(),
1039
+ lastError: String(error?.message ?? error).slice(0, 120)
1040
+ };
1041
+ mkdirSync(path2.dirname(sidecar), { recursive: true });
1042
+ writeFileSync(sidecar, `${JSON.stringify(state)}
1043
+ `);
1044
+ } catch {
1045
+ }
1046
+ }
647
1047
  function appendDecision(record, logFile = defaultLogPath()) {
648
- mkdirSync(path2.dirname(logFile), { recursive: true });
649
- appendFileSync(logFile, `${JSON.stringify(record)}
1048
+ try {
1049
+ appendPrivateLine(logFile, `${serializeDecisionRecord(record)}
650
1050
  `);
1051
+ } catch (error) {
1052
+ recordLogWriteFailure(logFile, error);
1053
+ }
1054
+ }
1055
+ function boundedStringList(values, maxItems, maxChars) {
1056
+ if (!Array.isArray(values)) return [];
1057
+ return values.filter((value) => typeof value === "string" && value.trim().length > 0).map((value) => value.replace(/[\u0000-\u001f\u007f]/g, " ").slice(0, maxChars)).slice(0, maxItems);
1058
+ }
1059
+ function isPlainObject(value) {
1060
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1061
+ }
1062
+ function sanitizeState(state, config) {
1063
+ const policy = telemetryPolicy(config);
1064
+ const failureEvidence = boundedStringList(state.failureEvidence, 8, 64).filter((value) => policy.allowlisted(value));
1065
+ const evidence = boundTrajectoryEvidence(state.evidence);
1066
+ const clean = {
1067
+ messageCount: state.messageCount,
1068
+ assistantTurns: state.assistantTurns,
1069
+ toolMessages: state.toolMessages,
1070
+ lastRole: String(state.lastRole ?? "").slice(0, 32),
1071
+ contextChars: state.contextChars,
1072
+ estimatedTokens: state.estimatedTokens,
1073
+ hasTools: state.hasTools,
1074
+ toolNames: boundedStringList(state.toolNames, 40, 120),
1075
+ lastToolNames: boundedStringList(state.lastToolNames, 12, 120),
1076
+ roundKind: state.roundKind,
1077
+ failure: state.failure,
1078
+ failureEvidence,
1079
+ ...state.contextTokens !== void 0 ? { contextTokens: state.contextTokens } : {},
1080
+ ...state.contextKnown !== void 0 ? { contextKnown: state.contextKnown } : {},
1081
+ ...state.contextGeneration !== void 0 ? { contextGeneration: state.contextGeneration } : {},
1082
+ ...state.repeatedFailure !== void 0 ? { repeatedFailure: state.repeatedFailure } : {},
1083
+ ...state.failureStreak !== void 0 ? { failureStreak: state.failureStreak } : {},
1084
+ ...state.contextWindow !== void 0 ? { contextWindow: state.contextWindow } : {},
1085
+ ...state.inputModalities ? { inputModalities: state.inputModalities.slice(0, 5) } : {},
1086
+ ...state.mediaCounts ? { mediaCounts: state.mediaCounts } : {},
1087
+ ...evidence.length > 0 ? { evidence } : {},
1088
+ ...state.verification ? { verification: { ...state.verification, receiptId: state.verification.receiptId?.slice(0, 128) } } : {},
1089
+ ...state.scopeCoverage ? {
1090
+ scopeCoverage: {
1091
+ ...state.scopeCoverage,
1092
+ ...state.scopeCoverage.missing ? { missing: boundedStringList(state.scopeCoverage.missing, 32, 120) } : {}
1093
+ }
1094
+ } : {}
1095
+ };
1096
+ return clean;
1097
+ }
1098
+ function sanitizeDecisionRecord(record, config) {
1099
+ const policy = telemetryPolicy(config);
1100
+ const state = isPlainObject(record.state) ? sanitizeState(record.state, config) : void 0;
1101
+ const sanitized = {
1102
+ ts: record.ts,
1103
+ sessionId: record.sessionId,
1104
+ sessionKnown: record.sessionKnown,
1105
+ requestId: record.requestId,
1106
+ client: record.client,
1107
+ turnId: record.turnId,
1108
+ servedModel: record.servedModel,
1109
+ alias: record.alias,
1110
+ mode: record.mode,
1111
+ rule: record.rule,
1112
+ tier: record.tier,
1113
+ reason: sanitizeReason(String(record.reason ?? ""), policy),
1114
+ upstream: record.upstream,
1115
+ upstreamModel: record.upstreamModel,
1116
+ stream: record.stream,
1117
+ state,
1118
+ judge: record.judge,
1119
+ usage: record.usage,
1120
+ cost: record.cost,
1121
+ latencyMs: record.latencyMs,
1122
+ ttftMs: record.ttftMs,
1123
+ outcome: record.outcome,
1124
+ ...record.error ? { error: sanitizeError(record.error) } : {},
1125
+ transport: record.transport,
1126
+ fallback: record.fallback,
1127
+ ...record.recovery ? {
1128
+ recovery: {
1129
+ ...record.recovery,
1130
+ action: normalizeRecoveryAction(record.recovery.action),
1131
+ evidenceGrade: record.recovery.evidenceGrade === "matched" || record.recovery.evidenceGrade === "replayed" ? record.recovery.evidenceGrade : "observed",
1132
+ failureSignature: String(record.recovery.failureSignature).slice(0, 64),
1133
+ stateFingerprint: String(record.recovery.stateFingerprint).slice(0, 64),
1134
+ ...record.recovery.route ? { route: String(record.recovery.route).slice(0, 160) } : {},
1135
+ ...record.recovery.receiptId ? { receiptId: String(record.recovery.receiptId).slice(0, 128) } : {}
1136
+ }
1137
+ } : {}
1138
+ };
1139
+ return sanitized;
1140
+ }
1141
+ function serializeDecisionRecord(record, config) {
1142
+ return JSON.stringify(sanitizeDecisionRecord(record, config));
651
1143
  }
652
1144
  function hashIdentity(kind, ...parts) {
653
- return createHash("sha256").update(JSON.stringify([kind, ...parts])).digest("hex");
1145
+ return createHmac("sha256", getIdentitySalt()).update(JSON.stringify([kind, ...parts])).digest("hex");
654
1146
  }
655
1147
  function sessionIdFor(session, client = "unknown") {
656
1148
  return session === void 0 ? randomUUID() : hashIdentity("session", client, session);
@@ -673,7 +1165,7 @@ function trajectoryFromRound(round, previous) {
673
1165
  const sameFailure = previous?.failure === "hard" && failure === "hard";
674
1166
  const repeatedFailure = sameFailure === true;
675
1167
  const failureStreak = repeatedFailure ? 2 : failure === "hard" ? 1 : 0;
676
- return {
1168
+ const state = {
677
1169
  messageCount: round.messageCount,
678
1170
  assistantTurns: round.assistantTurns,
679
1171
  toolMessages: round.calls.length,
@@ -694,6 +1186,12 @@ function trajectoryFromRound(round, previous) {
694
1186
  ...round.inputModalities ? { inputModalities: round.inputModalities } : {},
695
1187
  ...round.mediaCounts && Object.keys(round.mediaCounts).length ? { inputModalities: round.inputModalities ?? modalitiesOf(round.mediaCounts), mediaCounts: round.mediaCounts } : {}
696
1188
  };
1189
+ return decorateTrajectoryState(state, {
1190
+ generation: round.contextGeneration,
1191
+ verificationReceipt: round.verificationReceipt,
1192
+ summaryClaim: round.summaryClaim,
1193
+ scope: round.scope
1194
+ });
697
1195
  }
698
1196
  function roundMedia(round) {
699
1197
  return { counts: round.mediaCounts ?? {}, payloadChars: 0 };
@@ -725,54 +1223,6 @@ function planRound(state, policy, tiers, options = {}) {
725
1223
  };
726
1224
  }
727
1225
 
728
- // packages/core/src/telemetry.ts
729
- var ALLOWLIST = /* @__PURE__ */ new Set([
730
- "error-line",
731
- "python-traceback",
732
- "panic",
733
- "exception",
734
- "typescript-error",
735
- "fail-marker",
736
- "command-failed",
737
- "nonzero-exit",
738
- "failure-count",
739
- "command-not-found",
740
- "permission-denied",
741
- "missing-file",
742
- "soft-warning",
743
- "soft-deprecated",
744
- "soft-retrying",
745
- "soft-timeout",
746
- "tool-error",
747
- "permission-denial",
748
- "rate-limited",
749
- "quota-exceeded",
750
- "timeout"
751
- ]);
752
- function allowlisted(value) {
753
- const code = value.split(":")[0]?.trim();
754
- return ALLOWLIST.has(code);
755
- }
756
- function telemetryPolicy(config) {
757
- const captureSnippets = config?.captureSnippets === true;
758
- const captureChars = config?.captureChars ?? 800;
759
- return {
760
- captureSnippets,
761
- allowlisted: (value) => allowlisted(value),
762
- snippet: (value) => captureSnippets ? String(value).slice(0, captureChars) : ""
763
- };
764
- }
765
- function sanitizeReason(reason, policy) {
766
- if (!policy.captureSnippets) {
767
- if (!reason || allowlisted(reason)) return reason;
768
- const segments = reason.split(":").map((segment) => segment.trim());
769
- const last = segments[segments.length - 1] ?? "";
770
- if (allowlisted(last)) return reason;
771
- return "reason withheld (telemetry.allowlistOnly)";
772
- }
773
- return policy.snippet(reason);
774
- }
775
-
776
1226
  // packages/core/src/prompt.ts
777
1227
  import * as readline from "node:readline/promises";
778
1228
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vizuh/sabi",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Adaptive inference scheduling for Command Code: one bundled mod that routes each continuing round by model, effort and trajectory state.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/sabi.config.json CHANGED
@@ -68,6 +68,9 @@
68
68
  "exploration": "cheap",
69
69
  "unclassified": "cheap"
70
70
  },
71
+ "transportFallback": {
72
+ "enabled": false
73
+ },
71
74
  "telemetry": {
72
75
  "allowlistOnly": true,
73
76
  "captureSnippets": false,