@darkhunt-security/endpoint-codex 0.9.9 → 0.9.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "darkhunt-guard",
3
- "version": "0.9.9",
3
+ "version": "0.9.11",
4
4
  "hooks": "./hooks/hooks.json",
5
5
  "description": "Darkhunt endpoint plugin for Codex CLI \u2014 full session trace capture and PreToolUse guardrails.",
6
6
  "author": {
@@ -18308,6 +18308,7 @@ function attachmentLabel(record) {
18308
18308
  return `[${record.mediaType ?? "attachment"}${size}]`;
18309
18309
  }
18310
18310
  var RESUMED_USER_TEXT_LIMIT = 4e3;
18311
+ var USAGE_WIRE_NAMES = { thinking_tokens: "reasoning" };
18311
18312
  var SessionEmitter = class {
18312
18313
  client;
18313
18314
  options;
@@ -18331,6 +18332,8 @@ var SessionEmitter = class {
18331
18332
  /** Attachments seen before the trace could be opened knowing the prompt. */
18332
18333
  deferred = [];
18333
18334
  pendingUsage;
18335
+ /** Set when the model call this generation stands for failed. Cleared with it. */
18336
+ generationError;
18334
18337
  outputText = [];
18335
18338
  /** The last thing the assistant said in this pass — the trace's result. */
18336
18339
  lastAssistantText;
@@ -18386,9 +18389,10 @@ var SessionEmitter = class {
18386
18389
  this.count++;
18387
18390
  if (record.kind === "attachment")
18388
18391
  this.pendingAttachments.push(attachmentLabel(record));
18389
- if (record.kind === "user_message")
18392
+ if (record.kind === "user_message" && !record.isError && !record.synthetic) {
18390
18393
  this.absorbUserTurn(record);
18391
- if (!this.trace && record.kind === "attachment") {
18394
+ }
18395
+ if (!this.trace && (record.kind === "attachment" || record.synthetic === true)) {
18392
18396
  this.deferred.push(record);
18393
18397
  return;
18394
18398
  }
@@ -18426,6 +18430,15 @@ var SessionEmitter = class {
18426
18430
  this.owners.set(record.uuid, owner);
18427
18431
  switch (record.kind) {
18428
18432
  case "user_message":
18433
+ if (record.synthetic) {
18434
+ this.seeded(["injected", record.uuid], () => owner.span("context_injection", {
18435
+ startTime: record.ts,
18436
+ ...record.text !== void 0 ? { output: summarize(record.text) } : {}
18437
+ })).end({ endTime: record.ts });
18438
+ break;
18439
+ }
18440
+ if (record.isError)
18441
+ this.generationError = record.errorKind ?? "error";
18429
18442
  this.endGeneration();
18430
18443
  break;
18431
18444
  case "attachment": {
@@ -18442,7 +18455,9 @@ var SessionEmitter = class {
18442
18455
  }
18443
18456
  case "assistant_message":
18444
18457
  this.openGeneration(record, owner);
18445
- if (record.text)
18458
+ if (record.isError)
18459
+ this.generationError = record.errorKind ?? "error";
18460
+ else if (record.text)
18446
18461
  this.outputText.push(record.text);
18447
18462
  break;
18448
18463
  case "thinking": {
@@ -18471,15 +18486,44 @@ var SessionEmitter = class {
18471
18486
  case "tool_result": {
18472
18487
  const span = record.toolCallId ? this.toolSpans.get(record.toolCallId) : void 0;
18473
18488
  if (span) {
18474
- span.end({ output: record.output, endTime: record.ts });
18489
+ span.end({
18490
+ output: record.output,
18491
+ endTime: record.ts,
18492
+ ...record.isError ? { level: "ERROR" } : {},
18493
+ ...record.errorKind !== void 0 ? { statusMessage: record.errorKind } : {}
18494
+ });
18475
18495
  this.toolSpans.delete(record.toolCallId);
18476
18496
  }
18477
18497
  this.pendingToolResults.push(summarize(record.output));
18478
18498
  break;
18479
18499
  }
18500
+ case "compaction":
18501
+ this.endGeneration();
18502
+ this.seeded(["compaction", record.uuid], () => owner.span("compaction", {
18503
+ startTime: record.ts,
18504
+ ...record.text ? { output: record.text } : {}
18505
+ })).end({ endTime: record.ts });
18506
+ break;
18507
+ case "permission_grant": {
18508
+ const snapshot = record.input && typeof record.input === "object" ? record.input : void 0;
18509
+ const steps = Array.isArray(snapshot?.steps) ? snapshot.steps : [];
18510
+ this.seeded(["permission", record.uuid], () => owner.span("permission_grant", {
18511
+ startTime: record.ts,
18512
+ // `input`, not `output`: the list is what this record IS, not a result it
18513
+ // produced.
18514
+ input: steps,
18515
+ metadata: {
18516
+ // The true count, which is larger than `steps.length` when the mapper
18517
+ // truncated the snapshot to its byte budget.
18518
+ count: String(snapshot?.count ?? steps.length),
18519
+ full: String(snapshot?.full === true)
18520
+ }
18521
+ })).end({ endTime: record.ts });
18522
+ break;
18523
+ }
18480
18524
  case "usage":
18481
18525
  if (record.usage) {
18482
- this.pendingUsage = Object.fromEntries(Object.entries(record.usage).filter(([, v]) => typeof v === "number"));
18526
+ this.pendingUsage = Object.fromEntries(Object.entries(record.usage).filter(([, v]) => typeof v === "number").map(([key, value]) => [USAGE_WIRE_NAMES[key] ?? key, value]));
18483
18527
  }
18484
18528
  if (record.model)
18485
18529
  this.model = record.model;
@@ -18557,6 +18601,9 @@ var SessionEmitter = class {
18557
18601
  return this.options.parentAgentId ?? this.meta["parentSessionId"];
18558
18602
  }
18559
18603
  defaultTraceName() {
18604
+ const label = this.options.workflow?.label;
18605
+ if (label)
18606
+ return label;
18560
18607
  const agentType = this.agentType();
18561
18608
  if (agentType)
18562
18609
  return `subagent: ${agentType}`;
@@ -18565,6 +18612,27 @@ var SessionEmitter = class {
18565
18612
  }
18566
18613
  return this.options.agentType ? `subagent: ${this.options.agentType}` : `subagent ${this.options.agentId}`;
18567
18614
  }
18615
+ /**
18616
+ * The run this agent belongs to, flattened onto the trace.
18617
+ *
18618
+ * Flat scalars rather than a nested object: metadata is lifted onto the span one
18619
+ * attribute at a time downstream, so a nested value would arrive as a JSON blob that
18620
+ * nothing can group or filter on. `workflow_id` is the one that matters — it is what
18621
+ * lets a consumer collect the agents of one run out of a session that ran several.
18622
+ */
18623
+ workflowMetadata() {
18624
+ const workflow = this.options.workflow;
18625
+ if (!workflow)
18626
+ return {};
18627
+ return {
18628
+ workflow_id: workflow.id,
18629
+ ...workflow.name !== void 0 ? { workflow_name: workflow.name } : {},
18630
+ ...workflow.label !== void 0 ? { workflow_label: workflow.label } : {},
18631
+ ...workflow.phase !== void 0 ? { workflow_phase: workflow.phase } : {},
18632
+ ...workflow.phaseIndex !== void 0 ? { workflow_phase_index: workflow.phaseIndex } : {},
18633
+ ...workflow.attempt !== void 0 ? { workflow_attempt: workflow.attempt } : {}
18634
+ };
18635
+ }
18568
18636
  /** The session traces group under. Set from the path, or from `session_meta`. */
18569
18637
  grouping;
18570
18638
  ensureTrace(ts) {
@@ -18589,7 +18657,10 @@ var SessionEmitter = class {
18589
18657
  ...this.agentId() !== void 0 ? { agent_id: this.agentId() } : {},
18590
18658
  ...this.agentType() !== void 0 ? { agent_type: this.agentType() } : {},
18591
18659
  ...this.parentAgentId() !== void 0 ? { parent_agent_id: this.parentAgentId() } : {},
18592
- ...this.options.spawnDepth !== void 0 ? { spawn_depth: this.options.spawnDepth } : {}
18660
+ ...this.options.spawnDepth !== void 0 ? { spawn_depth: this.options.spawnDepth } : {},
18661
+ // snake_case for the same reason as `agent_id` above: trace-hub lifts each
18662
+ // key through unchanged, and these are read alongside it.
18663
+ ...this.workflowMetadata()
18593
18664
  },
18594
18665
  ...this.options.userId !== void 0 ? { userId: this.options.userId } : {},
18595
18666
  ...this.options.userEmail !== void 0 ? { userEmail: this.options.userEmail } : {},
@@ -18609,7 +18680,18 @@ var SessionEmitter = class {
18609
18680
  this.model = record.model;
18610
18681
  this.generation = this.seeded(["generation", id], () => owner.generation("assistant", {
18611
18682
  startTime: record.ts,
18612
- ...this.model !== void 0 ? { model: this.model } : {}
18683
+ ...this.model !== void 0 ? { model: this.model } : {},
18684
+ // What drove this model call — an installed skill, an MCP server's output, a
18685
+ // plugin. A flat scalar like every other metadata value emitted here, because
18686
+ // trace-hub lifts each key onto the span unchanged and a nested value would
18687
+ // arrive as a blob nothing can group or filter on. One lowercase word, so unlike
18688
+ // `agent_id` there is no camelCase to convert.
18689
+ //
18690
+ // Set once, at open, and never through update(): this method early-returns when
18691
+ // the generation is already open, so the value has to be constant across a
18692
+ // message group. Measured over the claude-code corpus, 0 of 1,381 attributed
18693
+ // groups carry two different tokens.
18694
+ ...record.attribution !== void 0 ? { metadata: { attribution: record.attribution } } : {}
18613
18695
  }));
18614
18696
  const input = this.buildInputMessages();
18615
18697
  if (input.inputMessages)
@@ -18649,12 +18731,14 @@ var SessionEmitter = class {
18649
18731
  this.generation.end({
18650
18732
  ...output ? { outputMessages: [{ role: "assistant", content: output }] } : {},
18651
18733
  ...this.model !== void 0 ? { model: this.model } : {},
18652
- ...this.pendingUsage !== void 0 ? { usage: this.pendingUsage } : {}
18734
+ ...this.pendingUsage !== void 0 ? { usage: this.pendingUsage } : {},
18735
+ ...this.generationError !== void 0 ? { level: "ERROR", statusMessage: this.generationError } : {}
18653
18736
  });
18654
18737
  this.generation = void 0;
18655
18738
  this.generationId = void 0;
18656
18739
  this.outputText = [];
18657
18740
  this.pendingUsage = void 0;
18741
+ this.generationError = void 0;
18658
18742
  }
18659
18743
  };
18660
18744
 
@@ -21746,6 +21830,7 @@ async function runForwarder(mapper, options = {}) {
21746
21830
  ...sub?.agentType !== void 0 ? { agentType: sub.agentType } : {},
21747
21831
  ...sub?.parentAgentId !== void 0 ? { parentAgentId: sub.parentAgentId } : {},
21748
21832
  ...sub?.spawnDepth !== void 0 ? { spawnDepth: sub.spawnDepth } : {},
21833
+ ...sub?.workflow !== void 0 ? { workflow: sub.workflow } : {},
21749
21834
  ...ids !== void 0 ? { ids } : {},
21750
21835
  ...userId !== void 0 ? { userId } : {},
21751
21836
  ...carried !== void 0 ? { resumeTurn: carried } : {}
@@ -21874,6 +21959,7 @@ import { join as join9 } from "node:path";
21874
21959
  var CLI_CREDENTIALS_PATH = join9(homedir3(), ".darkhunt", "credentials.json");
21875
21960
 
21876
21961
  // adapters/codex/dist/transcript.js
21962
+ import { readFileSync as readFileSync6 } from "node:fs";
21877
21963
  import { basename, join as join10 } from "node:path";
21878
21964
  import { homedir as homedir4 } from "node:os";
21879
21965
  function str(value) {
@@ -21910,17 +21996,128 @@ function textOf(content) {
21910
21996
  const parts = content.map((b) => b && typeof b === "object" ? str(b["text"]) : void 0).filter((t) => !!t);
21911
21997
  return parts.length > 0 ? parts.join("\n") : void 0;
21912
21998
  }
21999
+ var CODEX_APPROVAL_MODES = {
22000
+ never: "auto",
22001
+ "on-request": "ask"
22002
+ };
22003
+ var CODEX_SANDBOX_MODES = {
22004
+ "workspace-write": "workspace-write",
22005
+ "read-only": "read-only",
22006
+ "danger-full-access": "danger-full-access"
22007
+ };
22008
+ var PERMISSION_SNAPSHOT_LIMIT = 32e3;
22009
+ function budgeted(steps) {
22010
+ const kept = [];
22011
+ let bytes = 0;
22012
+ for (const step of steps) {
22013
+ bytes += JSON.stringify(step ?? null).length + 1;
22014
+ if (bytes > PERMISSION_SNAPSHOT_LIMIT)
22015
+ break;
22016
+ kept.push(step);
22017
+ }
22018
+ return kept;
22019
+ }
22020
+ function unwrapExecOutput(value) {
22021
+ if (typeof value !== "string")
22022
+ return { output: value };
22023
+ let parsed;
22024
+ try {
22025
+ parsed = JSON.parse(value);
22026
+ } catch {
22027
+ return { output: value };
22028
+ }
22029
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
22030
+ return { output: value };
22031
+ const bag = parsed;
22032
+ if (!Object.prototype.hasOwnProperty.call(bag, "output"))
22033
+ return { output: value };
22034
+ const metadata = bag["metadata"];
22035
+ if (!metadata || typeof metadata !== "object")
22036
+ return { output: value };
22037
+ const exitCode = num(metadata["exit_code"]);
22038
+ if (exitCode === void 0)
22039
+ return { output: value };
22040
+ return { output: bag["output"], exitCode };
22041
+ }
22042
+ function jwtClaims(token) {
22043
+ const parts = token.split(".");
22044
+ if (parts.length !== 3 || !parts[1])
22045
+ return void 0;
22046
+ try {
22047
+ const json = Buffer.from(parts[1], "base64url").toString("utf8");
22048
+ const claims = JSON.parse(json);
22049
+ return claims && typeof claims === "object" ? claims : void 0;
22050
+ } catch {
22051
+ return void 0;
22052
+ }
22053
+ }
22054
+ function emailFromAuth(raw) {
22055
+ let parsed;
22056
+ try {
22057
+ parsed = JSON.parse(raw);
22058
+ } catch {
22059
+ return void 0;
22060
+ }
22061
+ const tokens = parsed?.tokens;
22062
+ const token = tokens?.id_token;
22063
+ if (typeof token !== "string" || token === "")
22064
+ return void 0;
22065
+ const email = jwtClaims(token)?.["email"];
22066
+ return typeof email === "string" && email !== "" ? email : void 0;
22067
+ }
22068
+ function repositoryIdentity(url) {
22069
+ if (url === void 0)
22070
+ return void 0;
22071
+ const trimmed = url.trim();
22072
+ if (trimmed === "")
22073
+ return void 0;
22074
+ const scp = /^(?:[^@/\s]+@)?([^:/\s]+):(?!\/)(\S+)$/.exec(trimmed);
22075
+ if (scp)
22076
+ return `${scp[1]}/${stripSuffix(scp[2])}`;
22077
+ const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed) ? trimmed : `ssh://${trimmed}`;
22078
+ try {
22079
+ const parsed = new URL(withScheme);
22080
+ if (!parsed.hostname)
22081
+ return trimmed;
22082
+ const path = stripSuffix(parsed.pathname.replace(/^\/+/, ""));
22083
+ return path === "" ? parsed.hostname : `${parsed.hostname}/${path}`;
22084
+ } catch {
22085
+ return trimmed;
22086
+ }
22087
+ }
22088
+ function stripSuffix(path) {
22089
+ return path.replace(/\/+$/, "").replace(/\.git$/i, "");
22090
+ }
21913
22091
  var codexTranscript = {
21914
22092
  vendor: "codex",
21915
22093
  sessionRoots() {
21916
22094
  return [join10(homedir4(), ".codex", "sessions")];
21917
22095
  },
21918
- // No `resolveUserId` yet, so Codex traces fall back to the configured `userId`.
21919
- // The identity exists but not as a plain field: `~/.codex/auth.json` holds
21920
- // `tokens.account_id` (a UUID, not a person) and an `email` claim inside the
21921
- // `id_token` JWT. Reading it means decoding an OAuth token and honouring its expiry
21922
- // a stale token would attribute sessions to a *wrong* address, which is worse than
21923
- // leaving them unattributed. Tracked separately.
22096
+ /**
22097
+ * `~/.codex/auth.json` -> the `email` claim of `tokens.id_token`.
22098
+ *
22099
+ * Codex records no identity in the rollout itself and none in plain text anywhere
22100
+ * else: `tokens.account_id` is a UUID, not a person. The address is a claim inside the
22101
+ * OAuth id_token, so reading it means decoding that token — hence {@link jwtClaims}.
22102
+ *
22103
+ * The token's own `exp` is deliberately not enforced. It is an hour long and the file
22104
+ * is only rewritten on sign-in and on refresh, so gating on expiry would leave most
22105
+ * sessions unattributed while answering a question nobody asked: an expired id_token
22106
+ * is evidence that a token needs refreshing, never evidence that a different person is
22107
+ * now signed in. Signing in as someone else rewrites this file, which is what makes
22108
+ * the stale-address worry unfounded — the claim always names the last account to
22109
+ * authenticate on this machine, which is the account that wrote these rollouts.
22110
+ *
22111
+ * `undefined` for API-key auth (`OPENAI_API_KEY` with no tokens), for a signed-out
22112
+ * agent, and for anything unreadable; the caller then falls back to configuration.
22113
+ */
22114
+ resolveUserId() {
22115
+ try {
22116
+ return emailFromAuth(readFileSync6(join10(homedir4(), ".codex", "auth.json"), "utf8"));
22117
+ } catch {
22118
+ return void 0;
22119
+ }
22120
+ },
21924
22121
  sessionIdFor(path) {
21925
22122
  const name = basename(path, ".jsonl");
21926
22123
  const match = /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i.exec(name);
@@ -21943,12 +22140,20 @@ var codexTranscript = {
21943
22140
  const uuid = `codex:${offset}`;
21944
22141
  const base = { vendor: "codex", ts, uuid };
21945
22142
  if (raw.type === "session_meta") {
22143
+ const rawGit = payload["git"];
22144
+ const git = rawGit && typeof rawGit === "object" ? rawGit : void 0;
22145
+ const gitBranch = str(git?.["branch"]);
22146
+ const gitRepositoryUrl = repositoryIdentity(str(git?.["repository_url"]));
21946
22147
  return [
21947
22148
  {
21948
22149
  ...base,
21949
22150
  kind: "session_meta",
21950
22151
  meta: {
21951
22152
  ...str(payload["cwd"]) !== void 0 ? { cwd: str(payload["cwd"]) } : {},
22153
+ // Conditional spread rather than a plain assignment: the 7 `{}` blocks and
22154
+ // the 92 absent ones must leave the key absent, not present-and-undefined.
22155
+ ...gitBranch !== void 0 ? { gitBranch } : {},
22156
+ ...gitRepositoryUrl !== void 0 ? { gitRepositoryUrl } : {},
21952
22157
  ...str(payload["cli_version"]) !== void 0 ? { version: str(payload["cli_version"]) } : {},
21953
22158
  ...str(payload["originator"]) !== void 0 ? { entrypoint: str(payload["originator"]) } : {},
21954
22159
  // A Codex subagent is a rollout of its own, a peer of its parent in the
@@ -21964,15 +22169,61 @@ var codexTranscript = {
21964
22169
  }
21965
22170
  if (raw.type === "turn_context") {
21966
22171
  const model = str(payload["model"]);
21967
- if (!model)
22172
+ const approval = str(payload["approval_policy"]);
22173
+ const sandbox = payload["sandbox_policy"];
22174
+ const sandboxMode = (() => {
22175
+ if (!sandbox || typeof sandbox !== "object")
22176
+ return void 0;
22177
+ const bag = sandbox;
22178
+ return str(bag["mode"]) ?? str(bag["type"]);
22179
+ })();
22180
+ const meta = {
22181
+ ...model !== void 0 ? { model } : {},
22182
+ ...approval !== void 0 ? { approvalMode: CODEX_APPROVAL_MODES[approval] ?? approval } : {},
22183
+ ...sandboxMode !== void 0 ? { sandboxMode: CODEX_SANDBOX_MODES[sandboxMode] ?? sandboxMode } : {}
22184
+ };
22185
+ if (Object.keys(meta).length === 0)
21968
22186
  return [];
21969
- return [{ ...base, kind: "session_meta", meta: { model } }];
22187
+ return [{ ...base, kind: "session_meta", meta }];
21970
22188
  }
21971
22189
  const kind = str(payload["type"]);
21972
22190
  if (raw.type === "event_msg" && kind === "token_count") {
21973
22191
  const usage = mapTokenCount(payload["info"]);
21974
22192
  return usage ? [{ ...base, kind: "usage", usage }] : [];
21975
22193
  }
22194
+ if (raw.type === "compacted") {
22195
+ const note = str(payload["message"]);
22196
+ return [{ ...base, kind: "compaction", ...note ? { text: note } : {} }];
22197
+ }
22198
+ if (raw.type === "world_state") {
22199
+ const state = payload["state"];
22200
+ const perms = state && typeof state === "object" ? state["permissions"] : void 0;
22201
+ if (!perms || typeof perms !== "object")
22202
+ return [];
22203
+ const list = perms["approved_command_prefixes"];
22204
+ const steps = Array.isArray(list) ? list : [];
22205
+ return [
22206
+ {
22207
+ ...base,
22208
+ kind: "permission_grant",
22209
+ input: {
22210
+ steps: budgeted(steps),
22211
+ count: steps.length,
22212
+ ...payload["full"] === true ? { full: true } : {}
22213
+ }
22214
+ }
22215
+ ];
22216
+ }
22217
+ if (raw.type === "event_msg" && kind === "turn_aborted") {
22218
+ return [
22219
+ {
22220
+ ...base,
22221
+ kind: "user_message",
22222
+ isError: true,
22223
+ errorKind: `aborted:${str(payload["reason"]) ?? "unknown"}`
22224
+ }
22225
+ ];
22226
+ }
21976
22227
  if (raw.type !== "response_item")
21977
22228
  return [];
21978
22229
  switch (kind) {
@@ -21986,18 +22237,74 @@ var codexTranscript = {
21986
22237
  ...str(payload["call_id"]) !== void 0 ? { toolCallId: str(payload["call_id"]) } : {}
21987
22238
  }
21988
22239
  ];
21989
- case "function_call_output":
22240
+ case "function_call_output": {
22241
+ const flattened = textOf(payload["output"]) ?? payload["output"];
22242
+ const { output, exitCode } = unwrapExecOutput(flattened);
21990
22243
  return [
21991
22244
  {
21992
22245
  ...base,
21993
22246
  kind: "tool_result",
21994
- output: payload["output"],
22247
+ output,
22248
+ // Only where an exit status was actually found. On the 993 string outputs
22249
+ // that are not envelopes and the 144 array ones the key stays ABSENT rather
22250
+ // than `false`: the emitter tests `record.isError`, and "this tool reported
22251
+ // success" is a different claim from "nothing here reported an outcome".
22252
+ ...exitCode !== void 0 ? { isError: exitCode !== 0 } : {},
21995
22253
  ...str(payload["call_id"]) !== void 0 ? { toolCallId: str(payload["call_id"]) } : {}
21996
22254
  }
21997
22255
  ];
22256
+ }
22257
+ // Codex's OTHER tool-call shape, and the one it actually uses: `exec` and
22258
+ // `apply_patch` arrive as `custom_tool_call`, never as `function_call`. Measured
22259
+ // across 251 rollouts, 2179 of 5382 tool calls — 40% — took this shape, so
22260
+ // handling only `function_call` left every shell command and every file write
22261
+ // this endpoint made invisible to both the trace and anything reading it.
22262
+ case "custom_tool_call":
22263
+ return [
22264
+ {
22265
+ ...base,
22266
+ kind: "tool_call",
22267
+ toolName: str(payload["name"]) ?? "tool",
22268
+ input: payload["input"],
22269
+ ...str(payload["call_id"]) !== void 0 ? { toolCallId: str(payload["call_id"]) } : {}
22270
+ }
22271
+ ];
22272
+ case "custom_tool_call_output": {
22273
+ const flattened = textOf(payload["output"]) ?? payload["output"];
22274
+ const { output, exitCode } = unwrapExecOutput(flattened);
22275
+ return [
22276
+ {
22277
+ ...base,
22278
+ kind: "tool_result",
22279
+ output,
22280
+ ...exitCode !== void 0 ? { isError: exitCode !== 0 } : {},
22281
+ ...str(payload["call_id"]) !== void 0 ? { toolCallId: str(payload["call_id"]) } : {}
22282
+ }
22283
+ ];
22284
+ }
22285
+ // The model's own web reads. 49 across the corpus in 9 rollouts — `search` 31,
22286
+ // `open_page` 16, `find_in_page` 2 — every one of which mapped to nothing, so a
22287
+ // session where the agent searched and opened pages rendered as reasoning
22288
+ // followed by an answer with no source anywhere in it. The queries and the URLs
22289
+ // are the whole point of the record: they are the evidence of what the agent
22290
+ // read from outside the machine.
22291
+ //
22292
+ // `action` is passed through whole. It is already a vendor-neutral argument bag
22293
+ // ({type, query?, queries?, url?, pattern?}) and it is small — 322 bytes at the
22294
+ // worst, 121 on average across all 49.
22295
+ //
22296
+ // Deliberately unpaired: no `toolCallId`. Only 1 of the 49 lines carries an
22297
+ // `id`, `call_id` is the pairing key everywhere else in this stream, and no
22298
+ // response_item output line ever pairs with a search — so setting one would
22299
+ // register a span in the emitter's pairing table that nothing ever closes, and
22300
+ // `finish()` would end it at pass end and stamp a multi-minute duration on a
22301
+ // search that took seconds. Left unpaired, the emitter closes it at the call
22302
+ // timestamp, which is the truthful reading.
22303
+ case "web_search_call":
22304
+ return [{ ...base, kind: "tool_call", toolName: "web_search", input: payload["action"] }];
21998
22305
  case "reasoning": {
21999
22306
  const text = textOf(payload["summary"]) ?? textOf(payload["content"]);
22000
- return text ? [{ ...base, kind: "thinking", text }] : [];
22307
+ return [{ ...base, kind: "thinking", ...text !== void 0 ? { text } : {} }];
22001
22308
  }
22002
22309
  case "message": {
22003
22310
  const text = textOf(payload["content"]);