@sema-agent/core 7.26.2 → 7.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/dist/core/auto-mode-directive.d.ts +6 -4
  3. package/dist/core/auto-mode-directive.js +1 -5
  4. package/dist/core/engine-notice.d.ts +13 -6
  5. package/dist/core/runner/contracts.d.ts +31 -7
  6. package/dist/core/runner/denial-limit-arms.d.ts +17 -15
  7. package/dist/core/runner/denial-limit-arms.js +4 -1
  8. package/dist/core/runner/prepare-delegation-surface.d.ts +4 -0
  9. package/dist/core/runner/prepare-delegation-surface.js +2 -1
  10. package/dist/core/runner/prepare-gate-stations.d.ts +1 -2
  11. package/dist/core/runner/prepare-inherited-gate.d.ts +1 -2
  12. package/dist/core/runner/prepare-policy-chain.d.ts +1 -2
  13. package/dist/core/runner/prepare-task.js +2 -2
  14. package/dist/core/runner/run-leg.js +5 -1
  15. package/dist/core/tool-policy.d.ts +23 -2
  16. package/dist/core/tool-policy.js +106 -103
  17. package/dist/index.d.ts +6 -1
  18. package/dist/stores/file/background-agent-store.d.ts +4 -0
  19. package/dist/stores/file/background-agent-store.js +10 -0
  20. package/dist/stores/file/checkpoint-store.d.ts +3 -0
  21. package/dist/stores/file/checkpoint-store.js +18 -0
  22. package/dist/stores/file/corrupt-read-sink.d.ts +20 -0
  23. package/dist/stores/file/corrupt-read-sink.js +15 -0
  24. package/dist/stores/file/file-history-store.d.ts +1 -2
  25. package/dist/stores/file/file-history-store.js +3 -8
  26. package/dist/stores/file/fs-atomic.d.ts +8 -3
  27. package/dist/stores/file/fs-atomic.js +19 -2
  28. package/dist/stores/file/index.js +4 -4
  29. package/dist/stores/file/mailbox-store.d.ts +3 -0
  30. package/dist/stores/file/mailbox-store.js +24 -18
  31. package/dist/stores/file/permission-rule-store.js +18 -1
  32. package/dist/stores/file/session-policy-store.d.ts +17 -1
  33. package/dist/stores/file/session-policy-store.js +3 -12
  34. package/dist/stores/file/session-store.d.ts +1 -5
  35. package/dist/stores/file/session-store.js +9 -14
  36. package/dist/stores/file/shared-ledger.d.ts +18 -0
  37. package/dist/stores/file/shared-ledger.js +16 -2
  38. package/dist/stores/file/tool-result-store.d.ts +9 -1
  39. package/dist/stores/file/tool-result-store.js +17 -2
  40. package/dist/stores/file/workflow-journal-store.d.ts +10 -1
  41. package/dist/stores/file/workflow-journal-store.js +8 -12
  42. package/dist/stores/file/workflow-run-store.d.ts +3 -0
  43. package/dist/stores/file/workflow-run-store.js +10 -0
  44. package/dist/tools/artifact/local-stub.js +16 -1
  45. package/dist/tools/fs/dead-cwd.d.ts +48 -0
  46. package/dist/tools/fs/dead-cwd.js +56 -0
  47. package/dist/tools/fs/fs-bash.js +4 -32
  48. package/dist/tools/monitor.d.ts +4 -0
  49. package/dist/tools/monitor.js +6 -0
  50. package/package.json +1 -1
  51. package/test/export-surface.snapshot.json +3 -1
  52. package/wire-changes/core@7.26.3.json +65 -0
  53. package/wire-changes/core@7.27.0.json +152 -0
