cortad 0.3.1-rc.9 → 0.3.2

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 (57) hide show
  1. package/README.md +8 -5
  2. package/lib/app-port.mjs +9 -0
  3. package/lib/auto-reach.mjs +88 -0
  4. package/lib/backing.mjs +230 -0
  5. package/lib/calls.mjs +23 -0
  6. package/lib/cli.mjs +115 -30
  7. package/lib/contact-text.mjs +167 -0
  8. package/lib/data.mjs +16 -22
  9. package/lib/dissection-text.mjs +232 -0
  10. package/lib/document.mjs +75 -0
  11. package/lib/env-secrets.mjs +48 -0
  12. package/lib/failed-text.mjs +36 -0
  13. package/lib/fresh.mjs +33 -12
  14. package/lib/health.mjs +61 -0
  15. package/lib/held.mjs +67 -0
  16. package/lib/home.mjs +55 -17
  17. package/lib/hosted.mjs +201 -0
  18. package/lib/identities.mjs +1 -1
  19. package/lib/keeping.mjs +162 -0
  20. package/lib/ledger.mjs +20 -0
  21. package/lib/lock.mjs +6 -1
  22. package/lib/mcp.mjs +52 -18
  23. package/lib/mint.mjs +179 -38
  24. package/lib/mongo.mjs +74 -0
  25. package/lib/numbers-text.mjs +68 -0
  26. package/lib/pg.mjs +118 -0
  27. package/lib/proc.mjs +100 -0
  28. package/lib/proof.mjs +1014 -0
  29. package/lib/proxy.mjs +184 -0
  30. package/lib/pyhook/cortad_sites.py +218 -0
  31. package/lib/pyhook/cortad_tied.py +291 -0
  32. package/lib/pyhook/sitecustomize.py +1175 -166
  33. package/lib/qdrant.mjs +47 -0
  34. package/lib/reach.mjs +223 -0
  35. package/lib/read-text.mjs +78 -24
  36. package/lib/redis.mjs +130 -0
  37. package/lib/register.mjs +36 -27
  38. package/lib/replay.mjs +464 -34
  39. package/lib/runner.mjs +85 -0
  40. package/lib/service.mjs +25 -0
  41. package/lib/spec.mjs +7 -4
  42. package/lib/start.mjs +33 -2
  43. package/lib/stick.mjs +65 -25
  44. package/lib/stores.mjs +347 -0
  45. package/lib/switches.mjs +36 -0
  46. package/lib/tether.mjs +9 -0
  47. package/lib/text.mjs +314 -179
  48. package/lib/tied.cjs +217 -0
  49. package/lib/trace.cjs +386 -514
  50. package/lib/verbs.mjs +155 -38
  51. package/lib/wire.cjs +882 -0
  52. package/lib/words.mjs +2 -0
  53. package/lib/writes.mjs +2 -2
  54. package/local.mjs +661 -297
  55. package/package.json +31 -1
  56. package/skill/SKILL.md +49 -42
  57. package/skill/references/results.md +279 -102
package/lib/trace.cjs CHANGED
@@ -1,12 +1,13 @@
1
1
  // Loaded into your app by the command that started it (node --require), and only then. It watches
2
- // for two things. First, a request to your app during which your app called a model: that request is
3
- // your AI's door, with the exact body it takes and the sign-in it carried, learned from a message you
4
- // sent yourself rather than guessed from code. Second, every model call your app makes: the host, the
5
- // model it asked for, the status and the token counts the provider sent back, so a run can say which
6
- // model answered and what it cost you, which tools it asked for (names and clipped arguments, never a
7
- // secret) and the passages its prompt was handed as retrieved context. The words of a reply are never
8
- // written down. Both go to a file only you can read, in the command's own folder on this machine.
9
- // Nothing here talks to a network.
2
+ // for two things. First, a request to your app during which your app called a model: an exchange,
3
+ // with the exact body it took, the sign-in it carried, what your app answered, the lines of your code
4
+ // that wrote it and the prompt it sent, so the door is proven by a message rather than guessed from
5
+ // code. The first few per route are kept, so two turns of one conversation are both seen. Second,
6
+ // every model call your app makes: the host, the model it asked for, the status, the seconds and the
7
+ // token counts the provider sent back, which tools it asked for (names and clipped arguments, never a
8
+ // secret) and the passages its prompt was handed; the model's words only for a call inside a kept
9
+ // exchange or a run's turn. Both go to a file only you can read, in the command's own folder on this
10
+ // machine. Nothing here talks to a network.
10
11
  "use strict";
11
12
  const FILE = process.env.CORTAD_TRACE_FILE;
12
13
  if (FILE) {
@@ -16,28 +17,8 @@ if (FILE) {
16
17
  const http = require("node:http");
17
18
  const https = require("node:https");
18
19
  const als = new AsyncLocalStorage();
19
- // Every value this file writes passes one mask, whichever path wrote it: a key inside a value
20
- // ("sk-...", a bearer token, a JWT) and what anyone wrote after "password is" or "token:". The
21
- // door row's sign-in headers are kept on purpose: the command replays them from this machine.
22
- const SECRET_TEXT = [
23
- /((?:pass(?:word|phrase|wd)|pwd|密码|口令)["']?\s*(?:is\b|was\b|[:=:]|是|为)\s*["']?)([^\s"'\\,;},。;、]+)/gi,
24
- /((?:secret(?:[ _-]?key)?|api[ _-]?key|apikey|access[ _-]?key|private[ _-]?key|client[ _-]?secret|(?:auth|access|refresh)[ _-]?token|token)["']?\s*[:=:]\s*["']?)([^\s"'\\,;},。;、]+)/gi,
25
- ];
26
- const SECRET_WORD = /\b(?:Bearer\s+(?=[\w.~+/=-]*\d)[\w.~+/=-]{16,}|[spr]k[-_](?=[\w-]*\d)[\w-]{8,}|gh[po]_\w{16,}|github_pat_\w{16,}|xox[abpr]-[\w-]{8,}|AKIA[0-9A-Z]{12,}|AIza[\w-]{20,}|eyJ[\w-]{10,}\.[\w-]{4,}\.[\w-]*)/g;
27
- const maskText = (s) => SECRET_TEXT.reduce((t, re) => t.replace(re, "$1[secret]"), s).replace(SECRET_WORD, "[secret]");
28
- const scrub = (v, depth = 0) => (typeof v === "string" ? maskText(v)
29
- : Array.isArray(v) ? v.map((x) => scrub(x, depth + 1))
30
- : v && typeof v === "object" && depth < 12 ? Object.fromEntries(Object.entries(v).map(([k, x]) => [k, k === "headers" && depth === 0 ? x : scrub(x, depth + 1)])) : v);
31
- // A door's body goes up to the run as JSON, so it is masked as JSON: its string values, never its shape.
32
- const bodyScrubbed = (text) => { try { const b = JSON.parse(text); return b && typeof b === "object" ? JSON.stringify(scrub(b)) : maskText(text); } catch { return maskText(text); } };
33
- const row = (value) => {
34
- try {
35
- const plain = value.hello || value.listen || value.routes;
36
- const out = plain ? value : scrub(value);
37
- if (!plain && typeof value.body === "string") out.body = bodyScrubbed(value.body);
38
- fs.appendFileSync(FILE, JSON.stringify(out) + "\n", { mode: 0o600 });
39
- } catch { /* the command is gone */ }
40
- };
20
+ const { MAX, REPLY_MAX, EMBEDDING, TOOL_TEXT, toolText, scrubbed, list, clipped, argsText, turnOf, isModelCall, isRetrieval, passagesFrom, decoded, cookieNames, makeRecorder, asChat, GATEWAY_MODEL } = require("./wire.cjs");
21
+ const row = (value) => { try { fs.appendFileSync(FILE, JSON.stringify(scrubbed(value)) + "\n", { mode: 0o600 }); } catch { /* the command is gone */ } };
41
22
  // What the app writes into its own folder while a run is on: each file copied once before its
42
23
  // first write, so the command puts every one back when the run ends (lib/writes.mjs). Trials
43
24
  // wrote carts and tickets into an app's data/*.json and the developer's own tests read them.
