@sema-agent/core 7.5.0 → 7.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/dist/core/auto-mode.d.ts +9 -0
  3. package/dist/core/auto-mode.js +11 -0
  4. package/dist/core/checkpoint-store.js +5 -1
  5. package/dist/core/protocol-table.d.ts +5 -0
  6. package/dist/core/protocol-table.js +1 -0
  7. package/dist/core/runner/abort-race.d.ts +41 -0
  8. package/dist/core/runner/abort-race.js +38 -0
  9. package/dist/core/runner/checkpoint-scope.d.ts +44 -0
  10. package/dist/core/runner/checkpoint-scope.js +7 -0
  11. package/dist/core/runner/compaction-call-options.d.ts +1 -1
  12. package/dist/core/runner/content-ask-bindings.d.ts +27 -0
  13. package/dist/core/runner/content-ask-bindings.js +1 -0
  14. package/dist/core/runner/contracts.d.ts +1990 -0
  15. package/dist/core/runner/contracts.js +1 -0
  16. package/dist/core/runner/denial-limit-arms.d.ts +24 -1
  17. package/dist/core/runner/denial-limit-arms.js +21 -0
  18. package/dist/core/runner/derived-route-fallback.d.ts +34 -0
  19. package/dist/core/runner/derived-route-fallback.js +16 -0
  20. package/dist/core/runner/inherited-ask-grants.d.ts +46 -0
  21. package/dist/core/runner/inherited-ask-grants.js +29 -0
  22. package/dist/core/runner/park-commit.d.ts +108 -0
  23. package/dist/core/runner/park-commit.js +32 -0
  24. package/dist/core/runner/{prepare-permission-rules.d.ts → permission-rule-lanes.d.ts} +109 -3
  25. package/dist/core/runner/{prepare-permission-rules.js → permission-rule-lanes.js} +47 -1
  26. package/dist/core/runner/prepare-acquire-reconcile.d.ts +1 -1
  27. package/dist/core/runner/prepare-ask-lane.d.ts +110 -0
  28. package/dist/core/runner/prepare-ask-lane.js +133 -0
  29. package/dist/core/runner/prepare-boundary-parks.d.ts +105 -0
  30. package/dist/core/runner/prepare-boundary-parks.js +169 -0
  31. package/dist/core/runner/prepare-caps-and-workflow.d.ts +170 -0
  32. package/dist/core/runner/prepare-caps-and-workflow.js +255 -0
  33. package/dist/core/runner/prepare-config-doors.d.ts +2 -10
  34. package/dist/core/runner/prepare-context-lane.d.ts +119 -0
  35. package/dist/core/runner/prepare-context-lane.js +230 -0
  36. package/dist/core/runner/prepare-defer-classify.d.ts +86 -0
  37. package/dist/core/runner/prepare-defer-classify.js +107 -0
  38. package/dist/core/runner/prepare-delegation-surface.d.ts +104 -0
  39. package/dist/core/runner/prepare-delegation-surface.js +144 -0
  40. package/dist/core/runner/prepare-execution-env.d.ts +54 -0
  41. package/dist/core/runner/prepare-execution-env.js +86 -0
  42. package/dist/core/runner/prepare-file-history.d.ts +95 -0
  43. package/dist/core/runner/prepare-file-history.js +383 -0
  44. package/dist/core/runner/prepare-gate-stations.d.ts +177 -0
  45. package/dist/core/runner/prepare-gate-stations.js +290 -0
  46. package/dist/core/runner/prepare-hands-readface.d.ts +9 -11
  47. package/dist/core/runner/prepare-hands-readface.js +1 -1
  48. package/dist/core/runner/prepare-inherited-gate.d.ts +268 -0
  49. package/dist/core/runner/prepare-inherited-gate.js +266 -0
  50. package/dist/core/runner/prepare-listings.d.ts +77 -0
  51. package/dist/core/runner/prepare-listings.js +76 -0
  52. package/dist/core/runner/prepare-lsp.d.ts +55 -0
  53. package/dist/core/runner/prepare-lsp.js +27 -0
  54. package/dist/core/runner/prepare-memory-engine-session.d.ts +84 -0
  55. package/dist/core/runner/prepare-memory-engine-session.js +233 -0
  56. package/dist/core/runner/prepare-memory.d.ts +1 -1
  57. package/dist/core/runner/prepare-offload-wrappers.d.ts +62 -0
  58. package/dist/core/runner/prepare-offload-wrappers.js +45 -0
  59. package/dist/core/runner/prepare-park-ask.d.ts +164 -0
  60. package/dist/core/runner/prepare-park-ask.js +377 -0
  61. package/dist/core/runner/prepare-policy-chain.d.ts +208 -0
  62. package/dist/core/runner/prepare-policy-chain.js +584 -0
  63. package/dist/core/runner/prepare-project-context.d.ts +119 -0
  64. package/dist/core/runner/prepare-project-context.js +148 -0
  65. package/dist/core/runner/prepare-prompt-assembly.d.ts +95 -0
  66. package/dist/core/runner/prepare-prompt-assembly.js +162 -0
  67. package/dist/core/runner/prepare-prompt-inputs.d.ts +119 -0
  68. package/dist/core/runner/prepare-prompt-inputs.js +141 -0
  69. package/dist/core/runner/prepare-protocol-tools.d.ts +91 -0
  70. package/dist/core/runner/prepare-protocol-tools.js +179 -0
  71. package/dist/core/runner/prepare-question-face.d.ts +101 -0
  72. package/dist/core/runner/prepare-question-face.js +84 -0
  73. package/dist/core/runner/prepare-run-refs.d.ts +89 -0
  74. package/dist/core/runner/prepare-run-refs.js +39 -0
  75. package/dist/core/runner/prepare-safety-scan.d.ts +3 -7
  76. package/dist/core/runner/prepare-safety-scan.js +1 -2
  77. package/dist/core/runner/prepare-suspend-saga.d.ts +170 -0
  78. package/dist/core/runner/prepare-suspend-saga.js +308 -0
  79. package/dist/core/runner/prepare-task.d.ts +14 -1976
  80. package/dist/core/runner/prepare-task.js +182 -5162
  81. package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +111 -0
  82. package/dist/core/runner/prepare-tool-disclosure-mount.js +219 -0
  83. package/dist/core/runner/prepare-turn-wiring.d.ts +154 -0
  84. package/dist/core/runner/prepare-turn-wiring.js +201 -0
  85. package/dist/core/runner/prepare-wiring-manifest.d.ts +192 -0
  86. package/dist/core/runner/prepare-wiring-manifest.js +247 -0
  87. package/dist/core/runner/prepare-workspace-restore.d.ts +3 -56
  88. package/dist/core/runner/prepare-workspace-restore.js +4 -38
  89. package/dist/core/runner/prompt-hash-salt.d.ts +1 -0
  90. package/dist/core/runner/prompt-hash-salt.js +2 -0
  91. package/dist/core/runner/remote-env-retry.d.ts +29 -0
  92. package/dist/core/runner/remote-env-retry.js +16 -0
  93. package/dist/core/runner/rollback-stack.d.ts +32 -0
  94. package/dist/core/runner/rollback-stack.js +30 -0
  95. package/dist/core/runner/runtask.js +1 -1
  96. package/dist/core/runner/workspace-path.d.ts +33 -0
  97. package/dist/core/runner/workspace-path.js +22 -0
  98. package/dist/core/session.d.ts +12 -0
  99. package/dist/core/session.js +3 -0
  100. package/dist/core/tool-policy.d.ts +16 -0
  101. package/dist/core/tool-policy.js +3 -0
  102. package/dist/core/types.d.ts +2 -2
  103. package/dist/core/write-protect.js +3 -2
  104. package/package.json +6 -2
  105. /package/dist/core/runner/{prepare-announce-once.d.ts → announce-once-ledger.d.ts} +0 -0
  106. /package/dist/core/runner/{prepare-announce-once.js → announce-once-ledger.js} +0 -0