@@ -0,0 +1,56 @@
1
+ function errorMeansDead(code) {
2
+ return code === "not_found" || code === "not_directory";
3
+ }
4
+ function resolutionFailureMeansDead(code) {
5
+ return errorMeansDead(code) || code === "unknown";
6
+ }
7
+ function cancelled(signal) {
8
+ return signal?.aborted === true;
9
+ }
10
+ export async function isDeadCwd(env, dir, signal) {
11
+ const info = await env.fileInfo(dir, signal);
12
+ if (!info.ok) {
13
+ if (info.error.code !== "unknown")
14
+ return errorMeansDead(info.error.code);
15
+ const resolved = await env.canonicalPath(dir, signal);
16
+ return !resolved.ok && resolutionFailureMeansDead(resolved.error.code);
17
+ }
18
+ if (info.value.kind === "directory")
19
+ return false;
20
+ if (info.value.kind !== "symlink")
21
+ return true;
22
+ const target = await env.canonicalPath(dir, signal);
23
+ if (!target.ok)
24
+ return resolutionFailureMeansDead(target.error.code);
25
+ const followed = await env.fileInfo(target.value, signal);
26
+ if (!followed.ok)
27
+ return errorMeansDead(followed.error.code);
28
+ return followed.value.kind !== "directory" && followed.value.kind !== "symlink";
29
+ }
30
+ export async function diagnoseDeadCwd(env, toolName, effectiveCwd, root, cwdRef, signal) {
31
+ if (!(await isDeadCwd(env, effectiveCwd, signal)))
32
+ return undefined;
33
+ if (cancelled(signal))
34
+ return undefined;
35
+ if (root === undefined) {
36
+ return (`Error (${toolName}): command did not run — the working directory no longer exists (or is no longer a directory): ${effectiveCwd} ` +
37
+ `(it was likely deleted or replaced by a previous command). No reset target is known to this tool; ` +
38
+ "re-establish the working directory with a Bash `cd` to a live directory and retry.");
39
+ }
40
+ if (cwdRef && cwdRef.current !== root) {
41
+ const rootAlive = !(await isDeadCwd(env, root, signal));
42
+ if (cancelled(signal))
43
+ return undefined;
44
+ if (rootAlive) {
45
+ cwdRef.current = root;
46
+ return (`Error (${toolName}): command did not run — the working directory no longer exists (or is no longer a directory): ${effectiveCwd}\n` +
47
+ `(it was likely deleted or replaced by a previous command). The working directory has been reset to ${root}; ` +
48
+ `re-run your command (cd first if you need a different directory).`);
49
+ }
50
+ return (`Error (${toolName}): command did not run — the working directory ${effectiveCwd} AND the task root ${root} no longer exist. ` +
51
+ `The shell has no usable directory to run in; this task cannot recover its working tree.`);
52
+ }
53
+ return (`Error (${toolName}): command did not run — the task root directory no longer exists (or is no longer a directory): ${effectiveCwd}. ` +
54
+ `It was likely deleted by a previous command (e.g. rm -rf on the current directory). There is no directory to reset to; ` +
55
+ `this task cannot recover its working tree.`);
56
+ }
@@ -9,6 +9,7 @@ import { hasBackgroundShell } from "../../core/background-shell.js";
9
9
  import { delimitUntrusted } from "../../core/untrusted-text.js";
10
10
  import { MCP_IMAGE_MAX_BASE64 } from "../../core/mcp.js";
11
11
  import { imageMagicMatches, withinAnyRoot } from "./safety.js";
12
+ import { isDeadCwd, diagnoseDeadCwd } from "./dead-cwd.js";
12
13
  import { boundaryIsTheSandbox, hasDestroy, isIsolated } from "../../core/remote-env.js";
13
14
  import { ghRateLimitHint } from "./gh-rate-limit.js";
14
15
  import { resolveBashTimeoutCaps, bashTimeoutCapsSec, bashTimeoutArgRefusal, bashTimeoutParamDescription, envErrorDetail, bashMaxOutputChars, clipShellOutput, writeShellOverflowFile, shellRecoveryHint, CWD_SENTINEL, stripCwdSentinel, } from "./fs-shared.js";
@@ -98,35 +99,6 @@ function dataUriImageFromStdout(stdout) {
98
99
  }
99
100
  return undefined;
100
101
  }
