@sema-agent/core 5.65.0 → 7.0.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 (165) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/dist/agents/subagent.d.ts +2 -2
  3. package/dist/agents/subagent.js +11 -0
  4. package/dist/agents/verify.d.ts +1 -1
  5. package/dist/brain/anthropic.js +1 -1
  6. package/dist/brain/errors.d.ts +29 -0
  7. package/dist/brain/errors.js +20 -0
  8. package/dist/brain/open-responses.js +2 -2
  9. package/dist/brain/route-adjudicator.d.ts +8 -1
  10. package/dist/brain/route-adjudicator.js +1 -0
  11. package/dist/brain/status-sink.js +12 -1
  12. package/dist/brain/stream-engine.js +17 -6
  13. package/dist/core/auto-compaction.d.ts +26 -0
  14. package/dist/core/auto-compaction.js +7 -2
  15. package/dist/core/auto-mode-arming.d.ts +138 -0
  16. package/dist/core/auto-mode-arming.js +181 -0
  17. package/dist/core/auto-mode-defaults.d.ts +13 -0
  18. package/dist/core/auto-mode-defaults.js +5 -0
  19. package/dist/core/auto-mode-prompt.d.ts +14 -3
  20. package/dist/core/auto-mode-prompt.js +10 -7
  21. package/dist/core/auto-mode-rebuild.d.ts +75 -0
  22. package/dist/core/auto-mode-rebuild.js +41 -0
  23. package/dist/core/auto-mode.d.ts +15 -0
  24. package/dist/core/auto-mode.js +4 -2
  25. package/dist/core/checkpoint-store.d.ts +18 -0
  26. package/dist/core/context-edit.d.ts +47 -5
  27. package/dist/core/context-guard.d.ts +1 -1
  28. package/dist/core/file-history-retention.d.ts +106 -0
  29. package/dist/core/file-history-retention.js +36 -0
  30. package/dist/core/file-history-store.d.ts +768 -0
  31. package/dist/core/file-history-store.js +880 -0
  32. package/dist/core/governance-codes.d.ts +13 -3
  33. package/dist/core/governance-codes.js +15 -0
  34. package/dist/core/hooks.d.ts +39 -7
  35. package/dist/core/hooks.js +38 -21
  36. package/dist/core/lsp.d.ts +2 -2
  37. package/dist/core/mcp.d.ts +29 -7
  38. package/dist/core/memory-engine/consolidation-driver.d.ts +11 -0
  39. package/dist/core/memory-engine/consolidation-driver.js +71 -4
  40. package/dist/core/memory-engine/consolidation.d.ts +25 -2
  41. package/dist/core/memory-engine/consolidation.js +4 -1
  42. package/dist/core/memory-engine/distiller.d.ts +84 -1
  43. package/dist/core/memory-engine/distiller.js +68 -0
  44. package/dist/core/memory-engine/dual-root.js +3 -0
  45. package/dist/core/memory-engine/engine.d.ts +328 -15
  46. package/dist/core/memory-engine/engine.js +355 -29
  47. package/dist/core/memory-engine/file-backend.d.ts +30 -0
  48. package/dist/core/memory-engine/file-backend.js +14 -13
  49. package/dist/core/memory-engine/frontmatter.d.ts +22 -1
  50. package/dist/core/memory-engine/frontmatter.js +3 -0
  51. package/dist/core/memory-engine/header-hints.d.ts +5 -0
  52. package/dist/core/memory-engine/index.d.ts +5 -4
  53. package/dist/core/memory-engine/index.js +5 -4
  54. package/dist/core/memory-engine/layout.d.ts +88 -2
  55. package/dist/core/memory-engine/layout.js +112 -3
  56. package/dist/core/memory-engine/provenance-wording.d.ts +7 -0
  57. package/dist/core/memory-engine/provenance-wording.js +3 -0
  58. package/dist/core/memory-engine/tools.d.ts +89 -8
  59. package/dist/core/memory-engine/tools.js +263 -22
  60. package/dist/core/memory-engine/types.d.ts +64 -1
  61. package/dist/core/memory-recall.d.ts +6 -0
  62. package/dist/core/memory.d.ts +27 -1
  63. package/dist/core/memory.js +16 -2
  64. package/dist/core/permission-rule-consent.d.ts +62 -2
  65. package/dist/core/permission-rule-consent.js +105 -14
  66. package/dist/core/permission-rule-model.d.ts +118 -16
  67. package/dist/core/permission-rule-model.js +57 -9
  68. package/dist/core/permission-rule-session.d.ts +124 -0
  69. package/dist/core/permission-rule-session.js +121 -0
  70. package/dist/core/permission-rule-store.d.ts +65 -2
  71. package/dist/core/permission-rule-store.js +75 -16
  72. package/dist/core/permission-rule-sync.d.ts +9 -0
  73. package/dist/core/permission-rule-sync.js +52 -19
  74. package/dist/core/retention-policy.d.ts +9 -0
  75. package/dist/core/retention-policy.js +5 -2
  76. package/dist/core/retention.d.ts +13 -2
  77. package/dist/core/runner/assemble-result.d.ts +19 -1
  78. package/dist/core/runner/assemble-result.js +17 -2
  79. package/dist/core/runner/compaction-call-options.d.ts +93 -0
  80. package/dist/core/runner/compaction-call-options.js +3 -0
  81. package/dist/core/runner/memory-capture-optout.d.ts +80 -0
  82. package/dist/core/runner/memory-capture-optout.js +53 -0
  83. package/dist/core/runner/prepare-config-doors.d.ts +5 -0
  84. package/dist/core/runner/prepare-config-doors.js +16 -0
  85. package/dist/core/runner/prepare-hands-readface.d.ts +110 -5
  86. package/dist/core/runner/prepare-hands-readface.js +99 -7
  87. package/dist/core/runner/prepare-memory.d.ts +88 -0
  88. package/dist/core/runner/prepare-memory.js +305 -24
  89. package/dist/core/runner/prepare-task.d.ts +141 -1
  90. package/dist/core/runner/prepare-task.js +478 -81
  91. package/dist/core/runner/runtask.d.ts +9 -20
  92. package/dist/core/runner/runtask.js +133 -96
  93. package/dist/core/runner/session-file-state-replay.d.ts +18 -10
  94. package/dist/core/runner/session-file-state-replay.js +52 -1
  95. package/dist/core/runner/tool-disclosure.js +2 -1
  96. package/dist/core/runner/turn-attachments.d.ts +22 -12
  97. package/dist/core/session-store.d.ts +1 -1
  98. package/dist/core/session-store.js +6 -1
  99. package/dist/core/session.d.ts +34 -1
  100. package/dist/core/store-contracts/file-history-store-contract.d.ts +3 -0
  101. package/dist/core/store-contracts/file-history-store-contract.js +720 -0
  102. package/dist/core/store-contracts/permission-rule-sync-contract.js +15 -1
  103. package/dist/core/task-registry-shared.js +11 -1
  104. package/dist/core/tool-errors.js +1 -0
  105. package/dist/core/tool-policy.d.ts +172 -1
  106. package/dist/core/tool-policy.js +32 -1
  107. package/dist/core/tool-result-store.js +2 -1
  108. package/dist/core/trace.d.ts +31 -2
  109. package/dist/core/types.d.ts +795 -89
  110. package/dist/core/types.js +4 -3
  111. package/dist/core/untrusted-text.d.ts +1 -1
  112. package/dist/core/untrusted-text.js +8 -0
  113. package/dist/core/workflow-run-store-contract.js +6 -2
  114. package/dist/core/workflow-run-store.d.ts +4 -1
  115. package/dist/engine/compaction/compaction.d.ts +88 -10
  116. package/dist/engine/compaction/compaction.js +109 -30
  117. package/dist/engine/execution-env/node-execution-env.d.ts +9 -1
  118. package/dist/engine/execution-env/node-execution-env.js +28 -0
  119. package/dist/engine/harness/agent-harness.d.ts +52 -1
  120. package/dist/engine/harness/agent-harness.js +36 -1
  121. package/dist/engine/harness/types.d.ts +26 -1
  122. package/dist/engine/llm/types.d.ts +50 -4
  123. package/dist/engine/loop/agent-loop.d.ts +5 -1
  124. package/dist/engine/loop/agent-loop.js +25 -0
  125. package/dist/engine/loop/types.d.ts +19 -0
  126. package/dist/engine/lsp/node-lsp-manager.d.ts +1 -1
  127. package/dist/engine/session/session.js +1 -1
  128. package/dist/index.d.ts +20 -9
  129. package/dist/index.js +16 -7
  130. package/dist/orchestration/run-workflow-tool.d.ts +20 -2
  131. package/dist/orchestration/run-workflow-tool.js +22 -3
  132. package/dist/orchestration/workflow-governance.d.ts +59 -1
  133. package/dist/orchestration/workflow-governance.js +61 -8
  134. package/dist/orchestration/workflow-meta.d.ts +4 -2
  135. package/dist/orchestration/workflow-primitives.js +56 -13
  136. package/dist/orchestration/workflow-types.d.ts +78 -2
  137. package/dist/orchestration/workflow.d.ts +20 -0
  138. package/dist/orchestration/workflow.js +163 -14
  139. package/dist/prompt-assembly/event-registry.js +1 -1
  140. package/dist/prompts/default.d.ts +7 -7
  141. package/dist/stores/file/file-history-store.d.ts +368 -0
  142. package/dist/stores/file/file-history-store.js +1248 -0
  143. package/dist/stores/file/index.d.ts +22 -13
  144. package/dist/stores/file/index.js +4 -4
  145. package/dist/stores/file/permission-rule-store.d.ts +11 -0
  146. package/dist/stores/file/permission-rule-store.js +23 -9
  147. package/dist/stores/file/strategy-store.d.ts +3 -3
  148. package/dist/tools/fs/bash-readonly-classifier.d.ts +87 -3
  149. package/dist/tools/fs/bash-readonly-classifier.js +106 -4
  150. package/dist/tools/fs/fs-bash.js +9 -5
  151. package/dist/tools/fs/fs-shared.d.ts +52 -1
  152. package/dist/tools/fs/fs-shared.js +14 -0
  153. package/dist/tools/fs/fs-write.d.ts +5 -5
  154. package/dist/tools/fs/fs-write.js +71 -14
  155. package/dist/tools/fs/index.d.ts +6 -1
  156. package/dist/tools/fs/index.js +1 -1
  157. package/dist/tools/web.js +2 -1
  158. package/package.json +5 -1
  159. package/test/export-surface.snapshot.json +169 -23
  160. package/dist/core/file-snapshot-store.d.ts +0 -165
  161. package/dist/core/file-snapshot-store.js +0 -259
  162. package/dist/core/store-contracts/file-snapshot-store-contract.d.ts +0 -13
  163. package/dist/core/store-contracts/file-snapshot-store-contract.js +0 -134
  164. package/dist/stores/file/file-snapshot-store.d.ts +0 -58
  165. package/dist/stores/file/file-snapshot-store.js +0 -353