@@ -0,0 +1,383 @@
1
+ import { NodeExecutionEnv } from "../../engine/execution-env/node-execution-env.js";
2
+ import { isRemoteExecutionEnv } from "../remote-env.js";
3
+ import { resolveTrackKey } from "../file-history-store.js";
4
+ export async function prepareFileHistory(input) {
5
+ const { deps, spec, internals, executionEnv, taskRootFinal, handsEnabled, sessionId, session, resumeAtBeforeParentId, abortController } = input;
6
+ const rewindNotes = [];
7
+ const onTrackFailure = deps.onTrackFailure === undefined ? "refuse" : deps.onTrackFailure;
8
+ if (onTrackFailure !== "refuse" && onTrackFailure !== "proceed-unprotected") {
9
+ const e = new Error(`RunnerDeps.onTrackFailure must be "refuse" or "proceed-unprotected"; got ${JSON.stringify(deps.onTrackFailure)} — refusing prepare rather than silently defaulting a first-touch-failure policy.`);
10
+ e.code = "config.invalid_on_track_failure";
11
+ throw e;
12
+ }
13
+ const fileHistoryStore = resolveWiredFileHistoryStore(deps.fileHistoryStore);
14
+ const legacyRewindFiles = spec.rewindFiles;
15
+ if (legacyRewindFiles === true) {
16
+ if (spec.resumeAt !== undefined) {
17
+ const e = new Error(`rewind-files: TaskSpec.rewindFiles is retired (design/381 — rewind now converges the per-edited-file history, not a whole-tree snapshot). This request pairs it with resumeAt, i.e. the RESTORE sense: set restoreFiles: true instead. (The capture sense needs no request anymore — first-touch tracking is always on when a RunnerDeps.fileHistoryStore is wired.)`);
18
+ e.code = "rewind.rewind_files_retired";
19
+ throw e;
20
+ }
21
+ try {
22
+ deps.onError?.(new Error(`TaskSpec.rewindFiles (capture sense) is retired and was ignored: per-edited-file first-touch tracking is always on when RunnerDeps.fileHistoryStore is wired, so a per-turn capture request has nothing left to ask for. Drop the field; the restore sense moved to TaskSpec.restoreFiles.`), { phase: "config", sessionId });
23
+ }
24
+ catch {
25
+ }
26
+ }
27
+ if (spec.restoreFiles === true && spec.resumeAt === undefined) {
28
+ const e = new Error(`rewind-files: restoreFiles: true needs a resumeAt anchor — restoring files means converging them to a specific prior entry's boundary, and this spec names none. Use resumeAt + restoreFiles to move conversation and files together, or rewindFilesTo alone for a code-only restore.`);
29
+ e.code = "rewind.invalid_spec";
30
+ throw e;
31
+ }
32
+ if (spec.resumeAt !== undefined && spec.rewindFilesTo !== undefined) {
33
+ const e = new Error(`rewind-files: resumeAt ("${spec.resumeAt}") and rewindFilesTo ("${spec.rewindFilesTo}") were both set — resumeAt already anchors the file restore when restoreFiles is true, so a separate rewindFilesTo target is a conflicting request. Drop one of the two.`);
34
+ e.code = "rewind.conflicting_targets";
35
+ throw e;
36
+ }
37
+ let rewindTarget = spec.resumeAt !== undefined ? (spec.restoreFiles === true ? spec.resumeAt : undefined) : spec.rewindFilesTo;
38
+ const rewindBefore = rewindTarget !== undefined && spec.resumeAt !== undefined && spec.resumeAtMode === "before";
39
+ if (spec.resumeAt !== undefined && spec.restoreFiles !== true) {
40
+ rewindNotes.push({
41
+ code: "conversation_only",
42
+ message: `the conversation was branched at entry "${spec.resumeAt}" but the working tree was NOT rewound — this task set resumeAt without restoreFiles, so files remain at their current state`,
43
+ });
44
+ }
45
+ const fileHistoryEnabled = fileHistoryStore !== undefined && handsEnabled;
46
+ const { historyRoot, historyScope, historyFs } = await resolveFileHistoryCoordinates(fileHistoryEnabled, executionEnv, taskRootFinal, internals?.fileHistoryLineage, sessionId);
47
+ if (fileHistoryStore !== undefined && internals?.fileHistoryLineage === undefined)
48
+ await adoptForkedFileHistory(fileHistoryStore, session, sessionId, deps.onError);
49
+ if (rewindTarget !== undefined && historyScope !== sessionId)
50
+ throw childScopeRewindRefusal(historyScope, rewindTarget);
51
+ if (fileHistoryStore === undefined) {
52
+ if (rewindTarget !== undefined) {
53
+ const e = new Error(`rewind-files: restoring files to entry "${rewindTarget}" requires a history backend, but this deployment wired no RunnerDeps.fileHistoryStore — no file history was ever recorded, so the files were NOT rewound`);
54
+ e.code = "rewind.store_unconfigured";
55
+ throw e;
56
+ }
57
+ }
58
+ else if (!handsEnabled) {
59
+ if (rewindTarget !== undefined) {
60
+ rewindNotes.push({
61
+ code: "files_env_unsupported",
62
+ message: "the file side of rewind was inert: this deployment mounts no filesystem-capable ExecutionEnv, so no file history was recorded or restored",
63
+ });
64
+ }
65
+ }
66
+ const legacyEpochTail = (entryId, form = "at-or-above") => legacyRewindEpochTail(deps, sessionId, entryId, form);
67
+ let restoredFilePaths = [];
68
+ if (rewindTarget !== undefined && fileHistoryStore !== undefined && handsEnabled) {
69
+ const historyStore = fileHistoryStore;
70
+ if (rewindBefore) {
71
+ let anchor;
72
+ if (await historyStore.canRestore(sessionId, rewindTarget)) {
73
+ anchor = rewindTarget;
74
+ }
75
+ else {
76
+ const walked = new Set();
77
+ let cursor = resumeAtBeforeParentId;
78
+ while (cursor !== null && !walked.has(cursor)) {
79
+ walked.add(cursor);
80
+ if (await historyStore.canRestore(sessionId, cursor)) {
81
+ anchor = cursor;
82
+ break;
83
+ }
84
+ cursor = (await session.getEntry(cursor))?.parentId ?? null;
85
+ }
86
+ }
87
+ if (anchor === undefined) {
88
+ const e = new Error(`rewind-files: no file-history boundary exists at or above the "before" branch point on session "${sessionId}" — cannot restore files consistent with the rewound context. ${await legacyEpochTail(rewindTarget)}`);
89
+ e.code = "rewind_snapshot.unresolvable";
90
+ throw e;
91
+ }
92
+ rewindTarget = anchor;
93
+ }
94
+ const restoreRoot = historyRoot;
95
+ const restoreSignal = spec.signal ? AbortSignal.any([abortController.signal, spec.signal]) : abortController.signal;
96
+ const restored = await historyStore.restore(sessionId, rewindTarget, executionEnv, restoreRoot, restoreSignal);
97
+ const ledgerLine = (r) => `applied ${r.filesChanged.length}, identical ${r.identicalSkipped.length}, refused ${r.refused.length}${r.refused.length > 0 ? ` (${r.refused.map((f) => `${f.path}: ${f.reason}`).join("; ")})` : ""}, failed ${r.failed.length}${r.failed.length > 0 ? ` (${r.failed.map((f) => `${f.path}: ${f.reason}`).join("; ")})` : ""}`;
98
+ if (restored.outcome === "failed") {
99
+ if (restored.error?.code === "not_found") {
100
+ const e = new Error(rewindBefore
101
+ ? `rewind-files: the resolved "before" history anchor "${rewindTarget}" disappeared before restore — files were NOT rewound`
102
+ : `rewind-files: no file-history boundary exists for entry "${rewindTarget}" on session "${sessionId}" — the working tree was NOT rewound. ${await legacyEpochTail(rewindTarget, "keyed")}`);
103
+ e.code = "rewind_snapshot.unresolvable";
104
+ throw e;
105
+ }
106
+ const e = new Error(`rewind-files restore failed (${restored.error?.code ?? "restore_failed"}): ${restored.error?.message ?? "unknown"} — ledger: ${ledgerLine(restored)}`);
107
+ e.code = "rewind.restore_failed";
108
+ throw e;
109
+ }
110
+ if (restored.outcome === "partial") {
111
+ if (spec.acceptPartialRestore === true) {
112
+ rewindNotes.push({
113
+ code: "restore_partial",
114
+ message: `file restore to entry "${rewindTarget}" was PARTIAL and was tolerated by acceptPartialRestore — ledger: ${ledgerLine(restored)}. Re-running the same restore converges (per-file idempotent).`,
115
+ });
116
+ }
117
+ else {
118
+ const e = new Error(`rewind-files: restore to entry "${rewindTarget}" was PARTIAL — ledger: ${ledgerLine(restored)}. The task is refused rather than run on a mixed-state tree (set acceptPartialRestore: true to tolerate and disclose instead); re-running the same restore converges (per-file idempotent).`);
119
+ e.code = "rewind.restore_failed";
120
+ throw e;
121
+ }
122
+ }
123
+ restoredFilePaths = restoredAbsPathsOf(restored, restoreRoot);
124
+ }
125
+ const trackFileEdit = fileHistoryEnabled
126
+ ? async (req) => {
127
+ const r = await fileHistoryStore.trackEdit(historyScope, req.key, executionEnv, historyRoot, req.signal);
128
+ if (r.ok) {
129
+ if (!r.minted)
130
+ return { ok: true };
131
+ return {
132
+ ok: true,
133
+ annul: async (proof) => {
134
+ const a = await fileHistoryStore.annulTrack(historyScope, req.key, historyRoot, proof === "verify" ? { env: executionEnv, ...(req.signal !== undefined ? { signal: req.signal } : {}) } : undefined);
135
+ if (a.ok)
136
+ return;
137
+ try {
138
+ deps.onError?.(new Error(`file-history could not retract the first-touch record for "${req.key}" after its edit failed to land (${a.error.code}: ${a.error.message}) — the path stays tracked, so a rewind to a boundary below it may converge bytes this engine never wrote`), { phase: "rewind", sessionId });
139
+ }
140
+ catch {
141
+ }
142
+ },
143
+ };
144
+ }
145
+ if (onTrackFailure === "proceed-unprotected") {
146
+ try {
147
+ deps.onError?.(new Error(`file-history first-touch backup for "${req.key}" failed (${r.error.code}: ${r.error.message}) — proceeding UNPROTECTED per RunnerDeps.onTrackFailure: the pristine promise for this path is void, and only in this process's memory (no durable trace exists, so after a process restart the next edit can still mint already-modified bytes as pristine v1)`), { phase: "rewind", sessionId });
148
+ }
149
+ catch {
150
+ }
151
+ return { ok: true };
152
+ }
153
+ return {
154
+ ok: false,
155
+ refusal: `Error (${req.tool}): the edit was refused because its first-touch file-history backup could not be persisted (${r.error.code}: ${r.error.message}). Proceeding would leave "${req.path}" with no durable pristine record — after the history store recovers, a retried edit would mint already-modified bytes as the pristine baseline. Retry when the file-history store is healthy.`,
156
+ };
157
+ }
158
+ : undefined;
159
+ const fileHistoryBoundary = fileHistoryEnabled && historyScope === sessionId
160
+ ? createFileHistoryBoundarySeat({ store: fileHistoryStore, sessionId, env: executionEnv, root: historyRoot, onError: deps.onError })
161
+ : undefined;
162
+ return { trackFileEdit, fileHistoryBoundary, restoredFilePaths, rewindNotes, fileHistoryEnabled, historyScope, historyRoot, historyFs };
163
+ }
164
+ function restoredAbsPathsOf(restored, root) {
165
+ const out = [];
166
+ for (const key of [...restored.filesChanged, ...restored.failed.map((f) => f.path)]) {
167
+ const abs = resolveTrackKey(root, key);
168
+ if (abs.ok)
169
+ out.push(abs.abs);
170
+ }
171
+ return out;
172
+ }
173
+ function resolveWiredFileHistoryStore(candidate) {
174
+ if (candidate === undefined)
175
+ return undefined;
176
+ const verbs = ["trackEdit", "annulTrack", "snapshot", "restore", "canRestore", "reap", "adoptScope"];
177
+ const verbOf = (v) => {
178
+ try {
179
+ return candidate[v];
180
+ }
181
+ catch {
182
+ return undefined;
183
+ }
184
+ };
185
+ const probeable = (typeof candidate === "object" && candidate !== null) || typeof candidate === "function";
186
+ const missing = probeable ? verbs.filter((v) => typeof verbOf(v) !== "function") : [...verbs];
187
+ if (missing.length > 0) {
188
+ const shape = candidate === null ? "null" : probeable ? "a value that is not a FileHistoryStore" : `a ${typeof candidate}`;
189
+ const e = new Error(`RunnerDeps.fileHistoryStore was wired with ${shape} — missing contract verbs: ${missing.join(", ")}. Refusing prepare rather than half-wiring the rewind seat (a present-but-unusable store makes every Edit/Write/NotebookEdit fail its first-touch backup while restore requests report "no store wired"). Omit the key to run without file history.`);
190
+ e.code = "config.invalid_file_history_store";
191
+ throw e;
192
+ }
193
+ return candidate;
194
+ }
195
+ const LEGACY_PROBE_TIMEOUT_MS = 2_000;
196
+ const LEGACY_PROBE_TIMED_OUT = Symbol("legacy-rewind-probe-timeout");
197
+ async function legacyRewindEpochTail(deps, sessionId, entryId, form) {
198
+ const probe = deps.legacyRewindBoundaryProbe;
199
+ let verdict;
200
+ if (probe !== undefined) {
201
+ try {
202
+ let timer;
203
+ const timeout = new Promise((resolve) => {
204
+ timer = setTimeout(() => resolve(LEGACY_PROBE_TIMED_OUT), LEGACY_PROBE_TIMEOUT_MS);
205
+ });
206
+ try {
207
+ verdict = await Promise.race([Promise.resolve(probe({ sessionId, entryId })), timeout]);
208
+ }
209
+ finally {
210
+ clearTimeout(timer);
211
+ }
212
+ if (verdict === LEGACY_PROBE_TIMED_OUT) {
213
+ verdict = undefined;
214
+ try {
215
+ deps.onError?.(new Error(`RunnerDeps.legacyRewindBoundaryProbe did not answer within ${LEGACY_PROBE_TIMEOUT_MS}ms while explaining a rewind refusal — the refusal stands and names both possibilities`), { phase: "rewind", sessionId });
216
+ }
217
+ catch {
218
+ }
219
+ }
220
+ }
221
+ catch (err) {
222
+ verdict = undefined;
223
+ try {
224
+ deps.onError?.(new Error(`RunnerDeps.legacyRewindBoundaryProbe threw while explaining a rewind refusal (${err.message}) — the refusal stands and names both possibilities`), { phase: "rewind", sessionId });
225
+ }
226
+ catch {
227
+ }
228
+ }
229
+ if (verdict !== undefined && typeof verdict !== "boolean") {
230
+ try {
231
+ deps.onError?.(new Error(`RunnerDeps.legacyRewindBoundaryProbe answered ${JSON.stringify(String(verdict))} for entry "${entryId}" — only true/false/undefined are meaningful, so the refusal names both possibilities instead of trusting it`), { phase: "rewind", sessionId });
232
+ }
233
+ catch {
234
+ }
235
+ verdict = undefined;
236
+ }
237
+ }
238
+ if (verdict === true) {
239
+ return `This deployment's legacy-epoch probe reports that entry "${entryId}" DOES carry a boundary from the retired whole-tree snapshot era, which this engine's per-edited-file seat does not restore — the two epochs are not interchangeable (a whole-tree manifest carries no tracked-set provenance, so replaying it here would converge files this session never edited). Restore it with the whole-tree-era tooling this deployment kept, or re-run the turns under the current engine.`;
240
+ }
241
+ if (verdict === false) {
242
+ return `This deployment's legacy-epoch probe reports no whole-tree-era boundary for entry "${entryId}" either, so the boundary simply does not exist: its turns ran without a RunnerDeps.fileHistoryStore wired, or it was reaped.`;
243
+ }
244
+ return form === "keyed"
245
+ ? `Boundaries are keyed by a turn's INITIAL entry and exist only for turns that ran with a RunnerDeps.fileHistoryStore wired; a boundary from the retired whole-tree snapshot era is not restorable by this seat, and reaped boundaries have none.`
246
+ : `Either the boundary does not exist (its turns ran without a RunnerDeps.fileHistoryStore, or it was reaped), or it was produced in the retired whole-tree snapshot era, which this engine's per-edited-file seat does not restore.`;
247
+ }
248
+ const FILE_HISTORY_BOUNDARY_TIMEOUT_MS = 30_000;
249
+ const FILE_HISTORY_CARRY_MEMO = new WeakMap();
250
+ async function adoptForkedFileHistory(store, session, sessionId, onError) {
251
+ const memo = FILE_HISTORY_CARRY_MEMO.get(store) ?? { settled: new Set(), disclosed: new Set(), inFlight: new Map() };
252
+ FILE_HISTORY_CARRY_MEMO.set(store, memo);
253
+ const discloseOnce = (err) => {
254
+ if (memo.disclosed.has(sessionId))
255
+ return;
256
+ memo.disclosed.add(sessionId);
257
+ onError?.(err, { phase: "rewind", sessionId });
258
+ };
259
+ try {
260
+ if (memo.settled.has(sessionId))
261
+ return;
262
+ const forkedFrom = (await session.getMetadata()).forkedFrom;
263
+ if (forkedFrom === undefined || forkedFrom === sessionId)
264
+ return;
265
+ const shared = memo.inFlight.get(sessionId);
266
+ if (shared !== undefined)
267
+ return await shared;
268
+ if (memo.settled.has(sessionId))
269
+ return;
270
+ const attempt = store.adoptScope(forkedFrom, sessionId).then((carried) => {
271
+ if (carried.ok || carried.error.code === "conflict") {
272
+ memo.settled.add(sessionId);
273
+ return;
274
+ }
275
+ discloseOnce(new Error(`file-history fork carry from session "${forkedFrom}" failed (${carried.error.code}): ${carried.error.message} — this forked session starts with an EMPTY rewind history, so boundaries recorded before the fork are not restorable from it (the run itself proceeds; the carry is retried on this session's next turn while its scope is still empty)`));
276
+ }, (err) => discloseOnce(err));
277
+ memo.inFlight.set(sessionId, attempt);
278
+ try {
279
+ await attempt;
280
+ }
281
+ finally {
282
+ memo.inFlight.delete(sessionId);
283
+ }
284
+ }
285
+ catch (err) {
286
+ try {
287
+ discloseOnce(err);
288
+ }
289
+ catch {
290
+ }
291
+ }
292
+ }
293
+ function createFileHistoryBoundarySeat(opts) {
294
+ let inFlight;
295
+ let begun = false;
296
+ return {
297
+ begin(entryId) {
298
+ if (begun)
299
+ return;
300
+ begun = true;
301
+ const ac = new AbortController();
302
+ let timer;
303
+ const timeout = new Promise((resolve) => {
304
+ timer = setTimeout(() => {
305
+ ac.abort();
306
+ resolve({ ok: false });
307
+ }, FILE_HISTORY_BOUNDARY_TIMEOUT_MS);
308
+ });
309
+ let work;
310
+ try {
311
+ work = opts.store.snapshot(opts.sessionId, entryId, opts.env, opts.root, ac.signal);
312
+ }
313
+ catch (err) {
314
+ work = Promise.resolve({ ok: false, error: { code: "snapshot_failed", message: `history store threw synchronously: ${err.message}` } });
315
+ }
316
+ work.catch(() => { });
317
+ inFlight = Promise.race([
318
+ work.then((r) => (r.ok ? undefined : `boundary capture failed (${r.error.code}): ${r.error.message}`)),
319
+ timeout.then(() => `boundary capture timed out after ${FILE_HISTORY_BOUNDARY_TIMEOUT_MS / 1000}s (fenced — the attempt can never publish)`),
320
+ ])
321
+ .then((failure) => {
322
+ if (failure !== undefined) {
323
+ try {
324
+ opts.onError?.(new Error(`file-history ${failure} — this turn has no boundary and cannot be rewound to (the turn itself proceeds)`), { phase: "rewind", sessionId: opts.sessionId });
325
+ }
326
+ catch {
327
+ }
328
+ }
329
+ })
330
+ .catch((err) => {
331
+ try {
332
+ opts.onError?.(err, { phase: "rewind", sessionId: opts.sessionId });
333
+ }
334
+ catch {
335
+ }
336
+ })
337
+ .finally(() => clearTimeout(timer));
338
+ },
339
+ async settle() {
340
+ if (inFlight !== undefined)
341
+ await inFlight;
342
+ },
343
+ };
344
+ }
345
+ async function resolveFileHistoryCoordinates(enabled, env, taskRoot, lineage, sessionId) {
346
+ if (!enabled)
347
+ return { historyRoot: taskRoot, historyScope: sessionId, historyFs: "" };
348
+ const canonRoot = await env.canonicalPath(taskRoot);
349
+ const historyRoot = canonRoot.ok ? canonRoot.value : taskRoot;
350
+ const historyFs = fileHistoryFilesystemIdentity(env);
351
+ return { historyRoot, historyScope: resolveFileHistoryScope(lineage, historyRoot, historyFs, sessionId), historyFs };
352
+ }
353
+ export function resolveFileHistoryScope(lineage, historyRoot, historyFs, sessionId) {
354
+ if (lineage === undefined || lineage.scope === "" || lineage.root !== historyRoot || lineage.fs !== historyFs)
355
+ return sessionId;
356
+ return lineage.scope;
357
+ }
358
+ const envFilesystemTokens = new WeakMap();
359
+ let envFilesystemSeq = 0;
360
+ export function fileHistoryFilesystemIdentity(env) {
361
+ if (isRemoteExecutionEnv(env)) {
362
+ try {
363
+ const h = env.workspaceHandle();
364
+ return JSON.stringify(["remote", h.provider, h.sandboxId, h.deviceId !== undefined && h.deviceId !== "" ? h.deviceId : null]);
365
+ }
366
+ catch {
367
+ }
368
+ }
369
+ else if (env.hostLocalPaths === true || (env.hostLocalPaths === undefined && env instanceof NodeExecutionEnv)) {
370
+ return "host";
371
+ }
372
+ let token = envFilesystemTokens.get(env);
373
+ if (token === undefined) {
374
+ token = `instance:${++envFilesystemSeq}`;
375
+ envFilesystemTokens.set(env, token);
376
+ }
377
+ return token;
378
+ }
379
+ function childScopeRewindRefusal(rootScope, target) {
380
+ const e = new Error(`rewind-files: this run is a delegated child recording its edits into the ROOT session "${rootScope}"'s file history, so restoring files to entry "${target}" is the root session's action (rewindFilesTo / resumeAt+restoreFiles on that session), not the child's — the files were NOT rewound`);
381
+ e.code = "rewind.child_scope_unsupported";
382
+ return e;
383
+ }
@@ -0,0 +1,177 @@
1
+ /**
2
+ * design/390 §1.2 M20 — prepareTask's two GATE STATIONS, verbatim from the driver: the `tool_call` station (the final-roster
3
+ * effect sync, the write-protection judge compiled once per task, the arming predicate, the compliance and plan-mode
4
+ * short-circuits, the `runToolGate` call with every seat it is handed, the lifecycle sweep, the deferred sandbox-admission
5
+ * disclosure, the PreToolUse context stash, the settlement sideband write and the bare-human-rejection boundary) and the
6
+ * `tool_result` station (the PostToolUse / PostToolUseFailure seats and the PreToolUse context drain). The two module-scope
7
+ * helpers only these stations called moved with them (the settlement screen, the rejection-boundary judgment). The
8
+ * interface is the dependency list the stations had implicitly (design/238 R-1).
9
+ *
10
+ * The stations REGISTER handlers on the harness (the side effect this phase exists for) and report only two observation
11
+ * seats back; the order they register in — after the file-history boundary lease the driver registers, and in this order
12
+ * (call before result) — is the order handlers run in.
13
+ *
14
+ * SYNCHRONOUS FUNCTION, SYNCHRONOUS CALL: the stretch has no await of its own (the handlers run per tool call / per tool
15
+ * result), so the phase adds no yield between the park closure before it and the context lane after it (design/390 §1.5).
16
+ * Read-stability of the host handles for the call: {@link RunInternals} (`@contract prepare.deps-read-stable`).
17
+ *
18
+ * The `tool_call` station is built exactly when the ask lane and the park closure are (the same predicate as the whole
19
+ * gate-machinery block, kept as the block's one guard here); the `tool_result` station keys off the hook record alone, as
20
+ * it always did.
21
+ *
22
+ * Every station this file holds is a `denySource` / settlement WRITE site the decomposition's next slice maps onto one
23
+ * gate outcome; nothing here changes a word, a source or an order — the stations are moved, not re-adjudicated.
24
+ */
25
+ import type { AgentHarness, AgentTool } from "../../internal/harness.js";
26
+ import type { AskClass } from "../ask-class.js";
27
+ import type { AutoModeDecider, AutoModeDenialTracker } from "../auto-mode.js";
28
+ import { type ComplianceCapability } from "../compliance.js";
29
+ import { type HookEnvCapabilities, type HookInvocationIdentity, type Hooks } from "../hooks.js";
30
+ import { type ToolPolicy } from "../tool-policy.js";
31
+ import type { RunnerDeps, TaskSpec, ToolEffect } from "../types.js";
32
+ import type { CwdRef } from "../../tools/fs/fs-shared.js";
33
+ import type { AskLane, IrreversibilityTier, ParkAsk, Prepared, ReversibilityProbes } from "./contracts.js";
34
+ import type { StopForDenialLimit } from "./denial-limit-arms.js";
35
+ import type { PermissionRuleLanes } from "./permission-rule-lanes.js";
36
+ export interface PrepareGateStationsInput {
37
+ /** borrowed-readonly — the in-stream ask lane, or undefined when the gate machinery is inactive (then no `tool_call`
38
+ * station is built). Read: `adjudicate`, `resolveAskBound`, `notifyPermissionDenied`, `notifyHookError`. */
39
+ askLane: AskLane | undefined;
40
+ /** borrowed-readonly — the park closure and the content-ask routing closure, or undefined (same law). Handed to the gate. */
41
+ parkAsk: ParkAsk | undefined;
42
+ /** borrowed-readonly — the FINAL mount roster: iterated once for the effect sync and the write-protection arming. Never
43
+ * mutated here. */
44
+ tools: AgentTool[];
45
+ /** borrowed-mutable — the effect collection. Writer here: the final-roster sync fills MISSING entries from each
46
+ * first-party tool's declared effect (never overrides a scanned one); the plan-mode deny reads it per call. */
47
+ toolEffects: Map<string, ToolEffect>;
48
+ /** borrowed-readonly — the deployment seats these stations read, as a Pick over the SAME `deps` object (receiver
49
+ * preserved for `deps.onError?.()`): the write-protection table and its data root, the tracer, the error face. */
50
+ deps: Pick<RunnerDeps, "writeProtectedPaths" | "memoryEngineDir" | "tracer" | "onError">;
51
+ /** borrowed-mutable — the protocol-tools phase's arming belt. Writer here: `armed` ← the registration predicate, the
52
+ * one write; the refresh seam reads it. */
53
+ toolCallGateArmedRef: {
54
+ armed: boolean;
55
+ };
56
+ /** borrowed-readonly — the policy chain's effective policy, or undefined; presence only (the arming predicate). */
57
+ effectivePolicy: ToolPolicy | undefined;
58
+ /** borrowed-readonly — the resolved hook record: `preToolUse` presence (arming), the two post seats and the failure seat
59
+ * (the `tool_result` station), called through the record so a class-instance keeps its receiver. */
60
+ hooks: Hooks | undefined;
61
+ /** borrowed-readonly — the egress vocabulary: arming, and the per-call `egress` flag handed to the gate. */
62
+ egressTools: ReadonlySet<string>;
63
+ /** borrowed-readonly — the irreversible vocabulary: arming only. */
64
+ irreversibleTools: ReadonlySet<string>;
65
+ /** borrowed-readonly — the REBOUND spec: `enablePlanMode` (arming) and `signal` (the failure seat's interrupt fact). */
66
+ spec: Pick<TaskSpec, "enablePlanMode" | "signal">;
67
+ /** borrowed-readonly — the compliance denials (the web_fetch veto's call-time lock, and its arming term). */
68
+ complianceDenies: ReadonlySet<ComplianceCapability>;
69
+ /** borrowed-mutable — the built harness. Writer here: `on("tool_call")` / `on("tool_result")` — the registrations this
70
+ * phase exists for, in this order, after the driver's boundary-lease handler. */
71
+ harness: Pick<AgentHarness, "on">;
72
+ /** borrowed-mutable — the blocked/suspended call ids (`Prepared.blockedToolCalls`). Writers here: the head-of-evaluation
73
+ * hygiene `delete`, the two short-circuits' and the gate outcome's `add`, the `tool_result` station's one-shot `delete`. */
74
+ blockedToolCalls: Set<string>;
75
+ /** borrowed-mutable — the duplicate-frame ask-grant record. Writer here: the head-of-evaluation and lifecycle-end `delete`. */
76
+ inheritedAskGrants: Map<string, unknown>;
77
+ /** borrowed-mutable — the bare-human-rejection sideband. Writer here: the head-of-evaluation `delete` and the lifecycle
78
+ * sweep whose `delete` IS the read the boundary judgment consumes. */
79
+ humanBareRejections: Set<string>;
80
+ /** borrowed-mutable — the inherited-unavailable marker set. Writer here: the lifecycle-end `delete`; `has` is handed to
81
+ * the gate as the read-only `isMarkedUnresolvable`. */
82
+ inheritedUnavailableAsks: Set<string>;
83
+ /** borrowed-mutable — the per-call ask-class record the sandbox admission reads. Writer here: the lifecycle-end `delete`. */
84
+ foldAskClasses: Map<string, Array<{
85
+ cls: AskClass;
86
+ layer: string;
87
+ }>>;
88
+ /** borrowed-mutable — the deferred ancestor sandbox admissions. Writer here: the unconditional read-and-`delete` after the
89
+ * fold settles. */
90
+ ancestorSandboxAdmissions: Map<string, Array<{
91
+ cls: AskClass;
92
+ layer: string;
93
+ }>>;
94
+ /** borrowed-mutable — PreToolUse `additionalContext` per call id. Writers here: the `tool_call` station's stash (`set`, only
95
+ * for calls that will execute) and the `tool_result` station's one-shot drain (`delete`). */
96
+ preToolContexts: Map<string, string[]>;
97
+ /** borrowed-mutable — the settlement sideband (`Prepared.approvalSettlement`). Writer here: the ONE screened `set` per
98
+ * gate outcome; the frame mint reads and deletes. */
99
+ approvalSettlement: Prepared["approvalSettlement"];
100
+ /** borrowed-mutable — the turn halt fact (`Prepared.batchHaltRef`). Writer here: the bare-rejection boundary (`current`). */
101
+ batchHaltRef: Prepared["batchHaltRef"];
102
+ /** borrowed-readonly — whether blocked/suspended ids are tracked at all (the post-tool callbacks exist). */
103
+ blockedTracked: boolean;
104
+ /** borrowed-readonly — the leg's identity envelope every HookToolContext and PermissionDeniedPayload carries. */
105
+ hookIdentity: HookInvocationIdentity;
106
+ /** borrowed-readonly — the leg's reminder provenance mark (every model-facing deny reason and hook context carries it). */
107
+ reminderMark: string;
108
+ /** borrowed-readonly — the plan-mode latch (`Prepared.planModeRef`); `active` is read per call. Not written here. */
109
+ planModeRef: Prepared["planModeRef"];
110
+ /** borrowed-readonly — the per-task identity (the trace rows' taskId). */
111
+ hostTaskId: string;
112
+ /** borrowed-readonly — the acquired session id (the settlement defects' error tag). */
113
+ sessionId: string;
114
+ /** borrowed-readonly — the screening face as the gate must see it (the observational wrapper or the face itself). */
115
+ ownGatePreToolUse: Hooks["preToolUse"] | undefined;
116
+ /** borrowed-readonly — the validated per-prepare hook seat bound. */
117
+ hookTimeoutMs: number;
118
+ /** borrowed-readonly — the hand's live tracked cwd, or undefined; `current` is read per screening / per post seat. Not
119
+ * written here. */
120
+ handsCwdRef: CwdRef | undefined;
121
+ /** borrowed-readonly — the read-only env capability face every hook context carries, or undefined. */
122
+ hookEnvFace: HookEnvCapabilities | undefined;
123
+ /** borrowed-readonly — the per-tool irreversibility tier handed to the gate per call. */
124
+ irreversibilityTier: IrreversibilityTier;
125
+ /** borrowed-readonly — the per-tool reversibility probes handed to the gate per call. */
126
+ reversibilityProbes: ReversibilityProbes;
127
+ /** borrowed-readonly — prepare's abort seat; only `.signal` is read (the gate's probe bound, the post seats' bound, the
128
+ * failure seat's interrupt fact). */
129
+ abortController: {
130
+ readonly signal: AbortSignal;
131
+ };
132
+ /** borrowed-readonly — whether Bash is coarsely shell-gated (the gate's `shellGated` attribution). */
133
+ shellGatedBash: boolean;
134
+ /** borrowed-readonly — whether Monitor is coarsely shell-gated (same). */
135
+ shellGatedMonitor: boolean;
136
+ /** borrowed-readonly — the auto-mode decider, or undefined (the classifier lane the gate routes a surviving ask through). */
137
+ autoModeDecider: AutoModeDecider | undefined;
138
+ /** borrowed-readonly — the auto-mode denial tracker, or undefined (handed to the gate beside the decider). */
139
+ autoModeDenialTracking: AutoModeDenialTracker | undefined;
140
+ /** borrowed-readonly — the headless denial-limit stop seat (wired unconditionally). */
141
+ stopForDenialLimit: StopForDenialLimit;
142
+ /** borrowed-readonly — the persisted allow-rule lane, or undefined (absent entirely with no store). */
143
+ permissionRuleLane: PermissionRuleLanes["personal"];
144
+ /** borrowed-readonly — the org rule lane, or undefined (absent on an ungoverned deployment). */
145
+ permissionRuleOrgLane: PermissionRuleLanes["org"];
146
+ /** borrowed-readonly — whether the ENGINE's question tool is what is mounted under the reserved name (the org lane's
147
+ * content-ask exemption predicate). */
148
+ questionToolMounted: boolean;
149
+ /** borrowed-readonly — whether sandbox admission is armed (isolated env + park prerequisites). */
150
+ sandboxAdmissionArmed: boolean;
151
+ /** borrowed-readonly — the per-tool boundary-capability fact the sandbox admission reads. */
152
+ sandboxBoundaryCapable: (toolName: string) => boolean;
153
+ /** borrowed-readonly — the sandbox-admission trace sink (the deferred ancestor disclosure and the gate's own). */
154
+ emitSandboxAdmitted: (info: {
155
+ toolCallId: string;
156
+ toolName: string;
157
+ askClasses: ReadonlyArray<{
158
+ cls: AskClass;
159
+ layer: string;
160
+ }>;
161
+ boundary: "sandbox_internal";
162
+ }) => void;
163
+ /** borrowed-readonly — the effective delegation facts (the rejection boundary's isDelegatedChild). */
164
+ delegation: {
165
+ isDelegatedChild: boolean;
166
+ };
167
+ /** borrowed-readonly — the own hook-crash reporter (an expired post seat is reported through it). */
168
+ notifyOwnHookCrash: (err: unknown) => void;
169
+ }
170
+ export interface PrepareGateStationsResult {
171
+ /** owned — whether the `tool_call` station registered (the arming predicate's value; observation seat only). */
172
+ toolCallStationArmed: boolean;
173
+ /** owned — whether the `tool_result` station registered (a post or screening hook is wired; observation seat only). */
174
+ toolResultStationArmed: boolean;
175
+ }
176
+ /** The M20 phase body — prepareTask's two gate stations, verbatim (see the module header). */
177
+ export declare function prepareGateStations(input: PrepareGateStationsInput): PrepareGateStationsResult;