dsh-rewind-plugin 0.15.3 → 0.16.0-alpha.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.
- package/SECURITY.md +20 -16
- package/lib/client.js +12 -3
- package/lib/index.js +50 -44
- package/lib/types/client/portals.d.ts +13 -1
- package/lib/types/index.d.ts +13 -1
- package/lib/types/snapshot-cleanup.d.ts +7 -5
- package/lib/types/snapshot.d.ts +9 -5
- package/package.json +56 -51
- package/lib/types/session-cwd.d.ts +0 -21
package/SECURITY.md
CHANGED
|
@@ -91,19 +91,21 @@ hides the `[target, marker]` span from the rendered transcript.
|
|
|
91
91
|
## Filesystem containment
|
|
92
92
|
|
|
93
93
|
- **Store root**: `<dsh home>/rewind-snapshots/` by default (resolved via
|
|
94
|
-
`resolveDshHome`, so `~/.dsh
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
`resolveDshHome`, so `$DSH_HOME` feeds the base and `~/.dsh` is the fallback);
|
|
95
|
+
the `snapshotDir` config overrides the whole path. The plugin reads no
|
|
96
|
+
environment variable of its own. Deleting the root only removes file backups
|
|
97
|
+
and the store rebuilds from scratch.
|
|
97
98
|
- **Path sanitization**: session ids and call ids are scrubbed to
|
|
98
99
|
`[a-zA-Z0-9._-]` (`safeSessionId` / `safeFileId`) before joining the store
|
|
99
100
|
root; `.` and `..` bare values are replaced — hostile ids cannot traverse
|
|
100
101
|
out of the root.
|
|
101
|
-
- **
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
102
|
+
- **Links and moved directories are refused**: two checks guard a tracked path.
|
|
103
|
+
(1) Planning skips a path whose last component is a symlink or a hard link
|
|
104
|
+
(`lstat().nlink > 1`) and reports it, never restoring it — a symlink would
|
|
105
|
+
redirect the write outside the checkpoint, and a hard link would clobber every
|
|
106
|
+
other name of the same inode (e.g. pnpm-installed files); a link swapped in
|
|
107
|
+
after planning is not re-checked. (2) At apply time, every record this build
|
|
108
|
+
writes — entry, link and
|
|
107
109
|
journal action — carries the `realpath` of the file's directory at commit time
|
|
108
110
|
(the location pin; best-effort, and absent on released-v1 data or a commit
|
|
109
111
|
whose parent could not be resolved), and a restore re-checks it before
|
|
@@ -120,8 +122,8 @@ hides the `[target, marker]` span from the rendered transcript.
|
|
|
120
122
|
- **Restores name only recorded paths**: the store contains resolved display
|
|
121
123
|
paths of the session's own write-class tool calls (plus boundary re-checks
|
|
122
124
|
over that same tracked set) — a restore can never write an arbitrary path.
|
|
123
|
-
- **Path resolution rule**: relative paths resolve against the session
|
|
124
|
-
|
|
125
|
+
- **Path resolution rule**: relative paths resolve against the session's current
|
|
126
|
+
working directory, read from the same service the fs tools resolve through.
|
|
125
127
|
- **Bounded backups**: `prune` keeps the newest 100 anchor groups per session
|
|
126
128
|
(`MAX_ANCHOR_GROUPS`), except groups a non-terminal restore journal still
|
|
127
129
|
references (pinned so the op can be finished), so backup accumulation stays
|
|
@@ -145,11 +147,13 @@ hides the `[target, marker]` span from the rendered transcript.
|
|
|
145
147
|
planned path's pre-restore ("rescue") state and persists an intent journal,
|
|
146
148
|
then marks each action done as it is applied. A crash at any point leaves
|
|
147
149
|
the journal on disk.
|
|
148
|
-
- **Disk is truth**:
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
reached auto-heals to
|
|
150
|
+
- **Disk is truth**: an interrupted op keeps its journal on disk, and the store's
|
|
151
|
+
recovery API re-derives the true state from the real disk — `reconcileRestores`
|
|
152
|
+
reports which paths already match the goal (restored) and which are pending,
|
|
153
|
+
`continueRestore` finishes the op, `rollbackRestore` undoes it to the exact
|
|
154
|
+
pre-restore state, and a journal whose goal is already reached auto-heals to
|
|
155
|
+
its terminal state. The plugin never runs them on its own: an interrupted op
|
|
156
|
+
stays interrupted until an explicit caller resumes or undoes it.
|
|
153
157
|
- **Fail-loud vs fail-soft**: a corrupt **journal** is reported
|
|
154
158
|
`recovery-required` — never silently dropped (dropping it would erase the
|
|
155
159
|
interrupted restore's recovery record). Corrupt **checkpoint entries** are
|
package/lib/client.js
CHANGED
|
@@ -1095,6 +1095,13 @@ function hideWithdrawnSeats(chat, seats, hiddenSeqs, hidden) {
|
|
|
1095
1095
|
}
|
|
1096
1096
|
}
|
|
1097
1097
|
}
|
|
1098
|
+
function showHiddenSeats(hidden) {
|
|
1099
|
+
for (const element of hidden) {
|
|
1100
|
+
element.style.display = "";
|
|
1101
|
+
delete element.dataset.dshRewindHidden;
|
|
1102
|
+
}
|
|
1103
|
+
hidden.clear();
|
|
1104
|
+
}
|
|
1098
1105
|
function collectTargets(chat, hiddenSeqs) {
|
|
1099
1106
|
const rows = /* @__PURE__ */ new Map();
|
|
1100
1107
|
for (const element of document.querySelectorAll(USER_SEAT_SELECTOR)) {
|
|
@@ -1168,7 +1175,7 @@ function sameTargets(left, right) {
|
|
|
1168
1175
|
}
|
|
1169
1176
|
function RewindPortals({ sessionId, sessionOf, chatOf, isMainViewSession, watchChat, t, subscribeLocale, setComposerText, restoreImages }) {
|
|
1170
1177
|
const [targets, setTargets] = (0, import_react.useState)([]);
|
|
1171
|
-
const hidden = (0, import_react.useRef)(/* @__PURE__ */ new
|
|
1178
|
+
const hidden = (0, import_react.useRef)(/* @__PURE__ */ new Set());
|
|
1172
1179
|
const [, forceRender] = (0, import_react.useReducer)((count) => count + 1, 0);
|
|
1173
1180
|
(0, import_react.useEffect)(() => subscribeLocale(() => {
|
|
1174
1181
|
forceRender();
|
|
@@ -1189,6 +1196,7 @@ function RewindPortals({ sessionId, sessionOf, chatOf, isMainViewSession, watchC
|
|
|
1189
1196
|
const snapshot = session.getSnapshot();
|
|
1190
1197
|
const chat = chatOf(session);
|
|
1191
1198
|
const hiddenSeqs = chat === void 0 ? /* @__PURE__ */ new Set() : hiddenSeqsOf(chat);
|
|
1199
|
+
for (const element of hidden.current) if (!element.isConnected) hidden.current.delete(element);
|
|
1192
1200
|
hideWithdrawnSeats(
|
|
1193
1201
|
chat,
|
|
1194
1202
|
chat === void 0 ? [] : document.querySelectorAll(CHAT_SEAT_SELECTOR),
|
|
@@ -1225,6 +1233,7 @@ function RewindPortals({ sessionId, sessionOf, chatOf, isMainViewSession, watchC
|
|
|
1225
1233
|
return () => {
|
|
1226
1234
|
active = false;
|
|
1227
1235
|
observer.disconnect();
|
|
1236
|
+
showHiddenSeats(hidden.current);
|
|
1228
1237
|
unsubscribeInbox?.();
|
|
1229
1238
|
unsubscribeInbox = void 0;
|
|
1230
1239
|
inboxOwner = void 0;
|
|
@@ -1359,9 +1368,9 @@ function createRewindBridge(deps) {
|
|
|
1359
1368
|
}
|
|
1360
1369
|
|
|
1361
1370
|
// src/client/build-info.ts
|
|
1362
|
-
var PLUGIN_VERSION = true ? "0.
|
|
1371
|
+
var PLUGIN_VERSION = true ? "0.16.0-alpha.2" : "dev";
|
|
1363
1372
|
var PLUGIN_PACKAGE = "dsh-rewind-plugin";
|
|
1364
|
-
var BUILD_HASH = true ? "
|
|
1373
|
+
var BUILD_HASH = true ? "c6cfbc55" : "dev";
|
|
1365
1374
|
|
|
1366
1375
|
// src/client/locales.ts
|
|
1367
1376
|
var zh = {
|
package/lib/index.js
CHANGED
|
@@ -224,14 +224,6 @@ function planRewind(events, surface, target) {
|
|
|
224
224
|
};
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
-
// src/session-cwd.ts
|
|
228
|
-
function sessionCwd(cwd) {
|
|
229
|
-
return cwd;
|
|
230
|
-
}
|
|
231
|
-
function execSessionCwd(exec) {
|
|
232
|
-
return sessionCwd(exec.agent?.session.header.cwd);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
227
|
// src/snapshot.ts
|
|
236
228
|
import { createHash } from "node:crypto";
|
|
237
229
|
import { chmod, copyFile, lstat, mkdir, open, readFile, readdir, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
@@ -246,15 +238,7 @@ var LEGACY_JOURNAL_PREFIX = "restore-journal-";
|
|
|
246
238
|
var PENDING_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
247
239
|
var COMPARE_CHUNK_BYTES = 64 * 1024;
|
|
248
240
|
var REPLACEMENT_CHAR = "\uFFFD";
|
|
249
|
-
var SNAPSHOT_ROOT_ENV = "DSH_REWIND_SNAPSHOT_DIR";
|
|
250
|
-
var MAX_FILE_BYTES_ENV = "DSH_REWIND_MAX_FILE_BYTES";
|
|
251
241
|
var DEFAULT_MAX_FILE_BYTES = 8 * 1024 * 1024;
|
|
252
|
-
function maxFileBytesFromEnv() {
|
|
253
|
-
const raw = process.env[MAX_FILE_BYTES_ENV]?.trim();
|
|
254
|
-
if (raw === void 0 || raw === "") return void 0;
|
|
255
|
-
const parsed = Number(raw);
|
|
256
|
-
return Number.isInteger(parsed) ? parsed : void 0;
|
|
257
|
-
}
|
|
258
242
|
var MAX_ANCHOR_GROUPS = 100;
|
|
259
243
|
var CURRENT_STORE_VERSION = 2;
|
|
260
244
|
var UnknownStoreVersionError = class extends Error {
|
|
@@ -683,6 +667,11 @@ var SnapshotStore = class _SnapshotStore {
|
|
|
683
667
|
* and the restore planner refuses a pre-cap record over an over-cap file.
|
|
684
668
|
*/
|
|
685
669
|
maxFileBytes;
|
|
670
|
+
/**
|
|
671
|
+
* Newest anchor groups (user messages) kept per session; `prune` evicts past
|
|
672
|
+
* this bound. A `Config` field (`maxAnchorGroups`).
|
|
673
|
+
*/
|
|
674
|
+
maxAnchorGroups;
|
|
686
675
|
/** Resolved checkpoint store root (absolute); see the constructor's fallback. */
|
|
687
676
|
root;
|
|
688
677
|
/**
|
|
@@ -708,8 +697,9 @@ var SnapshotStore = class _SnapshotStore {
|
|
|
708
697
|
formatVersion = null;
|
|
709
698
|
constructor(root, opts) {
|
|
710
699
|
this.dedup = opts?.dedup ?? true;
|
|
711
|
-
this.maxFileBytes = opts?.maxFileBytes ??
|
|
712
|
-
this.
|
|
700
|
+
this.maxFileBytes = opts?.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES;
|
|
701
|
+
this.maxAnchorGroups = opts?.maxAnchorGroups ?? MAX_ANCHOR_GROUPS;
|
|
702
|
+
this.root = root ?? join(resolveDshHome(opts?.dshHome), SNAPSHOT_DIR_NAME);
|
|
713
703
|
}
|
|
714
704
|
/** Absolute path of one session's snapshot directory (id sanitized). */
|
|
715
705
|
sessionDir(sessionId) {
|
|
@@ -1687,7 +1677,7 @@ var SnapshotStore = class _SnapshotStore {
|
|
|
1687
1677
|
* real is still on disk and the kept link still resolves, so nothing dangles
|
|
1688
1678
|
* and a later prune simply re-materializes.
|
|
1689
1679
|
*/
|
|
1690
|
-
async prune(sessionId, keep =
|
|
1680
|
+
async prune(sessionId, keep = this.maxAnchorGroups, opts) {
|
|
1691
1681
|
const sessionDir = this.sessionDir(sessionId);
|
|
1692
1682
|
let names;
|
|
1693
1683
|
try {
|
|
@@ -2252,7 +2242,7 @@ async function runAutoCleanupCheck(deps, sessionId) {
|
|
|
2252
2242
|
}
|
|
2253
2243
|
if (!loaded.config.enabled) return;
|
|
2254
2244
|
if (isAborted(deps.signal)) return;
|
|
2255
|
-
if (!shouldRunAutoSweep(await loadLastSweepAt(deps.statePath), Date.now())) return;
|
|
2245
|
+
if (!shouldRunAutoSweep(await loadLastSweepAt(deps.statePath), Date.now(), deps.intervalMs)) return;
|
|
2256
2246
|
if (isAborted(deps.signal)) return;
|
|
2257
2247
|
await deps.pruner.pruneStale({ keepActiveId: sessionId, maxAgeDays: loaded.config.maxAgeDays });
|
|
2258
2248
|
if (isAborted(deps.signal)) return;
|
|
@@ -2317,8 +2307,8 @@ function parseCleanupCommand(rawInput) {
|
|
|
2317
2307
|
return { error: `unknown /snapshot-auto-cleanup subcommand "${parts[0]}"` };
|
|
2318
2308
|
}
|
|
2319
2309
|
}
|
|
2320
|
-
function shouldRunAutoSweep(lastAtMs, nowMs) {
|
|
2321
|
-
return nowMs - lastAtMs >=
|
|
2310
|
+
function shouldRunAutoSweep(lastAtMs, nowMs, intervalMs = AUTO_SWEEP_INTERVAL_MS) {
|
|
2311
|
+
return nowMs - lastAtMs >= intervalMs;
|
|
2322
2312
|
}
|
|
2323
2313
|
|
|
2324
2314
|
// src/index.ts
|
|
@@ -2329,6 +2319,9 @@ var Config = z.object({
|
|
|
2329
2319
|
snapshotDir: z.string().required(false),
|
|
2330
2320
|
dshHome: z.string().required(false),
|
|
2331
2321
|
dedup: z.boolean().default(true),
|
|
2322
|
+
maxFileBytes: z.number().step(1).min(0).default(DEFAULT_MAX_FILE_BYTES),
|
|
2323
|
+
maxAnchorGroups: z.number().step(1).min(1).default(MAX_ANCHOR_GROUPS),
|
|
2324
|
+
autoSweepIntervalMs: z.number().step(1).min(1).default(AUTO_SWEEP_INTERVAL_MS),
|
|
2332
2325
|
autoCleanupEnabled: z.boolean().default(DEFAULT_CLEANUP_CONFIG.enabled).volatile(),
|
|
2333
2326
|
autoCleanupMaxAgeDays: z.number().step(1).min(1).default(DEFAULT_CLEANUP_CONFIG.maxAgeDays).volatile()
|
|
2334
2327
|
});
|
|
@@ -2388,10 +2381,7 @@ function anchorSeqOf(session, cache) {
|
|
|
2388
2381
|
}
|
|
2389
2382
|
async function resolveTarget(fs, path, cwd, signal) {
|
|
2390
2383
|
try {
|
|
2391
|
-
return await fs.resolve(path, {
|
|
2392
|
-
...cwd !== void 0 ? { cwd } : {},
|
|
2393
|
-
signal
|
|
2394
|
-
});
|
|
2384
|
+
return await fs.resolve(path, { cwd, signal });
|
|
2395
2385
|
} catch {
|
|
2396
2386
|
return void 0;
|
|
2397
2387
|
}
|
|
@@ -2400,20 +2390,20 @@ function isNotFoundError(error) {
|
|
|
2400
2390
|
const code = error?.code;
|
|
2401
2391
|
return code === "ENOENT" || code === "FS_NOT_FOUND";
|
|
2402
2392
|
}
|
|
2403
|
-
async function captureBefore(fs, store, exec, pending) {
|
|
2393
|
+
async function captureBefore(fs, workingDirectory, store, exec, pending) {
|
|
2404
2394
|
if (!TRACKED_TOOLS.has(exec.name)) return;
|
|
2405
|
-
const
|
|
2406
|
-
if (
|
|
2395
|
+
const agent = exec.agent;
|
|
2396
|
+
if (agent === void 0) return;
|
|
2397
|
+
if (isSubagentSession(agent.session)) return;
|
|
2407
2398
|
const path = mutationPathOf(exec);
|
|
2408
2399
|
if (path === void 0) return;
|
|
2409
|
-
const cwd =
|
|
2400
|
+
const cwd = await workingDirectory.ensure(agent, exec.signal);
|
|
2410
2401
|
const target = await resolveTarget(fs, path, cwd, exec.signal);
|
|
2411
2402
|
if (target === void 0) return;
|
|
2412
2403
|
const info = await fs.stat(target, exec.signal).catch((error) => {
|
|
2413
2404
|
if (isNotFoundError(error)) return void 0;
|
|
2414
2405
|
throw error;
|
|
2415
2406
|
});
|
|
2416
|
-
if (session === void 0) return;
|
|
2417
2407
|
if (info !== void 0 && info.type !== "file") return;
|
|
2418
2408
|
if (info !== void 0 && store.isOverFileCap(info.size)) return;
|
|
2419
2409
|
const key = `${exec.agent?.id ?? "anon"}:${exec.callId}`;
|
|
@@ -2421,7 +2411,7 @@ async function captureBefore(fs, store, exec, pending) {
|
|
|
2421
2411
|
pending.set(key, { path: target.displayPath, backup: null });
|
|
2422
2412
|
return;
|
|
2423
2413
|
}
|
|
2424
|
-
const staged = await store.stageCapture(session.id, key);
|
|
2414
|
+
const staged = await store.stageCapture(agent.session.id, key);
|
|
2425
2415
|
let backup;
|
|
2426
2416
|
try {
|
|
2427
2417
|
await copyFile2(target.displayPath, staged);
|
|
@@ -2457,11 +2447,16 @@ async function commitEntry(store, pending, anchorCache, trackedBySession, exec,
|
|
|
2457
2447
|
await discardCapture(capture);
|
|
2458
2448
|
return;
|
|
2459
2449
|
}
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2450
|
+
try {
|
|
2451
|
+
await store.recordBackup(agent.session.id, {
|
|
2452
|
+
callId: exec.callId,
|
|
2453
|
+
anchorSeq,
|
|
2454
|
+
path: capture.path
|
|
2455
|
+
}, capture.backup);
|
|
2456
|
+
} catch (error) {
|
|
2457
|
+
await discardCapture(capture);
|
|
2458
|
+
throw error;
|
|
2459
|
+
}
|
|
2465
2460
|
let tracked = trackedBySession.get(agent.session.id);
|
|
2466
2461
|
if (tracked === void 0) {
|
|
2467
2462
|
tracked = /* @__PURE__ */ new Set();
|
|
@@ -2541,7 +2536,7 @@ async function syncRestoreObservations(ctx, fs, agent, outcome) {
|
|
|
2541
2536
|
for (const path of outcome.restored) {
|
|
2542
2537
|
const target = await resolveObservationTarget(fs, path);
|
|
2543
2538
|
if (target === void 0) continue;
|
|
2544
|
-
const info = await fs.stat(target);
|
|
2539
|
+
const info = await fs.stat(target).catch(() => void 0);
|
|
2545
2540
|
if (info === void 0) continue;
|
|
2546
2541
|
ctx.emit("fs/observed", target, { kind: "present", version: info.version }, actor);
|
|
2547
2542
|
}
|
|
@@ -2699,7 +2694,7 @@ async function handleRewind(ctx, store, fs, invocation, inflight) {
|
|
|
2699
2694
|
return executeRewind(ctx, store, fs, invocation, target, mode, inflight);
|
|
2700
2695
|
}
|
|
2701
2696
|
var autoSweepChecked = false;
|
|
2702
|
-
async function maybeRunAutoCleanup(ctx, store, sessionId, dshHome, signal) {
|
|
2697
|
+
async function maybeRunAutoCleanup(ctx, store, sessionId, dshHome, sweepIntervalMs, signal) {
|
|
2703
2698
|
if (autoSweepChecked || signal?.aborted === true) return;
|
|
2704
2699
|
autoSweepChecked = true;
|
|
2705
2700
|
try {
|
|
@@ -2708,6 +2703,7 @@ async function maybeRunAutoCleanup(ctx, store, sessionId, dshHome, signal) {
|
|
|
2708
2703
|
readConfig: () => readCleanupPolicy(),
|
|
2709
2704
|
statePath: resolveCleanupStatePath(dshHome),
|
|
2710
2705
|
log: (msg) => ctx.logger.warn(msg),
|
|
2706
|
+
...sweepIntervalMs === void 0 ? {} : { intervalMs: sweepIntervalMs },
|
|
2711
2707
|
...signal === void 0 ? {} : { signal }
|
|
2712
2708
|
}, sessionId);
|
|
2713
2709
|
} catch (error) {
|
|
@@ -2845,7 +2841,12 @@ function apply(ctx, config) {
|
|
|
2845
2841
|
child.effect(() => child.settings.configure({ auto: false }, ctx.fiber), "dsh-rewind page policy");
|
|
2846
2842
|
});
|
|
2847
2843
|
const dshHome = config?.dshHome;
|
|
2848
|
-
const store = new SnapshotStore(config?.snapshotDir, {
|
|
2844
|
+
const store = new SnapshotStore(config?.snapshotDir, {
|
|
2845
|
+
dedup: config?.dedup,
|
|
2846
|
+
dshHome,
|
|
2847
|
+
maxFileBytes: config?.maxFileBytes,
|
|
2848
|
+
maxAnchorGroups: config?.maxAnchorGroups
|
|
2849
|
+
});
|
|
2849
2850
|
const configKey = cleanupConfigKey(rewindConfigKey(ctx));
|
|
2850
2851
|
if (config !== void 0 && configKey !== void 0) {
|
|
2851
2852
|
const writer = {
|
|
@@ -2941,11 +2942,12 @@ function apply(ctx, config) {
|
|
|
2941
2942
|
ctx.on("session/event", (session, event) => {
|
|
2942
2943
|
if (event.type !== "user/message") return;
|
|
2943
2944
|
if (isSubagentSession(session)) return;
|
|
2945
|
+
if (inflight.has(session.id)) return;
|
|
2944
2946
|
void (async () => {
|
|
2945
2947
|
try {
|
|
2946
2948
|
if (lifecycle.signal.aborted) return;
|
|
2947
2949
|
const sessionId = session.id;
|
|
2948
|
-
void maybeRunAutoCleanup(ctx, store, sessionId, dshHome, lifecycle.signal);
|
|
2950
|
+
void maybeRunAutoCleanup(ctx, store, sessionId, dshHome, config?.autoSweepIntervalMs, lifecycle.signal);
|
|
2949
2951
|
let tracked = trackedBySession.get(sessionId);
|
|
2950
2952
|
if (tracked === void 0) {
|
|
2951
2953
|
tracked = await store.trackedPaths(sessionId);
|
|
@@ -2959,12 +2961,16 @@ function apply(ctx, config) {
|
|
|
2959
2961
|
}
|
|
2960
2962
|
})();
|
|
2961
2963
|
}, { global: true });
|
|
2962
|
-
ctx.inject(["fs"], (scope) => {
|
|
2964
|
+
ctx.inject(["fs", "workingDirectory"], (scope) => {
|
|
2963
2965
|
const fs = scope.fs;
|
|
2966
|
+
const workingDirectory = scope.workingDirectory;
|
|
2964
2967
|
fsService = fs;
|
|
2968
|
+
scope.effect(() => () => {
|
|
2969
|
+
fsService = void 0;
|
|
2970
|
+
}, "dsh-rewind fs capture");
|
|
2965
2971
|
scope.on("tools/execute", async (exec, next) => {
|
|
2966
2972
|
try {
|
|
2967
|
-
await captureBefore(fs, store, exec, pending);
|
|
2973
|
+
await captureBefore(fs, workingDirectory, store, exec, pending);
|
|
2968
2974
|
} catch (error) {
|
|
2969
2975
|
ctx.logger.warn(`[dsh-rewind] before-capture failed for ${exec.name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2970
2976
|
}
|
|
@@ -2974,7 +2980,7 @@ function apply(ctx, config) {
|
|
|
2974
2980
|
try {
|
|
2975
2981
|
const session = exec.agent?.session;
|
|
2976
2982
|
if (session !== void 0 && !isSubagentSession(session)) {
|
|
2977
|
-
void maybeRunAutoCleanup(ctx, store, session.id, dshHome, lifecycle.signal);
|
|
2983
|
+
void maybeRunAutoCleanup(ctx, store, session.id, dshHome, config?.autoSweepIntervalMs, lifecycle.signal);
|
|
2978
2984
|
}
|
|
2979
2985
|
await commitEntry(store, pending, anchorCache, trackedBySession, exec, result);
|
|
2980
2986
|
} catch (error) {
|
|
@@ -235,7 +235,19 @@ export declare function actionsContainerOf(row: HTMLElement | undefined): HTMLEl
|
|
|
235
235
|
* @param hiddenSeqs - anchor seqs withdrawn by the executed rewinds.
|
|
236
236
|
* @param hidden - elements this pass has hidden, so a re-created row is re-judged.
|
|
237
237
|
*/
|
|
238
|
-
export declare function hideWithdrawnSeats(chat: HiddenChat | undefined, seats: Iterable<HTMLElement>, hiddenSeqs: ReadonlySet<number>, hidden:
|
|
238
|
+
export declare function hideWithdrawnSeats(chat: HiddenChat | undefined, seats: Iterable<HTMLElement>, hiddenSeqs: ReadonlySet<number>, hidden: Set<HTMLElement>): void;
|
|
239
|
+
/**
|
|
240
|
+
* Undo every hide the given set recorded, so a live disable (or a session-slot
|
|
241
|
+
* collapse) cannot leave the transcript truncated.
|
|
242
|
+
*
|
|
243
|
+
* The hide is the plugin's OWN mutation of host rows ({@link hideWithdrawnSeats}),
|
|
244
|
+
* and nothing else would ever revert it: a disposed bridge runs no further
|
|
245
|
+
* refresh, so rows it hid would stay hidden with no affordance to bring them
|
|
246
|
+
* back.
|
|
247
|
+
*
|
|
248
|
+
* @param hidden - the elements this bridge currently hides; emptied.
|
|
249
|
+
*/
|
|
250
|
+
export declare function showHiddenSeats(hidden: Set<HTMLElement>): void;
|
|
239
251
|
/**
|
|
240
252
|
* Collect the portal targets of one session: user rows × snapshot nodes.
|
|
241
253
|
* Exported as a test seam — the DOM→targets pairing that drives the ↶ button
|
package/lib/types/index.d.ts
CHANGED
|
@@ -37,12 +37,18 @@ export declare const inject: string[];
|
|
|
37
37
|
export declare const ENGINE_COMMAND = "rewind-plugin";
|
|
38
38
|
/** Plugin config. */
|
|
39
39
|
export interface RewindConfig {
|
|
40
|
-
/** Checkpoint store root (exact path; beats
|
|
40
|
+
/** Checkpoint store root (exact path; beats the harness-home default). */
|
|
41
41
|
readonly snapshotDir?: string;
|
|
42
42
|
/** Harness home override (`config.dshHome` > `$DSH_HOME` > `~/.dsh`); feeds the default snapshot/cleanup paths. */
|
|
43
43
|
readonly dshHome?: string;
|
|
44
44
|
/** In-place content dedup (identical before-content → link). Default `true`. */
|
|
45
45
|
readonly dedup?: boolean;
|
|
46
|
+
/** Per-file backup cap in bytes; `0` disables the cap. Default {@link DEFAULT_MAX_FILE_BYTES}. */
|
|
47
|
+
readonly maxFileBytes?: number;
|
|
48
|
+
/** Newest anchor groups (user messages) kept per session. Default {@link MAX_ANCHOR_GROUPS}. */
|
|
49
|
+
readonly maxAnchorGroups?: number;
|
|
50
|
+
/** Automatic snapshot-sweep cadence in ms. Default {@link AUTO_SWEEP_INTERVAL_MS}. */
|
|
51
|
+
readonly autoSweepIntervalMs?: number;
|
|
46
52
|
}
|
|
47
53
|
/**
|
|
48
54
|
* The plugin's full configuration: the ordinary store options plus the
|
|
@@ -62,12 +68,18 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
|
62
68
|
snapshotDir: z<string, string, "plain">;
|
|
63
69
|
dshHome: z<string, string, "plain">;
|
|
64
70
|
dedup: z<boolean, boolean, "defined">;
|
|
71
|
+
maxFileBytes: z<number, number, "defined">;
|
|
72
|
+
maxAnchorGroups: z<number, number, "defined">;
|
|
73
|
+
autoSweepIntervalMs: z<number, number, "defined">;
|
|
65
74
|
autoCleanupEnabled: z<boolean, boolean, "volatile-defined">;
|
|
66
75
|
autoCleanupMaxAgeDays: z<number, number, "volatile-defined">;
|
|
67
76
|
}>>, Schemastery.ObjectT<NoInfer<{
|
|
68
77
|
snapshotDir: z<string, string, "plain">;
|
|
69
78
|
dshHome: z<string, string, "plain">;
|
|
70
79
|
dedup: z<boolean, boolean, "defined">;
|
|
80
|
+
maxFileBytes: z<number, number, "defined">;
|
|
81
|
+
maxAnchorGroups: z<number, number, "defined">;
|
|
82
|
+
autoSweepIntervalMs: z<number, number, "defined">;
|
|
71
83
|
autoCleanupEnabled: z<boolean, boolean, "volatile-defined">;
|
|
72
84
|
autoCleanupMaxAgeDays: z<number, number, "volatile-defined">;
|
|
73
85
|
}>>, "plain">;
|
|
@@ -74,7 +74,7 @@ export interface CleanupConfigStore {
|
|
|
74
74
|
* field is cleared rather than pinned.
|
|
75
75
|
*/
|
|
76
76
|
export declare function volatileCleanupStore(settings: CleanupSettings, writer: CleanupConfigWriter): CleanupConfigStore;
|
|
77
|
-
/**
|
|
77
|
+
/** Default auto-sweep cadence (once a day); `autoSweepIntervalMs` overrides it. */
|
|
78
78
|
export declare const AUTO_SWEEP_INTERVAL_MS: number;
|
|
79
79
|
/** The state file that records the last automatic-sweep wall-clock time. */
|
|
80
80
|
export declare const STATE_FILENAME = "snapshot-cleanup-last-sweep.json";
|
|
@@ -123,6 +123,8 @@ export declare function runAutoCleanupCheck(deps: {
|
|
|
123
123
|
}>;
|
|
124
124
|
statePath: string;
|
|
125
125
|
log: (msg: string) => void;
|
|
126
|
+
/** Sweep cadence in ms; defaults to {@link AUTO_SWEEP_INTERVAL_MS}. */
|
|
127
|
+
intervalMs?: number;
|
|
126
128
|
/** Optional mount lifecycle signal; absent means "never cancelled". */
|
|
127
129
|
signal?: AbortSignal;
|
|
128
130
|
}, sessionId: string | undefined): Promise<void>;
|
|
@@ -165,8 +167,8 @@ export declare function parseCleanupCommand(rawInput: string): CleanupCommand |
|
|
|
165
167
|
error: string;
|
|
166
168
|
};
|
|
167
169
|
/**
|
|
168
|
-
* The
|
|
169
|
-
* process), so the first call always sweeps; after that a call within
|
|
170
|
-
* a no-op
|
|
170
|
+
* The auto-sweep throttle: `lastAtMs` of `0` means "never ran" (a fresh
|
|
171
|
+
* process), so the first call always sweeps; after that a call within
|
|
172
|
+
* `intervalMs` is a no-op. `autoSweepIntervalMs` sets the cadence.
|
|
171
173
|
*/
|
|
172
|
-
export declare function shouldRunAutoSweep(lastAtMs: number, nowMs: number): boolean;
|
|
174
|
+
export declare function shouldRunAutoSweep(lastAtMs: number, nowMs: number, intervalMs?: number): boolean;
|
package/lib/types/snapshot.d.ts
CHANGED
|
@@ -36,15 +36,12 @@
|
|
|
36
36
|
*
|
|
37
37
|
* @module dsh-rewind/snapshot
|
|
38
38
|
*/
|
|
39
|
-
/** Environment variable overriding the store root (tests, exotic homes). */
|
|
40
|
-
export declare const SNAPSHOT_ROOT_ENV = "DSH_REWIND_SNAPSHOT_DIR";
|
|
41
|
-
/** Env override for the per-file backup cap (`0` = unlimited); tests set it too. */
|
|
42
|
-
export declare const MAX_FILE_BYTES_ENV = "DSH_REWIND_MAX_FILE_BYTES";
|
|
43
39
|
/**
|
|
44
40
|
* A regular file larger than this is NOT backed up. This plugin is a
|
|
45
41
|
* lightweight, conversation-scoped undo, not a backup system: copying a very
|
|
46
42
|
* large file on every edit is what grows the store without bound and saturates
|
|
47
|
-
* IO (issue #39). The cap is a declared limitation, not a delta scheme
|
|
43
|
+
* IO (issue #39). The cap is a declared limitation, not a delta scheme, and is
|
|
44
|
+
* exposed as the `maxFileBytes` config field (`0` disables the cap).
|
|
48
45
|
*/
|
|
49
46
|
export declare const DEFAULT_MAX_FILE_BYTES: number;
|
|
50
47
|
/** Number of newest anchor groups (user messages) kept per session. */
|
|
@@ -453,6 +450,11 @@ export declare class SnapshotStore {
|
|
|
453
450
|
* and the restore planner refuses a pre-cap record over an over-cap file.
|
|
454
451
|
*/
|
|
455
452
|
private readonly maxFileBytes;
|
|
453
|
+
/**
|
|
454
|
+
* Newest anchor groups (user messages) kept per session; `prune` evicts past
|
|
455
|
+
* this bound. A `Config` field (`maxAnchorGroups`).
|
|
456
|
+
*/
|
|
457
|
+
private readonly maxAnchorGroups;
|
|
456
458
|
/** Resolved checkpoint store root (absolute); see the constructor's fallback. */
|
|
457
459
|
readonly root: string;
|
|
458
460
|
/**
|
|
@@ -481,6 +483,8 @@ export declare class SnapshotStore {
|
|
|
481
483
|
readonly dshHome?: string;
|
|
482
484
|
/** Per-file backup cap in bytes; `<= 0` disables it. */
|
|
483
485
|
readonly maxFileBytes?: number;
|
|
486
|
+
/** Newest anchor groups kept per session; see {@link MAX_ANCHOR_GROUPS}. */
|
|
487
|
+
readonly maxAnchorGroups?: number;
|
|
484
488
|
});
|
|
485
489
|
/** Absolute path of one session's snapshot directory (id sanitized). */
|
|
486
490
|
sessionDir(sessionId: string): string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-rewind-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0-alpha.2",
|
|
4
4
|
"description": "In-window conversation rewind with workspace file restore · 同窗口内对话回退并可还原工作区文件",
|
|
5
5
|
"icon": "./assets/icon.svg",
|
|
6
6
|
"keywords": [
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"license": "MIT",
|
|
21
21
|
"engines": {
|
|
22
22
|
"node": "^22.19.0 || >=24.0.0",
|
|
23
|
-
"dsh": ">=0.2.0-
|
|
23
|
+
"dsh": ">=0.2.1-alpha.2 <0.3.0-0"
|
|
24
24
|
},
|
|
25
25
|
"type": "module",
|
|
26
26
|
"main": "lib/index.js",
|
|
@@ -74,20 +74,21 @@
|
|
|
74
74
|
},
|
|
75
75
|
"peerDependencies": {
|
|
76
76
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
77
|
-
"@deepseek-ai/dsh-client-locale": "^0.2.
|
|
78
|
-
"@deepseek-ai/dsh-client-ui-commands": "^0.2.
|
|
79
|
-
"@deepseek-ai/dsh-client-ui-plugin-manager": "^0.2.
|
|
80
|
-
"@deepseek-ai/dsh-client-ui-primitives": "^0.2.
|
|
81
|
-
"@deepseek-ai/dsh-client-ui-settings": "^0.2.
|
|
82
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.2.
|
|
83
|
-
"@deepseek-ai/dsh-commands": "^0.2.
|
|
84
|
-
"@deepseek-ai/dsh-fs": "^0.2.
|
|
85
|
-
"@deepseek-ai/dsh-home-paths": "^0.2.
|
|
86
|
-
"@deepseek-ai/dsh-llm": "^0.2.
|
|
87
|
-
"@deepseek-ai/dsh-sandbox": "^0.2.
|
|
88
|
-
"@deepseek-ai/dsh-session": "^0.2.
|
|
89
|
-
"@deepseek-ai/dsh-settings": "^0.2.
|
|
90
|
-
"@deepseek-ai/dsh-tools": "^0.2.
|
|
77
|
+
"@deepseek-ai/dsh-client-locale": "^0.2.1-alpha.2",
|
|
78
|
+
"@deepseek-ai/dsh-client-ui-commands": "^0.2.1-alpha.2",
|
|
79
|
+
"@deepseek-ai/dsh-client-ui-plugin-manager": "^0.2.1-alpha.2",
|
|
80
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.2.1-alpha.2",
|
|
81
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.2.1-alpha.2",
|
|
82
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.2.1-alpha.2",
|
|
83
|
+
"@deepseek-ai/dsh-commands": "^0.2.1-alpha.2",
|
|
84
|
+
"@deepseek-ai/dsh-fs": "^0.2.1-alpha.2",
|
|
85
|
+
"@deepseek-ai/dsh-home-paths": "^0.2.1-alpha.2",
|
|
86
|
+
"@deepseek-ai/dsh-llm": "^0.2.1-alpha.2",
|
|
87
|
+
"@deepseek-ai/dsh-sandbox": "^0.2.1-alpha.2",
|
|
88
|
+
"@deepseek-ai/dsh-session": "^0.2.1-alpha.2",
|
|
89
|
+
"@deepseek-ai/dsh-settings": "^0.2.1-alpha.2",
|
|
90
|
+
"@deepseek-ai/dsh-tools": "^0.2.1-alpha.2",
|
|
91
|
+
"@deepseek-ai/dsh-working-directory": "^0.2.1-alpha.2",
|
|
91
92
|
"@deepseek-ai/schemastery": "^3.18.4"
|
|
92
93
|
},
|
|
93
94
|
"peerDependenciesMeta": {
|
|
@@ -136,6 +137,9 @@
|
|
|
136
137
|
"@deepseek-ai/dsh-tools": {
|
|
137
138
|
"optional": true
|
|
138
139
|
},
|
|
140
|
+
"@deepseek-ai/dsh-working-directory": {
|
|
141
|
+
"optional": true
|
|
142
|
+
},
|
|
139
143
|
"@deepseek-ai/schemastery": {
|
|
140
144
|
"optional": true
|
|
141
145
|
}
|
|
@@ -143,41 +147,42 @@
|
|
|
143
147
|
"devDependencies": {
|
|
144
148
|
"@deepseek-ai/cordis": "4.0.3",
|
|
145
149
|
"@deepseek-ai/cosmokit": "1.8.4",
|
|
146
|
-
"@deepseek-ai/dsh-agent": "0.2.1-alpha.
|
|
147
|
-
"@deepseek-ai/dsh-api-session-controller": "0.2.1-alpha.
|
|
148
|
-
"@deepseek-ai/dsh-attachment": "0.2.1-alpha.
|
|
149
|
-
"@deepseek-ai/dsh-client-connection": "0.2.1-alpha.
|
|
150
|
-
"@deepseek-ai/dsh-client-locale": "0.2.1-alpha.
|
|
151
|
-
"@deepseek-ai/dsh-client-store": "0.2.1-alpha.
|
|
152
|
-
"@deepseek-ai/dsh-client-ui-commands": "0.2.1-alpha.
|
|
153
|
-
"@deepseek-ai/dsh-client-ui-conversation": "0.2.1-alpha.
|
|
154
|
-
"@deepseek-ai/dsh-client-ui-input-trigger": "0.2.1-alpha.
|
|
155
|
-
"@deepseek-ai/dsh-client-ui-plugin-manager": "0.2.1-alpha.
|
|
156
|
-
"@deepseek-ai/dsh-client-ui-primitives": "0.2.1-alpha.
|
|
157
|
-
"@deepseek-ai/dsh-client-ui-session": "0.2.1-alpha.
|
|
158
|
-
"@deepseek-ai/dsh-client-ui-settings": "0.2.1-alpha.
|
|
159
|
-
"@deepseek-ai/dsh-client-ui-slots": "0.2.1-alpha.
|
|
160
|
-
"@deepseek-ai/dsh-command-compact": "0.2.1-alpha.
|
|
161
|
-
"@deepseek-ai/dsh-commands": "0.2.1-alpha.
|
|
162
|
-
"@deepseek-ai/dsh-compaction": "0.2.1-alpha.
|
|
163
|
-
"@deepseek-ai/dsh-compaction-basic": "0.2.1-alpha.
|
|
164
|
-
"@deepseek-ai/dsh-compaction-image-offload": "0.2.1-alpha.
|
|
165
|
-
"@deepseek-ai/dsh-fs": "0.2.1-alpha.
|
|
166
|
-
"@deepseek-ai/dsh-goal": "0.2.1-alpha.
|
|
167
|
-
"@deepseek-ai/dsh-home-paths": "0.2.1-alpha.
|
|
168
|
-
"@deepseek-ai/dsh-llm": "0.2.1-alpha.
|
|
169
|
-
"@deepseek-ai/dsh-plan-mode": "0.2.1-alpha.
|
|
170
|
-
"@deepseek-ai/dsh-sandbox": "0.2.1-alpha.
|
|
171
|
-
"@deepseek-ai/dsh-scope": "0.2.1-alpha.
|
|
172
|
-
"@deepseek-ai/dsh-session": "0.2.1-alpha.
|
|
173
|
-
"@deepseek-ai/dsh-session-projection": "0.2.1-alpha.
|
|
174
|
-
"@deepseek-ai/dsh-session-stats": "0.2.1-alpha.
|
|
175
|
-
"@deepseek-ai/dsh-session-title": "0.2.1-alpha.
|
|
176
|
-
"@deepseek-ai/dsh-settings": "0.2.1-alpha.
|
|
177
|
-
"@deepseek-ai/dsh-token-meter": "0.2.1-alpha.
|
|
178
|
-
"@deepseek-ai/dsh-tools": "0.2.1-alpha.
|
|
179
|
-
"@deepseek-ai/dsh-typert-protocol": "0.2.1-alpha.
|
|
180
|
-
"@deepseek-ai/dsh-user-questions": "0.2.1-alpha.
|
|
150
|
+
"@deepseek-ai/dsh-agent": "0.2.1-alpha.4",
|
|
151
|
+
"@deepseek-ai/dsh-api-session-controller": "0.2.1-alpha.4",
|
|
152
|
+
"@deepseek-ai/dsh-attachment": "0.2.1-alpha.4",
|
|
153
|
+
"@deepseek-ai/dsh-client-connection": "0.2.1-alpha.4",
|
|
154
|
+
"@deepseek-ai/dsh-client-locale": "0.2.1-alpha.4",
|
|
155
|
+
"@deepseek-ai/dsh-client-store": "0.2.1-alpha.4",
|
|
156
|
+
"@deepseek-ai/dsh-client-ui-commands": "0.2.1-alpha.4",
|
|
157
|
+
"@deepseek-ai/dsh-client-ui-conversation": "0.2.1-alpha.4",
|
|
158
|
+
"@deepseek-ai/dsh-client-ui-input-trigger": "0.2.1-alpha.4",
|
|
159
|
+
"@deepseek-ai/dsh-client-ui-plugin-manager": "0.2.1-alpha.4",
|
|
160
|
+
"@deepseek-ai/dsh-client-ui-primitives": "0.2.1-alpha.4",
|
|
161
|
+
"@deepseek-ai/dsh-client-ui-session": "0.2.1-alpha.4",
|
|
162
|
+
"@deepseek-ai/dsh-client-ui-settings": "0.2.1-alpha.4",
|
|
163
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.2.1-alpha.4",
|
|
164
|
+
"@deepseek-ai/dsh-command-compact": "0.2.1-alpha.4",
|
|
165
|
+
"@deepseek-ai/dsh-commands": "0.2.1-alpha.4",
|
|
166
|
+
"@deepseek-ai/dsh-compaction": "0.2.1-alpha.4",
|
|
167
|
+
"@deepseek-ai/dsh-compaction-basic": "0.2.1-alpha.4",
|
|
168
|
+
"@deepseek-ai/dsh-compaction-image-offload": "0.2.1-alpha.4",
|
|
169
|
+
"@deepseek-ai/dsh-fs": "0.2.1-alpha.4",
|
|
170
|
+
"@deepseek-ai/dsh-goal": "0.2.1-alpha.4",
|
|
171
|
+
"@deepseek-ai/dsh-home-paths": "0.2.1-alpha.4",
|
|
172
|
+
"@deepseek-ai/dsh-llm": "0.2.1-alpha.4",
|
|
173
|
+
"@deepseek-ai/dsh-plan-mode": "0.2.1-alpha.4",
|
|
174
|
+
"@deepseek-ai/dsh-sandbox": "0.2.1-alpha.4",
|
|
175
|
+
"@deepseek-ai/dsh-scope": "0.2.1-alpha.4",
|
|
176
|
+
"@deepseek-ai/dsh-session": "0.2.1-alpha.4",
|
|
177
|
+
"@deepseek-ai/dsh-session-projection": "0.2.1-alpha.4",
|
|
178
|
+
"@deepseek-ai/dsh-session-stats": "0.2.1-alpha.4",
|
|
179
|
+
"@deepseek-ai/dsh-session-title": "0.2.1-alpha.4",
|
|
180
|
+
"@deepseek-ai/dsh-settings": "0.2.1-alpha.4",
|
|
181
|
+
"@deepseek-ai/dsh-token-meter": "0.2.1-alpha.4",
|
|
182
|
+
"@deepseek-ai/dsh-tools": "0.2.1-alpha.4",
|
|
183
|
+
"@deepseek-ai/dsh-typert-protocol": "0.2.1-alpha.4",
|
|
184
|
+
"@deepseek-ai/dsh-user-questions": "0.2.1-alpha.4",
|
|
185
|
+
"@deepseek-ai/dsh-working-directory": "0.2.1-alpha.4",
|
|
181
186
|
"@deepseek-ai/schemastery": "3.18.4",
|
|
182
187
|
"@types/node": "^24.0.0",
|
|
183
188
|
"@types/react": "^18.3.31",
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Session-cwd resolution for snapshot tracking reads: relative paths resolve
|
|
3
|
-
* against the calling agent's session workspace (`exec.agent.session.header.cwd`),
|
|
4
|
-
* returned verbatim — no canonicalization — or `undefined` when the session has
|
|
5
|
-
* none (the filesystem backend then applies its own default base).
|
|
6
|
-
*
|
|
7
|
-
* @module dsh-rewind/session-cwd
|
|
8
|
-
*/
|
|
9
|
-
import type { ToolExecution } from '@deepseek-ai/dsh-tools';
|
|
10
|
-
/**
|
|
11
|
-
* The session workspace cwd to resolve a requested path against.
|
|
12
|
-
* @param cwd - the session's `header.cwd`, if any.
|
|
13
|
-
* @returns the cwd unchanged, or undefined when there is none.
|
|
14
|
-
*/
|
|
15
|
-
export declare function sessionCwd(cwd: string | undefined): string | undefined;
|
|
16
|
-
/**
|
|
17
|
-
* Session cwd for one tool execution.
|
|
18
|
-
* @param exec - the tool-execution context; only its optional `agent` is read.
|
|
19
|
-
* @returns the calling agent's session cwd, or undefined for a non-agent caller.
|
|
20
|
-
*/
|
|
21
|
-
export declare function execSessionCwd(exec: ToolExecution): string | undefined;
|