101
- async function isDeadCwd(env, dir) {
102
- const info = await env.fileInfo(dir);
103
- if (!info.ok)
104
- return info.error.code === "not_found";
105
- if (info.value.kind === "directory")
106
- return false;
107
- if (info.value.kind === "symlink") {
108
- const target = await env.canonicalPath(dir);
109
- return !target.ok;
110
- }
111
- return true;
112
- }
113
- async function diagnoseDeadCwd(env, toolName, effectiveCwd, root, cwdRef) {
114
- if (!(await isDeadCwd(env, effectiveCwd)))
115
- return undefined;
116
- if (cwdRef && cwdRef.current !== root) {
117
- if (!(await isDeadCwd(env, root))) {
118
- cwdRef.current = root;
119
- return (`Error (${toolName}): command did not run — the working directory no longer exists (or is no longer a directory): ${effectiveCwd}\n` +
120
- `(it was likely deleted or replaced by a previous command). The working directory has been reset to ${root}; ` +
121
- `re-run your command (cd first if you need a different directory).`);
122
- }
123
- return (`Error (${toolName}): command did not run — the working directory ${effectiveCwd} AND the task root ${root} no longer exist. ` +
124
- `The shell has no usable directory to run in; this task cannot recover its working tree.`);
125
- }
126
- return (`Error (${toolName}): command did not run — the task root directory no longer exists (or is no longer a directory): ${effectiveCwd}. ` +
127
- `It was likely deleted by a previous command (e.g. rm -rf on the current directory). There is no directory to reset to; ` +
128
- `this task cannot recover its working tree.`);
129
- }
130
102
  const AUTO_BACKGROUND_BLOCKED_FIRST_WORDS = new Set(["sleep"]);
