@darkhunt-security/endpoint-codex 0.9.10 → 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.10",
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;
@@ -18636,7 +18680,18 @@ var SessionEmitter = class {
18636
18680
  this.model = record.model;
18637
18681
  this.generation = this.seeded(["generation", id], () => owner.generation("assistant", {
18638
18682
  startTime: record.ts,
18639
- ...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 } } : {}
18640
18695
  }));
18641
18696
  const input = this.buildInputMessages();
18642
18697
  if (input.inputMessages)
@@ -18676,12 +18731,14 @@ var SessionEmitter = class {
18676
18731
  this.generation.end({
18677
18732
  ...output ? { outputMessages: [{ role: "assistant", content: output }] } : {},
18678
18733
  ...this.model !== void 0 ? { model: this.model } : {},
18679
- ...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 } : {}
18680
18736
  });
18681
18737
  this.generation = void 0;
18682
18738
  this.generationId = void 0;
18683
18739
  this.outputText = [];
18684
18740
  this.pendingUsage = void 0;
18741
+ this.generationError = void 0;
18685
18742
  }
18686
18743
  };
18687
18744
 
@@ -21939,6 +21996,49 @@ function textOf(content) {
21939
21996
  const parts = content.map((b) => b && typeof b === "object" ? str(b["text"]) : void 0).filter((t) => !!t);
21940
21997
  return parts.length > 0 ? parts.join("\n") : void 0;
21941
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
+ }
21942
22042
  function jwtClaims(token) {
21943
22043
  const parts = token.split(".");
21944
22044
  if (parts.length !== 3 || !parts[1])
@@ -21965,6 +22065,29 @@ function emailFromAuth(raw) {
21965
22065
  const email = jwtClaims(token)?.["email"];
21966
22066
  return typeof email === "string" && email !== "" ? email : void 0;
21967
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
+ }
21968
22091
  var codexTranscript = {
21969
22092
  vendor: "codex",
21970
22093
  sessionRoots() {
@@ -22017,12 +22140,20 @@ var codexTranscript = {
22017
22140
  const uuid = `codex:${offset}`;
22018
22141
  const base = { vendor: "codex", ts, uuid };
22019
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"]));
22020
22147
  return [
22021
22148
  {
22022
22149
  ...base,
22023
22150
  kind: "session_meta",
22024
22151
  meta: {
22025
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 } : {},
22026
22157
  ...str(payload["cli_version"]) !== void 0 ? { version: str(payload["cli_version"]) } : {},
22027
22158
  ...str(payload["originator"]) !== void 0 ? { entrypoint: str(payload["originator"]) } : {},
22028
22159
  // A Codex subagent is a rollout of its own, a peer of its parent in the
@@ -22038,15 +22169,61 @@ var codexTranscript = {
22038
22169
  }
22039
22170
  if (raw.type === "turn_context") {
22040
22171
  const model = str(payload["model"]);
22041
- 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)
22042
22186
  return [];
22043
- return [{ ...base, kind: "session_meta", meta: { model } }];
22187
+ return [{ ...base, kind: "session_meta", meta }];
22044
22188
  }
22045
22189
  const kind = str(payload["type"]);
22046
22190
  if (raw.type === "event_msg" && kind === "token_count") {
22047
22191
  const usage = mapTokenCount(payload["info"]);
22048
22192
  return usage ? [{ ...base, kind: "usage", usage }] : [];
22049
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
+ }
22050
22227
  if (raw.type !== "response_item")
22051
22228
  return [];
22052
22229
  switch (kind) {
@@ -22060,18 +22237,74 @@ var codexTranscript = {
22060
22237
  ...str(payload["call_id"]) !== void 0 ? { toolCallId: str(payload["call_id"]) } : {}
22061
22238
  }
22062
22239
  ];
22063
- case "function_call_output":
22240
+ case "function_call_output": {
22241
+ const flattened = textOf(payload["output"]) ?? payload["output"];
22242
+ const { output, exitCode } = unwrapExecOutput(flattened);
22064
22243
  return [
22065
22244
  {
22066
22245
  ...base,
22067
22246
  kind: "tool_result",
22068
- 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 } : {},
22069
22253
  ...str(payload["call_id"]) !== void 0 ? { toolCallId: str(payload["call_id"]) } : {}
22070
22254
  }
22071
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"] }];
22072
22305
  case "reasoning": {
22073
22306
  const text = textOf(payload["summary"]) ?? textOf(payload["content"]);
22074
- return text ? [{ ...base, kind: "thinking", text }] : [];
22307
+ return [{ ...base, kind: "thinking", ...text !== void 0 ? { text } : {} }];
22075
22308
  }
22076
22309
  case "message": {
22077
22310
  const text = textOf(payload["content"]);
@@ -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;
@@ -18636,7 +18680,18 @@ var SessionEmitter = class {
18636
18680
  this.model = record.model;
18637
18681
  this.generation = this.seeded(["generation", id], () => owner.generation("assistant", {
18638
18682
  startTime: record.ts,
18639
- ...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 } } : {}
18640
18695
  }));
18641
18696
  const input = this.buildInputMessages();
18642
18697
  if (input.inputMessages)
@@ -18676,12 +18731,14 @@ var SessionEmitter = class {
18676
18731
  this.generation.end({
18677
18732
  ...output ? { outputMessages: [{ role: "assistant", content: output }] } : {},
18678
18733
  ...this.model !== void 0 ? { model: this.model } : {},
18679
- ...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 } : {}
18680
18736
  });
18681
18737
  this.generation = void 0;
18682
18738
  this.generationId = void 0;
18683
18739
  this.outputText = [];
18684
18740
  this.pendingUsage = void 0;
18741
+ this.generationError = void 0;
18685
18742
  }
18686
18743
  };
18687
18744
 
@@ -21926,6 +21983,49 @@ function textOf(content) {
21926
21983
  const parts = content.map((b) => b && typeof b === "object" ? str(b["text"]) : void 0).filter((t) => !!t);
21927
21984
  return parts.length > 0 ? parts.join("\n") : void 0;
21928
21985
  }
21986
+ var CODEX_APPROVAL_MODES = {
21987
+ never: "auto",
21988
+ "on-request": "ask"
21989
+ };
21990
+ var CODEX_SANDBOX_MODES = {
21991
+ "workspace-write": "workspace-write",
21992
+ "read-only": "read-only",
21993
+ "danger-full-access": "danger-full-access"
21994
+ };
21995
+ var PERMISSION_SNAPSHOT_LIMIT = 32e3;
21996
+ function budgeted(steps) {
21997
+ const kept = [];
21998
+ let bytes = 0;
21999
+ for (const step of steps) {
22000
+ bytes += JSON.stringify(step ?? null).length + 1;
22001
+ if (bytes > PERMISSION_SNAPSHOT_LIMIT)
22002
+ break;
22003
+ kept.push(step);
22004
+ }
22005
+ return kept;
22006
+ }
22007
+ function unwrapExecOutput(value) {
22008
+ if (typeof value !== "string")
22009
+ return { output: value };
22010
+ let parsed;
22011
+ try {
22012
+ parsed = JSON.parse(value);
22013
+ } catch {
22014
+ return { output: value };
22015
+ }
22016
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
22017
+ return { output: value };
22018
+ const bag = parsed;
22019
+ if (!Object.prototype.hasOwnProperty.call(bag, "output"))
22020
+ return { output: value };
22021
+ const metadata = bag["metadata"];
22022
+ if (!metadata || typeof metadata !== "object")
22023
+ return { output: value };
22024
+ const exitCode = num(metadata["exit_code"]);
22025
+ if (exitCode === void 0)
22026
+ return { output: value };
22027
+ return { output: bag["output"], exitCode };
22028
+ }
21929
22029
  function jwtClaims(token) {
21930
22030
  const parts = token.split(".");
21931
22031
  if (parts.length !== 3 || !parts[1])
@@ -21952,6 +22052,29 @@ function emailFromAuth(raw) {
21952
22052
  const email = jwtClaims(token)?.["email"];
21953
22053
  return typeof email === "string" && email !== "" ? email : void 0;
21954
22054
  }
22055
+ function repositoryIdentity(url) {
22056
+ if (url === void 0)
22057
+ return void 0;
22058
+ const trimmed = url.trim();
22059
+ if (trimmed === "")
22060
+ return void 0;
22061
+ const scp = /^(?:[^@/\s]+@)?([^:/\s]+):(?!\/)(\S+)$/.exec(trimmed);
22062
+ if (scp)
22063
+ return `${scp[1]}/${stripSuffix(scp[2])}`;
22064
+ const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed) ? trimmed : `ssh://${trimmed}`;
22065
+ try {
22066
+ const parsed = new URL(withScheme);
22067
+ if (!parsed.hostname)
22068
+ return trimmed;
22069
+ const path = stripSuffix(parsed.pathname.replace(/^\/+/, ""));
22070
+ return path === "" ? parsed.hostname : `${parsed.hostname}/${path}`;
22071
+ } catch {
22072
+ return trimmed;
22073
+ }
22074
+ }
22075
+ function stripSuffix(path) {
22076
+ return path.replace(/\/+$/, "").replace(/\.git$/i, "");
22077
+ }
21955
22078
  var codexTranscript = {
21956
22079
  vendor: "codex",
21957
22080
  sessionRoots() {
@@ -22004,12 +22127,20 @@ var codexTranscript = {
22004
22127
  const uuid = `codex:${offset}`;
22005
22128
  const base = { vendor: "codex", ts, uuid };
22006
22129
  if (raw.type === "session_meta") {
22130
+ const rawGit = payload["git"];
22131
+ const git = rawGit && typeof rawGit === "object" ? rawGit : void 0;
22132
+ const gitBranch = str(git?.["branch"]);
22133
+ const gitRepositoryUrl = repositoryIdentity(str(git?.["repository_url"]));
22007
22134
  return [
22008
22135
  {
22009
22136
  ...base,
22010
22137
  kind: "session_meta",
22011
22138
  meta: {
22012
22139
  ...str(payload["cwd"]) !== void 0 ? { cwd: str(payload["cwd"]) } : {},
22140
+ // Conditional spread rather than a plain assignment: the 7 `{}` blocks and
22141
+ // the 92 absent ones must leave the key absent, not present-and-undefined.
22142
+ ...gitBranch !== void 0 ? { gitBranch } : {},
22143
+ ...gitRepositoryUrl !== void 0 ? { gitRepositoryUrl } : {},
22013
22144
  ...str(payload["cli_version"]) !== void 0 ? { version: str(payload["cli_version"]) } : {},
22014
22145
  ...str(payload["originator"]) !== void 0 ? { entrypoint: str(payload["originator"]) } : {},
22015
22146
  // A Codex subagent is a rollout of its own, a peer of its parent in the
@@ -22025,15 +22156,61 @@ var codexTranscript = {
22025
22156
  }
22026
22157
  if (raw.type === "turn_context") {
22027
22158
  const model = str(payload["model"]);
22028
- if (!model)
22159
+ const approval = str(payload["approval_policy"]);
22160
+ const sandbox = payload["sandbox_policy"];
22161
+ const sandboxMode = (() => {
22162
+ if (!sandbox || typeof sandbox !== "object")
22163
+ return void 0;
22164
+ const bag = sandbox;
22165
+ return str(bag["mode"]) ?? str(bag["type"]);
22166
+ })();
22167
+ const meta = {
22168
+ ...model !== void 0 ? { model } : {},
22169
+ ...approval !== void 0 ? { approvalMode: CODEX_APPROVAL_MODES[approval] ?? approval } : {},
22170
+ ...sandboxMode !== void 0 ? { sandboxMode: CODEX_SANDBOX_MODES[sandboxMode] ?? sandboxMode } : {}
22171
+ };
22172
+ if (Object.keys(meta).length === 0)
22029
22173
  return [];
22030
- return [{ ...base, kind: "session_meta", meta: { model } }];
22174
+ return [{ ...base, kind: "session_meta", meta }];
22031
22175
  }
22032
22176
  const kind = str(payload["type"]);
22033
22177
  if (raw.type === "event_msg" && kind === "token_count") {
22034
22178
  const usage = mapTokenCount(payload["info"]);
22035
22179
  return usage ? [{ ...base, kind: "usage", usage }] : [];
22036
22180
  }
22181
+ if (raw.type === "compacted") {
22182
+ const note = str(payload["message"]);
22183
+ return [{ ...base, kind: "compaction", ...note ? { text: note } : {} }];
22184
+ }
22185
+ if (raw.type === "world_state") {
22186
+ const state = payload["state"];
22187
+ const perms = state && typeof state === "object" ? state["permissions"] : void 0;
22188
+ if (!perms || typeof perms !== "object")
22189
+ return [];
22190
+ const list = perms["approved_command_prefixes"];
22191
+ const steps = Array.isArray(list) ? list : [];
22192
+ return [
22193
+ {
22194
+ ...base,
22195
+ kind: "permission_grant",
22196
+ input: {
22197
+ steps: budgeted(steps),
22198
+ count: steps.length,
22199
+ ...payload["full"] === true ? { full: true } : {}
22200
+ }
22201
+ }
22202
+ ];
22203
+ }
22204
+ if (raw.type === "event_msg" && kind === "turn_aborted") {
22205
+ return [
22206
+ {
22207
+ ...base,
22208
+ kind: "user_message",
22209
+ isError: true,
22210
+ errorKind: `aborted:${str(payload["reason"]) ?? "unknown"}`
22211
+ }
22212
+ ];
22213
+ }
22037
22214
  if (raw.type !== "response_item")
22038
22215
  return [];
22039
22216
  switch (kind) {
@@ -22047,18 +22224,74 @@ var codexTranscript = {
22047
22224
  ...str(payload["call_id"]) !== void 0 ? { toolCallId: str(payload["call_id"]) } : {}
22048
22225
  }
22049
22226
  ];
22050
- case "function_call_output":
22227
+ case "function_call_output": {
22228
+ const flattened = textOf(payload["output"]) ?? payload["output"];
22229
+ const { output, exitCode } = unwrapExecOutput(flattened);
22051
22230
  return [
22052
22231
  {
22053
22232
  ...base,
22054
22233
  kind: "tool_result",
22055
- output: payload["output"],
22234
+ output,
22235
+ // Only where an exit status was actually found. On the 993 string outputs
22236
+ // that are not envelopes and the 144 array ones the key stays ABSENT rather
22237
+ // than `false`: the emitter tests `record.isError`, and "this tool reported
22238
+ // success" is a different claim from "nothing here reported an outcome".
22239
+ ...exitCode !== void 0 ? { isError: exitCode !== 0 } : {},
22056
22240
  ...str(payload["call_id"]) !== void 0 ? { toolCallId: str(payload["call_id"]) } : {}
22057
22241
  }
22058
22242
  ];
22243
+ }
22244
+ // Codex's OTHER tool-call shape, and the one it actually uses: `exec` and
22245
+ // `apply_patch` arrive as `custom_tool_call`, never as `function_call`. Measured
22246
+ // across 251 rollouts, 2179 of 5382 tool calls — 40% — took this shape, so
22247
+ // handling only `function_call` left every shell command and every file write
22248
+ // this endpoint made invisible to both the trace and anything reading it.
22249
+ case "custom_tool_call":
22250
+ return [
22251
+ {
22252
+ ...base,
22253
+ kind: "tool_call",
22254
+ toolName: str(payload["name"]) ?? "tool",
22255
+ input: payload["input"],
22256
+ ...str(payload["call_id"]) !== void 0 ? { toolCallId: str(payload["call_id"]) } : {}
22257
+ }
22258
+ ];
22259
+ case "custom_tool_call_output": {
22260
+ const flattened = textOf(payload["output"]) ?? payload["output"];
22261
+ const { output, exitCode } = unwrapExecOutput(flattened);
22262
+ return [
22263
+ {
22264
+ ...base,
22265
+ kind: "tool_result",
22266
+ output,
22267
+ ...exitCode !== void 0 ? { isError: exitCode !== 0 } : {},
22268
+ ...str(payload["call_id"]) !== void 0 ? { toolCallId: str(payload["call_id"]) } : {}
22269
+ }
22270
+ ];
22271
+ }
22272
+ // The model's own web reads. 49 across the corpus in 9 rollouts — `search` 31,
22273
+ // `open_page` 16, `find_in_page` 2 — every one of which mapped to nothing, so a
22274
+ // session where the agent searched and opened pages rendered as reasoning
22275
+ // followed by an answer with no source anywhere in it. The queries and the URLs
22276
+ // are the whole point of the record: they are the evidence of what the agent
22277
+ // read from outside the machine.
22278
+ //
22279
+ // `action` is passed through whole. It is already a vendor-neutral argument bag
22280
+ // ({type, query?, queries?, url?, pattern?}) and it is small — 322 bytes at the
22281
+ // worst, 121 on average across all 49.
22282
+ //
22283
+ // Deliberately unpaired: no `toolCallId`. Only 1 of the 49 lines carries an
22284
+ // `id`, `call_id` is the pairing key everywhere else in this stream, and no
22285
+ // response_item output line ever pairs with a search — so setting one would
22286
+ // register a span in the emitter's pairing table that nothing ever closes, and
22287
+ // `finish()` would end it at pass end and stamp a multi-minute duration on a
22288
+ // search that took seconds. Left unpaired, the emitter closes it at the call
22289
+ // timestamp, which is the truthful reading.
22290
+ case "web_search_call":
22291
+ return [{ ...base, kind: "tool_call", toolName: "web_search", input: payload["action"] }];
22059
22292
  case "reasoning": {
22060
22293
  const text = textOf(payload["summary"]) ?? textOf(payload["content"]);
22061
- return text ? [{ ...base, kind: "thinking", text }] : [];
22294
+ return [{ ...base, kind: "thinking", ...text !== void 0 ? { text } : {} }];
22062
22295
  }
22063
22296
  case "message": {
22064
22297
  const text = textOf(payload["content"]);
@@ -1265,6 +1265,49 @@ function textOf(content) {
1265
1265
  const parts = content.map((b) => b && typeof b === "object" ? str(b["text"]) : void 0).filter((t) => !!t);
1266
1266
  return parts.length > 0 ? parts.join("\n") : void 0;
1267
1267
  }
1268
+ var CODEX_APPROVAL_MODES = {
1269
+ never: "auto",
1270
+ "on-request": "ask"
1271
+ };
1272
+ var CODEX_SANDBOX_MODES = {
1273
+ "workspace-write": "workspace-write",
1274
+ "read-only": "read-only",
1275
+ "danger-full-access": "danger-full-access"
1276
+ };
1277
+ var PERMISSION_SNAPSHOT_LIMIT = 32e3;
1278
+ function budgeted(steps) {
1279
+ const kept = [];
1280
+ let bytes = 0;
1281
+ for (const step of steps) {
1282
+ bytes += JSON.stringify(step ?? null).length + 1;
1283
+ if (bytes > PERMISSION_SNAPSHOT_LIMIT)
1284
+ break;
1285
+ kept.push(step);
1286
+ }
1287
+ return kept;
1288
+ }
1289
+ function unwrapExecOutput(value) {
1290
+ if (typeof value !== "string")
1291
+ return { output: value };
1292
+ let parsed;
1293
+ try {
1294
+ parsed = JSON.parse(value);
1295
+ } catch {
1296
+ return { output: value };
1297
+ }
1298
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
1299
+ return { output: value };
1300
+ const bag = parsed;
1301
+ if (!Object.prototype.hasOwnProperty.call(bag, "output"))
1302
+ return { output: value };
1303
+ const metadata = bag["metadata"];
1304
+ if (!metadata || typeof metadata !== "object")
1305
+ return { output: value };
1306
+ const exitCode = num(metadata["exit_code"]);
1307
+ if (exitCode === void 0)
1308
+ return { output: value };
1309
+ return { output: bag["output"], exitCode };
1310
+ }
1268
1311
  function jwtClaims(token) {
1269
1312
  const parts = token.split(".");
1270
1313
  if (parts.length !== 3 || !parts[1])
@@ -1291,6 +1334,29 @@ function emailFromAuth(raw) {
1291
1334
  const email = jwtClaims(token)?.["email"];
1292
1335
  return typeof email === "string" && email !== "" ? email : void 0;
1293
1336
  }
1337
+ function repositoryIdentity(url) {
1338
+ if (url === void 0)
1339
+ return void 0;
1340
+ const trimmed = url.trim();
1341
+ if (trimmed === "")
1342
+ return void 0;
1343
+ const scp = /^(?:[^@/\s]+@)?([^:/\s]+):(?!\/)(\S+)$/.exec(trimmed);
1344
+ if (scp)
1345
+ return `${scp[1]}/${stripSuffix(scp[2])}`;
1346
+ const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed) ? trimmed : `ssh://${trimmed}`;
1347
+ try {
1348
+ const parsed = new URL(withScheme);
1349
+ if (!parsed.hostname)
1350
+ return trimmed;
1351
+ const path = stripSuffix(parsed.pathname.replace(/^\/+/, ""));
1352
+ return path === "" ? parsed.hostname : `${parsed.hostname}/${path}`;
1353
+ } catch {
1354
+ return trimmed;
1355
+ }
1356
+ }
1357
+ function stripSuffix(path) {
1358
+ return path.replace(/\/+$/, "").replace(/\.git$/i, "");
1359
+ }
1294
1360
  var codexTranscript = {
1295
1361
  vendor: "codex",
1296
1362
  sessionRoots() {
@@ -1343,12 +1409,20 @@ var codexTranscript = {
1343
1409
  const uuid = `codex:${offset}`;
1344
1410
  const base = { vendor: "codex", ts, uuid };
1345
1411
  if (raw.type === "session_meta") {
1412
+ const rawGit = payload["git"];
1413
+ const git = rawGit && typeof rawGit === "object" ? rawGit : void 0;
1414
+ const gitBranch = str(git?.["branch"]);
1415
+ const gitRepositoryUrl = repositoryIdentity(str(git?.["repository_url"]));
1346
1416
  return [
1347
1417
  {
1348
1418
  ...base,
1349
1419
  kind: "session_meta",
1350
1420
  meta: {
1351
1421
  ...str(payload["cwd"]) !== void 0 ? { cwd: str(payload["cwd"]) } : {},
1422
+ // Conditional spread rather than a plain assignment: the 7 `{}` blocks and
1423
+ // the 92 absent ones must leave the key absent, not present-and-undefined.
1424
+ ...gitBranch !== void 0 ? { gitBranch } : {},
1425
+ ...gitRepositoryUrl !== void 0 ? { gitRepositoryUrl } : {},
1352
1426
  ...str(payload["cli_version"]) !== void 0 ? { version: str(payload["cli_version"]) } : {},
1353
1427
  ...str(payload["originator"]) !== void 0 ? { entrypoint: str(payload["originator"]) } : {},
1354
1428
  // A Codex subagent is a rollout of its own, a peer of its parent in the
@@ -1364,15 +1438,61 @@ var codexTranscript = {
1364
1438
  }
1365
1439
  if (raw.type === "turn_context") {
1366
1440
  const model = str(payload["model"]);
1367
- if (!model)
1441
+ const approval = str(payload["approval_policy"]);
1442
+ const sandbox = payload["sandbox_policy"];
1443
+ const sandboxMode = (() => {
1444
+ if (!sandbox || typeof sandbox !== "object")
1445
+ return void 0;
1446
+ const bag = sandbox;
1447
+ return str(bag["mode"]) ?? str(bag["type"]);
1448
+ })();
1449
+ const meta = {
1450
+ ...model !== void 0 ? { model } : {},
1451
+ ...approval !== void 0 ? { approvalMode: CODEX_APPROVAL_MODES[approval] ?? approval } : {},
1452
+ ...sandboxMode !== void 0 ? { sandboxMode: CODEX_SANDBOX_MODES[sandboxMode] ?? sandboxMode } : {}
1453
+ };
1454
+ if (Object.keys(meta).length === 0)
1368
1455
  return [];
1369
- return [{ ...base, kind: "session_meta", meta: { model } }];
1456
+ return [{ ...base, kind: "session_meta", meta }];
1370
1457
  }
1371
1458
  const kind = str(payload["type"]);
1372
1459
  if (raw.type === "event_msg" && kind === "token_count") {
1373
1460
  const usage = mapTokenCount(payload["info"]);
1374
1461
  return usage ? [{ ...base, kind: "usage", usage }] : [];
1375
1462
  }
1463
+ if (raw.type === "compacted") {
1464
+ const note = str(payload["message"]);
1465
+ return [{ ...base, kind: "compaction", ...note ? { text: note } : {} }];
1466
+ }
1467
+ if (raw.type === "world_state") {
1468
+ const state = payload["state"];
1469
+ const perms = state && typeof state === "object" ? state["permissions"] : void 0;
1470
+ if (!perms || typeof perms !== "object")
1471
+ return [];
1472
+ const list = perms["approved_command_prefixes"];
1473
+ const steps = Array.isArray(list) ? list : [];
1474
+ return [
1475
+ {
1476
+ ...base,
1477
+ kind: "permission_grant",
1478
+ input: {
1479
+ steps: budgeted(steps),
1480
+ count: steps.length,
1481
+ ...payload["full"] === true ? { full: true } : {}
1482
+ }
1483
+ }
1484
+ ];
1485
+ }
1486
+ if (raw.type === "event_msg" && kind === "turn_aborted") {
1487
+ return [
1488
+ {
1489
+ ...base,
1490
+ kind: "user_message",
1491
+ isError: true,
1492
+ errorKind: `aborted:${str(payload["reason"]) ?? "unknown"}`
1493
+ }
1494
+ ];
1495
+ }
1376
1496
  if (raw.type !== "response_item")
1377
1497
  return [];
1378
1498
  switch (kind) {
@@ -1386,18 +1506,74 @@ var codexTranscript = {
1386
1506
  ...str(payload["call_id"]) !== void 0 ? { toolCallId: str(payload["call_id"]) } : {}
1387
1507
  }
1388
1508
  ];
1389
- case "function_call_output":
1509
+ case "function_call_output": {
1510
+ const flattened = textOf(payload["output"]) ?? payload["output"];
1511
+ const { output, exitCode } = unwrapExecOutput(flattened);
1512
+ return [
1513
+ {
1514
+ ...base,
1515
+ kind: "tool_result",
1516
+ output,
1517
+ // Only where an exit status was actually found. On the 993 string outputs
1518
+ // that are not envelopes and the 144 array ones the key stays ABSENT rather
1519
+ // than `false`: the emitter tests `record.isError`, and "this tool reported
1520
+ // success" is a different claim from "nothing here reported an outcome".
1521
+ ...exitCode !== void 0 ? { isError: exitCode !== 0 } : {},
1522
+ ...str(payload["call_id"]) !== void 0 ? { toolCallId: str(payload["call_id"]) } : {}
1523
+ }
1524
+ ];
1525
+ }
1526
+ // Codex's OTHER tool-call shape, and the one it actually uses: `exec` and
1527
+ // `apply_patch` arrive as `custom_tool_call`, never as `function_call`. Measured
1528
+ // across 251 rollouts, 2179 of 5382 tool calls — 40% — took this shape, so
1529
+ // handling only `function_call` left every shell command and every file write
1530
+ // this endpoint made invisible to both the trace and anything reading it.
1531
+ case "custom_tool_call":
1532
+ return [
1533
+ {
1534
+ ...base,
1535
+ kind: "tool_call",
1536
+ toolName: str(payload["name"]) ?? "tool",
1537
+ input: payload["input"],
1538
+ ...str(payload["call_id"]) !== void 0 ? { toolCallId: str(payload["call_id"]) } : {}
1539
+ }
1540
+ ];
1541
+ case "custom_tool_call_output": {
1542
+ const flattened = textOf(payload["output"]) ?? payload["output"];
1543
+ const { output, exitCode } = unwrapExecOutput(flattened);
1390
1544
  return [
1391
1545
  {
1392
1546
  ...base,
1393
1547
  kind: "tool_result",
1394
- output: payload["output"],
1548
+ output,
1549
+ ...exitCode !== void 0 ? { isError: exitCode !== 0 } : {},
1395
1550
  ...str(payload["call_id"]) !== void 0 ? { toolCallId: str(payload["call_id"]) } : {}
1396
1551
  }
1397
1552
  ];
1553
+ }
1554
+ // The model's own web reads. 49 across the corpus in 9 rollouts — `search` 31,
1555
+ // `open_page` 16, `find_in_page` 2 — every one of which mapped to nothing, so a
1556
+ // session where the agent searched and opened pages rendered as reasoning
1557
+ // followed by an answer with no source anywhere in it. The queries and the URLs
1558
+ // are the whole point of the record: they are the evidence of what the agent
1559
+ // read from outside the machine.
1560
+ //
1561
+ // `action` is passed through whole. It is already a vendor-neutral argument bag
1562
+ // ({type, query?, queries?, url?, pattern?}) and it is small — 322 bytes at the
1563
+ // worst, 121 on average across all 49.
1564
+ //
1565
+ // Deliberately unpaired: no `toolCallId`. Only 1 of the 49 lines carries an
1566
+ // `id`, `call_id` is the pairing key everywhere else in this stream, and no
1567
+ // response_item output line ever pairs with a search — so setting one would
1568
+ // register a span in the emitter's pairing table that nothing ever closes, and
1569
+ // `finish()` would end it at pass end and stamp a multi-minute duration on a
1570
+ // search that took seconds. Left unpaired, the emitter closes it at the call
1571
+ // timestamp, which is the truthful reading.
1572
+ case "web_search_call":
1573
+ return [{ ...base, kind: "tool_call", toolName: "web_search", input: payload["action"] }];
1398
1574
  case "reasoning": {
1399
1575
  const text = textOf(payload["summary"]) ?? textOf(payload["content"]);
1400
- return text ? [{ ...base, kind: "thinking", text }] : [];
1576
+ return [{ ...base, kind: "thinking", ...text !== void 0 ? { text } : {} }];
1401
1577
  }
1402
1578
  case "message": {
1403
1579
  const text = textOf(payload["content"]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@darkhunt-security/endpoint-codex",
3
- "version": "0.9.10",
3
+ "version": "0.9.11",
4
4
  "type": "module",
5
5
  "description": "Darkhunt endpoint adapter for Codex CLI: hook codec, rollout transcript mapper, plugin manifest.",
6
6
  "bin": {
@@ -13,8 +13,8 @@
13
13
  ".codex-plugin"
14
14
  ],
15
15
  "devDependencies": {
16
- "@darkhunt-security/endpoint-contracts": "0.9.2",
17
- "@darkhunt-security/endpoint-core": "0.9.2"
16
+ "@darkhunt-security/endpoint-contracts": "0.9.3",
17
+ "@darkhunt-security/endpoint-core": "0.9.3"
18
18
  },
19
19
  "publishConfig": {
20
20
  "access": "public",