@sema-agent/core 7.4.0 → 7.5.1
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.
- package/CHANGELOG.md +69 -0
- package/dist/agents/cascade.d.ts +2 -1
- package/dist/agents/peer-notices.d.ts +11 -1
- package/dist/agents/peer-session-drain.js +2 -0
- package/dist/agents/verify.d.ts +2 -1
- package/dist/core/ask-origin.d.ts +130 -0
- package/dist/core/ask-origin.js +35 -0
- package/dist/core/auto-mode-arming.d.ts +40 -1
- package/dist/core/auto-mode-arming.js +51 -3
- package/dist/core/auto-mode.d.ts +87 -10
- package/dist/core/auto-mode.js +34 -6
- package/dist/core/checkpoint-store.d.ts +41 -6
- package/dist/core/checkpoint-store.js +8 -0
- package/dist/core/hooks.d.ts +63 -19
- package/dist/core/hooks.js +37 -16
- package/dist/core/mcp.d.ts +47 -55
- package/dist/core/mcp.js +89 -31
- package/dist/core/park-selfcheck.js +3 -0
- package/dist/core/permission-rule-consent.d.ts +2 -11
- package/dist/core/permission-rule-consent.js +13 -62
- package/dist/core/permission-rule-org.d.ts +70 -54
- package/dist/core/permission-rule-org.js +47 -61
- package/dist/core/permission-rule-provider.d.ts +162 -0
- package/dist/core/permission-rule-provider.js +148 -0
- package/dist/core/permission-rule-session.d.ts +23 -19
- package/dist/core/permission-rule-session.js +5 -5
- package/dist/core/permission-rule-store.d.ts +46 -21
- package/dist/core/permission-rule-store.js +13 -6
- package/dist/core/permission-rule-sync.d.ts +2 -1
- package/dist/core/permission-rule-sync.js +11 -0
- package/dist/core/runner/assemble-result.d.ts +3 -2
- package/dist/core/runner/checkpoint-scope.d.ts +32 -0
- package/dist/core/runner/checkpoint-scope.js +4 -0
- package/dist/core/runner/contracts.d.ts +1878 -0
- package/dist/core/runner/contracts.js +1 -0
- package/dist/core/runner/denial-limit-arms.d.ts +57 -31
- package/dist/core/runner/denial-limit-arms.js +42 -17
- package/dist/core/runner/derived-route-fallback.d.ts +34 -0
- package/dist/core/runner/derived-route-fallback.js +16 -0
- package/dist/core/runner/prepare-acquire-reconcile.d.ts +1 -1
- package/dist/core/runner/prepare-announce-once.d.ts +83 -0
- package/dist/core/runner/prepare-announce-once.js +105 -0
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +170 -0
- package/dist/core/runner/prepare-caps-and-workflow.js +255 -0
- package/dist/core/runner/prepare-config-doors.d.ts +2 -10
- package/dist/core/runner/prepare-defer-classify.d.ts +86 -0
- package/dist/core/runner/prepare-defer-classify.js +107 -0
- package/dist/core/runner/prepare-delegation-surface.d.ts +104 -0
- package/dist/core/runner/prepare-delegation-surface.js +144 -0
- package/dist/core/runner/prepare-execution-env.d.ts +54 -0
- package/dist/core/runner/prepare-execution-env.js +86 -0
- package/dist/core/runner/prepare-file-history.d.ts +95 -0
- package/dist/core/runner/prepare-file-history.js +383 -0
- package/dist/core/runner/prepare-hands-readface.d.ts +6 -8
- package/dist/core/runner/prepare-hands-readface.js +3 -3
- package/dist/core/runner/prepare-inherited-gate.d.ts +268 -0
- package/dist/core/runner/prepare-inherited-gate.js +266 -0
- package/dist/core/runner/prepare-listings.d.ts +77 -0
- package/dist/core/runner/prepare-listings.js +76 -0
- package/dist/core/runner/prepare-lsp.d.ts +55 -0
- package/dist/core/runner/prepare-lsp.js +27 -0
- package/dist/core/runner/prepare-memory.d.ts +1 -1
- package/dist/core/runner/prepare-offload-wrappers.d.ts +62 -0
- package/dist/core/runner/prepare-offload-wrappers.js +45 -0
- package/dist/core/runner/prepare-permission-rules.d.ts +132 -0
- package/dist/core/runner/prepare-permission-rules.js +140 -0
- package/dist/core/runner/prepare-project-context.d.ts +131 -0
- package/dist/core/runner/prepare-project-context.js +150 -0
- package/dist/core/runner/prepare-prompt-inputs.d.ts +138 -0
- package/dist/core/runner/prepare-prompt-inputs.js +141 -0
- package/dist/core/runner/prepare-protocol-tools.d.ts +91 -0
- package/dist/core/runner/prepare-protocol-tools.js +182 -0
- package/dist/core/runner/prepare-question-face.d.ts +119 -0
- package/dist/core/runner/prepare-question-face.js +83 -0
- package/dist/core/runner/prepare-run-refs.d.ts +89 -0
- package/dist/core/runner/prepare-run-refs.js +39 -0
- package/dist/core/runner/prepare-safety-scan.d.ts +3 -2
- package/dist/core/runner/prepare-task.d.ts +11 -1815
- package/dist/core/runner/prepare-task.js +138 -2542
- package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +111 -0
- package/dist/core/runner/prepare-tool-disclosure-mount.js +219 -0
- package/dist/core/runner/prepare-wiring-manifest.d.ts +184 -0
- package/dist/core/runner/prepare-wiring-manifest.js +240 -0
- package/dist/core/runner/prepare-workspace-restore.d.ts +1 -27
- package/dist/core/runner/prepare-workspace-restore.js +1 -22
- package/dist/core/runner/rollback-stack.d.ts +32 -0
- package/dist/core/runner/rollback-stack.js +30 -0
- package/dist/core/runner/runtask.d.ts +11 -2
- package/dist/core/runner/runtask.js +27 -9
- package/dist/core/runner/workspace-path.d.ts +33 -0
- package/dist/core/runner/workspace-path.js +22 -0
- package/dist/core/sensitive-path-policy.d.ts +16 -0
- package/dist/core/sensitive-path-policy.js +1 -1
- package/dist/core/tool-policy.d.ts +57 -9
- package/dist/core/tool-policy.js +11 -0
- package/dist/core/types.d.ts +63 -51
- package/dist/core/wiring-manifest.d.ts +40 -3
- package/dist/core/wiring-manifest.js +4 -3
- package/dist/core/write-protect.d.ts +13 -2
- package/dist/core/write-protect.js +58 -29
- package/dist/engine/harness/types.d.ts +38 -16
- package/dist/engine/harness/types.js +25 -1
- package/dist/engine/session/session.d.ts +3 -11
- package/dist/index.d.ts +10 -6
- package/dist/index.js +9 -5
- package/dist/internal/harness.d.ts +1 -0
- package/dist/stores/file/adoption/adopt.d.ts +1 -1
- package/dist/stores/file/adoption/marker.d.ts +1 -1
- package/dist/stores/file/permission-rule-adopt.js +4 -3
- package/dist/stores/file/permission-rule-store.d.ts +65 -25
- package/dist/stores/file/permission-rule-store.js +215 -37
- package/dist/stores/file/task-list-store.d.ts +1 -1
- package/dist/tools/fs/read-face.d.ts +1 -1
- package/package.json +8 -2
- package/test/export-surface.snapshot.json +76 -28
|
@@ -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
|
+
}
|
|
@@ -4,9 +4,7 @@ import type { RunnerDeps, TaskSpec } from "../types.js";
|
|
|
4
4
|
import type { Model } from "../../internal/llm.js";
|
|
5
5
|
import type { AgentTool, ExecutionEnv, WorkspaceState } from "../../internal/harness.js";
|
|
6
6
|
import type { StoredSession } from "../session.js";
|
|
7
|
-
import type {
|
|
8
|
-
import type { PrepareSafetyScanResult } from "./prepare-safety-scan.js";
|
|
9
|
-
import type { PrepareResume, RunInternals } from "./prepare-task.js";
|
|
7
|
+
import type { IrreversibilityTier, PrepareResume, ReversibilityProbes, RunInternals, ToolFaceSnapshot } from "./contracts.js";
|
|
10
8
|
/** Test seam (mirrors `__resetMalformedNoticeSeatAnnouncement`): never called by production code.
|
|
11
9
|
* Deliberately asymmetric — it resets only the console latch: the WeakSet arm needs no seam
|
|
12
10
|
* because a test resets it by minting a fresh sink function (identity IS the ledger key), while
|
|
@@ -139,7 +137,7 @@ export interface PrepareHandsReadFaceInput {
|
|
|
139
137
|
effectiveShellGate: "off" | "classify" | "always";
|
|
140
138
|
/** borrowed-readonly — B-1's frozen task-start face snapshot; only `.exclude` is read here (the
|
|
141
139
|
* Monitor-mount honesty bit for the gh rate-limit hint, #181-F6). */
|
|
142
|
-
toolFaceSnapshot:
|
|
140
|
+
toolFaceSnapshot: ToolFaceSnapshot;
|
|
143
141
|
/** borrowed-readonly — B-1's resolved main model; read ONLY for its pdf capability profile
|
|
144
142
|
* (the Read tool's document-degradation fork). */
|
|
145
143
|
model: Model;
|
|
@@ -150,7 +148,7 @@ export interface PrepareHandsReadFaceInput {
|
|
|
150
148
|
* object (R-4: `deps.onError?.()` keeps its receiver; a new deps read must widen this Pick).
|
|
151
149
|
* `readDenyBuiltinTiers`/`readDenyBuiltinExclude` are SNAPSHOTTED before the first await that
|
|
152
150
|
* precedes the toolkit compile (#245/#244 — the caller retains these arrays by reference). */
|
|
153
|
-
deps: Pick<RunnerDeps, "onError" | "onNotice" | "tracer" | "readDenyPatterns" | "readDenyBuiltinTiers" | "readDenyBuiltinExclude" | "readFace" | "
|
|
151
|
+
deps: Pick<RunnerDeps, "onError" | "onNotice" | "tracer" | "readDenyPatterns" | "readDenyBuiltinTiers" | "readDenyBuiltinExclude" | "readFace" | "permissionRuleStore" | "hands">;
|
|
154
152
|
/** borrowed-readonly — the trusted spawn-side seats the band forwards: the finished-command
|
|
155
153
|
* notification lane, the mid-flight detach hub, and (#483 rung-1) the Runner's per-session
|
|
156
154
|
* read-state seats (absent on a standalone prepare ⇒ the transcript-replay rung covers alone). */
|
|
@@ -185,13 +183,13 @@ export interface PrepareHandsReadFaceInput {
|
|
|
185
183
|
/** borrowed-mutable — the shellGate fold's tier write-back (`.get` for the provenance bit,
|
|
186
184
|
* monotonic `.set` — tighten-only). Same object B-2 returned; owner table lives on the B-2
|
|
187
185
|
* Result. */
|
|
188
|
-
irreversibilityTier:
|
|
186
|
+
irreversibilityTier: IrreversibilityTier;
|
|
189
187
|
/** borrowed-mutable — the shellGate fold `.add`s Bash/Monitor here. Same-object contract as
|
|
190
188
|
* above. */
|
|
191
|
-
irreversibleTools:
|
|
189
|
+
irreversibleTools: Set<string>;
|
|
192
190
|
/** borrowed-mutable — the classify doctrine's probe installs (`.set`, only where the doctrine
|
|
193
191
|
* owns the seat). Same-object contract as above. */
|
|
194
|
-
reversibilityProbes:
|
|
192
|
+
reversibilityProbes: ReversibilityProbes;
|
|
195
193
|
/** borrowed-readonly — design/319 (A ticket): the run's reminder provenance mark (minted/adopted
|
|
196
194
|
* by the driver's adoption ladder); threaded into the hands band so the fs-side engine mints
|
|
197
195
|
* (the Read cyber/dedup/offset/empty reminders, the gh rate-limit hint) stamp the same mark the
|
|
@@ -7,7 +7,7 @@ import { resolveKey, sha256 } from "../../tools/fs/safety.js";
|
|
|
7
7
|
import { normalizeFileText } from "../../tools/fs/encoding.js";
|
|
8
8
|
import { wholeFileRecordsFromTranscript } from "./session-file-state-replay.js";
|
|
9
9
|
import { deliverEngineNotice } from "../types.js";
|
|
10
|
-
import { rebaseWorkspacePath } from "./
|
|
10
|
+
import { rebaseWorkspacePath } from "./workspace-path.js";
|
|
11
11
|
const readFaceClampAnnouncedSinks = new WeakSet();
|
|
12
12
|
let readFaceClampConsoleAnnounced = false;
|
|
13
13
|
export function __resetReadFaceClampAnnouncement() {
|
|
@@ -86,7 +86,7 @@ export function resolveHandsLessReadFace(input) {
|
|
|
86
86
|
specReadFace: spec.readFace,
|
|
87
87
|
depsReadFace: deps.readFace,
|
|
88
88
|
readOnlyMount: spec.handsReadOnly === true,
|
|
89
|
-
orgGoverned: deps.
|
|
89
|
+
orgGoverned: deps.permissionRuleStore?.partitions.org === true,
|
|
90
90
|
fullShellReachable,
|
|
91
91
|
});
|
|
92
92
|
const seed = resume?.seed.readFace;
|
|
@@ -231,7 +231,7 @@ export async function prepareHandsMount(input) {
|
|
|
231
231
|
specReadFace: spec.readFace,
|
|
232
232
|
depsReadFace: deps.readFace,
|
|
233
233
|
readOnlyMount: handsReadOnly,
|
|
234
|
-
orgGoverned: deps.
|
|
234
|
+
orgGoverned: deps.permissionRuleStore?.partitions.org === true,
|
|
235
235
|
fullShellReachable,
|
|
236
236
|
onDeploymentClamp: () => {
|
|
237
237
|
const sink = deps.onNotice;
|