@@ -48,7 +29,7 @@ if (FILE) {
48
29
  const crypto = require("node:crypto");
49
30
  const { fileURLToPath } = require("node:url");
50
31
  // Not the app's data: what a build, a package manager or a log writes.
51
- const NOT_DATA = /(?:^|\/)(?:node_modules|\.git|\.next|\.nuxt|\.svelte-kit|\.turbo|\.cache|\.parcel-cache|\.venv|venv|__pycache__|\.pytest_cache|\.mypy_cache|\.ruff_cache|dist|build|out|coverage|\.cortad)(?:\/|$)|\.(?:log|pyc|tmp|swp)$/;
32
+ const NOT_DATA = /(?:^|\/)(?:node_modules|\.git|\.next|\.nuxt|\.svelte-kit|\.turbo|\.cache|\.parcel-cache|\.venv|venv|__pycache__|\.pytest_cache|\.mypy_cache|\.ruff_cache|dist|build|out|coverage|\.cortad|logs?)(?:\/|$)|\.log(?:\.[\w-]+)?$|\.(?:pyc|tmp|swp)$/;
52
33
  const raw = { copyFileSync: fs.copyFileSync, appendFileSync: fs.appendFileSync, existsSync: fs.existsSync, mkdirSync: fs.mkdirSync, statSync: fs.statSync, readFileSync: fs.readFileSync };
53
34
  const root = p.resolve(APP_ROOT);
54
35
  let run = null, runAt = -1;
@@ -122,28 +103,52 @@ if (FILE) {
122
103
  // The frames of the stack it is called from, nearest first, outside this hook and node_modules,
123
104
  // relative to the folder the app was started in. An awaited call keeps its awaiting frames in
124
105
  // the stack, and an SDK is deeper than the default ten frames.
125
- const stackFrames = () => {
126
- const limit = Error.stackTraceLimit;
106
+ // A frame still inside a build folder is a bundle line no one edits: it is kept by its absolute
107
+ // path and column, and the command places it through the chunk's source map (lib/replay.mjs), so
108
+ // the app never reads a map on a request's path.
109
+ const BUILT = /\/(?:\.next|\.nuxt|\.svelte-kit|\.output|\.turbo|\.vercel|dist|build|out)\//;
110
+ // `typed`: each frame also carries the type of the object it was called on, read off the same
111
+ // stack as it is formatted; none at all when a formatter of the app's own does not keep one line a
112
+ // frame.
113
+ const stackFrames = (typed) => {
114
+ const limit = Error.stackTraceLimit, prepare = Error.prepareStackTrace;
115
+ let types = null;
127
116
  Error.stackTraceLimit = 60;
128
- const stack = String(new Error().stack);
129
- Error.stackTraceLimit = limit;
117
+ if (typed) Error.prepareStackTrace = (err, calls) => { types = calls.map((c) => c.getTypeName()); return typeof prepare === "function" ? prepare(err, calls) : [String(err), ...calls.map((c) => ` at ${c}`)].join("\n"); };
118
+ let stack;
119
+ try { stack = String(new Error().stack); } finally { Error.stackTraceLimit = limit; if (typed) Error.prepareStackTrace = prepare; }
120
+ const lines = stack.split("\n").slice(1);
121
+ if (typed && lines.length !== types?.length) return [];
130
122
  const out = [];
131
- for (const line of stack.split("\n")) {
132
- const m = /\(?(?:file:\/\/)?(\/[^()]+?):(\d+):\d+\)?$/.exec(line.trim());
133
- if (m && m[1] !== __filename && !m[1].includes("/node_modules/")) out.push({ file: nodePath.relative(process.cwd(), decodeURI(m[1])), line: Number(m[2]) });
134
- }
123
+ lines.forEach((line, i) => {
124
+ const m = /\(?(?:file:\/\/)?(\/[^()]+?):(\d+):(\d+)\)?$/.exec(line.trim());
125
+ if (!m || m[1] === __filename || m[1].includes("/node_modules/")) return;
126
+ const type = typed ? { type: types[i] } : {};
127
+ if (BUILT.test(m[1])) out.push({ file: decodeURI(m[1]), line: Number(m[2]), column: Number(m[3]), built: true, ...type });
128
+ else out.push({ file: nodePath.relative(process.cwd(), decodeURI(m[1])), line: Number(m[2]), ...type });
129
+ });
135
130
  return out;
136
131
  };
137
132
  // The file that registered a route: the first frame outside this hook and outside node_modules.
138
- const callerFile = () => { const f = stackFrames()[0]; return f && !f.file.startsWith("..") ? f.file : ""; };
133
+ const callerFile = () => { const f = stackFrames().find((x) => !x.built); return f && !f.file.startsWith("..") ? f.file : ""; };
134
+ // A built frame is written as its chunk's absolute path, line and column, which no app frame's
135
+ // relative path can be mistaken for.
136
+ const atOf = (f) => (f.built ? `${f.file}:${f.line}:${f.column}` : `${f.file}:${f.line}`);
139
137
  // Which of the app's own lines made a model call, nearest first: the run names the code path that
140
- // ran by it, where the read could only name the door.
138
+ // ran by it, where the read could only name the door. Five lines: a provider class's stream, post
139
+ // and retry can be three lines of one file, and the line that says which path ran is past them.
140
+ // Built frames are handed on past five, since the SDK bundled beside the app's code fills the
141
+ // first of them; the command keeps five once its map has placed them.
142
+ const CALLERS_MAX = 5, CALLERS_BUILT = 20;
141
143
  const callers = () => {
142
144
  const out = [];
145
+ let own = 0;
143
146
  for (const f of stackFrames()) {
144
- const at = `${f.file}:${f.line}`;
145
- if (!f.file.startsWith("..") && !out.includes(at)) out.push(at);
146
- if (out.length === 3) break;
147
+ const at = atOf(f);
148
+ if (f.file.startsWith("..") || out.includes(at)) continue;
149
+ out.push(at);
150
+ if (!f.built) own++;
151
+ if (own === CALLERS_MAX || out.length === CALLERS_BUILT) break;
147
152
  }
148
153
  return out.length ? out : undefined;
149
154
  };
@@ -308,408 +313,120 @@ if (FILE) {
308
313
  recheck = false;
309
314
  setImmediate(() => { try { if (table().routes.length !== routesSaid) sayRoutes(lastPort); } catch { /* as above */ } });
310
315
  };
311
- const MAX = 65536;
312
- // The turn a message came in under, when the run tagged it: one opaque id per request, so a
313
- // model call and its prompt can be pinned to the reply they produced even while five turns are
314
- // in flight. It is read here and never shown to your app's own code path beyond the header.
315
- const TURN = /^[A-Za-z0-9:_.-]{1,80}$/;
316
- const turnOf = (headers) => {
317
- const t = headers && (typeof headers.get === "function" ? headers.get("x-cortad-turn") : headers["x-cortad-turn"]);
318
- return typeof t === "string" && TURN.test(t) ? t : undefined;
319
- };
320
- // The customer's own rule sentences, written beside the trace by the run, so each model call can
321
- // say which of them its prompt carried: a rule is then asked only of a reply whose call was told
322
- // it. Absent file, nothing is claimed either way. Reloaded when the file changes.
323
- const RULES = process.env.CORTAD_RULES_FILE;
324
- let rules = null, rulesAt = -1;
325
- const norm = (s) => String(s || "")
326
- .replace(/\\u([0-9a-fA-F]{4})/g, (_, h) => String.fromCharCode(parseInt(h, 16)))
327
- .replace(/\\[nrt]/g, " ").replace(/\\"/g, '"').replace(/\\\\/g, "\\")
328
- .toLowerCase().replace(/\s+/g, " ").trim();
329
- // A sentence with a slot in it ("answer in {language}") is matched by its literal parts.
330
- const partsOf = (text) => text.split(/\{[^}]*\}|\$\{[^}]*\}|%[sd]|<[^>]{1,40}>/).map(norm).filter((p) => p.length >= 12);
331
- const rulesNow = () => {
332
- if (!RULES) return null;
333
- try {
334
- const at = fs.statSync(RULES).mtimeMs;
335
- if (at !== rulesAt) {
336
- rulesAt = at;
337
- const list = JSON.parse(fs.readFileSync(RULES, "utf8"));
338
- rules = Array.isArray(list)
339
- ? list.filter((r) => r && typeof r.id === "string" && typeof r.text === "string").map((r) => ({ id: r.id, parts: partsOf(r.text) })).filter((r) => r.parts.length)
340
- : [];
341
- }
342
- } catch { /* not written yet, or gone */ }
343
- return rules;
344
- };
345
- // What the app's tools answered, as the prompt of the next model call carries them: the tool
346
- // messages of a chat body, the function outputs of a responses body, the tool_result blocks of
347
- // an Anthropic one, the functionResponse parts of a Gemini one. They are the material a reply's
348
- // facts rest on, and without them a real ticket id read as invented. Bounded per call.
349
- const TOOL_TEXT = 3000, TOOLS_MAX = 12;
350
- const textOf = (v) => (typeof v === "string" ? v : Array.isArray(v) ? v.map((p) => (p && typeof p === "object" ? (p.text || p.content || "") : String(p || ""))).filter(Boolean).join("\n") : v && typeof v === "object" ? JSON.stringify(v) : "");
351
- // Only what came after the person's latest message belongs to this turn: a thread the app
352
- // resends whole carries every earlier turn's tool answers too.
353
- const list = (v) => (Array.isArray(v) ? v.filter((x) => x && typeof x === "object") : []);
354
- const since = (items, isPerson) => { let at = -1; items.forEach((m, i) => { if (isPerson(m)) at = i; }); return items.slice(at + 1); };
355
- // A user message that only hands a tool's answer back ("Observation: ...", <tool_response>) is
356
- // the agent loop talking, not the person.
357
- const HANDED_BACK = /^\s*(?:Observation\s*:|<(?:tool_response|tool_result|function_results?)>)/i;
358
- const personSaid = (m) => m.role === "user" && !(Array.isArray(m.content) && m.content.some((c) => c && c.type === "tool_result")) && !HANDED_BACK.test(textOf(m.content));
359
- const personAsked = (c) => c.role === "user" && !list(c.parts).some((p) => p.functionResponse);
360
- const toolsIn = (sent) => {
361
- let body; try { body = JSON.parse(sent); } catch { return undefined; }
362
- if (!body || typeof body !== "object") return undefined;
363
- const out = [];
364
- const names = new Map();
365
- const add = (name, text) => { const t = textOf(text).slice(0, TOOL_TEXT); if (t.trim() && out.length < TOOLS_MAX && !out.some((o) => o.text === t)) out.push({ name: String(name || "").slice(0, 80), text: t }); };
366
- for (const m of list(body.messages)) {
367
- for (const c of list(m.tool_calls)) if (c.id && c.function) names.set(c.id, c.function.name);
368
- for (const c of list(m.content)) if (c.type === "tool_use" && c.id) names.set(c.id, c.name);
369
- }
370
- for (const m of since(list(body.messages), personSaid)) {
371
- if (m.role === "tool" || m.role === "function") add(m.name || names.get(m.tool_call_id), m.content);
372
- for (const c of list(m.content)) if (c.type === "tool_result") add(names.get(c.tool_use_id), c.content);
373
- }
374
- const items = list(body.input);
375
- for (const it of items) if (it.type === "function_call" && it.call_id) names.set(it.call_id, it.name);
376
- for (const it of since(items, (x) => x.role === "user")) if (it.type === "function_call_output") add(names.get(it.call_id), it.output);
377
- for (const c of since(list(body.contents), personAsked)) for (const p of list(c.parts)) if (p.functionResponse) add(p.functionResponse.name, p.functionResponse.response);
378
- const declared = declaredIn(body);
379
- for (const o of observedIn(turnTexts(body, true, declared), declared)) add(o.name, o.text);
380
- return out.length ? out : undefined;
381
- };
382
-
383
- // What the model asked the app to run: chat tool_calls (a stream's pieces joined by position), a
384
- // legacy function_call, a responses function_call item, an Anthropic or Bedrock tool use, a
385
- // Gemini functionCall. Read off the model's reply, and off this turn's earlier calls as the
386
- // prompt resends them. Names and arguments only: every value clipped, a secret never written.
387
- const CALLS_MAX = 12, VALUE_MAX = 200, ARGS_MAX = 1200;
388
- const SECRET_KEY = /(?:^|_)(?:pass(?:word|phrase)?|secret|token|api_?key|authorization|cookie|session(?:_id)?|credentials?|private_key)$/;
389
- const SECRET_VALUE = /^(?:Bearer\s|Basic\s|sk-|pk_|rk_|ghp_|gho_|github_pat_|xox[abpr]-|AKIA|AIza|eyJ[\w-]{10,}\.)/;
390
- const clipped = (v, depth, max = VALUE_MAX) => {
391
- if (typeof v === "string") return SECRET_VALUE.test(v) ? "[secret]" : v.length > max ? v.slice(0, max) + "…" : v;
392
- if (Array.isArray(v)) return depth > 4 ? [] : v.slice(0, 20).map((x) => clipped(x, depth + 1, max));
393
- if (!v || typeof v !== "object") return v;
394
- const o = {};
395
- if (depth > 4) return o;
396
- for (const [k, x] of Object.entries(v).slice(0, 40)) o[k] = SECRET_KEY.test(k.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase()) ? "[secret]" : clipped(x, depth + 1, max);
397
- return o;
398
- };
399
- const argsText = (raw) => {
400
- let v = raw;
401
- if (typeof raw === "string") { try { v = raw.trim() ? JSON.parse(raw) : {}; } catch { v = raw; } }
402
- return JSON.stringify(clipped(v === undefined || v === null ? {} : v, 0)).slice(0, ARGS_MAX);
403
- };
404
- const callsOf = (events) => {
405
- const whole = new Map();
406
- const parts = new Map();
407
- let n = 0;
408
- const put = (key, name, args) => { if (name) whole.set(key || `w${n++}`, { name, args }); };
409
- const piece = (key, name, args) => {
410
- const p = parts.get(key) || { name: "", args: "" };
411
- if (name && !p.name) p.name = name;
412
- if (typeof args === "string") p.args += args;
413
- parts.set(key, p);
414
- };
415
- const blocks = (content) => {
416
- for (const b of list(content)) {
417
- if (b.type === "tool_use") put(b.id, b.name, b.input);
418
- if (b.toolUse) put(b.toolUse.toolUseId, b.toolUse.name, b.toolUse.input);
419
- }
316
+ // When the run last sent a request: a write made outside every request (a queue's worker, a
317
+ // timer) is held while a run is live, since it may be carrying out a trial's ask.
318
+ let lastTurnAt = -Infinity;
319
+ const { tied, requestCtx, note, ended, meter } = makeRecorder({
320
+ row,
321
+ rulesFile: process.env.CORTAD_RULES_FILE,
322
+ onTurn: () => { heldWarm(); lastTurnAt = Date.now(); },
323
+ onEnded: (ctx, steps) => writtenAt(ctx, steps),
324
+ });
325
+ // Where in the app's own code each part of a reply was written: the app's line on the stack of the
326
+ // write, or, for a web stream the app hands back, where its code put the chunk into the stream,
327
+ // kept when the reply carried it. One entry per line and event name, with the first bytes written
328
+ // there, so a block in the reply is placed at the write that sent it (lib/pyhook/cortad_sites.py).
329
+ // ponytail: a reply's first thousand writes are traced; a leak later in a longer stream goes unplaced.
330
+ const SITES_MAX = 16, SITE_TEXT = 300, SITE_WRITES = 1000, BUILT_WALK = 12;
331
+ const EVENT = /^event:[ \t]*([\w.:-]{1,40})/m, JSON_EVENT = /"(?:type|event)"\s*:\s*"([\w.:-]{1,40})"/;
332
+ const siteText = (chunk) => {
333
+ if (typeof chunk === "string") return chunk.slice(0, SITE_TEXT);
334
+ if (ArrayBuffer.isView(chunk)) return Buffer.from(chunk.buffer, chunk.byteOffset, Math.min(chunk.byteLength, SITE_TEXT)).toString("utf8").replace(/\uFFFD+$/, "");
335
+ try { return chunk && typeof chunk === "object" ? String(JSON.stringify(chunk)).slice(0, SITE_TEXT) : ""; } catch { return ""; }
336
+ };
337
+ const wrote = (ctx, chunk, streamed) => {
338
+ if (!ctx || !ctx.sites || ++ctx.traced > SITE_WRITES || ctx.sites.length >= SITES_MAX) return;
339
+ // A method the app put on the response, such as a res.end that logs and calls the one it replaced,
340
+ // only hands the write on: the app's line that called it wrote it.
341
+ const frames = stackFrames(true);
342
+ const i = frames.findIndex((f) => !f.file.startsWith("..") && f.type !== ctx.responseType);
343
+ const text = i >= 0 && siteText(chunk);
344
+ if (!text) return;
345
+ // In a bundle the first frames are the SDK compiled into the same chunk: the next few are
346
+ // handed on, and the command keeps the first that its map places in the app's own source.
347
+ const own = frames[i];
348
+ const at = own.built ? frames.slice(i, i + BUILT_WALK).filter((f) => f.built).map(atOf) : atOf(own);
349
+ const m = EVENT.exec(text) || JSON_EVENT.exec(text);
350
+ const event = m ? m[1] : undefined;
351
+ if (ctx.sites.some((s) => String(s.at) === String(at) && s.event === event)) return;
352
+ ctx.sites.push({ at, ...(event ? { event } : {}), text, ...(streamed ? { streamed } : {}) });
353
+ };
354
+ for (const Controller of [globalThis.ReadableStreamDefaultController, globalThis.TransformStreamDefaultController]) {
355
+ const enqueue = Controller && Controller.prototype.enqueue;
356
+ if (typeof enqueue !== "function") continue;
357
+ Controller.prototype.enqueue = function (chunk) { try { wrote(als.getStore(), chunk, true); } catch { /* the chunk goes on */ } return enqueue.apply(this, arguments); };
358
+ }
359
+ // Written under the id its exchange is written by.
360
+ const writtenAt = (ctx, steps) => {
361
+ const asker = (steps && steps.at(-1)) || ctx;
362
+ const turn = ctx.turn || asker.turn;
363
+ const list = ctx.sites.filter((s) => !s.streamed || ctx.reply.text.includes(s.text)).map(({ streamed, ...s }) => s);
364
+ if (list.length && (asker.kept || turn)) row({ sites: { ex: asker.id, ...(turn ? { turn } : {}), list } });
365
+ };
366
+ // What the app sends back, as it writes it: its own listeners and pipes see exactly the same.
367
+ const watchReply = (ctx, res) => {
368
+ if (!res || typeof res.write !== "function" || typeof res.end !== "function") return;
369
+ ctx.responseType = res.constructor && res.constructor.name;
370
+ const got = [];
371
+ let size = 0, writes = 0;
372
+ const keep = (chunk, encoding) => {
373
+ if (chunk == null || typeof chunk === "function") return;
374
+ writes += 1;
375
+ wrote(ctx, chunk);
376
+ if (size >= MAX) return;
377
+ const b = Buffer.isBuffer(chunk) ? chunk : typeof chunk === "string" ? Buffer.from(chunk, typeof encoding === "string" && Buffer.isEncoding(encoding) ? encoding : "utf8") : Buffer.from(chunk);
378
+ got.push(b);
379
+ size += b.length;
420
380
  };
421
- for (const e of events) {
422
- if (!e || typeof e !== "object") continue;
423
- for (const c of list(e.choices)) {
424
- const m = c.message;
425
- if (m) {
426
- for (const t of list(m.tool_calls)) if (t.function) put(t.id, t.function.name, t.function.arguments);
427
- if (m.function_call) put(null, m.function_call.name, m.function_call.arguments);
428
- }
429
- const d = c.delta;
430
- if (d) {
431
- for (const t of list(d.tool_calls)) piece(`c${c.index || 0}.${t.index ?? t.id}`, t.function && t.function.name, t.function && t.function.arguments);
432
- if (d.function_call) piece(`f${c.index || 0}`, d.function_call.name, d.function_call.arguments);
433
- }
434
- }
435
- for (const it of [...list(e.output), ...list(e.item ? [e.item] : []), ...list(e.response && e.response.output)]) {
436
- if (it.type === "function_call") put(it.call_id || it.id, it.name, it.arguments);
437
- }
438
- blocks(e.content);
439
- blocks(e.message && e.message.content);
440
- blocks(e.output && e.output.message && e.output.message.content);
441
- if (e.type === "content_block_start" && e.content_block && e.content_block.type === "tool_use") piece(`a${e.index}`, e.content_block.name, "");
442
- if (e.type === "content_block_delta" && e.delta && e.delta.type === "input_json_delta") piece(`a${e.index}`, "", e.delta.partial_json);
443
- for (const c of list(e.candidates)) for (const p of list(c.content && c.content.parts)) if (p.functionCall) put(null, p.functionCall.name, p.functionCall.args);
444
- }
445
- return [...whole.values(), ...parts.values()].filter((p) => p.name).map((p) => ({ name: String(p.name).slice(0, 80), arguments: argsText(p.args) }));
446
- };
447
- // This turn's earlier calls, as the prompt resends them after the person's latest message.
448
- const calledBefore = (body) => callsOf([
449
- ...since(list(body.messages), personSaid).filter((m) => m.role === "assistant").map((m) => ({ choices: [{ message: m }], content: m.content })),
450
- { output: since(list(body.input), (x) => x.role === "user") },
451
- ...since(list(body.contents), personAsked).filter((c) => c.role === "model").map((c) => ({ candidates: [{ content: c }] })),
452
- ]);
453
- // Tool use a model writes in its words instead of as a structured call. A ReAct agent (CrewAI,
454
- // LangChain) writes "Action: name" then "Action Input: {...}" and is handed "Observation: ..."
455
- // back in its next prompt; others write <tool_call>{...}</tool_call>, <function=name>,
456
- // <invoke name="..."> or a JSON object that names the tool. A name counts only when the request
457
- // declares that tool, in its tools field or in the tool list its prompt carries, so a thought, a
458
- // "Final Answer" or prose that says Action is never a call.
459
- const FINAL = /^final[\s_-]*answer$/i;
460
- const TOOL_NAME = /^[A-Za-z_][\w.-]{0,79}$/;
461
- const promptText = (body) => [
462
- ...list(body.messages).filter((m) => m.role !== "assistant").map((m) => textOf(m.content)),
463
- textOf(body.system), textOf(body.instructions), typeof body.prompt === "string" ? body.prompt : "", typeof body.input === "string" ? body.input : "",
464
- ...list(body.input).filter((x) => x.role && x.role !== "assistant").map((x) => textOf(x.content)),
465
- textOf(body.systemInstruction && body.systemInstruction.parts), ...list(body.contents).filter((c) => c.role !== "model").map((c) => textOf(c.parts)),
466
- ].join("\n");
467
- const declaredIn = (body) => {
468
- const out = new Set();
469
- const add = (n) => { const s = typeof n === "string" ? n.trim().replace(/^["'`]+|["'`]+$/g, "") : ""; if (TOOL_NAME.test(s) && !FINAL.test(s)) out.add(s); };
470
- for (const t of list(body.tools)) { add(t.name); add(t.function && t.function.name); add(t.toolSpec && t.toolSpec.name); for (const d of list(t.functionDeclarations || t.function_declarations)) add(d.name); }
471
- for (const f of list(body.functions)) add(f.name);
472
- for (const t of list(body.toolConfig && body.toolConfig.tools)) add(t.toolSpec && t.toolSpec.name);
473
- const text = promptText(body).slice(0, 200000);
474
- for (const m of text.matchAll(/^[ \t]*Tool Name:[ \t]*([^\n]+)/gim)) add(m[1]);
475
- for (const m of text.matchAll(/\b(?:one of|name of|names? from)[ \t]*\[([^\]\n]{1,2000})\]/gi)) for (const x of m[1].split(",")) add(x);
476
- for (const m of text.matchAll(/valid "?action"? values?:?[ \t]*([^\n]{1,2000})/gi)) for (const x of m[1].split(/,|\bor\b/)) add(x);
477
- for (const m of text.matchAll(/<(tools|functions)>([\s\S]*?)<\/\1>/gi)) for (const k of m[2].matchAll(/"name"\s*:\s*"([^"]+)"/g)) add(k[1]);
478
- for (const m of text.matchAll(/"name"\s*:\s*"([^"]+)"\s*,\s*"(?:description|parameters|input_schema)"/g)) add(m[1]);
479
- for (const m of text.matchAll(/<(?:tool|function)\s+name\s*=\s*["']([^"']+)["']/gi)) add(m[1]);
480
- return out;
481
- };
482
- const REACT = /(?:^|\n)[ \t>*_#]*Action[ \t]*\d*[ \t*_]*:[ \t*_`]*([^\n`*]*?)[ \t*_`]*\n+[ \t>*_#]*Action[ \t]*\d*[ \t_]*Input[ \t*_]*:[ \t*_]*([\s\S]*?)(?=\n[ \t>*_#]*(?:Observation|Thought|Final[ \t]*Answer|Action)\b|\u0000|$)/gi;
483
- const TAG_CALL = /<(tool_call|function_call|tool_use)>([\s\S]*?)<\/\1>/gi;
484
- const FN_TAG = /<function=([\w.-]+)>([\s\S]*?)<\/function>/gi;
485
- const INVOKE = /<invoke\s+name\s*=\s*["']([^"']+)["'][^>]*>([\s\S]*?)<\/invoke>/gi;
486
- const OBSERVED = /(?:^|\n)[ \t>*_]*Observation[ \t*_]*:[ \t]*([\s\S]*?)(?=\n[ \t>*_#]*(?:Thought|Action|Final[ \t]*Answer)\b|\u0000|$)|<(tool_response|tool_result|function_results?|observation)>([\s\S]*?)<\/\2>/gi;
487
- const paramsOf = (s) => { const o = {}; for (const m of s.matchAll(/<parameter(?:=|\s+name\s*=\s*["'])([\w.-]+)["']?\s*>([\s\S]*?)<\/parameter>/gi)) o[m[1]] = m[2].trim(); return o; };
488
- // Each balanced JSON object in the text, outermost first; its insides are not read again.
489
- const objectsIn = (text) => {
490
- const out = [];
491
- for (let i = text.indexOf("{"); i !== -1 && out.length < 20; i = text.indexOf("{", i + 1)) {
492
- let depth = 0, str = false, esc = false, end = -1;
493
- for (let j = i; j < text.length && j < i + 20000; j++) {
494
- const ch = text[j];
495
- if (str) { if (esc) esc = false; else if (ch === "\\") esc = true; else if (ch === '"') str = false; continue; }
496
- if (ch === '"') str = true; else if (ch === "{") depth++; else if (ch === "}" && --depth === 0) { end = j; break; }
497
- }
498
- const v = end === -1 ? null : parse(text.slice(i, end + 1));
499
- if (v && typeof v === "object") { out.push({ at: i, v }); i = end; }
500
- }
501
- return out;
502
- };
503
- // An Action Input that opens with a JSON object is that object: a model that runs on past it
504
- // ("Observ: ...") does not put its own invention into the arguments.
505
- const leadingJson = (s) => { const t = s.trim(); const o = t[0] === "{" ? objectsIn(t)[0] : null; return o && o.at === 0 ? o.v : t; };
506
- // A tool's schema carries a description; a call does not.
507
- const jsonCall = (v) => {
508
- const f = v.function && typeof v.function === "object" ? v.function : v;
509
- if ("description" in f) return null;
510
- const name = [f.name, v.tool, v.tool_name, v.action, typeof v.function === "string" ? v.function : undefined].find((x) => typeof x === "string");
511
- return name ? { name, args: [f.arguments, f.args, f.parameters, v.action_input, v.tool_input, v.input, v.args, v.arguments, v.parameters].find((x) => x !== undefined) } : null;
512
- };
513
- const writtenCalls = (text, declared) => {
514
- const found = [];
515
- if (!text || !declared.size) return found;
516
- const s = text.slice(0, 50000);
517
- const take = (at, name, args) => { const n = String(name || "").trim(); if (declared.has(n)) found.push({ at, name: n, args: typeof args === "string" ? args.trim() : args }); };
518
- for (const m of s.matchAll(REACT)) take(m.index, m[1], leadingJson(m[2]));
519
- for (const m of s.matchAll(TAG_CALL)) { const v = parse(m[2].trim()); const c = v && typeof v === "object" && jsonCall(v); if (c) take(m.index, c.name, c.args); }
520
- for (const m of s.matchAll(FN_TAG)) { const v = parse(m[2].trim()); take(m.index, m[1], v && typeof v === "object" ? v : paramsOf(m[2])); }
521
- for (const m of s.matchAll(INVOKE)) take(m.index, m[1], paramsOf(m[2]));
522
- for (const { at, v } of objectsIn(s)) { const c = jsonCall(v); if (c) take(at, c.name, c.args); }
523
- return found.sort((a, b) => a.at - b.at);
524
- };
525
- // What a tool answered, named by the call written just before it.
526
- const observedIn = (text, declared) => {
527
- const s = String(text || "").slice(0, 50000);
528
- const calls = writtenCalls(s, declared);
529
- const out = [];
530
- if (calls.length) for (const m of s.matchAll(OBSERVED)) { const call = calls.filter((c) => c.at < m.index).pop(); if (call) out.push({ name: call.name, text: (m[1] ?? m[3]).trim() }); }
531
- return out;
532
- };
533
- // The model's own words: the text it answered, whole or streamed, in every provider's shape.
534
- const replyText = (events) => {
535
- let s = "";
536
- for (const e of events) {
537
- if (!e || typeof e !== "object") continue;
538
- for (const c of list(e.choices)) { if (c.message) s += textOf(c.message.content); if (c.delta && typeof c.delta.content === "string") s += c.delta.content; if (typeof c.text === "string") s += c.text; }
539
- if (e.type === "response.output_text.delta" && typeof e.delta === "string") s += e.delta;
540
- for (const it of [...list(e.output), ...list(e.response && e.response.output)]) if (it.type === "message") for (const p of list(it.content)) if (typeof p.text === "string") s += p.text;
541
- if (e.type === "content_block_delta" && e.delta && typeof e.delta.text === "string") s += e.delta.text;
542
- for (const b of [...list(e.content), ...list(e.message && e.message.content), ...list(e.output && e.output.message && e.output.message.content)]) if (typeof b.text === "string") s += b.text;
543
- if (e.message && typeof e.message.content === "string" && !e.choices) s += e.message.content;
544
- for (const c of list(e.candidates)) for (const p of list(c.content && c.content.parts)) if (typeof p.text === "string" && !p.thought) s += p.text;
545
- if (typeof e.response === "string") s += e.response;
381
+ for (const name of ["write", "end"]) {
382
+ const orig = res[name];
383
+ res[name] = function (chunk, encoding) { try { keep(chunk, encoding); if (name === "end") tied.replied(ctx, res.statusCode, () => decoded(Buffer.concat(got), sentHeader("content-encoding"))); } catch { /* the reply goes on */ } return orig.apply(this, arguments); };
546
384
  }
547
- return s;
548
- };
549
- // This turn as the prompt carries it: the model's earlier words after the person's latest
550
- // message, and everything from that message on, where a single-prompt agent keeps its scratchpad.
551
- // A user message right after the model wrote a tool call is the agent loop's nudge ("Analyze the
552
- // tool result"), never the person: after a call, only the loop speaks.
553
- const turnTexts = (body, withPerson, declared) => {
554
- const from = (items, isPerson) => { const at = items.findLastIndex(isPerson); return items.slice(withPerson ? Math.max(0, at) : at + 1); };
555
- const ours = (r) => withPerson ? r !== "system" && r !== "developer" : r === "assistant" || r === "model";
556
- const ms = list(body.messages);
557
- const person = (m) => personSaid(m) && !(ms[ms.indexOf(m) - 1] && ms[ms.indexOf(m) - 1].role === "assistant" && writtenCalls(textOf(ms[ms.indexOf(m) - 1].content), declared).length);
558
- return [
559
- ...from(ms, person).filter((m) => ours(m.role)).map((m) => textOf(m.content)),
560
- ...from(list(body.input), (x) => x.role === "user").filter((x) => ours(x.role)).map((x) => textOf(x.content ?? x.output)),
561
- ...from(list(body.contents), personAsked).filter((c) => ours(c.role)).map((c) => textOf(c.parts)),
562
- ...(withPerson ? [typeof body.prompt === "string" ? body.prompt : "", typeof body.input === "string" ? body.input : ""] : []),
563
- ].join("\u0000\n");
564
- };
565
-
566
- const calledIn = (sent, events) => {
567
- const body = parse(sent);
568
- const out = [];
569
- const declared = body && typeof body === "object" ? declaredIn(body) : new Set();
570
- const written = [...writtenCalls(body && typeof body === "object" ? turnTexts(body, false, declared) : "", declared), ...writtenCalls(replyText(events), declared)].map((c) => ({ name: c.name, arguments: argsText(c.args) }));
571
- for (const c of [...(body && typeof body === "object" ? calledBefore(body) : []), ...callsOf(events), ...written]) {
572
- if (out.length < CALLS_MAX && !out.some((o) => o.name === c.name && o.arguments === c.arguments)) out.push(c);
573
- }
574
- return out.length ? out : undefined;
575
- };
576
-
577
- // The passages the prompt carried as retrieved context: a block the app itself labels as
578
- // context, documents, knowledge, sources or search results, in its system prompt or from the
579
- // person's latest message on, and Anthropic document and search_result blocks. The app's own
580
- // instructions are not passages: only what sits under such a label is.
581
- const PASSAGE_TEXT = 3000, PASSAGES_MAX = 6;
582
- const MATERIAL = /\b(?:retriev\w*|context|knowledge|documents?|sources?|search[ _-]?results?|references?|passages?|excerpts?|snippets?|chunks?|background|faq|relevant)\b|检索|知识|参考资料|资料|上下文|文档|背景|相关/i;
583
- const HEADING = /^\s*(?:#{1,6}\s+[^\n]{1,80}|[^\n]{1,80}[::]\s*(?:\([^\n)]*\))?|\[[^\n\]]{1,80}\]|={2,}\s*[^\n]{1,80}?\s*={2,})\s*$/;
584
- // An inline label is a noun phrase that ends in the material word ("context:", "background:",
585
- // "retrieved passages:"): "4. Knowledge:" in a numbered instruction and a JSON key are not.
586
- const INLINE = /^\s*(?:[-*]\s+)?([A-Za-z\u4e00-\u9fff][A-Za-z\u4e00-\u9fff '’_-]{0,29})[::]\s*\S/;
587
- const NAMED = new RegExp(`(?:${MATERIAL.source})\\s*$`, "i");
588
- const TAGGED = /<([A-Za-z][\w-]*)[^>]*>([\s\S]*?)<\/\1>/g;
589
- // A label's block runs on to the next heading of its own kind: "资料:" over retrieved chunks
590
- // that open on markdown headings holds every chunk, where stopping at its first paragraph kept
591
- // one chunk of five and the reader called the knowledge base's own 1% fee made up. A markdown
592
- // label holds deeper headings. A closing paragraph that opens on a label of its own ("问题:...",
593
- // "Question: ...") is the prompt's ask, not material. A long block is several passages, cut
594
- // between its paragraphs.
595
- const MARKDOWN = /^\s*(#{1,6})\s/;
596
- const endsBlock = (label, line) => {
597
- if (!HEADING.test(line)) return false;
598
- const outer = MARKDOWN.exec(label), inner = MARKDOWN.exec(line);
599
- return !inner || Boolean(outer && inner[1].length <= outer[1].length);
600
- };
601
- const asks = (para) => { const m = INLINE.exec(para.split("\n")[0]); return Boolean(m && !NAMED.test(m[1])); };
602
- // A heading inside a fenced block is the material's own text: a README chunk's "## Features".
603
- const fences = (para) => (para.match(/^\s*(?:```|~~~)/gm) || []).length;
604
- const addBlock = (add, name, paras) => {
605
- let text = "";
606
- for (const p of paras) {
607
- if (text && text.length + p.length + 2 > PASSAGE_TEXT) { add(name, text); text = ""; }
608
- text += (text ? "\n\n" : "") + p;
609
- }
610
- add(name, text);
611
- };
612
- const labelled = (text, add) => {
613
- const rest = String(text || "").replace(TAGGED, (all, tag, inner) => { if (MATERIAL.test(tag.replace(/_/g, " "))) { add(tag, inner); return ""; } return all; });
614
- const paras = rest.split(/\n\s*\n/);
615
- for (let i = 0; i < paras.length; i++) {
616
- const lines = paras[i].split("\n");
617
- const head = lines[0];
618
- if (HEADING.test(head) && MATERIAL.test(head)) {
619
- const block = [lines.slice(1).join("\n")];
620
- let fenced = fences(block[0]) % 2 === 1;
621
- while (i + 1 < paras.length && (fenced || (!endsBlock(head, paras[i + 1].split("\n")[0]) && !(i + 2 === paras.length && asks(paras[i + 1]))))) {
622
- block.push(paras[++i]);
623
- if (fences(paras[i]) % 2 === 1) fenced = !fenced;
624
- }
625
- addBlock(add, head.replace(/[#::[\]=]/g, "").trim(), block);
626
- continue;
627
- }
628
- const inline = lines.map((l) => INLINE.exec(l)).find((m) => m && NAMED.test(m[1]));
629
- if (inline) add(inline[1].trim(), paras[i]);
630
- }
631
- };
632
- const passagesIn = (sent) => {
633
- const body = parse(sent);
634
- if (!body || typeof body !== "object") return undefined;
635
- const out = [];
636
- const add = (name, text) => { const t = String(text || "").trim().slice(0, PASSAGE_TEXT); if (t.length >= 20 && out.length < PASSAGES_MAX && !out.some((o) => o.text === t)) out.push({ name: String(name || "").slice(0, 80), text: t }); };
637
- const scan = (content) => {
638
- if (typeof content === "string") return labelled(content, add);
639
- for (const b of list(content)) {
640
- if (b.type === "document") add(b.title || "document", b.source ? textOf(b.source.data ?? b.source.content) : textOf(b.content));
641
- else if (b.type === "search_result") add(b.title || b.source || "search result", textOf(b.content));
642
- else if (typeof b.text === "string") labelled(b.text, add);
643
- }
385
+ // writeHead(status, headers) sends its headers without keeping them where getHeader looks: the
386
+ // header block as sent says them either way.
387
+ const sentHeader = (name) => {
388
+ const kept = res.getHeader(name);
389
+ if (kept !== undefined) return kept;
390
+ const block = typeof res._header === "string" ? res._header : "";
391
+ const found = block.split("\r\n").filter((l) => l.toLowerCase().startsWith(`${name}:`)).map((l) => l.slice(name.length + 1).trim());
392
+ return name === "set-cookie" ? found : found[0];
644
393
  };
645
- const messages = list(body.messages);
646
- for (const m of messages) if (m.role === "system" || m.role === "developer") scan(m.content);
647
- for (const m of messages.slice(Math.max(0, messages.findLastIndex(personSaid)))) if (m.role === "user") scan(m.content);
648
- scan(body.system);
649
- scan(body.instructions);
650
- scan(body.systemInstruction && body.systemInstruction.parts);
651
- const input = typeof body.input === "string" ? [{ role: "user", content: body.input }] : list(body.input);
652
- for (const it of input.slice(Math.max(0, input.findLastIndex((x) => x.role === "user")))) if (it.role === "user" || it.role === "system" || it.role === "developer") scan(it.content);
653
- return out.length ? out : undefined;
654
- };
655
- // The passages a retrieval call answered with: a vector store, a search index or a web search,
656
- // read off its reply when it is JSON. Text fields only, bounded like the rest.
657
- const RETRIEVAL_HOST = /pinecone\.io|qdrant|weaviate|chroma|zilliz|milvus|turbopuffer|upstash\.io|algolia|typesense|meilisearch|elastic|opensearch|vespa|tavily\.com|exa\.ai|serper\.dev|serpapi\.com|search\.brave\.com|bing\.microsoft\.com|jina\.ai/i;
658
- const RETRIEVAL_PATH = /\/(?:query|search|_search|retrieve|similarity_search|rerank|hybrid)(?:\/|$)|\/points\/(?:search|query)|\/rpc\/match\w*/i;
659
- const isRetrieval = (u) => RETRIEVAL_HOST.test(u.hostname) || RETRIEVAL_PATH.test(u.pathname);
660
- const PASSAGE_KEY = /^(?:text|content|page_?content|pageContent|chunk|snippet|passage|body|document|documents|description|answer|raw_content|highlights?|excerpt)$/i;
661
- const passagesFrom = (raw) => {
662
- const out = [];
663
- let nodes = 0;
664
- const walk = (v, key, depth) => {
665
- if (out.length >= PASSAGES_MAX || depth > 7 || ++nodes > 4000) return;
666
- const t = typeof v === "string" ? v.trim().slice(0, PASSAGE_TEXT) : "";
667
- if (t) { if (PASSAGE_KEY.test(key) && t.length >= 20 && !out.some((o) => o.text === t)) out.push({ name: key, text: t }); return; }
668
- if (Array.isArray(v)) { for (const x of v) walk(x, key, depth + 1); return; }
669
- if (v && typeof v === "object") for (const [k, x] of Object.entries(v)) walk(x, k, depth + 1);
394
+ // `cut`: the app began its reply and the connection closed before it finished, so the row holds
395
+ // part of it. Either side may have closed it (a client that stopped reading, an app that broke
396
+ // off mid-stream), so nothing downstream says which.
397
+ const done = () => {
398
+ try { ended(ctx, { status: res.statusCode, type: sentHeader("content-type"), writes, text: decoded(Buffer.concat(got), sentHeader("content-encoding")), cookies: cookieNames(sentHeader("set-cookie")), cut: res.headersSent && !res.writableFinished }); } catch { /* the command is gone */ }
670
399
  };
671
- walk(parse(raw), "", 0);
672
- return out.length ? out : undefined;
673
- };
674
- const rulesIn = (sent) => {
675
- const list = rulesNow();
676
- if (!list) return undefined;
677
- const body = norm(sent);
678
- return list.filter((r) => r.parts.every((p) => body.includes(p))).map((r) => r.id);
679
- };
680
- // Where models are served, by host, and the paths every OpenAI-shaped or vendor endpoint ends with.
681
- const MODEL_HOST = /(?:^|\.)(?:openai\.com|anthropic\.com|fireworks\.ai|openrouter\.ai|groq\.com|mistral\.ai|together\.xyz|together\.ai|deepseek\.com|cohere\.ai|cohere\.com|perplexity\.ai|x\.ai|googleapis\.com|openai\.azure\.com|cognitiveservices\.azure\.com|amazonaws\.com|replicate\.com|huggingface\.co|cerebras\.ai|deepinfra\.com|novita\.ai|moonshot\.cn|dashscope\.aliyuncs\.com|bigmodel\.cn|ai-gateway\.vercel\.sh|gateway\.ai\.cloudflare\.com|helicone\.ai|portkey\.ai)$/i;
682
- const MODEL_PATH = /\/(?:chat\/completions|completions|responses|messages|embeddings)$|:(?:generateContent|streamGenerateContent)|\/invoke(?:-with-response-stream)?$|\/api\/(?:chat|generate)$/i;
683
- // An embedding call answers nobody, so where it was made names no path a reply came down.
684
- const EMBEDDING = /\/embeddings$/i;
685
- const isModelCall = (host, path) => {
686
- const h = String(host || "").replace(/:\d+$/, "");
687
- const p = String(path || "").split("?")[0];
688
- if (/googleapis\.com$/i.test(h)) return /generativelanguage|aiplatform/i.test(h) && MODEL_PATH.test(p);
689
- if (/amazonaws\.com$/i.test(h)) return /^bedrock/i.test(h);
690
- return MODEL_HOST.test(h) ? true : MODEL_PATH.test(p) && /\/v\d|\/api\//.test(p);
691
- };
692
- const said = new Set();
693
- const note = (ctx, sent) => {
694
- if (!ctx || ctx.noted) return;
695
- ctx.noted = true;
696
- const key = `${ctx.method} ${ctx.path.split("?")[0]}`;
697
- if (said.has(key)) return;
698
- said.add(key);
699
- const write = (chunks) => row({ at: Date.now(), method: ctx.method, path: ctx.path, headers: ctx.headers, body: Buffer.concat(chunks).toString("utf8").slice(0, MAX), sent: String(sent || "").slice(0, MAX) });
700
- if (ctx.body) ctx.body.then((b) => write([b]));
701
- else write(ctx.chunks);
400
+ res.once("finish", done);
401
+ res.once("close", done);
402
+ };
403
+ // Bun hands back a Response; its body is read from a clone, so the client's copy is untouched.
404
+ const watchResponse = (ctx, res) => {
405
+ // Nothing to read: the request is closed and never written.
406
+ const gone = () => { ctx.ended = Date.now(); ctx.reply = { status: 0, type: "", text: "", cookies: [] }; tied.closed(ctx); };
407
+ if (!(res instanceof Response) || Number(res.headers.get("content-length") || 0) > REPLY_MAX) return gone();
408
+ let copy;
409
+ try { copy = res.clone(); } catch { return gone(); }
410
+ copy.arrayBuffer().then((b) => Buffer.from(b), () => Buffer.alloc(0)).then((buf) => {
411
+ const set = typeof res.headers.getSetCookie === "function" ? res.headers.getSetCookie() : res.headers.get("set-cookie");
412
+ tied.replied(ctx, res.status, () => decoded(buf.subarray(0, MAX), res.headers.get("content-encoding")));
413
+ ended(ctx, { status: res.status, type: res.headers.get("content-type"), text: decoded(buf.subarray(0, MAX), res.headers.get("content-encoding")), cookies: cookieNames(set) });
414
+ }).catch(() => { /* the command is gone */ });
702
415
  };
703
416
 
704
417
  // Inbound: each request is handled inside its own context, and its body is seen as it arrives
705
- // without reading it, so your own body parser is untouched.
418
+ // without reading it, so your own body parser is untouched. A GET is watched but never made the
419
+ // context a model call is pinned by: it can only be the step an answer comes on.
706
420
  const emit = http.Server.prototype.emit;
707
421
  http.Server.prototype.emit = function (type, req, ...rest) {
422
+ if (this[HELD]) return emit.call(this, type, req, ...rest);
708
423
  if (type === "listening") { try { listening(this.address()?.port); } catch { /* not a TCP server */ } }
709
424
  if (type === "request") { recheckRoutes(); sweepTools(); }
710
- if (type !== "request" || !req || !req.method || /^(?:GET|HEAD|OPTIONS)$/.test(req.method)) return emit.call(this, type, req, ...rest);
425
+ if (type !== "request" || !req || !req.method || /^(?:HEAD|OPTIONS)$/.test(req.method)) return emit.call(this, type, req, ...rest);
711
426
  const { "x-cortad-turn": _turn, ...headers } = req.headers;
712
- const ctx = { method: req.method, path: req.url || "/", headers, chunks: [], size: 0, noted: false, turn: turnOf(req.headers) };
427
+ const ctx = requestCtx(req.method, req.url || "/", headers, turnOf(req.headers));
428
+ try { watchReply(ctx, rest[0]); } catch { /* the reply goes on unwatched */ }
429
+ if (req.method === "GET") return emit.call(this, type, req, ...rest);
713
430
  const push = req.push;
714
431
  req.push = function (chunk, encoding) {
715
432
  if (chunk && ctx.size < MAX) { const b = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding); ctx.chunks.push(b); ctx.size += b.length; }
@@ -743,16 +460,26 @@ if (FILE) {
743
460
  const inside = (handler, self) => function (req, server) {
744
461
  recheckRoutes();
745
462
  sweepTools();
746
- if (!req || /^(?:GET|HEAD|OPTIONS)$/.test(req.method)) return handler.call(self, req, server);
463
+ if (!req || /^(?:HEAD|OPTIONS)$/.test(req.method)) return handler.call(self, req, server);
747
464
  let path = "/";
748
465
  try { const u = new URL(req.url); path = u.pathname + u.search; } catch { /* keep "/" */ }
749
- const ctx = { method: req.method, path, headers: Object.fromEntries([...req.headers].filter(([k]) => k !== "x-cortad-turn")), chunks: [], size: 0, noted: false, turn: turnOf(req.headers) };
466
+ const ctx = requestCtx(req.method, path, Object.fromEntries([...req.headers].filter(([k]) => k !== "x-cortad-turn")), turnOf(req.headers));
467
+ if (req.method === "GET") {
468
+ const out = handler.call(self, req, server);
469
+ Promise.resolve(out).then((res) => watchResponse(ctx, res), () => watchResponse(ctx, null));
470
+ return out;
471
+ }
750
472
  // ponytail: a body is copied only when it is small or says it is text; an upload is left alone.
751
473
  const size = Number(req.headers.get("content-length") || 0);
752
474
  if (size <= MAX || /json|text|form/i.test(req.headers.get("content-type") || "")) {
753
- try { ctx.body = req.clone().arrayBuffer().then((b) => Buffer.from(b).subarray(0, MAX), () => Buffer.alloc(0)); } catch { /* unread */ }
475
+ // Read as it arrives, so a model call made while the request is open knows the person's words.
476
+ try { ctx.body = req.clone().arrayBuffer().then((b) => { ctx.bodyText = Buffer.from(b).subarray(0, MAX).toString("utf8"); }, () => { ctx.bodyText = ""; }); } catch { /* unread */ }
754
477
  }
755
- return als.run(ctx, () => handler.call(self, req, server));
478
+ return als.run(ctx, () => {
479
+ const out = handler.call(self, req, server);
480
+ Promise.resolve(out).then((res) => watchResponse(ctx, res), () => {});
481
+ return out;
482
+ });
756
483
  };
757
484
  const routes = (table) => {
758
485
  if (!table || typeof table !== "object") return table;
@@ -791,61 +518,6 @@ if (FILE) {
791
518
  };
792
519
  }
793
520
 
794
- // The meter. One row per model call: host, model id, status, and the provider's own token counts.
795
- const zlib = require("node:zlib");
796
- const REPLY_MAX = 8 * 1024 * 1024;
797
- const decoded = (buf, encoding) => {
798
- try {
799
- const e = String(encoding || "").toLowerCase();
800
- return (e === "gzip" ? zlib.gunzipSync(buf) : e === "br" ? zlib.brotliDecompressSync(buf) : e === "deflate" ? zlib.inflateSync(buf) : buf).toString("utf8");
801
- } catch { return ""; }
802
- };
803
- const parse = (text) => { try { return JSON.parse(text); } catch { return null; } };
804
- const n = (v) => (Number.isInteger(v) && v > 0 ? v : 0);
805
- // The shapes the providers answer in. promptTokens is the whole input, cache included, so totals add up.
806
- const tokensOf = (u) => {
807
- if (!u || typeof u !== "object") return null;
808
- if (u.tokens && typeof u.tokens === "object") return tokensOf(u.tokens);
809
- if ("prompt_tokens" in u) return { promptTokens: n(u.prompt_tokens), cachedTokens: n(u.prompt_tokens_details && u.prompt_tokens_details.cached_tokens), completionTokens: n(u.completion_tokens) };
810
- if ("input_tokens" in u && u.input_tokens_details) return { promptTokens: n(u.input_tokens), cachedTokens: n(u.input_tokens_details.cached_tokens), completionTokens: n(u.output_tokens) };
811
- if ("input_tokens" in u) { const read = n(u.cache_read_input_tokens); return { promptTokens: n(u.input_tokens) + read + n(u.cache_creation_input_tokens), cachedTokens: read, completionTokens: n(u.output_tokens) }; }
812
- if ("inputTokens" in u) { const read = n(u.cacheReadInputTokens); return { promptTokens: n(u.inputTokens) + read + n(u.cacheWriteInputTokens), cachedTokens: read, completionTokens: n(u.outputTokens) }; }
813
- if ("promptTokenCount" in u) return { promptTokens: n(u.promptTokenCount), cachedTokens: n(u.cachedContentTokenCount), completionTokens: n(u.candidatesTokenCount) };
814
- return null;
815
- };
816
- // A stream spreads its counts over events (Anthropic's input in the first, output in the last);
817
- // later values win, so the fold ends on the final figures.
818
- const readReply = (type, text) => {
819
- const events = /event-stream/i.test(type || "")
820
- ? text.split("\n").filter((l) => l.startsWith("data:")).map((l) => parse(l.slice(5).trim()))
821
- : [].concat(parse(text));
822
- const usage = {};
823
- let model = null;
824
- for (const e of events) {
825
- if (!e || typeof e !== "object") continue;
826
- const part = e.usage || (e.message && e.message.usage) || (e.response && e.response.usage) || e.usageMetadata;
827
- if (part && typeof part === "object") Object.assign(usage, part);
828
- model = e.model || (e.message && e.message.model) || (e.response && e.response.model) || e.modelVersion || model;
829
- }
830
- return { tokens: tokensOf(Object.keys(usage).length ? usage : null), model, events };
831
- };
832
- // The model asked for, from what was sent; Gemini and Bedrock put it in the path instead.
833
- const askedFor = (path, sent) => {
834
- const body = parse(sent);
835
- if (body && typeof body.model === "string") return body.model;
836
- const m = /\/models\/([^/:]+):|\/model\/([^/]+)\/(?:invoke|converse)/.exec(path || "");
837
- return m ? decodeURIComponent(m[1] || m[2]) : "";
838
- };
839
- const meter = ({ host, path, sent, status, type, body, turn, caller }) => {
840
- try {
841
- const reply = readReply(type, String(body || "").slice(0, REPLY_MAX));
842
- const rules = rulesIn(sent);
843
- const tools = toolsIn(sent);
844
- const called = calledIn(sent, reply.events);
845
- const passages = passagesIn(sent);
846
- row({ call: { at: Date.now(), host: String(host).replace(/:443$/, ""), model: String(askedFor(path, sent) || reply.model || "").slice(0, 160), status, ...(reply.tokens || { promptTokens: 0, cachedTokens: 0, completionTokens: 0 }), usage: Boolean(reply.tokens), ...(turn ? { turn } : {}), ...(rules ? { rules } : {}), ...(tools ? { tools } : {}), ...(called ? { called } : {}), ...(passages ? { passages } : {}), ...(caller ? { caller } : {}) } });
847
- } catch { /* the command is gone */ }
848
- };
849
521
 
850
522
  // Outbound to a service their own settings name (a vector store, a database API, a search
851
523
  // host): which setting, which host and whether it answered. No body, no headers, no query. A
@@ -868,8 +540,10 @@ if (FILE) {
868
540
  return name ? { env: name, host: u.hostname } : null;
869
541
  } catch { return null; }
870
542
  };
871
- const depRow = (dep, status, code, passages, turn = (als.getStore() || {}).turn) => {
872
- row({ dep: { at: Date.now(), ...(dep.env ? { env: dep.env } : {}), host: dep.host, status, code: code ? String(code).slice(0, 40) : undefined, ...(turn ? { turn } : {}), ...(passages ? { passages } : {}) } });
543
+ // A retrieval says so, with the app's line that asked, so one that came back empty has an address.
544
+ const depRow = (dep, status, { code, passages, ctx, retrieval, caller } = {}) => {
545
+ const turn = ctx && ctx.turn;
546
+ row({ dep: { at: Date.now(), ...(dep.env ? { env: dep.env } : {}), host: dep.host, status, code: code ? String(code).slice(0, 40) : undefined, ...(ctx ? { ex: ctx.id } : {}), ...(turn ? { turn } : {}), ...(retrieval ? { retrieval: true } : {}), ...(caller ? { caller } : {}), ...(passages ? { passages } : {}) } });
873
547
  };
874
548
 
875
549
  // The app's own functions the read names as its tools. An app whose code picks the tool itself
@@ -931,12 +605,13 @@ if (FILE) {
931
605
  if (!m || /[{[(]/.test(m[1])) return [];
932
606
  return m[1].split(",").map((p) => p.replace(/=[\s\S]*$/, "").replace(/^\s*\.\.\./, "").replace(/:[\s\S]*$/, "").trim());
933
607
  };
934
- const toolRow = (name, names, args, value, error, turn) => {
608
+ const toolRow = (name, names, args, value, error, ctx) => {
609
+ const turn = ctx && ctx.turn;
935
610
  const named = {};
936
611
  args.forEach((a, i) => { named[IDENT.test(names[i] || "") ? names[i] : String(i)] = plainOf(a); });
937
612
  let text;
938
613
  try { text = error ? `raised ${(error && error.name) || "an error"}: ${(error && error.message) || String(error)}` : returned(value); } catch { text = "returned a value this hook cannot read"; }
939
- row({ dep: { at: Date.now(), host: "in-app", status: 200, ...(turn ? { turn } : {}), called: [{ name, arguments: argsText(named) }], tools: [{ name, text: String(text).slice(0, TOOL_TEXT) }] } });
614
+ row({ dep: { at: Date.now(), host: "in-app", status: 200, ...(ctx ? { ex: ctx.id } : {}), ...(turn ? { turn } : {}), called: [{ name, arguments: argsText(named) }], tools: [toolText(name, String(text))] } });
940
615
  };
941
616
  const TOOL = Symbol.for("cortad.wrapped");
942
617
  const wrappers = new WeakMap();
@@ -946,14 +621,14 @@ if (FILE) {
946
621
  try { if (/^class\b/.test(Function.prototype.toString.call(fn))) return fn; } catch { return fn; }
947
622
  const names = paramNames(fn);
948
623
  const w = function (...args) {
949
- const turn = (als.getStore() || {}).turn;
624
+ const ctx = als.getStore();
950
625
  let out;
951
626
  try { out = new.target ? Reflect.construct(fn, args, new.target) : fn.apply(this, args); }
952
- catch (e) { toolRow(name, names, args, undefined, e, turn); throw e; }
627
+ catch (e) { toolRow(name, names, args, undefined, e, ctx); throw e; }
953
628
  // A promise is followed, and the one handed back is a new one, so a rejection nobody
954
629
  // handles still reaches the app as unhandled.
955
- if (out instanceof Promise) return out.then((v) => { toolRow(name, names, args, v, undefined, turn); return v; }, (e) => { toolRow(name, names, args, undefined, e, turn); throw e; });
956
- toolRow(name, names, args, out, undefined, turn);
630
+ if (out instanceof Promise) return out.then((v) => { toolRow(name, names, args, v, undefined, ctx); return v; }, (e) => { toolRow(name, names, args, undefined, e, ctx); throw e; });
631
+ toolRow(name, names, args, out, undefined, ctx);
957
632
  return out;
958
633
  };
959
634
  try {
@@ -1026,16 +701,208 @@ if (FILE) {
1026
701
  }
1027
702
  };
1028
703
 
704
+ // A fetched body read to its end, or to where it broke off. Aborting the call (the client that
705
+ // asked went away, or the framework ended the request once its own reply was sent) errors this
706
+ // copy too, and what the model had said by then is still what it said.
707
+ const readAll = async (res) => {
708
+ const parts = [];
709
+ let size = 0;
710
+ try {
711
+ const reader = res.body && res.body.getReader();
712
+ for (let r = reader && await reader.read(); r && !r.done; r = await reader.read()) {
713
+ if (size < REPLY_MAX) { parts.push(Buffer.from(r.value)); size += r.value.length; }
714
+ }
715
+ } catch { /* broken off: what arrived stands */ }
716
+ return Buffer.concat(parts).toString("utf8");
717
+ };
718
+
719
+ // Every server the app opens a connection to, once each, by host and port only: a database named
720
+ // in a config file rather than in the environment is seen the first time the app reaches it.
721
+ // HTTP clients connect through here too; the command names only the ports stores listen on.
722
+ const net = require("node:net");
723
+ const connected = new Set();
724
+ const socketConnect = net.Socket.prototype.connect;
725
+ net.Socket.prototype.connect = function (...args) {
726
+ try {
727
+ const first = Array.isArray(args[0]) ? args[0][0] : args[0];
728
+ const o = first && typeof first === "object" ? first : { port: first, host: typeof args[1] === "string" ? args[1] : undefined };
729
+ const port = Number(o.port);
730
+ if (!o.path && Number.isInteger(port) && port > 0 && port !== heldPort && connected.size < 64) {
731
+ const host = String(o.host || "localhost").toLowerCase().slice(0, 253);
732
+ if (!connected.has(`${host}:${port}`)) { connected.add(`${host}:${port}`); row({ conn: { host, port } }); }
733
+ }
734
+ } catch { /* the connection goes on unrecorded */ }
735
+ return socketConnect.apply(this, args);
736
+ };
737
+
738
+ // Outbound writes a trial makes are held in the app, never sent: a trial that books, charges,
739
+ // emails or deletes would otherwise do it for real. Held: a call made inside a request the run
740
+ // tagged, with a writing method, to a host that is not a model, a retrieval, this machine, a copy
741
+ // of ours or a host the person said yes to. A call carrying a provider's test key goes to that
742
+ // provider's own sandbox as sent. The app is answered with a success shaped like what it sent,
743
+ // and the run is told by a dep row with `held` and the clipped call.
744
+ const OUTBOUND = process.env.CORTAD_OUTBOUND_FILE;
745
+ let passed = [], passedAt = -1;
746
+ const passNow = () => {
747
+ try {
748
+ const at = fs.statSync(OUTBOUND).mtimeMs;
749
+ if (at !== passedAt) { passedAt = at; const got = JSON.parse(fs.readFileSync(OUTBOUND, "utf8")); passed = Array.isArray(got.pass) ? got.pass.filter((h) => typeof h === "string" && h) : []; }
750
+ } catch { /* not written yet: nothing passes */ }
751
+ return passed;
752
+ };
753
+ const WRITES = /^(?:POST|PUT|PATCH|DELETE)$/;
754
+ const THIS_MACHINE = /^(?:localhost|127(?:\.\d+){3}|\[?::1\]?|0\.0\.0\.0)$/i;
755
+ // A provider's own sandbox, named so in the host: api.sandbox.paypal.com, sandbox-api.polar.sh.
756
+ const SANDBOX_HOST = /(?:^|[.-])(?:sandbox|sbox)[.-]/i;
757
+ // ponytail: a sign-in's token refresh is a POST that changes nothing, told apart by its path only.
758
+ const TOKEN_PATH = /\/(?:oauth2?\/)?token(?:[/?]|$)/i;
759
+ // An API where every call is a POST names what a call does in its last segment, as
760
+ // resource.verb_words (customers.get, billing.preview_attach) or as a gRPC or Connect method
761
+ // (acme.v1.UserService/GetUser). A POST so named that starts with a reading word, and holds no word
762
+ // that makes the read a write (get_or_create, check_in, GetLease), goes out.
763
+ // A preview commits nothing whatever it previews; any other reading word reads only while no word
764
+ // after it writes: credits.check_deduct and usage.count_increment went out as reads.
765
+ // ponytail: decided on the name alone, so a POST named as a plain read whose body writes goes out.
766
+ // A rule on the body is the upgrade if one is ever seen.
767
+ const WRITE_WORD = "create|update|delete|remove|set|add|put|insert|upsert|write|save|store|record|track|log|send|deduct|increment|decrement|consume|use|spend|charge|pay|refund|cancel|reserve|lock|unlock|commit|apply|attach|detach|start|stop|run|trigger|reset|rotate|revoke|grant|issue|mint|transfer|move|merge|sync|import|export|upload|publish|enqueue|push|pop|ack|touch|mark|close|approve|reject|submit|confirm|redeem|claim";
768
+ const DOT_CALL = /^[A-Za-z][\w-]*\.[a-z][a-z_]*$/, DOT_READ = /^(?:get|list|check|search|query|find|lookup|retrieve|describe|count|preview)$/, DOT_NOT = new RegExp(`^(?:or|and|in|out|next|${WRITE_WORD})$`);
769
+ const RPC_CALL = /^(?:[A-Z][a-z0-9]*)+$/, RPC_READ = /^(?:Get|List|Search|Query|Find|Lookup|Describe)$/, RPC_NOT = new RegExp(`^(?:or|and|next|lease|${WRITE_WORD})$`, "i");
770
+ const readWords = (words, read, not) => read.test(words[0]) && (/^preview$/i.test(words[0]) || !words.slice(1).some((w) => not.test(w)));
771
+ const readsByName = (method, pathname) => {
772
+ if (method !== "POST") return false;
773
+ const parts = pathname.split("/").filter(Boolean);
774
+ const last = parts[parts.length - 1] || "", before = parts[parts.length - 2] || "";
775
+ if (DOT_CALL.test(last)) return readWords(last.split(".")[1].split("_"), DOT_READ, DOT_NOT);
776
+ return /\.[A-Z]\w*$/.test(before) && RPC_CALL.test(last) && readWords(last.split(/(?=[A-Z])/), RPC_READ, RPC_NOT);
777
+ };
778
+ // A provider's test-mode key says so in the prefix of the key itself, set off by underscores
779
+ // (Stripe's sk_test_, am_sk_test_, pdl_sdbx_), or it is a value of a setting the app keeps as a
780
+ // test one. The word elsewhere is not it: a Basic user named test-admin and a live key holding
781
+ // "-Test-" both went out as test keys.
782
+ const TEST_KEY = /^(?:[a-z0-9]{2,8}_){0,3}(?:test|sandbox|sbox|sdbx)_[\w-]{4,}$/i;
783
+ const testKey = (auth) => Boolean(auth) && (auth.split(/[\s:]+/).some((piece) => TEST_KEY.test(piece))
784
+ || Object.entries(process.env).some(([k, v]) => /(?:^|_)TEST(?:_|$)/i.test(k) && v && v.length >= 8 && auth.includes(v)));
785
+ const authText = (value) => { const a = String(value || ""); const m = /^Basic\s+(\S+)$/i.exec(a); return m ? Buffer.from(m[1], "base64").toString("utf8") : a; };
786
+ // The headers a key travels in.
787
+ const KEY_HEADERS = ["authorization", "x-api-key", "api-key", "apikey", "x-auth-token"];
788
+ // Neon's HTTP driver sends every query, reads too, to its region's API host and names the
789
+ // database in a header: the call is decided on that database's host. No header: the API host.
790
+ const NEON_SQL = /^api(?:auth)?\.[a-z0-9.-]+\.neon\.tech$/;
791
+ const targetOf = (u, headerOf) => {
792
+ const host = u.hostname.toLowerCase();
793
+ if (!NEON_SQL.test(host) || u.pathname !== "/sql") return host;
794
+ try { return new URL(headerOf("neon-connection-string")).hostname.toLowerCase() || host; } catch { return host; }
795
+ };
796
+ // `headerOf(name)`: one of the call's headers, read only for a call that is otherwise held. Once a
797
+ // tagged write is being decided, a failure to decide holds it.
798
+ // ponytail: while a run is live, a person's own queued write is held too; per-ask pinning of
799
+ // background writes is the upgrade when that matters.
800
+ const RUN_LIVE_MS = 10 * 60_000;
801
+ const heldHere = (u, method, headerOf) => {
802
+ const ctx = als.getStore();
803
+ if (!WRITES.test(method) || (ctx ? !ctx.turn : Date.now() - lastTurnAt > RUN_LIVE_MS)) return false;
804
+ try {
805
+ const host = targetOf(u, headerOf);
806
+ if (THIS_MACHINE.test(host) || SANDBOX_HOST.test(host) || isModelCall(u.host, u.pathname) || isRetrieval(u) || TOKEN_PATH.test(u.pathname) || readsByName(method, u.pathname) || KEY_HEADERS.some((name) => testKey(authText(headerOf(name))))) return false;
807
+ return !passNow().some((p) => host === p || host.endsWith("." + p));
808
+ } catch { return true; }
809
+ };
810
+ // A GraphQL query reads; a mutation writes. A `query` that is not GraphQL (Neon's SQL) is held.
811
+ const readsOnly = (text) => { try { const q = JSON.parse(text)?.query; return typeof q === "string" && /^\s*(?:\{|query\b|fragment\b)/.test(q) && !/\bmutation\b/.test(q); } catch { return false; } };
812
+ let heldCount = 0;
813
+ const fieldsOf = (text, type) => {
814
+ try { const b = /form/i.test(type || "") ? Object.fromEntries(new URLSearchParams(text)) : JSON.parse(text); return b && typeof b === "object" && !Array.isArray(b) ? b : {}; } catch { return {}; }
815
+ };
816
+ const heldReply = (text, type) => JSON.stringify({ ...fieldsOf(text, type), id: `cortad-held-${++heldCount}`, status: "ok" });
817
+ // Said once per method and host in the app's own output, where the error that follows is read:
818
+ // a stand-in an SDK cannot parse otherwise reads as a bug in the app. It holds no word that marks
819
+ // an error, so it is never quoted back as the app's own.
820
+ const heldSaid = new Set();
821
+ const heldSay = (method, host) => {
822
+ if (heldSaid.has(`${method} ${host}`)) return;
823
+ heldSaid.add(`${method} ${host}`);
824
+ try { process.stderr.write(`cortad: a request Cortad sent made this app send ${method} to ${host}. Cortad kept that call on this machine and answered it with a stand-in success, since it could change something real. A failure right after this line comes from that stand-in, not from your code. With that service's test key the call goes out as sent.\n`); } catch { /* no stderr */ }
825
+ };
826
+ // Named by method and host only: a webhook's path is its credential.
827
+ const heldRow = (u, method, text, type, ctx) => { heldSay(method, u.hostname); row({ dep: { at: Date.now(), host: u.hostname, status: 200, held: true, ...(ctx ? { ex: ctx.id, turn: ctx.turn } : {}), called: [{ name: `${method} ${u.hostname}`.slice(0, 80), arguments: argsText(fieldsOf(text, type)) }] } }); };
828
+ // http.request hands back a request before its body is written, so a held one is sent to a server
829
+ // of this hook's own on this machine, which answers it. Started at the first tagged request.
830
+ const HELD = Symbol("cortad.held");
831
+ const heldWaiting = new Map();
832
+ let heldPort = null, heldStarted = false, heldSent = 0;
833
+ function heldWarm() {
834
+ if (heldStarted) return;
835
+ heldStarted = true;
836
+ try {
837
+ const server = http.createServer((req, res) => {
838
+ const parts = [];
839
+ req.on("data", (c) => { if (parts.length < 256) parts.push(c); });
840
+ req.on("end", () => {
841
+ const id = String(req.headers["x-cortad-held"] || "");
842
+ const w = heldWaiting.get(id);
843
+ heldWaiting.delete(id);
844
+ const text = Buffer.concat(parts).toString("utf8").slice(0, MAX);
845
+ if (w) heldRow(w.u, w.method, text, req.headers["content-type"], w.ctx);
846
+ res.writeHead(200, { "content-type": "application/json" });
847
+ res.end(heldReply(text, req.headers["content-type"]));
848
+ });
849
+ });
850
+ server[HELD] = true;
851
+ server.listen(0, "127.0.0.1", () => { heldPort = server.address().port; });
852
+ server.unref();
853
+ } catch { /* no server: a held write fails to connect instead of leaving */ }
854
+ }
855
+ const headerIn = (headers, name) => {
856
+ if (!headers || typeof headers !== "object" || Array.isArray(headers)) return "";
857
+ const k = Object.keys(headers).find((n) => n.toLowerCase() === name);
858
+ return k ? String(headers[k]) : "";
859
+ };
860
+ // The call http.request was asked for, when it is one to hold.
861
+ const heldCall = (args, secure) => {
862
+ const first = args[0];
863
+ const given = typeof first === "string" || first instanceof URL ? new URL(String(first)) : null;
864
+ const o = (given ? (args[1] && typeof args[1] === "object" ? args[1] : {}) : first) || {};
865
+ const host = String(o.hostname || (o.host && String(o.host).replace(/:\d+$/, "")) || (given && given.hostname) || "localhost");
866
+ const path = String(o.path || (given ? given.pathname + given.search : "/"));
867
+ const method = String(o.method || "GET").toUpperCase();
868
+ const u = new URL(`${secure ? "https" : "http"}://${host.includes(":") && !host.startsWith("[") ? `[${host}]` : host}${path.startsWith("/") ? path : "/" + path}`);
869
+ const headerOf = (name) => headerIn(o.headers, name) || (name === "authorization" && o.auth ? `Basic ${Buffer.from(String(o.auth)).toString("base64")}` : "");
870
+ return heldHere(u, method, headerOf) ? { u, method, o, cb: args.find((a) => typeof a === "function") } : null;
871
+ };
872
+ const httpRequest = http.request;
873
+ const sendHeld = ({ u, method, o, cb }, secure) => {
874
+ const id = String(++heldSent);
875
+ heldWaiting.set(id, { u, method, ctx: als.getStore() });
876
+ const headers = { ...(o.headers && typeof o.headers === "object" && !Array.isArray(o.headers) ? o.headers : {}), "x-cortad-held": id };
877
+ // Port 1 answers nothing: a write held before the server listens fails instead of leaving.
878
+ const req = httpRequest({ host: "127.0.0.1", port: heldPort || 1, method, path: u.pathname + u.search, headers, ...(o.timeout ? { timeout: o.timeout } : {}), ...(o.signal ? { signal: o.signal } : {}) }, cb);
879
+ // A TLS client waits for the handshake before it writes (Stripe's does).
880
+ if (secure) req.once("socket", (socket) => socket.once("connect", () => socket.emit("secureConnect")));
881
+ return req;
882
+ };
883
+ const heldFetch = async (realFetch, input, init) => {
884
+ const req = new Request(input, init);
885
+ const text = await req.clone().text().catch(() => "");
886
+ if (readsOnly(text)) return realFetch(req);
887
+ const u = new URL(req.url);
888
+ heldRow(u, req.method, text, req.headers.get("content-type"), als.getStore());
889
+ return new Response(heldReply(text, req.headers.get("content-type")), { status: 200, headers: { "content-type": "application/json" } });
890
+ };
891
+
1029
892
  // Outbound: the model call your app makes while it handles that request.
1030
893
  const bodyText = (body) => (typeof body === "string" ? body : Buffer.isBuffer(body) ? body.toString("utf8") : body instanceof Uint8Array ? Buffer.from(body).toString("utf8") : "");
1031
894
  if (typeof globalThis.fetch === "function") {
1032
895
  const realFetch = globalThis.fetch;
1033
896
  globalThis.fetch = function (input, init) {
1034
897
  let url = null;
898
+ let hold = false;
1035
899
  try {
1036
900
  const u = new URL(typeof input === "string" ? input : input && input.url ? input.url : String(input));
1037
901
  if (isModelCall(u.host, u.pathname)) url = u;
902
+ const method = String((init && init.method) || (input && input.method) || "GET").toUpperCase();
903
+ hold = !url && heldHere(u, method, (name) => new Headers((init && init.headers) || (input && input.headers) || undefined).get(name));
1038
904
  } catch { /* not a URL we can read */ }
905
+ if (hold) return heldFetch(realFetch, input, init);
1039
906
  if (!url) {
1040
907
  // Every outbound host, named by the setting that points at it when one does, and by the
1041
908
  // host alone otherwise: the search provider behind TAVILY_API_KEY has no URL setting.
@@ -1048,26 +915,27 @@ if (FILE) {
1048
915
  if (!dep && /^https?:$/.test(u.protocol) && (retrieval || !/^(localhost|127\.0\.0\.1|\[::1\])$/.test(u.hostname))) dep = { host: u.hostname };
1049
916
  } catch { /* not a URL */ }
1050
917
  if (!dep) return realFetch.apply(this, arguments);
1051
- const turn = (als.getStore() || {}).turn;
918
+ const asked = { ctx: als.getStore(), retrieval, caller: retrieval ? callers() : undefined };
1052
919
  return realFetch.apply(this, arguments).then(
1053
920
  (res) => {
1054
- if (!retrieval) { depRow(dep, res.status, undefined, undefined, turn); return res; }
1055
- try { res.clone().text().then((t) => depRow(dep, res.status, undefined, passagesFrom(t), turn), () => depRow(dep, res.status, undefined, undefined, turn)); } catch { depRow(dep, res.status, undefined, undefined, turn); }
921
+ if (!retrieval) { depRow(dep, res.status, asked); return res; }
922
+ try { res.clone().text().then((t) => depRow(dep, res.status, { ...asked, passages: passagesFrom(t) }), () => depRow(dep, res.status, asked)); } catch { depRow(dep, res.status, asked); }
1056
923
  return res;
1057
924
  },
1058
- (err) => { depRow(dep, 0, err && err.cause && err.cause.code); throw err; },
925
+ (err) => { depRow(dep, 0, { ...asked, code: err && err.cause && err.cause.code }); throw err; },
1059
926
  );
1060
927
  }
1061
- const sent = bodyText(init && init.body);
1062
- note(als.getStore(), sent);
1063
- const call = { host: url.host, path: url.pathname, sent, turn: (als.getStore() || {}).turn, caller: EMBEDDING.test(url.pathname) ? undefined : callers() };
928
+ let named = null;
929
+ try { const headers = new Headers((init && init.headers) || (input && input.headers) || undefined); named = GATEWAY_MODEL.map((h) => headers.get(h)).find(Boolean); } catch { /* headers we cannot read name no model */ }
930
+ const sent = asChat(bodyText(init && init.body), named);
931
+ const ctx = note(als.getStore(), sent);
932
+ const call = { host: url.host, path: url.pathname, sent, ctx, t0: Date.now(), caller: EMBEDDING.test(url.pathname) ? undefined : callers() };
1064
933
  return realFetch.apply(this, arguments).then((res) => {
934
+ if (ctx) ctx.heard = true;
1065
935
  try {
1066
936
  // A clone is a tee: your app's branch gets every byte as fast as it reads, this one is read to the end.
1067
- res.clone().text().then(
1068
- (text) => meter({ ...call, status: res.status, type: res.headers.get("content-type"), body: text }),
1069
- () => meter({ ...call, status: res.status, type: "", body: "" }),
1070
- );
937
+ const type = res.headers.get("content-type");
938
+ readAll(res.clone()).then((text) => meter({ ...call, status: res.status, type, body: text }));
1071
939
  } catch { meter({ ...call, status: res.status, type: "", body: "" }); }
1072
940
  return res;
1073
941
  }, (err) => { meter({ ...call, status: 0, type: "", body: "" }); throw err; });
@@ -1076,6 +944,9 @@ if (FILE) {
1076
944
  for (const mod of [http, https]) {
1077
945
  const request = mod.request;
1078
946
  mod.request = function (...args) {
947
+ let held = null;
948
+ try { held = heldCall(args, mod === https); } catch { /* not a call we can read: sent as asked */ }
949
+ if (held) return sendHeld(held, mod === https);
1079
950
  const req = request.apply(this, args);
1080
951
  try {
1081
952
  const o = typeof args[0] === "string" || args[0] instanceof URL ? new URL(String(args[0])) : args[0] || {};
@@ -1088,13 +959,14 @@ if (FILE) {
1088
959
  const write = req.write;
1089
960
  const end = req.end;
1090
961
  req.write = function (chunk, ...r) { if (chunk && typeof chunk !== "function") parts.push(Buffer.from(chunk)); return write.call(this, chunk, ...r); };
1091
- req.end = function (chunk, ...r) { if (chunk && typeof chunk !== "function") parts.push(Buffer.from(chunk)); sent = Buffer.concat(parts).toString("utf8"); note(ctx, sent); return end.call(this, chunk, ...r); };
962
+ req.end = function (chunk, ...r) { if (chunk && typeof chunk !== "function") parts.push(Buffer.from(chunk)); sent = asChat(Buffer.concat(parts).toString("utf8"), GATEWAY_MODEL.map((h) => req.getHeader && req.getHeader(h)).find(Boolean)); call.ctx = note(ctx, sent); return end.call(this, chunk, ...r); };
1092
963
  const bare = String(path || "").split("?")[0];
1093
- const call = { host: String(host || ""), path: bare, turn: ctx && ctx.turn, caller: EMBEDDING.test(bare) ? undefined : callers() };
964
+ const call = { host: String(host || ""), path: bare, ctx, t0: Date.now(), caller: EMBEDDING.test(bare) ? undefined : callers() };
1094
965
  let done = false;
1095
966
  const once = (row) => { if (!done) { done = true; meter({ ...call, sent, ...row }); } };
1096
967
  req.once("error", () => once({ status: 0, type: "", body: "" }));
1097
968
  req.once("response", (res) => {
969
+ if (call.ctx) call.ctx.heard = true;
1098
970
  // The reply's bytes as the parser hands them over, without reading the stream: your
1099
971
  // app's own listeners and pipes see exactly what they would have.
1100
972
  const got = [];