131
103
  function stripAutoBackgroundWrapperPrefixes(segment) {
132
104
  let s = segment.trim();
@@ -371,7 +343,7 @@ async function runShell(env, cwd, toolName, command, timeoutSec, caps, signal, c
371
343
  }
372
344
  if (!res.ok) {
373
345
  if (res.error.code === "spawn_error" || res.error.code === "shell_unavailable") {
374
- const diagnosis = await diagnoseDeadCwd(env, toolName, effectiveCwd, cwd, cwdRef);
346
+ const diagnosis = await diagnoseDeadCwd(env, toolName, effectiveCwd, cwd, cwdRef, signal);
375
347
  throw new Error(diagnosis ?? `Error (${toolName}): command did not run (${res.error.code}): ${res.error.message}`);
376
348
  }
377
349
  if (res.error.code === "transport_lost") {
@@ -656,8 +628,8 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
656
628
  if (!hasBackgroundShell(env)) {
657
629
  return errorResult("Error (Bash): this environment does not support background processes (run_in_background).");
658
630
  }
659
- if (cwdRef && (await isDeadCwd(env, cwdRef.current))) {
660
- const diagnosis = await diagnoseDeadCwd(env, "Bash", cwdRef.current, rootCanonical, cwdRef);
631
+ if (cwdRef && (await isDeadCwd(env, cwdRef.current, ctx.signal))) {
632
+ const diagnosis = await diagnoseDeadCwd(env, "Bash", cwdRef.current, rootCanonical, cwdRef, ctx.signal);
661
633
  if (diagnosis !== undefined)
662
634
  throw new Error(diagnosis);
663
635
  }
@@ -78,5 +78,9 @@ export interface MonitorToolOptions {
78
78
  * could only see the env lane, so a deployment retaining per spec still got the "session ends it"
79
79
  * wording in the persistent-watch receipt. Purely a RECEIPT input — enforcement stays at the sweep. */
80
80
  retainBackgroundProcesses?: boolean;
81
+ /** The task root in the SAME canonical spelling `createBashTool` is built with, so a dead-cwd self-heal
82
+ * lands both tools on one directory. Absent ⇒ the diagnosis says it has no reset target, rather than
83
+ * naming the cwd "the task root" or promising a recovery it cannot make. */
84
+ rootCanonical?: string;
81
85
  }
82
86
  export declare function createMonitorTool(env: ExecutionEnv, opts: MonitorToolOptions): AgentTool;
@@ -3,6 +3,7 @@ import { defineTool, errorResult } from "../core/tools.js";
3
3
  import { hasBackgroundShell } from "../core/background-shell.js";
4
4
  import { MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_MAX_TIMEOUT_MS, } from "../core/task-registry.js";
5
5
  import { toolFace } from "../core/tool-catalog-entries.js";
6
+ import { isDeadCwd, diagnoseDeadCwd } from "./fs/dead-cwd.js";
6
7
  import { thrownText } from "../core/thrown-value.js";
7
8
  const MONITOR_DESCRIPTION = "Run a shell command in the background and watch its stdout as a stream of events.\n" +
8
9
  "\n" +
@@ -71,6 +72,11 @@ export function createMonitorTool(env, opts) {
71
72
  const refused = await opts.shellReadAdmission?.("Monitor", ctx.toolCallId, command, ctx.signal);
72
73
  if (refused !== undefined)
73
74
  return refused;
75
+ if (opts.cwdRef !== undefined && (await isDeadCwd(env, opts.cwdRef.current, ctx.signal))) {
76
+ const diagnosis = await diagnoseDeadCwd(env, "Monitor", opts.cwdRef.current, opts.rootCanonical, opts.cwdRef, ctx.signal);
77
+ if (diagnosis !== undefined)
78
+ return errorResult(diagnosis);
79
+ }
74
80
  if (ctx.signal?.aborted === true)
75
81
  return errorResult("Error (Monitor): the call was cancelled before the watch started.");
76
82
  const spawn = await env.spawnBackground(command, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "7.26.2",
3
+ "version": "7.27.0",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "_comment": "design/87 L3 — frozen public export surface of src/index.ts (name -> kind). DO NOT edit by hand to silence a red test. A removed/changed entry = a SemVer-BREAKING change; bump MAJOR and update this fixture in the SAME commit (design/87 §4.2 / §5.2). Regenerate via REGEN in test/export-surface.test.ts.",
3
3
  "_tierComment": "#435 v1 — machine-readable layering of the public surface: stable = demonstrated by README.md / src/examples; internal = an `Internal`-marked name or a runner/engine deep-subtree declaration (the model seam src/engine/llm is excluded — it is the BYOM contract, not an engine internal); advanced = a supported export the front door does not walk you through. THIS IS AN INITIAL HEURISTIC, derived mechanically and expected to be refined ticket by ticket: no human reviewed these 1700+ entries one by one, and nothing here claims otherwise. Known bias: a short or English-word export name (ok, err, Result, Usage) can match ordinary prose in README.md and land `stable` on a coincidence. Every export MUST carry a tier — a new export with no row fails the gate in export-surface.test.ts.",
4
- "count": 2449,
4
+ "count": 2450,
5
5
  "exports": {
6
6
  "A2ATaskState": "type",
7
7
  "A2ATaskStateReversal": "type",
@@ -84,6 +84,7 @@
84
84
  "AggregateBudgetOptions": "interface",
85
85
  "AnthropicBrainConfig": "interface",
86
86
  "AppendLog": "class",
87
+ "ApprovalSeatOutcome": "type",
87
88
  "ApproverPosture": "type",
88
89
  "ArtifactAction": "type",
89
90
  "ArtifactCallContext": "interface",
@@ -2535,6 +2536,7 @@
2535
2536
  "AggregateBudgetOptions": "advanced",
2536
2537
  "AnthropicBrainConfig": "advanced",
2537
2538
  "AppendLog": "advanced",
2539
+ "ApprovalSeatOutcome": "advanced",
2538
2540
  "ApproverPosture": "advanced",
2539
2541
  "ArtifactAction": "advanced",
2540
2542
  "ArtifactCallContext": "advanced",
@@ -0,0 +1,65 @@
1
+ {
2
+ "repo": "core",
3
+ "version": "7.26.3",
4
+ "from": "f41acbdc6dace7426c1a4dafa1a9c184d010cdb7",
5
+ "to": "v7.26.3",
6
+ "added": [],
7
+ "removed": [],
8
+ "changed": [],
9
+ "narrowed": [
10
+ {
11
+ "id": "F1",
12
+ "source": "behaviorFlips.summary",
13
+ "text": "NARROWING (#1018, P3, Monitor tool — user-visible): the watch tool probes the tracked working directory before it spawns, through the same probe and diagnosis the shell's legs use (`src/tools/fs/dead-cwd.ts`, one mint). A dead cwd (gone / replaced by a file / standing under a file / a symlink that does not resolve to a directory) refuses loudly with the shell's dead-cwd sentence and, when the task root is alive, resets the tracked cwd to the root (self-heal, same as Bash); a mount that was handed no root says it has no reset target. `MonitorToolOptions.rootCanonical?` (additive) carries the root in the same canonical spelling `createBashTool` receives; `prepareTask` wires it. Two probe shapes got truthful on BOTH legs: an ENOTDIR-class probe error and a symlink retargeted at a file now count as dead."
14
+ }
15
+ ],
16
+ "closedSetMembers": [],
17
+ "closedSetsUnreadable": [],
18
+ "typeKeys": [
19
+ {
20
+ "type": "MonitorToolOptions",
21
+ "at": [
22
+ "src/tools/monitor.ts:61"
23
+ ],
24
+ "added": [
25
+ "rootCanonical"
26
+ ],
27
+ "removed": [],
28
+ "breaking": false,
29
+ "criteriaCovered": true
30
+ }
31
+ ],
32
+ "typeKeysUnreadableArms": [],
33
+ "typeKeysFlattenedCount": 212,
34
+ "declarationMoves": [],
35
+ "mintMoves": [],
36
+ "criteriaObserves": [
37
+ {
38
+ "kind": "typeMember",
39
+ "name": "MonitorToolOptions.rootCanonical",
40
+ "assertions": [
41
+ "G1.a"
42
+ ]
43
+ },
44
+ {
45
+ "kind": "typeMember",
46
+ "name": "RunInternals.inheritedGate",
47
+ "assertions": [
48
+ "G2.a"
49
+ ]
50
+ }
51
+ ],
52
+ "consumersHint": {
53
+ "cli": [
54
+ "MonitorToolOptions"
55
+ ],
56
+ "server": [
57
+ "MonitorToolOptions"
58
+ ],
59
+ "sdk": [],
60
+ "note": "scanned cli@7ea76c7c server@bcf41646 sdk@2bff51e — a name listed is MENTIONED there today, which is evidence of consumption, not proof of it"
61
+ },
62
+ "breakingCount": 0,
63
+ "breakingItems": [],
64
+ "toResolvedAt": "ee393c7d629f3ae8a2f3dbd906441aab40c31cd1"
65
+ }
@@ -0,0 +1,152 @@
1
+ {
2
+ "repo": "core",
3
+ "version": "7.27.0",
4
+ "from": "e90527d7630a7098366f7843b8aae7346ea0a07c",
5
+ "to": "v7.27.0",
6
+ "added": [
7
+ {
8
+ "name": "ApprovalSeatOutcome",
9
+ "kind": "type",
10
+ "tier": "advanced",
11
+ "at": [
12
+ "src/core/tool-policy.ts:1318",
13
+ "src/index.ts:1345"
14
+ ],
15
+ "criteriaCovered": true,
16
+ "breaking": false
17
+ }
18
+ ],
19
+ "removed": [],
20
+ "changed": [],
21
+ "narrowed": [
22
+ {
23
+ "id": "F1",
24
+ "source": "behaviorFlips.summary",
25
+ "text": "NARROWING (#1019, P2, approval face — durable): the auto-mode classifier's denial-limit fallback, when PARKED durably and then settled by nobody (a policy refusal, or no approver), stops the leg with `classifier.denial_limit` exactly as the synchronous station does — the deny stands as the call's result, the stop ends the leg; a person's refusal on the same park does NOT stop (one table, `SETTLEMENT_NOBODY_JUDGED`, both stations)."
26
+ },
27
+ {
28
+ "id": "F3",
29
+ "source": "behaviorFlips.summary",
30
+ "text": "NARROWING (#1002 ①, C-R80, prompt face): the engine-minted approval-posture directive is EXCLUSIVE, bypass first — a run declaring both `autoModeRequested` and `approverPosture:\"bypass\"` receives the bypass segment alone (CC 2.1.259 `case\"auto_mode\"` arm order); single-seat runs are byte-unchanged. Cross-leg coexistence of two segments (a later leg adding the bypass seat after an auto-only leg already stated auto) is unchanged and recorded as KL-1002c."
31
+ },
32
+ {
33
+ "id": "F4",
34
+ "source": "behaviorFlips.summary",
35
+ "text": "NARROWING (#1013, P2, file stores): every JSONL read-back judges the record's SHAPE — a line that parses but is not an object (`123`, `\"str\"`, `null`, `[1]`) is disclosed once through the store's corrupt-read seat with a `CORRUPT_BODY_FORMS` word and skipped, never handed to a consumer; the eight roster faces of `storeReadBackShapeContract` are bound; every tolerant file store has one disclosure seat (one shared guard, sync throw and async rejection both contained); an artifact-ledger idempotency-replay record with a bad shape is refused at the ledger door; the permission-rule adoption marker is judged per arm."
36
+ }
37
+ ],
38
+ "closedSetMembers": [],
39
+ "closedSetsUnreadable": [],
40
+ "typeKeys": [
41
+ {
42
+ "type": "FileBackgroundAgentStoreOptions",
43
+ "at": [
44
+ "src/stores/file/background-agent-store.ts:43"
45
+ ],
46
+ "added": [
47
+ "onCorruptRead"
48
+ ],
49
+ "removed": [],
50
+ "breaking": false,
51
+ "criteriaCovered": false
52
+ },
53
+ {
54
+ "type": "FileCheckpointStoreOptions",
55
+ "at": [
56
+ "src/stores/file/checkpoint-store.ts:86"
57
+ ],
58
+ "added": [
59
+ "onCorruptRead"
60
+ ],
61
+ "removed": [],
62
+ "breaking": false,
63
+ "criteriaCovered": false
64
+ },
65
+ {
66
+ "type": "FileWorkflowRunStoreOptions",
67
+ "at": [
68
+ "src/stores/file/workflow-run-store.ts:47"
69
+ ],
70
+ "added": [
71
+ "onCorruptRead"
72
+ ],
73
+ "removed": [],
74
+ "breaking": false,
75
+ "criteriaCovered": false
76
+ },
77
+ {
78
+ "type": "Prepared",
79
+ "at": [
80
+ "src/core/runner/contracts.ts:231"
81
+ ],
82
+ "via": "Runner.SuspendReap.env",
83
+ "added": [
84
+ "stopForDenialLimit"
85
+ ],
86
+ "removed": [],
87
+ "breaking": false,
88
+ "criteriaCovered": false
89
+ }
90
+ ],
91
+ "typeKeysUnreadableArms": [],
92
+ "typeKeysFlattenedCount": 212,
93
+ "declarationMoves": [
94
+ {
95
+ "name": "StopForDenialLimit",
96
+ "kind": null,
97
+ "onSurface": false,
98
+ "from": [
99
+ "src/core/runner/denial-limit-arms.ts"
100
+ ],
101
+ "to": [
102
+ "src/core/runner/contracts.ts"
103
+ ]
104
+ }
105
+ ],
106
+ "mintMoves": [],
107
+ "criteriaObserves": [
108
+ {
109
+ "kind": "taskEventField",
110
+ "name": "tool_end.gate",
111
+ "assertions": [
112
+ "G1.a"
113
+ ]
114
+ },
115
+ {
116
+ "kind": "typeMember",
117
+ "name": "ApprovalSeatOutcome.settledBy",
118
+ "assertions": [
119
+ "G2.a"
120
+ ]
121
+ },
122
+ {
123
+ "kind": "typeMember",
124
+ "name": "FileMailboxStoreOptions.onCorruptRead",
125
+ "assertions": [
126
+ "G4.a"
127
+ ]
128
+ },
129
+ {
130
+ "kind": "typeMember",
131
+ "name": "TaskSpec.approverPosture",
132
+ "assertions": [
133
+ "G3.a"
134
+ ]
135
+ }
136
+ ],
137
+ "consumersHint": {
138
+ "cli": [
139
+ "Prepared"
140
+ ],
141
+ "server": [
142
+ "FileBackgroundAgentStoreOptions",
143
+ "FileCheckpointStoreOptions",
144
+ "FileWorkflowRunStoreOptions"
145
+ ],
146
+ "sdk": [],
147
+ "note": "scanned cli@17a6b331 server@73a73469 sdk@bfb4fd0 — a name listed is MENTIONED there today, which is evidence of consumption, not proof of it"
148
+ },
149
+ "breakingCount": 0,
150
+ "breakingItems": [],
151
+ "toResolvedAt": "bc100bca1f15c0658cc4d0f615bb31aca7027c0b"
152
+ }