@tangle-network/agent-app 0.46.60 → 0.47.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/README.md +12 -0
- package/dist/assistant/index.js +1 -1
- package/dist/chat-routes/index.js +2 -2
- package/dist/{chunk-QY4BRKRJ.js → chunk-HZVVO6VR.js} +4 -3
- package/dist/chunk-HZVVO6VR.js.map +1 -0
- package/dist/{chunk-WSRECTKA.js → chunk-P6YXUGNA.js} +29 -47
- package/dist/chunk-P6YXUGNA.js.map +1 -0
- package/dist/sandbox/index.d.ts +4 -11
- package/dist/sandbox/index.js +1 -3
- package/dist/web-react/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-QY4BRKRJ.js.map +0 -1
- package/dist/chunk-WSRECTKA.js.map +0 -1
package/README.md
CHANGED
|
@@ -173,6 +173,18 @@ Two `createMissionService` seams adopters hit on day one:
|
|
|
173
173
|
- **`generateId`** (on `MissionServiceOptions`) defaults to `crypto.randomUUID()` — a 36-char dashed UUID. If your mission table has an existing id shape (e.g. 32-hex to match D1 row defaults), inject your own generator; the service stamps it verbatim on the inserted record.
|
|
174
174
|
- **`CreateMissionInput.extras`** carries opaque product-column values (a `workflowId` FK, a source-turn pointer) verbatim to `MissionStorePort.insert(record, extras)`, so creation is a single write — no post-insert stamp. The service never reads them.
|
|
175
175
|
|
|
176
|
+
### Sandbox reuse
|
|
177
|
+
|
|
178
|
+
Set `livenessProbe: {}` to verify a reused runtime with `echo alive`.
|
|
179
|
+
The command must exit successfully and return only `alive`, allowing surrounding whitespace.
|
|
180
|
+
A healthy idle runtime does not need a running CLI process.
|
|
181
|
+
Remove `sidecarProcessPattern`, `psTimeoutMs`, and imports of `DEFAULT_SIDECAR_PROCESS_PATTERN` when upgrading.
|
|
182
|
+
|
|
183
|
+
`execTimeoutMs` sets the SDK command timeout, from 100 to 600,000 milliseconds; the default is 5,000.
|
|
184
|
+
SDK readiness and transport deadlines apply separately, so this value is not an end-to-end provisioning deadline.
|
|
185
|
+
Recovery waits for the SDK request to settle before restarting the sandbox.
|
|
186
|
+
Failures retain their SDK cause for the redacted diagnostic serializer and preserve the workspace.
|
|
187
|
+
|
|
176
188
|
## Compatibility
|
|
177
189
|
|
|
178
190
|
- **ESM only.** Ships `import` + `types` conditions per subpath.
|
package/dist/assistant/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
} from "../chunk-MH74DY2I.js";
|
|
4
4
|
import {
|
|
5
5
|
createSandboxFileIndexRoute
|
|
6
|
-
} from "../chunk-
|
|
6
|
+
} from "../chunk-HZVVO6VR.js";
|
|
7
7
|
import {
|
|
8
8
|
ALLOWED_ATTACHMENT_SNIFFED_MIMES,
|
|
9
9
|
ATTACHMENT_ACCEPT,
|
|
@@ -112,7 +112,7 @@ import {
|
|
|
112
112
|
flattenHistory,
|
|
113
113
|
readSandboxBinaryBytes,
|
|
114
114
|
statSandboxFileSize
|
|
115
|
-
} from "../chunk-
|
|
115
|
+
} from "../chunk-P6YXUGNA.js";
|
|
116
116
|
import "../chunk-MCYJON3F.js";
|
|
117
117
|
import "../chunk-LWSJK546.js";
|
|
118
118
|
import "../chunk-P5PIAQVS.js";
|
|
@@ -10,6 +10,7 @@ var DEFAULT_IGNORE_SEGMENTS = [
|
|
|
10
10
|
"venv"
|
|
11
11
|
];
|
|
12
12
|
function isIgnored(relPath, ignoreSegments) {
|
|
13
|
+
if (!relPath || relPath.startsWith("/") || relPath.includes("\\")) return true;
|
|
13
14
|
for (const segment of relPath.split("/")) {
|
|
14
15
|
if (!segment) continue;
|
|
15
16
|
if (segment.startsWith(".")) return true;
|
|
@@ -59,10 +60,10 @@ function createSandboxFileIndexRoute(options) {
|
|
|
59
60
|
if (!isMissingRootError(err, auth.root)) throw err;
|
|
60
61
|
return Response.json({ status: "warming" });
|
|
61
62
|
}
|
|
62
|
-
const filtered = scan.files.filter((f) => !isIgnored(relativeTo(
|
|
63
|
+
const filtered = scan.files.filter((f) => !isIgnored(relativeTo(auth.root, f.path), ignoreSegments));
|
|
63
64
|
const truncated = scan.stats.truncated || filtered.length > maxEntries;
|
|
64
65
|
const files = filtered.slice(0, maxEntries).map((f) => {
|
|
65
|
-
const path = relativeTo(
|
|
66
|
+
const path = relativeTo(auth.root, f.path);
|
|
66
67
|
const entry = { path, name: basename(path) };
|
|
67
68
|
if (typeof f.size === "number") entry.size = f.size;
|
|
68
69
|
return entry;
|
|
@@ -81,4 +82,4 @@ function createSandboxFileIndexRoute(options) {
|
|
|
81
82
|
export {
|
|
82
83
|
createSandboxFileIndexRoute
|
|
83
84
|
};
|
|
84
|
-
//# sourceMappingURL=chunk-
|
|
85
|
+
//# sourceMappingURL=chunk-HZVVO6VR.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/chat-routes/file-index.ts"],"sourcesContent":["/**\n * `createSandboxFileIndexRoute` — server side of `@`-file-mentions\n * (companion to sandbox-ui#184's composer mention primitive). Serves a flat,\n * ignore-filtered listing of the workspace sandbox so `useFileMentions`\n * (`/web-react`) can filter it client-side without a round trip per\n * keystroke.\n *\n * Same seam style as `createUploadRoute`: `authorize({ request })` resolves a\n * structural `{ tree(path, opts) }` handle (the shape of the sandbox SDK's\n * `box.fs.tree`) — no SDK import here. `authorize` also carries the\n * cold-box signal: a sandbox that isn't running yet answers `{ status:\n * 'warming' }` directly, never provisions-and-waits inside this route.\n *\n * A box can also be running with its workspace root not yet materialised, which\n * `authorize` cannot see; the route recognises that one signal off `fs.tree`\n * and answers `warming` too, so every consumer gets the retry-and-wait state\n * instead of a 500. Every other `tree()` failure propagates.\n */\n\nimport type { FileMention } from './wire'\n\n/** One entry from a structural `tree()` scan. Mirrors the sandbox SDK's\n * `FileTreeFile` (`path`, `size`, `mtime`) — `mtime` is unused here so it's\n * omitted from the structural match. */\nexport interface SandboxTreeFile {\n path: string\n size: number\n}\n\n/** Structural match of the sandbox SDK's `box.fs.tree` result shape\n * (`FileTreeResult`). `stats.truncated` is the only stat this route reads;\n * the rest ride through unread on the real SDK type. */\nexport interface SandboxTreeResult {\n root: string\n files: SandboxTreeFile[]\n stats: { truncated: boolean }\n}\n\n/** Structural match of the sandbox SDK's `box.fs` tree surface. */\nexport interface SandboxFileTreeSource {\n tree(path: string, options?: { maxDepth?: number }): Promise<SandboxTreeResult>\n}\n\n/** Describe a ready file index response with workspace-relative entries and truncation status */\nexport interface FileIndexReadyResponse {\n status: 'ready'\n /** Workspace-relative entries. Same shape as `FileMention` (`./wire`) so a\n * client can hand a response entry straight to `fileMentionsToParts` /\n * `buildMentionPromptBlock` without remapping. */\n files: FileMention[]\n /** True when either the underlying scan truncated (SDK-side cap) or this\n * route's own `maxEntries` cap trimmed the filtered list. The client\n * should show \"showing first N files\" rather than imply completeness. */\n truncated: boolean\n generatedAt: string\n}\n\n/** Cold-box answer: no provisioning happened, no files were scanned. The\n * client shows a warming state and retries — this route never blocks on a\n * box coming up. Two situations produce it: `authorize` reporting a box that\n * is not running, and a running box whose workspace root does not exist yet\n * (see `isMissingRootError`). */\nexport interface FileIndexWarmingResponse {\n status: 'warming'\n}\n\n/** Resolve a response indicating the file index is either ready or warming up */\nexport type FileIndexResponse = FileIndexReadyResponse | FileIndexWarmingResponse\n\n/** Short-TTL cache seam so repeat popover opens in the same session don't\n * re-scan the workspace. Host-provided (e.g. a KV binding); `key` is\n * whatever `authorize` returns as `cacheKey` — this route treats it opaquely. */\nexport interface FileIndexCache {\n get(key: string): Promise<FileIndexReadyResponse | null> | FileIndexReadyResponse | null\n put(key: string, value: FileIndexReadyResponse, options?: { ttlSeconds?: number }): Promise<void> | void\n}\n\n/** Define authorization details and parameters for indexing a file workspace with optional caching and ignore rules */\nexport type FileIndexAuthorization =\n | {\n status: 'ready'\n /** Structural sandbox `fs` handle, usually `ensureWorkspaceSandbox(...)` → `box.fs`. */\n fs: SandboxFileTreeSource\n /** Workspace root to index (e.g. `/home/agent`). */\n root: string\n /** Extra ignore segments for this request, merged with the route's\n * defaults + `CreateSandboxFileIndexRouteOptions.ignore`. */\n ignore?: string[]\n /** Opaque cache key for the optional cache seam. Omit to skip caching\n * for this request (e.g. a workspace the host chooses not to cache). */\n cacheKey?: string\n }\n | { status: 'warming' }\n | { status: 'denied'; response: Response }\n\n/** Define options to authorize and configure sandbox file index route behavior */\nexport interface CreateSandboxFileIndexRouteOptions {\n /** Authenticate the caller, resolve the sandbox `fs` handle, and signal a\n * cold box — never provisions or waits. */\n authorize(args: { request: Request }): Promise<FileIndexAuthorization>\n /** Extra ignore segments beyond the route's defaults (node_modules, .git,\n * dotfiles/dot-dirs, common build dirs). Matched as exact path-segment\n * names, same rule as the defaults. */\n ignore?: string[]\n /** Passed to `fs.tree` as `options.maxDepth`. Default 12. */\n maxDepth?: number\n /** Hard cap on entries returned after filtering. Default 5000. */\n maxEntries?: number\n /** Optional host-provided cache seam. */\n cache?: FileIndexCache\n /** Cache TTL in seconds when `cache` is set. Default 20. */\n cacheTtlSeconds?: number\n}\n\n/** Segment names ignored anywhere in a path, beyond the generic dotfile rule\n * below. Intentionally small and language/framework-agnostic — callers\n * extend it via `ignore` for anything domain-specific (e.g. a vault's\n * `uploads` dir). */\nconst DEFAULT_IGNORE_SEGMENTS = [\n 'node_modules',\n 'dist',\n 'build',\n 'out',\n 'coverage',\n 'target',\n '__pycache__',\n 'venv',\n]\n\n/** A path segment starting with `.` (`.git`, `.env`, `.next`, `.cache`, …) is\n * always ignored — this single rule covers most dot-prefixed VCS/tooling\n * dirs and dotfiles without enumerating them. */\nfunction isIgnored(relPath: string, ignoreSegments: ReadonlySet<string>): boolean {\n if (!relPath || relPath.startsWith('/') || relPath.includes('\\\\')) return true\n for (const segment of relPath.split('/')) {\n if (!segment) continue\n if (segment.startsWith('.')) return true\n if (ignoreSegments.has(segment)) return true\n }\n return false\n}\n\n/** Strips the tree result's echoed `root` prefix so entries are always\n * workspace-relative, whichever convention the structural `fs.tree` uses\n * (root-relative already, or root-prefixed). */\nfunction relativeTo(root: string, path: string): string {\n const prefix = root.endsWith('/') ? root : `${root}/`\n if (path.startsWith(prefix)) return path.slice(prefix.length)\n if (path === root) return ''\n return path\n}\n\nfunction basename(path: string): string {\n const segments = path.split('/').filter(Boolean)\n return segments[segments.length - 1] ?? path\n}\n\n/**\n * A box can answer `running` before it has materialised the workspace root —\n * `authorize` has already committed to `ready` by then, so `fs.tree` is the\n * first thing to notice, and it rejects with the sandbox SDK's\n * `ValidationError` wrapping a box-side `ENOENT … lstat` on the root. That is\n * the SAME \"not usable yet\" state `authorize` collapses onto `warming` for an\n * absent or stopped box, just discovered one step later, so it gets the same\n * answer instead of escaping as a 500.\n *\n * Matched STRUCTURALLY, not with `instanceof`: importing the SDK's error class\n * would make `@tangle-network/sandbox` a hard dependency of a route factory\n * whose entire `fs` seam is structural (`SandboxFileTreeSource`), and would\n * break any host feeding it a non-SDK handle.\n *\n * Deliberately narrow — the error code, `ENOENT`, the ENOENT message text, AND\n * the failing syscall's own operand all have to line up. A permission error, a\n * timeout, an auth failure, or an ENOENT on some other path inside the tree is\n * a real failure and still surfaces.\n *\n * The operand is matched as the quoted `lstat '<root>'` clause rather than by\n * substring, because the root is a PREFIX of everything under it: a plain\n * `includes(root)` would also swallow an ENOENT on `<root>/gone/x.md`, and on\n * a prefix sibling like `/home/agent-old/...`.\n */\nfunction isMissingRootError(err: unknown, root: string): boolean {\n if (!(err instanceof Error)) return false\n if ((err as { code?: unknown }).code !== 'VALIDATION_ERROR') return false\n return (\n /ENOENT/.test(err.message) &&\n /no such file or directory/.test(err.message) &&\n new RegExp(`\\\\blstat '${escapeRegExp(root)}'`).test(err.message)\n )\n}\n\nfunction escapeRegExp(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n}\n\n/** Resolve a sandbox file index route with authorization, caching, and configurable depth and entries limits */\nexport function createSandboxFileIndexRoute(\n options: CreateSandboxFileIndexRouteOptions,\n): (request: Request) => Promise<Response> {\n const maxDepth = options.maxDepth ?? 12\n const maxEntries = options.maxEntries ?? 5000\n const cacheTtlSeconds = options.cacheTtlSeconds ?? 20\n const staticIgnore = new Set([...DEFAULT_IGNORE_SEGMENTS, ...(options.ignore ?? [])])\n\n return async function fileIndex(request: Request): Promise<Response> {\n const auth = await options.authorize({ request })\n if (auth.status === 'denied') return auth.response\n if (auth.status === 'warming') {\n return Response.json({ status: 'warming' } satisfies FileIndexWarmingResponse)\n }\n\n const cache = options.cache\n if (cache && auth.cacheKey) {\n const cached = await cache.get(auth.cacheKey)\n if (cached) return Response.json(cached)\n }\n\n const ignoreSegments = auth.ignore?.length\n ? new Set([...staticIgnore, ...auth.ignore])\n : staticIgnore\n\n let scan: SandboxTreeResult\n try {\n scan = await auth.fs.tree(auth.root, { maxDepth })\n } catch (err) {\n if (!isMissingRootError(err, auth.root)) throw err\n return Response.json({ status: 'warming' } satisfies FileIndexWarmingResponse)\n }\n const filtered = scan.files.filter((f) => !isIgnored(relativeTo(auth.root, f.path), ignoreSegments))\n const truncated = scan.stats.truncated || filtered.length > maxEntries\n const files: FileMention[] = filtered.slice(0, maxEntries).map((f) => {\n const path = relativeTo(auth.root, f.path)\n const entry: FileMention = { path, name: basename(path) }\n if (typeof f.size === 'number') entry.size = f.size\n return entry\n })\n\n const body: FileIndexReadyResponse = {\n status: 'ready',\n files,\n truncated,\n generatedAt: new Date().toISOString(),\n }\n\n if (cache && auth.cacheKey) await cache.put(auth.cacheKey, body, { ttlSeconds: cacheTtlSeconds })\n\n return Response.json(body)\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1093,7 +1093,6 @@ function buildProductEgressPolicy(publicOrigin, extraDomains = []) {
|
|
|
1093
1093
|
includeImplicitDomains: false
|
|
1094
1094
|
};
|
|
1095
1095
|
}
|
|
1096
|
-
var DEFAULT_SIDECAR_PROCESS_PATTERN = "opencode|claude|codex";
|
|
1097
1096
|
var DEFAULT_SANDBOX_RESOURCES = {
|
|
1098
1097
|
image: "universal",
|
|
1099
1098
|
cpuCores: 2,
|
|
@@ -1590,39 +1589,16 @@ function assertEnvWithinLimits(env) {
|
|
|
1590
1589
|
);
|
|
1591
1590
|
}
|
|
1592
1591
|
}
|
|
1593
|
-
function
|
|
1594
|
-
|
|
1595
|
-
if (pattern.includes("\\|")) {
|
|
1596
|
-
throw new Error(
|
|
1597
|
-
'Invalid livenessProbe.sidecarProcessPattern: pgrep -f uses extended regular expressions; use a bare "|" for alternation, not "\\|".'
|
|
1598
|
-
);
|
|
1599
|
-
}
|
|
1600
|
-
return pattern;
|
|
1601
|
-
}
|
|
1602
|
-
async function isBoxAlive(box, harness, probe) {
|
|
1603
|
-
if (!probe) return true;
|
|
1604
|
-
const execTimeout = probe.execTimeoutMs ?? 5e3;
|
|
1605
|
-
const psTimeout = probe.psTimeoutMs ?? 3e3;
|
|
1606
|
-
const pattern = resolveSidecarProcessPattern(probe, harness);
|
|
1607
|
-
const race = (p, ms, label) => Promise.race([
|
|
1608
|
-
p,
|
|
1609
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error(label)), ms))
|
|
1610
|
-
]);
|
|
1592
|
+
async function isBoxAlive(box, probe) {
|
|
1593
|
+
if (!probe) return ok(void 0);
|
|
1611
1594
|
try {
|
|
1612
|
-
const alive = await
|
|
1613
|
-
if (
|
|
1614
|
-
|
|
1615
|
-
const ps = await race(
|
|
1616
|
-
box.exec(`pgrep -f ${shellSingleQuote(pattern)} || echo no-sidecar`),
|
|
1617
|
-
psTimeout,
|
|
1618
|
-
"ps check timeout"
|
|
1619
|
-
);
|
|
1620
|
-
if (ps.stdout.includes("no-sidecar")) return false;
|
|
1621
|
-
} catch {
|
|
1595
|
+
const alive = await box.exec("echo alive", { timeoutMs: probe.execTimeoutMs ?? 5e3 });
|
|
1596
|
+
if (alive.exitCode !== 0 || alive.stdout.trim() !== "alive") {
|
|
1597
|
+
return fail(new Error("alive check did not return a successful alive marker"));
|
|
1622
1598
|
}
|
|
1623
|
-
return
|
|
1624
|
-
} catch {
|
|
1625
|
-
return
|
|
1599
|
+
return ok(void 0);
|
|
1600
|
+
} catch (cause) {
|
|
1601
|
+
return fail(new Error("alive check failed", { cause }));
|
|
1626
1602
|
}
|
|
1627
1603
|
}
|
|
1628
1604
|
var DEFAULT_LIVENESS_CACHE_TTL_MS = 5e3;
|
|
@@ -1755,15 +1731,15 @@ async function writeDeferredFilesWithRuntimeAuthRefresh(client, box, files, stag
|
|
|
1755
1731
|
)
|
|
1756
1732
|
);
|
|
1757
1733
|
}
|
|
1758
|
-
async function isReusableBox(box,
|
|
1734
|
+
async function isReusableBox(box, probe) {
|
|
1759
1735
|
if (sandboxEdgeFailed(box) || !sandboxRuntimeUrl(box)) {
|
|
1760
1736
|
livenessVerifiedAt.delete(box.id);
|
|
1761
|
-
return
|
|
1737
|
+
return fail(new Error(sandboxEdgeFailed(box) ? "runtime edge readiness failed" : "runtime URL is missing"));
|
|
1762
1738
|
}
|
|
1763
|
-
if (!probe) return
|
|
1764
|
-
if (hasRecentLivenessVerification(box, probe)) return
|
|
1765
|
-
const alive = await isBoxAlive(box,
|
|
1766
|
-
if (alive) livenessVerifiedAt.set(box.id, Date.now());
|
|
1739
|
+
if (!probe) return ok(void 0);
|
|
1740
|
+
if (hasRecentLivenessVerification(box, probe)) return ok(void 0);
|
|
1741
|
+
const alive = await isBoxAlive(box, probe);
|
|
1742
|
+
if (alive.succeeded) livenessVerifiedAt.set(box.id, Date.now());
|
|
1767
1743
|
else livenessVerifiedAt.delete(box.id);
|
|
1768
1744
|
return alive;
|
|
1769
1745
|
}
|
|
@@ -1794,7 +1770,7 @@ async function resumeStoppedBox(box, timeoutMs, onProgress) {
|
|
|
1794
1770
|
return fail(stoppedBoxResumeError(box, cause));
|
|
1795
1771
|
}
|
|
1796
1772
|
}
|
|
1797
|
-
async function recoverUnresponsiveBox(client, box,
|
|
1773
|
+
async function recoverUnresponsiveBox(client, box, probe, stage, name, resumeTimeout, onProgress) {
|
|
1798
1774
|
try {
|
|
1799
1775
|
await box.stop();
|
|
1800
1776
|
} catch (err) {
|
|
@@ -1817,12 +1793,14 @@ async function recoverUnresponsiveBox(client, box, harness, probe, stage, name,
|
|
|
1817
1793
|
);
|
|
1818
1794
|
}
|
|
1819
1795
|
const recovered = await refreshRuntimeConnection(client, resumed.value);
|
|
1820
|
-
|
|
1796
|
+
const reusable = await isReusableBox(recovered, probe);
|
|
1797
|
+
if (!reusable.succeeded) {
|
|
1821
1798
|
throw new SandboxRecoveryFailedError(
|
|
1822
1799
|
stage,
|
|
1823
1800
|
name,
|
|
1824
1801
|
"probe",
|
|
1825
|
-
|
|
1802
|
+
`the box is still unresponsive after a state-preserving restart: ${reusable.error.message}`,
|
|
1803
|
+
reusable.error
|
|
1826
1804
|
);
|
|
1827
1805
|
}
|
|
1828
1806
|
return recovered;
|
|
@@ -1993,6 +1971,9 @@ async function requestSandboxReplacement(recover, failure, label) {
|
|
|
1993
1971
|
return { ...recovery.value, replacementBoxKey };
|
|
1994
1972
|
}
|
|
1995
1973
|
async function requestMissingSandboxReplacement(shell, failure) {
|
|
1974
|
+
if (failure.error instanceof SandboxRecoveryFailedError && failure.error.phase === "probe") {
|
|
1975
|
+
throw failure.error;
|
|
1976
|
+
}
|
|
1996
1977
|
const diagnostics = serializeSandboxProvisioningError(failure.error);
|
|
1997
1978
|
if (!isSandboxApiSandboxMissingFailure(diagnostics)) throw failure.error;
|
|
1998
1979
|
return requestSandboxReplacement(
|
|
@@ -2002,6 +1983,10 @@ async function requestMissingSandboxReplacement(shell, failure) {
|
|
|
2002
1983
|
);
|
|
2003
1984
|
}
|
|
2004
1985
|
async function provisionWorkspaceSandbox(shell, options) {
|
|
1986
|
+
const execTimeoutMs = shell.livenessProbe?.execTimeoutMs;
|
|
1987
|
+
if (execTimeoutMs !== void 0 && (!Number.isInteger(execTimeoutMs) || execTimeoutMs < 100 || execTimeoutMs > 6e5)) {
|
|
1988
|
+
throw new Error("livenessProbe.execTimeoutMs must be an integer between 100 and 600000");
|
|
1989
|
+
}
|
|
2005
1990
|
const { workspaceId, userId, harness, forceNew, onProgress, billingOwnerId } = options;
|
|
2006
1991
|
const resolved = await resolveWorkspaceSandboxClient(shell, workspaceId, userId);
|
|
2007
1992
|
const { scope, client } = resolved;
|
|
@@ -2021,13 +2006,12 @@ async function provisionWorkspaceSandbox(shell, options) {
|
|
|
2021
2006
|
} else if (found.metadata?.harness === harness) {
|
|
2022
2007
|
try {
|
|
2023
2008
|
const ready = await refreshRuntimeConnection(client, found);
|
|
2024
|
-
if (await isReusableBox(ready,
|
|
2009
|
+
if ((await isReusableBox(ready, shell.livenessProbe)).succeeded) {
|
|
2025
2010
|
return await finalizeExistingBox(shell, client, ready, "reused", name, workspaceId, userId, harness, scope);
|
|
2026
2011
|
}
|
|
2027
2012
|
const recovered = await recoverUnresponsiveBox(
|
|
2028
2013
|
client,
|
|
2029
2014
|
ready,
|
|
2030
|
-
harness,
|
|
2031
2015
|
shell.livenessProbe,
|
|
2032
2016
|
"reused",
|
|
2033
2017
|
name,
|
|
@@ -2074,13 +2058,12 @@ async function provisionWorkspaceSandbox(shell, options) {
|
|
|
2074
2058
|
} else {
|
|
2075
2059
|
try {
|
|
2076
2060
|
const box2 = await refreshRuntimeConnection(client, resumed.value);
|
|
2077
|
-
if (await isReusableBox(box2,
|
|
2061
|
+
if ((await isReusableBox(box2, shell.livenessProbe)).succeeded) {
|
|
2078
2062
|
return await finalizeExistingBox(shell, client, box2, "resumed", name, workspaceId, userId, harness, scope);
|
|
2079
2063
|
}
|
|
2080
2064
|
const recovered = await recoverUnresponsiveBox(
|
|
2081
2065
|
client,
|
|
2082
2066
|
box2,
|
|
2083
|
-
harness,
|
|
2084
2067
|
shell.livenessProbe,
|
|
2085
2068
|
"resumed",
|
|
2086
2069
|
name,
|
|
@@ -2785,7 +2768,6 @@ export {
|
|
|
2785
2768
|
resolveSandboxClientCredentials,
|
|
2786
2769
|
PYPI_EGRESS_DOMAINS,
|
|
2787
2770
|
buildProductEgressPolicy,
|
|
2788
|
-
DEFAULT_SIDECAR_PROCESS_PATTERN,
|
|
2789
2771
|
DEFAULT_SANDBOX_RESOURCES,
|
|
2790
2772
|
getClient,
|
|
2791
2773
|
resetClientCache,
|
|
@@ -2834,4 +2816,4 @@ export {
|
|
|
2834
2816
|
isTerminalPromptEvent,
|
|
2835
2817
|
detectInteractiveQuestion
|
|
2836
2818
|
};
|
|
2837
|
-
//# sourceMappingURL=chunk-
|
|
2819
|
+
//# sourceMappingURL=chunk-P6YXUGNA.js.map
|