@@ -74,6 +74,35 @@ export declare const BASH_READONLY_CONFINEMENT_NOTE = "bash_readonly is confined
74
74
  * false-stale defect for those targets. Round-tripping the real bytes sidesteps the case analysis
75
75
  * and stays correct if the encoding rules ever change. */
76
76
  export declare function persistedTextOf(encoded: string | Uint8Array): string;
77
+ /**
78
+ * #483 — one APPLIED edit as the Edit tool's `details` card records it: the RESOLVED old/new strings
79
+ * (post quote/escape resolution, post deletion-newline widening) in the normalized originalFile
80
+ * coordinate. This is the card shape a transcript replay must re-apply, so the application step is a
81
+ * single source shared by the Edit tool's own apply loop and the replay — two spellings of "apply"
82
+ * could silently drift (e.g. one expanding `$&` and one not) and the replay's recomputed hash would
83
+ * refuse edits the tool actually made.
84
+ */
85
+ export interface RecordedEdit {
86
+ oldString: string;
87
+ newString: string;
88
+ replaceAll: boolean;
89
+ }
90
+ /**
91
+ * #483 — apply ONE recorded edit mechanically (no matching layers, no validation: the recording site
92
+ * already resolved and validated the strings; a replay consumer that cannot prove `oldString` is
93
+ * present must fall back to retracting its record, never guess here).
94
+ *
95
+ * Two load-bearing details, both shared with the Edit tool's historical inline apply:
96
+ * - Function-form replacement: the callback's return value is NOT interpreted as a replacement
97
+ * pattern, so `$&`/`$$`/`` $` ``/`$'` in `newString` are written literally (S-1; the replaceAll
98
+ * split/join path was always literal).
99
+ * - `oldString === ""` is the whole-content OVERWRITE shape: the Edit tool's whitespace-only-file
100
+ * overwrite lane records `{oldString: "", newString: <full new text>}` after REPLACING the file's
101
+ * content wholesale (fs-write.ts, the `old_string:""` existing-file branch). The tool's own apply
102
+ * loop never reaches this arm (checkEditMatch refuses an empty old_string before application), so
103
+ * it exists for the replay consumer alone — a plain `replace("", ...)` would PREPEND instead.
104
+ */
105
+ export declare function applyRecordedEdit(text: string, edit: RecordedEdit): string;
77
106
  /** fidelity-sweep F3 (stopgap wording): render the read-before-edit refusal, appending the slice/bash
78
107
  * escape hatch when the target is over the Read byte cap — a bare "read it first" is un-followable
79
108
  * there (a default whole-file Read refuses such files). Best-effort: an un-stattable file keeps the
@@ -114,7 +143,7 @@ export declare function resolveAutoDownsampler(): Promise<ImageDownsampler | und
114
143
  * proxy — `KMu` @279761 pre-filters with a PER-EXTENSION chars-per-token coefficient (json/jsonl = 2,
115
144
  * i.e. half of the 4 assumed here) and then confirms with a REAL tokenizer (`BLd` @518589); the
116
145
  * budget itself is also overridable per call (`fileReadingLimits`) and by the `tengu_amber_wren` gate
117
- * (`eze` @335063), neither of which has a sema lane. Consequence, stated rather than hidden: on
146
+ * (`eze` @335063 (cli250.js:39563)), neither of which has a sema lane. Consequence, stated rather than hidden: on
118
147
  * token-dense text (JSON/CJK/base64) this proxy UNDER-counts, so sema serves a page CC would have cut
119
148
  * further; on token-sparse text it over-counts and paginates earlier than CC. A real tokenizer here
120
149
  * would need a provider-neutral counting seam (BYOM — no single vocabulary), which is why the proxy
@@ -416,6 +445,28 @@ export declare function seedReadFileStateFromContext(state: ReadFileState, key:
416
445
  * compaction. This text lives in the transcript and does not.
417
446
  */
