@tanstack/ai-sandbox 0.2.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/agents-file.js +53 -34
- package/dist/esm/agents-file.js.map +1 -1
- package/dist/esm/align.d.ts +121 -0
- package/dist/esm/align.js +197 -0
- package/dist/esm/align.js.map +1 -0
- package/dist/esm/approvals.js +63 -29
- package/dist/esm/approvals.js.map +1 -1
- package/dist/esm/attach-preflight.d.ts +85 -0
- package/dist/esm/attach-preflight.js +189 -0
- package/dist/esm/attach-preflight.js.map +1 -0
- package/dist/esm/bootstrap.js +103 -117
- package/dist/esm/bootstrap.js.map +1 -1
- package/dist/esm/bridge-events.js +96 -71
- package/dist/esm/bridge-events.js.map +1 -1
- package/dist/esm/capabilities.d.ts +0 -5
- package/dist/esm/capabilities.js +32 -28
- package/dist/esm/capabilities.js.map +1 -1
- package/dist/esm/chunk-identity.d.ts +52 -0
- package/dist/esm/chunk-identity.js +102 -0
- package/dist/esm/chunk-identity.js.map +1 -0
- package/dist/esm/claim.d.ts +187 -0
- package/dist/esm/claim.js +349 -0
- package/dist/esm/claim.js.map +1 -0
- package/dist/esm/contracts.d.ts +13 -0
- package/dist/esm/driver.d.ts +83 -0
- package/dist/esm/driver.js +138 -0
- package/dist/esm/driver.js.map +1 -0
- package/dist/esm/durability.d.ts +263 -0
- package/dist/esm/durability.js +230 -0
- package/dist/esm/durability.js.map +1 -0
- package/dist/esm/errors.js +28 -24
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/file-diff.js +151 -135
- package/dist/esm/file-diff.js.map +1 -1
- package/dist/esm/git-exec.js +51 -62
- package/dist/esm/git-exec.js.map +1 -1
- package/dist/esm/harness-cwd.js +24 -19
- package/dist/esm/harness-cwd.js.map +1 -1
- package/dist/esm/index.d.ts +30 -8
- package/dist/esm/index.js +23 -91
- package/dist/esm/instance-store.d.ts +88 -0
- package/dist/esm/instance-store.js +67 -0
- package/dist/esm/instance-store.js.map +1 -0
- package/dist/esm/journal-bytes.d.ts +67 -0
- package/dist/esm/journal-bytes.js +110 -0
- package/dist/esm/journal-bytes.js.map +1 -0
- package/dist/esm/journal-reader.d.ts +66 -0
- package/dist/esm/journal-reader.js +228 -0
- package/dist/esm/journal-reader.js.map +1 -0
- package/dist/esm/journal-sweep.d.ts +113 -0
- package/dist/esm/journal-sweep.js +309 -0
- package/dist/esm/journal-sweep.js.map +1 -0
- package/dist/esm/journal.d.ts +542 -0
- package/dist/esm/journal.js +679 -0
- package/dist/esm/journal.js.map +1 -0
- package/dist/esm/key.js +36 -33
- package/dist/esm/key.js.map +1 -1
- package/dist/esm/middleware.d.ts +50 -2
- package/dist/esm/middleware.js +335 -208
- package/dist/esm/middleware.js.map +1 -1
- package/dist/esm/ngrok.js +75 -49
- package/dist/esm/ngrok.js.map +1 -1
- package/dist/esm/policy.js +43 -34
- package/dist/esm/policy.js.map +1 -1
- package/dist/esm/projection.js +16 -8
- package/dist/esm/projection.js.map +1 -1
- package/dist/esm/reap.d.ts +238 -0
- package/dist/esm/reap.js +355 -0
- package/dist/esm/reap.js.map +1 -0
- package/dist/esm/reclaim.d.ts +84 -0
- package/dist/esm/reclaim.js +106 -0
- package/dist/esm/reclaim.js.map +1 -0
- package/dist/esm/remote-tools.js +73 -62
- package/dist/esm/remote-tools.js.map +1 -1
- package/dist/esm/run.d.ts +93 -25
- package/dist/esm/run.js +274 -79
- package/dist/esm/run.js.map +1 -1
- package/dist/esm/runner.d.ts +119 -2
- package/dist/esm/runner.js +270 -51
- package/dist/esm/runner.js.map +1 -1
- package/dist/esm/sandbox.d.ts +3 -2
- package/dist/esm/sandbox.js +139 -123
- package/dist/esm/sandbox.js.map +1 -1
- package/dist/esm/secrets.js +39 -47
- package/dist/esm/secrets.js.map +1 -1
- package/dist/esm/setup-plan.js +22 -14
- package/dist/esm/setup-plan.js.map +1 -1
- package/dist/esm/shell.d.ts +8 -0
- package/dist/esm/shell.js +197 -158
- package/dist/esm/shell.js.map +1 -1
- package/dist/esm/testkit/conformance.d.ts +16 -0
- package/dist/esm/testkit/conformance.js +97 -0
- package/dist/esm/testkit/conformance.js.map +1 -0
- package/dist/esm/testkit/durable-run-fields-conformance.d.ts +4 -0
- package/dist/esm/testkit/durable-run-fields-conformance.js +95 -0
- package/dist/esm/testkit/durable-run-fields-conformance.js.map +1 -0
- package/dist/esm/testkit/journal-conformance.d.ts +51 -0
- package/dist/esm/testkit/journal-conformance.js +378 -0
- package/dist/esm/testkit/journal-conformance.js.map +1 -0
- package/dist/esm/testkit/reaper-conformance.d.ts +37 -0
- package/dist/esm/testkit/reaper-conformance.js +847 -0
- package/dist/esm/testkit/reaper-conformance.js.map +1 -0
- package/dist/esm/testkit/shell-spawn.d.ts +2 -0
- package/dist/esm/testkit/shell-spawn.js +60 -0
- package/dist/esm/testkit/shell-spawn.js.map +1 -0
- package/dist/esm/testkit/takeover-conformance.d.ts +24 -0
- package/dist/esm/testkit/takeover-conformance.js +685 -0
- package/dist/esm/testkit/takeover-conformance.js.map +1 -0
- package/dist/esm/tool-bridge.js +227 -180
- package/dist/esm/tool-bridge.js.map +1 -1
- package/dist/esm/tool-history.d.ts +62 -0
- package/dist/esm/tool-history.js +171 -0
- package/dist/esm/tool-history.js.map +1 -0
- package/dist/esm/watch.js +310 -236
- package/dist/esm/watch.js.map +1 -1
- package/dist/esm/workspace.d.ts +1 -1
- package/dist/esm/workspace.js +49 -28
- package/dist/esm/workspace.js.map +1 -1
- package/package.json +16 -6
- package/skills/ai-sandbox/SKILL.md +658 -20
- package/src/align.ts +297 -0
- package/src/attach-preflight.ts +292 -0
- package/src/capabilities.ts +4 -13
- package/src/chunk-identity.ts +154 -0
- package/src/claim.ts +479 -0
- package/src/contracts.ts +13 -0
- package/src/driver.ts +205 -0
- package/src/durability.ts +380 -0
- package/src/index.ts +212 -27
- package/src/instance-store.ts +122 -0
- package/src/journal-bytes.ts +136 -0
- package/src/journal-reader.ts +359 -0
- package/src/journal-sweep.ts +406 -0
- package/src/journal.ts +875 -0
- package/src/middleware.ts +470 -30
- package/src/reap.ts +723 -0
- package/src/reclaim.ts +191 -0
- package/src/run.ts +365 -75
- package/src/runner.ts +347 -3
- package/src/sandbox.ts +38 -8
- package/src/shell.ts +106 -38
- package/src/testkit/conformance.ts +117 -0
- package/src/testkit/durable-run-fields-conformance.ts +147 -0
- package/src/testkit/journal-conformance.ts +676 -0
- package/src/testkit/reaper-conformance.ts +1201 -0
- package/src/testkit/shell-spawn.ts +67 -0
- package/src/testkit/takeover-conformance.ts +1040 -0
- package/src/tool-history.ts +245 -0
- package/src/workspace.ts +1 -1
- package/dist/esm/index.js.map +0 -1
- package/dist/esm/run-log.d.ts +0 -81
- package/dist/esm/run-log.js +0 -107
- package/dist/esm/run-log.js.map +0 -1
- package/dist/esm/store.d.ts +0 -53
- package/dist/esm/store.js +0 -34
- package/dist/esm/store.js.map +0 -1
- package/src/run-log.ts +0 -224
- package/src/store.ts +0 -83
package/dist/esm/watch.js
CHANGED
|
@@ -1,254 +1,328 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import "./bootstrap.js";
|
|
2
|
+
//#region src/watch.ts
|
|
3
|
+
/**
|
|
4
|
+
* Sandbox file-event hooks — observe create / change / delete of files inside a
|
|
5
|
+
* sandbox (e.g. as an in-sandbox agent edits the workspace).
|
|
6
|
+
*
|
|
7
|
+
* Provider-agnostic: coded against the {@link SandboxHandle} contract only.
|
|
8
|
+
* Two mechanisms, auto-selected:
|
|
9
|
+
*
|
|
10
|
+
* - **Native** — when a provider implements the optional `fs.watch` seam
|
|
11
|
+
* (local-process does, via Node `fs.watch`), OS events drive the feed with low
|
|
12
|
+
* latency.
|
|
13
|
+
* - **Exec-poll** — otherwise (Docker, Cloudflare, any exec-only provider), a
|
|
14
|
+
* single `find … -printf` snapshot of `mtime\tsize\tpath` is taken every
|
|
15
|
+
* `intervalMs` and diffed. Works on any Linux container with GNU findutils
|
|
16
|
+
* (true for `node:*` / debian images) with no extra deps or image changes.
|
|
17
|
+
*
|
|
18
|
+
* The feed intentionally rides only the portable surface, so the same
|
|
19
|
+
* `watchWorkspace` call behaves identically across providers.
|
|
20
|
+
*/
|
|
21
|
+
var DEFAULT_INTERVAL_MS = 700;
|
|
22
|
+
var DEFAULT_IGNORE = [".git", "node_modules"];
|
|
23
|
+
/** POSIX single-quote escape for embedding values in a shell command. */
|
|
4
24
|
function q(value) {
|
|
5
|
-
|
|
25
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
6
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Diff two file snapshots (`Map<path, signature>`, signature = `mtime\tsize`).
|
|
29
|
+
* Pure — the heart of the exec-poll path, unit-tested in isolation.
|
|
30
|
+
*/
|
|
7
31
|
function diffSnapshots(prev, next, timestamp) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
32
|
+
const events = [];
|
|
33
|
+
for (const [path, sig] of next) {
|
|
34
|
+
const before = prev.get(path);
|
|
35
|
+
if (before === void 0) events.push({
|
|
36
|
+
type: "create",
|
|
37
|
+
path,
|
|
38
|
+
timestamp
|
|
39
|
+
});
|
|
40
|
+
else if (before !== sig) events.push({
|
|
41
|
+
type: "change",
|
|
42
|
+
path,
|
|
43
|
+
timestamp
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
for (const path of prev.keys()) if (!next.has(path)) events.push({
|
|
47
|
+
type: "delete",
|
|
48
|
+
path,
|
|
49
|
+
timestamp
|
|
50
|
+
});
|
|
51
|
+
return events;
|
|
18
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Build the `find` command that prints `mtime\tsize\tpath` for every file.
|
|
55
|
+
* Searches `.` (relative to the exec `cwd`) rather than an absolute root: a
|
|
56
|
+
* provider's `exec` maps only `cwd` onto the real filesystem, not literal path
|
|
57
|
+
* arguments, so `find <virtual-root>` would look at a non-existent host path on
|
|
58
|
+
* mapped-root providers (e.g. local-process). Emitted `%p` values are
|
|
59
|
+
* root-normalized in {@link parseFindOutput}.
|
|
60
|
+
*/
|
|
19
61
|
function buildFindCommand(ignore) {
|
|
20
|
-
|
|
21
|
-
return `find . -type f ${prunes} -printf '%T@\\t%s\\t%p\\n'`;
|
|
62
|
+
return `find . -type f ${ignore.map((entry) => `-not -path ${q(`*/${entry}/*`)}`).join(" ")} -printf '%T@\\t%s\\t%p\\n'`;
|
|
22
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Parse `find -printf` output into a `Map<path, signature>`. `find .` prints
|
|
66
|
+
* paths like `./sub/file`; map them back under `root` so event paths match the
|
|
67
|
+
* native-watch shape (`<root>/sub/file`).
|
|
68
|
+
*/
|
|
23
69
|
function parseFindOutput(stdout, root) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
70
|
+
const base = root.replace(/\/+$/, "");
|
|
71
|
+
const snapshot = /* @__PURE__ */ new Map();
|
|
72
|
+
for (const line of stdout.split("\n")) {
|
|
73
|
+
if (line === "") continue;
|
|
74
|
+
const firstTab = line.indexOf(" ");
|
|
75
|
+
const secondTab = line.indexOf(" ", firstTab + 1);
|
|
76
|
+
if (firstTab === -1 || secondTab === -1) continue;
|
|
77
|
+
const mtime = line.slice(0, firstTab);
|
|
78
|
+
const size = line.slice(firstTab + 1, secondTab);
|
|
79
|
+
const rel = line.slice(secondTab + 1).replace(/^\.\/?/, "");
|
|
80
|
+
const path = rel === "" ? base : `${base}/${rel}`;
|
|
81
|
+
snapshot.set(path, `${mtime}\t${size}`);
|
|
82
|
+
}
|
|
83
|
+
return snapshot;
|
|
38
84
|
}
|
|
85
|
+
/** Whether a path should be ignored (contains a `/<entry>/` fragment). */
|
|
39
86
|
function isIgnored(path, ignore) {
|
|
40
|
-
|
|
87
|
+
return ignore.some((entry) => path.includes(`/${entry}/`));
|
|
41
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Start watching a sandbox workspace for file events. Picks the native
|
|
91
|
+
* `fs.watch` fast-path when the provider advertises it, otherwise polls via
|
|
92
|
+
* `find`. Returns a handle whose `stop()` tears everything down.
|
|
93
|
+
*/
|
|
42
94
|
async function watchWorkspace(handle, options) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
95
|
+
const root = options.root ?? "/workspace";
|
|
96
|
+
const ignore = options.ignore ?? DEFAULT_IGNORE;
|
|
97
|
+
const intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
98
|
+
if (options.signal?.aborted) return { stop: () => Promise.resolve() };
|
|
99
|
+
if (handle.fs.watch) return startNativeWatch(handle, {
|
|
100
|
+
...options,
|
|
101
|
+
root,
|
|
102
|
+
ignore
|
|
103
|
+
});
|
|
104
|
+
return startPollWatch(handle, {
|
|
105
|
+
...options,
|
|
106
|
+
root,
|
|
107
|
+
ignore,
|
|
108
|
+
intervalMs
|
|
109
|
+
});
|
|
51
110
|
}
|
|
111
|
+
/** Native fs.watch path: OS events, disambiguated against a known-path set. */
|
|
52
112
|
async function startNativeWatch(handle, options) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
113
|
+
const { onEvent, root, ignore, logger } = options;
|
|
114
|
+
const watch = handle.fs.watch;
|
|
115
|
+
if (!watch) throw new Error("native watch is unavailable on this provider");
|
|
116
|
+
const seed = await collectPaths(handle, root, ignore, logger);
|
|
117
|
+
const known = seed.files;
|
|
118
|
+
let seeded = seed.rootOk;
|
|
119
|
+
let reseeding = null;
|
|
120
|
+
const ensureSeeded = () => {
|
|
121
|
+
if (seeded) return Promise.resolve();
|
|
122
|
+
if (!reseeding) reseeding = collectPaths(handle, root, ignore, logger).then((r) => {
|
|
123
|
+
if (r.rootOk) {
|
|
124
|
+
for (const p of r.files) known.add(p);
|
|
125
|
+
seeded = true;
|
|
126
|
+
logger?.sandbox("sandbox watch: re-seeded after failed initial seed", { root });
|
|
127
|
+
}
|
|
128
|
+
reseeding = null;
|
|
129
|
+
});
|
|
130
|
+
return reseeding;
|
|
131
|
+
};
|
|
132
|
+
const subscription = await watch(root, (raw) => {
|
|
133
|
+
const path = raw.path;
|
|
134
|
+
if (isIgnored(path, ignore)) return;
|
|
135
|
+
(async () => {
|
|
136
|
+
await ensureSeeded();
|
|
137
|
+
const exists = await handle.fs.exists(path);
|
|
138
|
+
const timestamp = Date.now();
|
|
139
|
+
if (!exists) {
|
|
140
|
+
if (known.delete(path)) onEvent({
|
|
141
|
+
type: "delete",
|
|
142
|
+
path,
|
|
143
|
+
timestamp
|
|
144
|
+
});
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (known.has(path)) onEvent({
|
|
148
|
+
type: "change",
|
|
149
|
+
path,
|
|
150
|
+
timestamp
|
|
151
|
+
});
|
|
152
|
+
else {
|
|
153
|
+
known.add(path);
|
|
154
|
+
onEvent({
|
|
155
|
+
type: "create",
|
|
156
|
+
path,
|
|
157
|
+
timestamp
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
})().catch((error) => {
|
|
161
|
+
logger?.warn("sandbox watch: native event classify failed", {
|
|
162
|
+
path,
|
|
163
|
+
error
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
const logStopFailure = (error) => logger?.warn("sandbox watch: native subscription.stop() failed", {
|
|
168
|
+
root,
|
|
169
|
+
error
|
|
170
|
+
});
|
|
171
|
+
const onAbort = () => void subscription.stop().catch(logStopFailure);
|
|
172
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
173
|
+
if (options.signal?.aborted) subscription.stop().catch(logStopFailure);
|
|
174
|
+
return { stop: async () => {
|
|
175
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
176
|
+
await subscription.stop();
|
|
177
|
+
} };
|
|
115
178
|
}
|
|
179
|
+
/** Exec-poll path: snapshot `find -printf` on an interval and diff. */
|
|
116
180
|
async function startPollWatch(handle, options) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
181
|
+
const { onEvent, root, ignore, intervalMs, logger } = options;
|
|
182
|
+
const command = buildFindCommand(ignore);
|
|
183
|
+
const controller = new AbortController();
|
|
184
|
+
const STEADY_STATE_THROW_WARN_AFTER = 3;
|
|
185
|
+
let consecutiveThrows = 0;
|
|
186
|
+
const snapshot = async (isInitial = false) => {
|
|
187
|
+
let result;
|
|
188
|
+
try {
|
|
189
|
+
result = await handle.process.exec(command, {
|
|
190
|
+
cwd: root,
|
|
191
|
+
signal: controller.signal
|
|
192
|
+
});
|
|
193
|
+
consecutiveThrows = 0;
|
|
194
|
+
} catch (error) {
|
|
195
|
+
if (isInitial) logger?.warn("sandbox watch: initial `find` poll threw", {
|
|
196
|
+
root,
|
|
197
|
+
error
|
|
198
|
+
});
|
|
199
|
+
else if (controller.signal.aborted) logger?.sandbox("sandbox watch: `find` poll threw during teardown", {
|
|
200
|
+
root,
|
|
201
|
+
error
|
|
202
|
+
});
|
|
203
|
+
else {
|
|
204
|
+
consecutiveThrows += 1;
|
|
205
|
+
if (consecutiveThrows >= STEADY_STATE_THROW_WARN_AFTER) logger?.warn("sandbox watch: `find` poll threw repeatedly", {
|
|
206
|
+
root,
|
|
207
|
+
error,
|
|
208
|
+
consecutiveThrows
|
|
209
|
+
});
|
|
210
|
+
else logger?.sandbox("sandbox watch: `find` poll threw", {
|
|
211
|
+
root,
|
|
212
|
+
error
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
if (result.exitCode === 0) return {
|
|
218
|
+
map: parseFindOutput(result.stdout, root),
|
|
219
|
+
complete: true
|
|
220
|
+
};
|
|
221
|
+
if (result.stdout !== "") {
|
|
222
|
+
logger?.sandbox("sandbox watch: `find` non-zero exit with partial output", {
|
|
223
|
+
root,
|
|
224
|
+
exitCode: result.exitCode,
|
|
225
|
+
stderr: result.stderr
|
|
226
|
+
});
|
|
227
|
+
return {
|
|
228
|
+
map: parseFindOutput(result.stdout, root),
|
|
229
|
+
complete: false
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
logger?.warn("sandbox watch: `find` poll exited non-zero with no output", {
|
|
233
|
+
root,
|
|
234
|
+
exitCode: result.exitCode,
|
|
235
|
+
stderr: result.stderr
|
|
236
|
+
});
|
|
237
|
+
return null;
|
|
238
|
+
};
|
|
239
|
+
let previous = null;
|
|
240
|
+
let seededFromComplete = false;
|
|
241
|
+
{
|
|
242
|
+
const poll = await snapshot(true);
|
|
243
|
+
if (poll) {
|
|
244
|
+
previous = poll.map;
|
|
245
|
+
seededFromComplete = poll.complete;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const state = { running: true };
|
|
249
|
+
const tick = async () => {
|
|
250
|
+
if (!state.running) return;
|
|
251
|
+
try {
|
|
252
|
+
const poll = await snapshot();
|
|
253
|
+
if (poll === null) return;
|
|
254
|
+
if (previous === null) {
|
|
255
|
+
previous = poll.map;
|
|
256
|
+
seededFromComplete = poll.complete;
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (!seededFromComplete && poll.complete) {
|
|
260
|
+
logger?.sandbox("sandbox watch: re-baselined after provisional partial seed", { root });
|
|
261
|
+
previous = poll.map;
|
|
262
|
+
seededFromComplete = true;
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
const next = poll.complete ? poll.map : new Map([...previous, ...poll.map]);
|
|
266
|
+
for (const event of diffSnapshots(previous, next, Date.now())) onEvent(event);
|
|
267
|
+
previous = next;
|
|
268
|
+
} catch (error) {
|
|
269
|
+
logger?.sandbox("sandbox watch: tick failed", {
|
|
270
|
+
root,
|
|
271
|
+
error
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
const timer = setInterval(() => void tick(), intervalMs);
|
|
276
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
277
|
+
const stop = () => {
|
|
278
|
+
if (state.running) {
|
|
279
|
+
state.running = false;
|
|
280
|
+
clearInterval(timer);
|
|
281
|
+
controller.abort();
|
|
282
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
283
|
+
}
|
|
284
|
+
return Promise.resolve();
|
|
285
|
+
};
|
|
286
|
+
const onAbort = () => void stop();
|
|
287
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
288
|
+
if (options.signal?.aborted) stop();
|
|
289
|
+
return { stop };
|
|
225
290
|
}
|
|
291
|
+
/**
|
|
292
|
+
* Recursively collect file paths under `root`, honoring `ignore`. `rootOk` is
|
|
293
|
+
* `false` when the ROOT `list` itself failed — the seed is then untrustworthy
|
|
294
|
+
* (empty/partial), which the native watcher uses to trigger a lazy re-seed. A
|
|
295
|
+
* failed *subdirectory* list is logged but doesn't flip `rootOk` (its files are
|
|
296
|
+
* simply absent, a smaller misclassification surface).
|
|
297
|
+
*/
|
|
226
298
|
async function collectPaths(handle, root, ignore, logger) {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
299
|
+
const files = /* @__PURE__ */ new Set();
|
|
300
|
+
let rootOk = true;
|
|
301
|
+
const walk = async (dir, isRoot) => {
|
|
302
|
+
let entries;
|
|
303
|
+
try {
|
|
304
|
+
entries = await handle.fs.list(dir);
|
|
305
|
+
} catch (error) {
|
|
306
|
+
if (isRoot) rootOk = false;
|
|
307
|
+
logger?.warn("sandbox watch: failed to list directory while seeding", {
|
|
308
|
+
dir,
|
|
309
|
+
error
|
|
310
|
+
});
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
for (const entry of entries) {
|
|
314
|
+
if (ignore.includes(entry.name)) continue;
|
|
315
|
+
if (entry.type === "dir") await walk(entry.path, false);
|
|
316
|
+
else files.add(entry.path);
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
await walk(root, true);
|
|
320
|
+
return {
|
|
321
|
+
files,
|
|
322
|
+
rootOk
|
|
323
|
+
};
|
|
249
324
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
//# sourceMappingURL=watch.js.map
|
|
325
|
+
//#endregion
|
|
326
|
+
export { diffSnapshots, watchWorkspace };
|
|
327
|
+
|
|
328
|
+
//# sourceMappingURL=watch.js.map
|