@zanii/blackbox 0.5.0 → 0.6.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.
package/README.md CHANGED
@@ -83,6 +83,11 @@ verifyAnswerCredential(credential, lines, bodies, packs); // { ok, problems }: t
83
83
 
84
84
  The same checks run offline as pure functions: `toolHallucinations`, `groundingFindings`, `factsIn`, `loadReferencePacks`, `answerRisk`, `detectorAccuracy`.
85
85
 
86
+ ## New in 0.6.0
87
+
88
+ - Serverless: a session joined with its id and token in a fresh function (Vercel, Lambda, Cloud Run) numbers its events from the server's `sdk_next`, so none are lost. Join in each invocation and flush before returning.
89
+ - Fewer false alarms: an ISO date-time (`2026-10-03T09:30:00+04:00`) in a tool's arguments is a date, not an invented number, and an MCP call carrying out the model's own tool call isn't flagged a second time for the same value.
90
+
86
91
  ## New in 0.5.0
87
92
 
88
93
  - Held streams: open a session with `holdStreams: true` (or set `BLACKBOX_HOLD_STREAMS=all` on the gateway) and a risky streamed answer waits for a person, as a non-streamed one does. The client gets keep-alives, then the whole stream, or one error in the provider's own format.
@@ -114,6 +114,7 @@ export function normaliseValue(s) {
114
114
  .replace(/[\s\-_./()+:,]/g, "");
115
115
  }
116
116
  const DATE = /^[0-9]{4}-[0-9]{2}-[0-9]{2}$|^[0-9]{2}-[0-9]{2}-[0-9]{4}$/;
117
+ const DATE_TIME = /(?<![0-9])[0-9]{4}-[0-9]{2}-[0-9]{2}(?:[T ][0-9]{2}:[0-9]{2}(?::[0-9]{2}(?:\.[0-9]+)?)?(?:Z|[+-][0-9]{2}:?[0-9]{2})?)?(?![0-9])/g;
117
118
  const VALUE_KINDS = [
118
119
  ["email", /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g],
119
120
  ["iban", /\b[A-Z]{2}[0-9]{2}(?:[ ]?[A-Z0-9]){11,30}\b/g],
@@ -124,6 +125,12 @@ const VALUE_KINDS = [
124
125
  /** The identifying values in a string (Western digits) and where they are, skipping `taken` spans. */
125
126
  export function identifierSpans(s, taken = []) {
126
127
  const out = [];
128
+ // a date or an ISO 8601 date-time (2026-10-03T00:00:00+04:00) is a date, not part of a number
129
+ for (const m of s.matchAll(DATE_TIME)) {
130
+ const at = m.index ?? 0;
131
+ if (!taken.some(([a, b]) => at < b && at + m[0].length > a))
132
+ taken.push([at, at + m[0].length]);
133
+ }
127
134
  for (const [kind, re] of VALUE_KINDS)
128
135
  for (const m of s.matchAll(re)) {
129
136
  const at = m.index ?? 0;
@@ -245,9 +252,14 @@ export function toolHallucinations(lines, bodies) {
245
252
  let request = "";
246
253
  let toolText = "";
247
254
  let sawModel = false;
248
- for (const t of modelCalls)
255
+ // §8: what the model asked for is where an agent's MCP call got its values; the model's call is
256
+ // checked above, so the MCP call it led to isn't flagged a second time for the same act
257
+ const modelArgs = new Map();
258
+ for (const t of modelCalls) {
249
259
  if (!called.has(t.tool))
250
260
  called.set(t.tool, t.seq);
261
+ modelArgs.set(t.seq, `${modelArgs.get(t.seq) ?? ""}\n${normaliseValue(canonical(t.args ?? {}))}`);
262
+ }
251
263
  for (const e of events) {
252
264
  const m = e.meta;
253
265
  const text = () => {
@@ -258,6 +270,9 @@ export function toolHallucinations(lines, bodies) {
258
270
  sawModel = true;
259
271
  request = normaliseValue(text());
260
272
  }
273
+ else if (e.kind === "llm.response" && modelArgs.has(e.seq)) {
274
+ toolText += modelArgs.get(e.seq);
275
+ }
261
276
  else if (e.kind === "tool.result" && typeof m.server === "string" && isObj(m.tool_defs)) {
262
277
  const tools = rpcOf(bodies(e.body_hash))?.result;
263
278
  const schemas = new Map();
@@ -58,6 +58,25 @@ export function imageType(image) {
58
58
  }
59
59
  const LOCK_WAIT_MS = 2_000;
60
60
  const LOCK_STALE_MS = 10_000;
61
+ const spoolDirOf = (options) => options.spoolDir ?? process.env.BLACKBOX_SPOOL_DIR ?? join(tmpdir(), "zanii-blackbox-spool");
62
+ /** spec/sdk.md §1: a session joined without its spool (a new process, a serverless function)
63
+ * numbers its events from the server's `next`; from 0, they'd be skipped as already recorded. */
64
+ async function startAtServerNext(options, id, token) {
65
+ const dir = spoolDirOf(options);
66
+ if (existsSync(join(dir, `${id}.jsonl`)) || existsSync(join(dir, `${id}.ack`)))
67
+ return;
68
+ try {
69
+ const r = await send("GET", options.url, `/v1/sessions/${id}/state`, token);
70
+ const n = r.status === 200 ? r.json.sdk_next : undefined;
71
+ if (typeof n === "number" && Number.isInteger(n) && n > 0) {
72
+ mkdirSync(dir, { recursive: true });
73
+ writeFileSync(join(dir, `${id}.ack`), JSON.stringify({ next: n, offset: 0, gen: 0 }));
74
+ }
75
+ }
76
+ catch {
77
+ // offline: the spool keeps the events, numbered from 0
78
+ }
79
+ }
61
80
  /** Opens (or joins) a session. Never throws: on failure it returns a disabled session and logs why. */
62
81
  export async function session(options) {
63
82
  const log = options.logger ?? (() => { });
@@ -95,6 +114,7 @@ export async function session(options) {
95
114
  token = String(r.json.token);
96
115
  return new BlackboxSession(options, id, token, true, true);
97
116
  }
117
+ await startAtServerNext(options, id, token);
98
118
  return new BlackboxSession(options, id, token);
99
119
  }
100
120
  catch (error) {
@@ -143,7 +163,7 @@ export class BlackboxSession {
143
163
  this.id = id;
144
164
  this.token = token;
145
165
  this.enabled = enabled;
146
- const dir = options.spoolDir ?? process.env.BLACKBOX_SPOOL_DIR ?? join(tmpdir(), "zanii-blackbox-spool");
166
+ const dir = spoolDirOf(options);
147
167
  this.spool = join(dir, `${id}.jsonl`);
148
168
  this.ackFile = join(dir, `${id}.ack`);
149
169
  if (!enabled)
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.5.0";
1
+ export declare const VERSION = "0.6.0";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // The SDK version, on its own so the CLI can print it without loading the whole SDK.
2
- export const VERSION = "0.5.0";
2
+ export const VERSION = "0.6.0";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@zanii/blackbox",
3
3
  "license": "Apache-2.0",
4
- "version": "0.5.0",
4
+ "version": "0.6.0",
5
5
  "description": "The flight recorder for AI agents: sessions, a zero-loss spool, framework hooks, approvals, and offline verification of the gateway's hash-chained record.",
6
6
  "keywords": [
7
7
  "ai-agents",