418
447
  export declare function seedReadFileStateFromTranscript(state: ReadFileState, key: string, content: string, lastReadAt: number): void;
448
+ /**
449
+ * #483 rung-1 — copy a SESSION-inherited read-file state into a new task's map, under the new task's
450
+ * own containment judgment (CC parity: readFileState is session-scoped — minted once per REPL session,
451
+ * mutated across turns, with no turn-boundary or abort clear site; sema's per-task map inherits the
452
+ * previous turn's live state instead).
453
+ *
454
+ * Two rules, each a claim inheritance cannot honestly skip:
455
+ * - **Every key passes the CURRENT task's own key judgment** (`judgeKey` = the same resolveKey
456
+ * containment the transcript-replay seed uses: write roots ∪ read roots, resolved read face). A
457
+ * root or face narrowed between the turns must narrow what the state remembers, exactly as the
458
+ * replay leg already judges — an unjudged copy would smuggle out-of-face entries into the task
459
+ * (and into its checkpoints).
460
+ * - **`seededFromContext` entries do not inherit.** That flag claims the file's text lives in the
461
+ * system-prompt lane OF THIS TASK — a claim only this task's own startup seeding (T10) can make.
462
+ * A still-declared file is re-seeded the same turn (no loss); a no-longer-declared one must not
463
+ * keep answering reads with an "already in your context" reminder that is no longer true.
464
+ *
465
+ * Entries are copied by REFERENCE (ReadEntry values are never mutated in place — every write-back
466
+ * `set`s a fresh object), so recorded facts (view / isPartialView / lastReadAt) ride verbatim: the
467
+ * dedup stub and the partial-view refusal keep working across turns exactly as inside one task.
468
+ */
469
+ export declare function inheritSessionReadFileState(target: ReadFileState, inherited: ReadFileState, judgeKey: (recordedKey: string) => Promise<string | undefined>): Promise<void>;
419
470
  /**
420
471
  * RB-197 (form-one audit, CC 2.1.220 @388663-388664): a landed compaction summarizes old Read results OUT
421
472
  * of the context, so every non-seeded read-state entry must be dropped — CC snapshots then
@@ -39,6 +39,11 @@ export const BASH_READONLY_CONFINEMENT_NOTE = "bash_readonly is confined to the
39
39
  export function persistedTextOf(encoded) {
40
40
  return decodeTextBytes(typeof encoded === "string" ? Buffer.from(encoded, "utf8") : encoded).text;
41
41
  }
42
+ export function applyRecordedEdit(text, edit) {
43
+ if (edit.oldString === "")
44
+ return edit.newString;
45
+ return edit.replaceAll ? text.split(edit.oldString).join(edit.newString) : text.replace(edit.oldString, () => edit.newString);
46
+ }
42
47
  export async function notReadRefusalText(env, toolName, key, v, signal, fallbackHint) {
43
48
  const base = `Error (${toolName}): ${v.message}`;
44
49
  if (v.partialView === true && !isNotebookPath(key))
@@ -276,6 +281,15 @@ export function seedReadFileStateFromTranscript(state, key, content, lastReadAt)
276
281
  const text = normalizeFileText(content);
277
282
  state.set(key, { hash: sha256(text), totalLines: countLines(text), truncated: false, lastReadAt });
278
283
  }
284
+ export async function inheritSessionReadFileState(target, inherited, judgeKey) {
285
+ for (const [key, entry] of inherited) {
286
+ if (entry.seededFromContext === true)
287
+ continue;
288
+ const judged = await judgeKey(key);
289
+ if (judged !== undefined)
290
+ target.set(judged, entry);
291
+ }
292
+ }
279
293
  export function applyCompactionToReadFileState(state, attachedComplete, preserveKeys = []) {
280
294
  const preserve = new Set(preserveKeys);
281
295
  for (const [k, v] of [...state]) {
@@ -1,5 +1,5 @@
1
1
  import type { AgentTool, ExecutionEnv } from "../../internal/harness-types.js";
2
- import type { BeforeWriteHook } from "../../core/types.js";
2
+ import type { BeforeWriteHook, TrackFileEditHook } from "../../core/types.js";
3
3
  import { type ReadFileState } from "./safety.js";
4
4
  import { type CwdRef } from "./fs-shared.js";
5
5
  /**
@@ -11,9 +11,9 @@ import { type CwdRef } from "./fs-shared.js";
11
11
  * MemoryEngine's write gate (`MemoryEngine.gateWrite`) — a non-memory path passes with one string
12
12
  * prefix comparison there (零开销直通). Absent hook ⇒ byte-identical behavior.
13
13
  */
14
- export type { BeforeWriteRequest, BeforeWriteResult, BeforeWriteHook } from "../../core/types.js";
15
- export declare function createEditFileTool(env: ExecutionEnv, state: ReadFileState, rootCanonical: string, cwdRef?: CwdRef, additionalRoots?: readonly string[], beforeWrite?: BeforeWriteHook): AgentTool;
16
- export declare function createWriteFileTool(env: ExecutionEnv, state: ReadFileState, rootCanonical: string, cwdRef?: CwdRef, additionalRoots?: readonly string[], beforeWrite?: BeforeWriteHook): AgentTool;
14
+ export type { BeforeWriteRequest, BeforeWriteResult, BeforeWriteHook, TrackEditRequest, TrackEditResult, TrackFileEditHook } from "../../core/types.js";
15
+ export declare function createEditFileTool(env: ExecutionEnv, state: ReadFileState, rootCanonical: string, cwdRef?: CwdRef, additionalRoots?: readonly string[], beforeWrite?: BeforeWriteHook, trackEdit?: TrackFileEditHook): AgentTool;
16
+ export declare function createWriteFileTool(env: ExecutionEnv, state: ReadFileState, rootCanonical: string, cwdRef?: CwdRef, additionalRoots?: readonly string[], beforeWrite?: BeforeWriteHook, trackEdit?: TrackFileEditHook): AgentTool;
17
17
  /**
18
18
  * design v1.163 — NotebookEdit: replace/insert/delete a single cell in a .ipynb. CC-parity tool over the SAME hand-band
19
19
  * safety skeleton as Edit/Write (resolveKey containment → requireRead read-before-edit → checkStale content-hash
@@ -21,4 +21,4 @@ export declare function createWriteFileTool(env: ExecutionEnv, state: ReadFileSt
21
21
  * .ipynb as the RB-227 cell projection but records read state (hash/totalLines) in the RAW notebook-text
22
22
  * coordinate — that raw-coordinate read record is what this tool's freshness check depends on.
23
23
  */
24
- export declare function createNotebookEditTool(env: ExecutionEnv, state: ReadFileState, rootCanonical: string, cwdRef?: CwdRef, additionalRoots?: readonly string[], beforeWrite?: BeforeWriteHook): AgentTool;
24
+ export declare function createNotebookEditTool(env: ExecutionEnv, state: ReadFileState, rootCanonical: string, cwdRef?: CwdRef, additionalRoots?: readonly string[], beforeWrite?: BeforeWriteHook, trackEdit?: TrackFileEditHook): AgentTool;
@@ -3,8 +3,22 @@ import { Type } from "typebox";
3
3
  import { defineTool, errorResult } from "../../core/tools.js";
4
4
  import { sha256, resolveKey, violationText, violationDetails, requireRead, checkStale, checkEditMatch, checkNoChange, fileArgPath, resolveQuoteMatch, adaptNewStringQuotes, resolveEscapeMatch, adaptNewStringEscapes, escapeMatchWasAttempted, ESCAPE_MATCH_MISS_NOTE, deletionOldString, countOccurrences, READ_REFUSED_ESCAPE_HINT, } from "./safety.js";
5
5
  import { decodeTextBytes, encodeTextForFile, normalizeEditText, normalizeFileText } from "./encoding.js";
6
- import { MAX_EDIT_BYTES, decodeEditBytes, tooLargeToEditMessage, truncatedUtf16BodyMessage, persistedTextOf, notReadRefusalText, enoentMessage, FILE_STATE_TRAILER, FILE_PATH_PARAMS, ipynbRedirect, countLines, } from "./fs-shared.js";
6
+ import { MAX_EDIT_BYTES, decodeEditBytes, tooLargeToEditMessage, truncatedUtf16BodyMessage, persistedTextOf, notReadRefusalText, enoentMessage, FILE_STATE_TRAILER, FILE_PATH_PARAMS, ipynbRedirect, countLines, applyRecordedEdit, } from "./fs-shared.js";
7
7
  async function envFinalWrite(env, key, content, signal, opts) {
8
+ let r;
9
+ try {
10
+ r = await writeThroughEnv(env, key, content, signal, opts);
11
+ }
12
+ catch (err) {
13
+ await discardTrackedEdit(opts?.track, "verify");
14
+ throw err;
15
+ }
16
+ if (!r.ok)
17
+ await discardTrackedEdit(opts?.track, NOTHING_WAS_WRITTEN.has(r.error.code) ? "proven" : "verify");
18
+ return r;
19
+ }
20
+ const NOTHING_WAS_WRITTEN = new Set(["already_exists", "precondition_failed"]);
21
+ async function writeThroughEnv(env, key, content, signal, opts) {
8
22
  if (env.writeFileGuarded !== undefined) {
9
23
  const r = await env.writeFileGuarded(key, content, { canonicalPath: key, ...(opts?.exclusive === true ? { exclusive: true } : {}) }, signal);
10
24
  return r.ok ? { ok: true } : r;
@@ -14,6 +28,30 @@ async function envFinalWrite(env, key, content, signal, opts) {
14
28
  }
15
29
  return env.writeFile(key, content, signal);
16
30
  }
31
+ async function discardTrackedEdit(lease, proof) {
32
+ if (lease?.annul === undefined)
33
+ return;
34
+ try {
35
+ await lease.annul(proof);
36
+ }
37
+ catch {
38
+ }
39
+ }
40
+ const clipHookText = (s) => (s.length > 300 ? `${s.slice(0, 300)}…` : s);
41
+ async function trackToolEdit(hook, tool, path, key, signal) {
42
+ if (hook === undefined)
43
+ return {};
44
+ let res;
45
+ try {
46
+ res = await hook({ tool, path, key, ...(signal !== undefined ? { signal } : {}) });
47
+ }
48
+ catch (err) {
49
+ return { refusal: `Error (${tool}): the file-history first-touch backup failed (${clipHookText(err instanceof Error ? err.message : String(err))}) — nothing was written.` };
50
+ }
51
+ if (res.ok === false)
52
+ return { refusal: clipHookText(res.refusal) };
53
+ return res.annul !== undefined ? { annul: res.annul } : {};
54
+ }
17
55
  async function gateToolWrite(hook, tool, path, key, content) {
18
56
  if (hook === undefined)
19
57
  return undefined;
@@ -22,13 +60,13 @@ async function gateToolWrite(hook, tool, path, key, content) {
22
60
  res = await hook({ tool, path, key, content });
23
61
  }
24
62
  catch (err) {
25
- return `Error (${tool}): the write gate failed (${err instanceof Error ? err.message : String(err)}) — nothing was written.`;
63
+ return `Error (${tool}): the write gate failed (${clipHookText(err instanceof Error ? err.message : String(err))}) — nothing was written.`;
26
64
  }
27
65
  if (res !== undefined && res.ok === false)
28
- return `Error (${tool}): write rejected by the write gate (${res.code}): ${res.reason}`;
66
+ return `Error (${tool}): write rejected by the write gate (${res.code}): ${clipHookText(res.reason)}`;
29
67
  return undefined;
30
68
  }
31
- export function createEditFileTool(env, state, rootCanonical, cwdRef, additionalRoots, beforeWrite) {
69
+ export function createEditFileTool(env, state, rootCanonical, cwdRef, additionalRoots, beforeWrite, trackEdit) {
32
70
  return defineTool({
33
71
  name: "Edit",
34
72
  contract: { contractId: "core.edit@1", implementationRevision: "1" },
@@ -81,7 +119,10 @@ export function createEditFileTool(env, state, rootCanonical, cwdRef, additional
81
119
  const gated = await gateToolWrite(beforeWrite, "Edit", path, r.key, created);
82
120
  if (gated !== undefined)
83
121
  return errorResult(gated);
84
- const write = await envFinalWrite(env, r.key, created, ctx.signal, { exclusive: true });
122
+ const tracked = await trackToolEdit(trackEdit, "Edit", path, r.key, ctx.signal);
123
+ if (tracked.refusal !== undefined)
124
+ return errorResult(tracked.refusal);
125
+ const write = await envFinalWrite(env, r.key, created, ctx.signal, { exclusive: true, track: tracked });
85
126
  if (!write.ok) {
86
127
  if (write.error.code === "already_exists") {
87
128
  return errorResult(`Error (Edit): cannot create "${path}": file already exists (created concurrently since the existence check). Read the file first, then edit it normally (or Write after the Read to overwrite it).`);
@@ -131,8 +172,11 @@ export function createEditFileTool(env, state, rootCanonical, cwdRef, additional
131
172
  const gated = await gateToolWrite(beforeWrite, "Edit", path, r.key, newContent);
132
173
  if (gated !== undefined)
133
174
  return errorResult(gated);
175
+ const trackedOverwrite = await trackToolEdit(trackEdit, "Edit", path, r.key, ctx.signal);
176
+ if (trackedOverwrite.refusal !== undefined)
177
+ return errorResult(trackedOverwrite.refusal);
134
178
  const encodedOverwrite = encodeTextForFile(newContent, preDec.encoding, preDec.endings);
135
- const write = await envFinalWrite(env, r.key, encodedOverwrite, ctx.signal);
179
+ const write = await envFinalWrite(env, r.key, encodedOverwrite, ctx.signal, { track: trackedOverwrite });
136
180
  if (!write.ok)
137
181
  return errorResult(`Error (Edit): cannot write "${path}": ${write.error.message}`);
138
182
  const persistedOverwrite = persistedTextOf(encodedOverwrite);
@@ -198,14 +242,18 @@ export function createEditFileTool(env, state, rootCanonical, cwdRef, additional
198
242
  }
199
243
  replacements += e.replace_all === true ? countOccurrences(working, oldS) : 1;
200
244
  const effOld = e.replace_all === true ? oldS : deletionOldString(working, oldS, newS);
201
- working = e.replace_all === true ? working.split(effOld).join(newS) : working.replace(effOld, () => newS);
202
- appliedEdits.push({ oldString: effOld, newString: newS, replaceAll: e.replace_all === true });
245
+ const applied = { oldString: effOld, newString: newS, replaceAll: e.replace_all === true };
246
+ working = applyRecordedEdit(working, applied);
247
+ appliedEdits.push(applied);
203
248
  }
204
249
  const gated = await gateToolWrite(beforeWrite, "Edit", path, r.key, working);
205
250
  if (gated !== undefined)
206
251
  return errorResult(gated);
252
+ const trackedEdit = await trackToolEdit(trackEdit, "Edit", path, r.key, ctx.signal);
253
+ if (trackedEdit.refusal !== undefined)
254
+ return errorResult(trackedEdit.refusal);
207
255
  const encodedEdit = encodeTextForFile(working, decoded.encoding, decoded.endings);
208
- const write = await envFinalWrite(env, r.key, encodedEdit, ctx.signal);
256
+ const write = await envFinalWrite(env, r.key, encodedEdit, ctx.signal, { track: trackedEdit });
209
257
  if (!write.ok)
210
258
  return errorResult(`Error (Edit): cannot write "${path}": ${write.error.message}`);
211
259
  const persistedEdit = persistedTextOf(encodedEdit);
@@ -228,7 +276,7 @@ export function createEditFileTool(env, state, rootCanonical, cwdRef, additional
228
276
  },
229
277
  });
230
278
  }
231
- export function createWriteFileTool(env, state, rootCanonical, cwdRef, additionalRoots, beforeWrite) {
279
+ export function createWriteFileTool(env, state, rootCanonical, cwdRef, additionalRoots, beforeWrite, trackEdit) {
232
280
  return defineTool({
233
281
  name: "Write",
234
282
  contract: { contractId: "core.write@1", implementationRevision: "1" },
@@ -275,8 +323,11 @@ export function createWriteFileTool(env, state, rootCanonical, cwdRef, additiona
275
323
  const gated = await gateToolWrite(beforeWrite, "Write", path, r.key, content);
276
324
  if (gated !== undefined)
277
325
  return errorResult(gated);
326
+ const trackedWrite = await trackToolEdit(trackEdit, "Write", path, r.key, ctx.signal);
327
+ if (trackedWrite.refusal !== undefined)
328
+ return errorResult(trackedWrite.refusal);
278
329
  const encodedWrite = encodeTextForFile(content, decodedPrev.encoding, "preserve");
279
- const write = await envFinalWrite(env, r.key, encodedWrite, ctx.signal);
330
+ const write = await envFinalWrite(env, r.key, encodedWrite, ctx.signal, { track: trackedWrite });
280
331
  if (!write.ok)
281
332
  return errorResult(`Error (Write): cannot write "${path}": ${write.error.message}`);
282
333
  const persistedWrite = persistedTextOf(encodedWrite);
@@ -289,7 +340,10 @@ export function createWriteFileTool(env, state, rootCanonical, cwdRef, additiona
289
340
  const gatedCreate = await gateToolWrite(beforeWrite, "Write", path, r.key, content);
290
341
  if (gatedCreate !== undefined)
291
342
  return errorResult(gatedCreate);
292
- const write = await envFinalWrite(env, r.key, content, ctx.signal);
343
+ const trackedCreate = await trackToolEdit(trackEdit, "Write", path, r.key, ctx.signal);
344
+ if (trackedCreate.refusal !== undefined)
345
+ return errorResult(trackedCreate.refusal);
346
+ const write = await envFinalWrite(env, r.key, content, ctx.signal, { track: trackedCreate });
293
347
  if (!write.ok)
294
348
  return errorResult(`Error (Write): cannot write "${path}": ${write.error.message}`);
295
349
  const totalLines = countLines(content);
@@ -302,7 +356,7 @@ export function createWriteFileTool(env, state, rootCanonical, cwdRef, additiona
302
356
  },
303
357
  });
304
358
  }
305
- export function createNotebookEditTool(env, state, rootCanonical, cwdRef, additionalRoots, beforeWrite) {
359
+ export function createNotebookEditTool(env, state, rootCanonical, cwdRef, additionalRoots, beforeWrite, trackEdit) {
306
360
  return defineTool({
307
361
  name: "NotebookEdit",
308
362
  contract: { contractId: "core.notebook_edit@1", implementationRevision: "1" },
@@ -420,7 +474,10 @@ export function createNotebookEditTool(env, state, rootCanonical, cwdRef, additi
420
474
  const gated = await gateToolWrite(beforeWrite, "NotebookEdit", notebook_path, r.key, updated);
421
475
  if (gated !== undefined)
422
476
  return errorResult(gated);
423
- const w = await envFinalWrite(env, r.key, encodeTextForFile(updated, decodedNb.encoding, decodedNb.endings), ctx.signal);
477
+ const trackedNb = await trackToolEdit(trackEdit, "NotebookEdit", notebook_path, r.key, ctx.signal);
478
+ if (trackedNb.refusal !== undefined)
479
+ return errorResult(trackedNb.refusal);
480
+ const w = await envFinalWrite(env, r.key, encodeTextForFile(updated, decodedNb.encoding, decodedNb.endings), ctx.signal, { track: trackedNb });
424
481
  if (!w.ok)
425
482
  return errorResult(`Error (NotebookEdit): cannot write "${notebook_path}": ${w.error.message}`);
426
483
  state.set(r.key, { hash: sha256(updated), totalLines: countLines(updated), truncated: false, lastReadAt: Date.now() });
@@ -1,5 +1,5 @@
1
1
  import type { AgentTool, ExecutionEnv } from "../../internal/harness-types.js";
2
- import { type BeforeWriteHook } from "../../core/types.js";
2
+ import { type BeforeWriteHook, type TrackFileEditHook } from "../../core/types.js";
3
3
  import { type TaskRegistry } from "../../core/task-registry.js";
4
4
  import { type ReadFileState } from "./safety.js";
5
5
  import type { PdfModelCapabilities } from "./pdf.js";
@@ -115,6 +115,11 @@ export interface HandsToolkitOptions {
115
115
  /** design/138 S2-C — write-time content gate for Write/Edit/NotebookEdit (see {@link BeforeWriteHook}).
116
116
  * The Runner wires the MemoryEngine's memory-domain scan here; absent ⇒ byte-identical behavior. */
117
117
  beforeWrite?: BeforeWriteHook;
118
+ /** design/381 — first-touch file-history hook for Write/Edit/NotebookEdit (see
119
+ * {@link TrackFileEditHook}): called after the content gates pass, immediately before the final
120
+ * env write, so the file's pre-edit state is durably recorded before it changes. The Runner
121
+ * wires it to `FileHistoryStore.trackEdit`; absent ⇒ byte-identical behavior (no history). */
122
+ trackFileEdit?: TrackFileEditHook;
118
123
  /** #181-F6 — see createBashTool's taskOpts field of the same name: whether the Monitor tool is on
119
124
  * this run's roster (the Runner mounts Monitor, this band never does). `false` drops the gh
120
125
  * rate-limit hint's Monitor clause; absent ⇒ historic full wording (byte-compat). */
@@ -50,7 +50,7 @@ export function createHandsToolkit(env, readFileState, rootCanonical, opts = {})
50
50
  createReadFileTool(env, readFileState, rootCanonical, readOnly ? undefined : cwdRef, readFaceRoots, opts.readImageDownsampler, opts.pdfModelCapabilities, bgOutputReadExemption, opts.readCyberReminder, readDeny, readFace, opts.reminderMark, opts.reminderDisclosureCounts),
51
51
  ];
52
52
  if (!readOnly) {
53
- tools.push(createEditFileTool(env, readFileState, rootCanonical, cwdRef, additionalRoots, opts.beforeWrite), createWriteFileTool(env, readFileState, rootCanonical, cwdRef, additionalRoots, opts.beforeWrite), createNotebookEditTool(env, readFileState, rootCanonical, cwdRef, additionalRoots, opts.beforeWrite));
53
+ tools.push(createEditFileTool(env, readFileState, rootCanonical, cwdRef, additionalRoots, opts.beforeWrite, opts.trackFileEdit), createWriteFileTool(env, readFileState, rootCanonical, cwdRef, additionalRoots, opts.beforeWrite, opts.trackFileEdit), createNotebookEditTool(env, readFileState, rootCanonical, cwdRef, additionalRoots, opts.beforeWrite, opts.trackFileEdit));
54
54
  }
55
55
  tools.push(createGrepTool(env, rootCanonical, readFaceRoots, readDeny, readFace), createGlobTool(env, rootCanonical, readFaceRoots, readDeny, readFace), createRepoMapTool(env, rootCanonical, readFaceRoots, readDeny, readFace));
56
56
  if (includeShell) {
package/dist/tools/web.js CHANGED
@@ -4,6 +4,7 @@ import { delimitUntrusted, inlineUntrusted } from "../core/untrusted-text.js";
4
4
  import { discloseReminderShaped, observeReminderMarkEcho } from "../core/reminder-disclosure.js";
5
5
  import { redactSecrets } from "../core/untrusted-egress.js";
6
6
  import { isPrivateHost } from "../core/runner/image.js";
7
+ import { DEFAULT_CHARS_PER_TOKEN } from "../internal/harness.js";
7
8
  import { binaryMagicFormat } from "./fs/safety.js";
8
9
  import { pdfMagicMatches } from "./fs/pdf.js";
9
10
  const MAX_REDIRECTS = 10;
@@ -701,7 +702,7 @@ export function resolveSummaryInputChars(model, override) {
701
702
  return Math.floor(override);
702
703
  }
703
704
  const window = model.contextTokens ?? model.contextWindow;
704
- const charsPerToken = model.charsPerToken ?? 4;
705
+ const charsPerToken = model.charsPerToken ?? DEFAULT_CHARS_PER_TOKEN;
705
706
  for (const [name, value] of [
706
707
  ["contextTokens/contextWindow", window],
707
708
  ["maxTokens", model.maxTokens],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "5.65.0",
3
+ "version": "7.0.0",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",
@@ -48,6 +48,7 @@
48
48
  "scripts": {
49
49
  "build": "rm -rf dist && tsc -p tsconfig.build.json && tsc -p tsconfig.build.json --emitDeclarationOnly --removeComments false",
50
50
  "typecheck": "tsc --noEmit",
51
+ "pretest": "npm run gate:debts",
51
52
  "test": "vitest run --exclude \"**/*.local.test.ts\"",
52
53
  "test:watch": "vitest",
53
54
  "gate:skips": "vitest run --exclude \"**/node_modules/**\" --exclude \"**/*.local.test.ts\" --reporter=default --reporter=json --outputFile.json=.skip-report.json && node scripts/verify-skip-baseline.mjs --report .skip-report.json",
@@ -65,6 +66,9 @@
65
66
  "gate:single-mint": "node scripts/verify-single-mint.mjs",
66
67
  "gate:nuia": "node scripts/verify-nuia-baseline.mjs",
67
68
  "gate:field-liveness": "node scripts/verify-field-liveness.mjs",
69
+ "gate:debts": "node scripts/verify-debts.mjs",
70
+ "handoff:diff": "node scripts/dist-handoff-diff.mjs",
71
+ "gate:debts:rc": "node scripts/verify-debts.mjs --rc-gate",
68
72
  "gate:error-surface": "node scripts/verify-error-surface.mjs",
69
73
  "gate:criteria": "node scripts/criteria-lint.mjs",
70
74
  "gate:reminder-literal": "node scripts/verify-reminder-literal.mjs"