gsd-pi 2.76.0-dev.76f9a2dc5 → 2.76.0-dev.97807402
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/claude-cli-check.js +32 -3
- package/dist/mcp-server.d.ts +7 -0
- package/dist/mcp-server.js +35 -1
- package/dist/resources/extensions/claude-code-cli/readiness.js +4 -3
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +77 -17
- package/dist/resources/extensions/gsd/auto/phases.js +14 -0
- package/dist/resources/extensions/gsd/auto/run-unit.js +27 -0
- package/dist/resources/extensions/gsd/auto-model-selection.js +1 -1
- package/dist/resources/extensions/gsd/auto-post-unit.js +1 -1
- package/dist/resources/extensions/gsd/auto-recovery.js +13 -0
- package/dist/resources/extensions/gsd/auto-start.js +27 -18
- package/dist/resources/extensions/gsd/auto-worktree.js +30 -48
- package/dist/resources/extensions/gsd/auto.js +13 -17
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +17 -1
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +39 -9
- package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +93 -0
- package/dist/resources/extensions/gsd/bootstrap/register-extension.js +2 -0
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +40 -4
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +12 -1
- package/dist/resources/extensions/gsd/commands-prefs-wizard.js +968 -23
- package/dist/resources/extensions/gsd/compaction-snapshot.js +121 -0
- package/dist/resources/extensions/gsd/error-classifier.js +10 -3
- package/dist/resources/extensions/gsd/exec-history.js +120 -0
- package/dist/resources/extensions/gsd/exec-sandbox.js +258 -0
- package/dist/resources/extensions/gsd/gsd-db.js +115 -7
- package/dist/resources/extensions/gsd/guided-flow.js +189 -0
- package/dist/resources/extensions/gsd/health-widget.js +4 -1
- package/dist/resources/extensions/gsd/key-manager.js +6 -0
- package/dist/resources/extensions/gsd/model-router.js +36 -3
- package/dist/resources/extensions/gsd/pre-execution-checks.js +35 -9
- package/dist/resources/extensions/gsd/preferences-types.js +9 -0
- package/dist/resources/extensions/gsd/preferences-validation.js +83 -0
- package/dist/resources/extensions/gsd/preferences.js +17 -17
- package/dist/resources/extensions/gsd/prompts/discuss-headless.md +8 -0
- package/dist/resources/extensions/gsd/prompts/discuss.md +29 -2
- package/dist/resources/extensions/gsd/prompts/parallel-research-slices.md +5 -2
- package/dist/resources/extensions/gsd/safety/file-change-validator.js +9 -3
- package/dist/resources/extensions/gsd/safety/safety-harness.js +4 -0
- package/dist/resources/extensions/gsd/token-counter.js +22 -5
- package/dist/resources/extensions/gsd/tools/exec-search-tool.js +59 -0
- package/dist/resources/extensions/gsd/tools/exec-tool.js +126 -0
- package/dist/resources/extensions/gsd/tools/resume-tool.js +23 -0
- package/dist/resources/extensions/gsd/workflow-mcp.js +3 -0
- package/dist/resources/skills/verify-before-complete/SKILL.md +2 -1
- package/dist/resources/skills/write-docs/SKILL.md +2 -1
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +14 -14
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/required-server-files.json +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +14 -14
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/server.js +1 -1
- package/package.json +1 -1
- package/packages/mcp-server/dist/remote-questions.d.ts +45 -0
- package/packages/mcp-server/dist/remote-questions.d.ts.map +1 -0
- package/packages/mcp-server/dist/remote-questions.js +732 -0
- package/packages/mcp-server/dist/remote-questions.js.map +1 -0
- package/packages/mcp-server/dist/server.d.ts.map +1 -1
- package/packages/mcp-server/dist/server.js +18 -1
- package/packages/mcp-server/dist/server.js.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +64 -25
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/package.json +2 -1
- package/packages/mcp-server/src/remote-questions.test.ts +294 -0
- package/packages/mcp-server/src/remote-questions.ts +916 -0
- package/packages/mcp-server/src/server.ts +19 -1
- package/packages/mcp-server/src/workflow-tools.test.ts +146 -1
- package/packages/mcp-server/src/workflow-tools.ts +84 -43
- package/packages/mcp-server/tsconfig.test.json +19 -0
- package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic-shared.js +2 -0
- package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -1
- package/packages/pi-ai/dist/providers/simple-options.d.ts +10 -0
- package/packages/pi-ai/dist/providers/simple-options.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/simple-options.js +16 -1
- package/packages/pi-ai/dist/providers/simple-options.js.map +1 -1
- package/packages/pi-ai/src/providers/anthropic-shared.ts +3 -1
- package/packages/pi-ai/src/providers/simple-options.ts +17 -1
- package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.js +203 -0
- package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.js +14 -0
- package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/redact-secrets.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/redact-secrets.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/redact-secrets.js +49 -0
- package/packages/pi-coding-agent/dist/core/redact-secrets.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/redact-secrets.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/redact-secrets.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/redact-secrets.test.js +67 -0
- package/packages/pi-coding-agent/dist/core/redact-secrets.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager.js +9 -5
- package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager.test.js +25 -1
- package/packages/pi-coding-agent/dist/core/session-manager.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +13 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/core/model-registry-custom-caps.test.ts +245 -0
- package/packages/pi-coding-agent/src/core/model-registry.ts +16 -0
- package/packages/pi-coding-agent/src/core/redact-secrets.test.ts +86 -0
- package/packages/pi-coding-agent/src/core/redact-secrets.ts +58 -0
- package/packages/pi-coding-agent/src/core/session-manager.test.ts +36 -1
- package/packages/pi-coding-agent/src/core/session-manager.ts +9 -5
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +13 -1
- package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
- package/src/resources/extensions/claude-code-cli/readiness.ts +4 -3
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +78 -17
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +149 -5
- package/src/resources/extensions/gsd/auto/phases.ts +14 -0
- package/src/resources/extensions/gsd/auto/run-unit.ts +29 -0
- package/src/resources/extensions/gsd/auto-model-selection.ts +1 -1
- package/src/resources/extensions/gsd/auto-post-unit.ts +1 -2
- package/src/resources/extensions/gsd/auto-recovery.ts +15 -0
- package/src/resources/extensions/gsd/auto-start.ts +29 -19
- package/src/resources/extensions/gsd/auto-worktree.ts +34 -52
- package/src/resources/extensions/gsd/auto.ts +12 -17
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +23 -1
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +40 -9
- package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +109 -0
- package/src/resources/extensions/gsd/bootstrap/register-extension.ts +2 -0
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +42 -4
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +13 -1
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +898 -32
- package/src/resources/extensions/gsd/compaction-snapshot.ts +165 -0
- package/src/resources/extensions/gsd/error-classifier.ts +10 -3
- package/src/resources/extensions/gsd/exec-history.ts +153 -0
- package/src/resources/extensions/gsd/exec-sandbox.ts +326 -0
- package/src/resources/extensions/gsd/gsd-db.ts +122 -7
- package/src/resources/extensions/gsd/guided-flow.ts +221 -0
- package/src/resources/extensions/gsd/health-widget.ts +3 -1
- package/src/resources/extensions/gsd/journal.ts +2 -1
- package/src/resources/extensions/gsd/key-manager.ts +6 -0
- package/src/resources/extensions/gsd/model-router.ts +42 -1
- package/src/resources/extensions/gsd/pre-execution-checks.ts +36 -10
- package/src/resources/extensions/gsd/preferences-types.ts +46 -0
- package/src/resources/extensions/gsd/preferences-validation.ts +79 -0
- package/src/resources/extensions/gsd/preferences.ts +17 -17
- package/src/resources/extensions/gsd/prompts/discuss-headless.md +8 -0
- package/src/resources/extensions/gsd/prompts/discuss.md +29 -2
- package/src/resources/extensions/gsd/prompts/parallel-research-slices.md +5 -2
- package/src/resources/extensions/gsd/safety/file-change-validator.ts +13 -2
- package/src/resources/extensions/gsd/safety/safety-harness.ts +6 -0
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +116 -0
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +49 -0
- package/src/resources/extensions/gsd/tests/compaction-snapshot.test.ts +123 -0
- package/src/resources/extensions/gsd/tests/complete-slice.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/complete-task.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +31 -0
- package/src/resources/extensions/gsd/tests/ensure-db-open.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/escalation.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/exec-history.test.ts +124 -0
- package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +210 -0
- package/src/resources/extensions/gsd/tests/file-change-validator.test.ts +38 -0
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +152 -1
- package/src/resources/extensions/gsd/tests/isolation-none-branch-guard.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/issue-4540-regressions.test.ts +288 -0
- package/src/resources/extensions/gsd/tests/key-manager.test.ts +7 -0
- package/src/resources/extensions/gsd/tests/md-importer.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/memory-store.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/parallel-research-dispatch.test.ts +19 -0
- package/src/resources/extensions/gsd/tests/pre-exec-backtick-strip.test.ts +14 -0
- package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +234 -0
- package/src/resources/extensions/gsd/tests/preferences.test.ts +110 -0
- package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +44 -0
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +48 -0
- package/src/resources/extensions/gsd/tests/ready-phrase-no-files-4573.test.ts +388 -0
- package/src/resources/extensions/gsd/tests/restore-tools-after-discuss.test.ts +9 -3
- package/src/resources/extensions/gsd/tests/save-gate-result-render.test.ts +95 -0
- package/src/resources/extensions/gsd/tests/session-start-footer.test.ts +32 -40
- package/src/resources/extensions/gsd/tests/stash-queued-context-files.test.ts +56 -0
- package/src/resources/extensions/gsd/tests/token-counter.test.ts +105 -1
- package/src/resources/extensions/gsd/tests/tool-compatibility.test.ts +107 -0
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +65 -2
- package/src/resources/extensions/gsd/tests/write-gate.test.ts +64 -0
- package/src/resources/extensions/gsd/tests/zombie-gsd-state.test.ts +3 -1
- package/src/resources/extensions/gsd/token-counter.ts +22 -5
- package/src/resources/extensions/gsd/tools/exec-search-tool.ts +81 -0
- package/src/resources/extensions/gsd/tools/exec-tool.ts +183 -0
- package/src/resources/extensions/gsd/tools/resume-tool.ts +40 -0
- package/src/resources/extensions/gsd/workflow-logger.ts +2 -1
- package/src/resources/extensions/gsd/workflow-mcp.ts +3 -0
- package/src/resources/skills/verify-before-complete/SKILL.md +2 -1
- package/src/resources/skills/write-docs/SKILL.md +2 -1
- /package/dist/web/standalone/.next/static/{UMCfv_sVnLXawpUAjvArc → pI48IF3dgfs0CBrYi2bh_}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{UMCfv_sVnLXawpUAjvArc → pI48IF3dgfs0CBrYi2bh_}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// GSD Compaction Snapshot — writes a ≤2 KB markdown digest of durable
|
|
2
|
+
// project state before the session context is compacted. On resume, an
|
|
3
|
+
// agent can `gsd_resume` (or Read .gsd/last-snapshot.md) to re-orient
|
|
4
|
+
// without re-deriving the same memories.
|
|
5
|
+
//
|
|
6
|
+
// Inspired by mksglu/context-mode. Independent implementation.
|
|
7
|
+
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { resolve } from "node:path";
|
|
10
|
+
|
|
11
|
+
import { getActiveMemoriesRanked, type Memory } from "./memory-store.js";
|
|
12
|
+
import { listExecHistory, type ExecHistoryEntry } from "./exec-history.js";
|
|
13
|
+
|
|
14
|
+
export const DEFAULT_SNAPSHOT_BYTES = 2048;
|
|
15
|
+
export const SNAPSHOT_FILENAME = "last-snapshot.md";
|
|
16
|
+
|
|
17
|
+
export interface SnapshotSources {
|
|
18
|
+
memories: Memory[];
|
|
19
|
+
execHistory: ExecHistoryEntry[];
|
|
20
|
+
generatedAt: Date;
|
|
21
|
+
/** Optional free-form context string (e.g. active unit id). */
|
|
22
|
+
activeContext?: string | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface BuildSnapshotOptions {
|
|
26
|
+
/** Hard cap in bytes (UTF-8). Default 2048. */
|
|
27
|
+
maxBytes?: number;
|
|
28
|
+
/** Memory count cap before truncation (default 6). */
|
|
29
|
+
maxMemories?: number;
|
|
30
|
+
/** Exec history cap (default 5). */
|
|
31
|
+
maxExec?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Build a priority-tiered markdown snapshot. Pure — no I/O. Tiers:
|
|
36
|
+
* 1. Active context (if any)
|
|
37
|
+
* 2. Top memories by rank
|
|
38
|
+
* 3. Recent exec runs (failures highlighted)
|
|
39
|
+
* The result is guaranteed to be <= opts.maxBytes (truncated with an
|
|
40
|
+
* ellipsis marker if necessary).
|
|
41
|
+
*/
|
|
42
|
+
export function buildSnapshot(sources: SnapshotSources, opts: BuildSnapshotOptions = {}): string {
|
|
43
|
+
const maxBytes = opts.maxBytes ?? DEFAULT_SNAPSHOT_BYTES;
|
|
44
|
+
const maxMemories = opts.maxMemories ?? 6;
|
|
45
|
+
const maxExec = opts.maxExec ?? 5;
|
|
46
|
+
|
|
47
|
+
const lines: string[] = [];
|
|
48
|
+
lines.push(`# GSD context snapshot (${sources.generatedAt.toISOString()})`);
|
|
49
|
+
lines.push("");
|
|
50
|
+
|
|
51
|
+
if (sources.activeContext && sources.activeContext.trim().length > 0) {
|
|
52
|
+
lines.push("## Active context");
|
|
53
|
+
lines.push(sources.activeContext.trim());
|
|
54
|
+
lines.push("");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const memories = sources.memories.slice(0, maxMemories);
|
|
58
|
+
if (memories.length > 0) {
|
|
59
|
+
lines.push("## Top project memories");
|
|
60
|
+
for (const memory of memories) {
|
|
61
|
+
lines.push(`- [${memory.id}] (${memory.category}) ${memory.content.trim()}`);
|
|
62
|
+
}
|
|
63
|
+
lines.push("");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const exec = sources.execHistory.slice(0, maxExec);
|
|
67
|
+
if (exec.length > 0) {
|
|
68
|
+
lines.push("## Recent gsd_exec runs");
|
|
69
|
+
for (const entry of exec) {
|
|
70
|
+
const status = entry.timed_out
|
|
71
|
+
? "timeout"
|
|
72
|
+
: entry.exit_code === null
|
|
73
|
+
? "exit:null"
|
|
74
|
+
: `exit:${entry.exit_code}`;
|
|
75
|
+
const purpose = entry.purpose ? ` — ${entry.purpose}` : "";
|
|
76
|
+
lines.push(`- [${entry.id}] ${entry.runtime} ${status}${purpose}`);
|
|
77
|
+
}
|
|
78
|
+
lines.push("");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (memories.length === 0 && exec.length === 0 && !sources.activeContext) {
|
|
82
|
+
lines.push("_No durable memories, active context, or exec history to surface._");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return enforceByteCap(lines.join("\n").trimEnd(), maxBytes);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function enforceByteCap(input: string, maxBytes: number): string {
|
|
89
|
+
if (Buffer.byteLength(input, "utf-8") <= maxBytes) return input;
|
|
90
|
+
const marker = "\n…[truncated]";
|
|
91
|
+
const markerBytes = Buffer.byteLength(marker, "utf-8");
|
|
92
|
+
const budget = Math.max(0, maxBytes - markerBytes);
|
|
93
|
+
// Walk backwards until the trimmed string fits. utf-8 is variable-width;
|
|
94
|
+
// naive char slicing is safe for ASCII but may split a multi-byte char.
|
|
95
|
+
// Guard by decoding the trimmed Buffer and relying on the replacement-char
|
|
96
|
+
// fallback in TextDecoder (implicit via toString).
|
|
97
|
+
const buf = Buffer.from(input, "utf-8").subarray(0, budget);
|
|
98
|
+
return `${buf.toString("utf-8")}${marker}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface WriteSnapshotOptions extends BuildSnapshotOptions {
|
|
102
|
+
activeContext?: string | null;
|
|
103
|
+
now?: () => Date;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface WriteSnapshotResult {
|
|
107
|
+
path: string;
|
|
108
|
+
bytes: number;
|
|
109
|
+
memories: number;
|
|
110
|
+
execRuns: number;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function writeCompactionSnapshot(
|
|
114
|
+
baseDir: string,
|
|
115
|
+
opts: WriteSnapshotOptions = {},
|
|
116
|
+
): WriteSnapshotResult {
|
|
117
|
+
const memories = safeGetMemories();
|
|
118
|
+
const execHistory = safeListExec(baseDir);
|
|
119
|
+
const content = buildSnapshot(
|
|
120
|
+
{
|
|
121
|
+
memories,
|
|
122
|
+
execHistory,
|
|
123
|
+
generatedAt: (opts.now ?? (() => new Date()))(),
|
|
124
|
+
activeContext: opts.activeContext ?? null,
|
|
125
|
+
},
|
|
126
|
+
opts,
|
|
127
|
+
);
|
|
128
|
+
const gsdDir = resolve(baseDir, ".gsd");
|
|
129
|
+
if (!existsSync(gsdDir)) mkdirSync(gsdDir, { recursive: true });
|
|
130
|
+
const path = resolve(gsdDir, SNAPSHOT_FILENAME);
|
|
131
|
+
const finalContent = `${content}\n`;
|
|
132
|
+
writeFileSync(path, finalContent, "utf-8");
|
|
133
|
+
return {
|
|
134
|
+
path,
|
|
135
|
+
bytes: Buffer.byteLength(finalContent, "utf-8"),
|
|
136
|
+
memories: memories.length,
|
|
137
|
+
execRuns: execHistory.length,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function readCompactionSnapshot(baseDir: string): string | null {
|
|
142
|
+
const path = resolve(baseDir, ".gsd", SNAPSHOT_FILENAME);
|
|
143
|
+
if (!existsSync(path)) return null;
|
|
144
|
+
try {
|
|
145
|
+
return readFileSync(path, "utf-8");
|
|
146
|
+
} catch {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function safeGetMemories(): Memory[] {
|
|
152
|
+
try {
|
|
153
|
+
return getActiveMemoriesRanked(12);
|
|
154
|
+
} catch {
|
|
155
|
+
return [];
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function safeListExec(baseDir: string): ExecHistoryEntry[] {
|
|
160
|
+
try {
|
|
161
|
+
return listExecHistory(baseDir);
|
|
162
|
+
} catch {
|
|
163
|
+
return [];
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -47,12 +47,19 @@ const PERMANENT_RE = /auth|unauthorized|forbidden|invalid.*key|invalid.*api|bill
|
|
|
47
47
|
// Include provider-specific quota-window phrasing like:
|
|
48
48
|
// - "You've hit your limit"
|
|
49
49
|
// - "usage limit" / "quota reached"
|
|
50
|
-
|
|
50
|
+
// - "out of extra usage"
|
|
51
|
+
const RATE_LIMIT_RE = /rate.?limit|too many requests|429|hit your limit|usage limit|out of extra usage|quota (?:reached|hit)|limit.*resets?/i;
|
|
51
52
|
// OpenRouter affordability-style quota errors should be treated as transient
|
|
52
53
|
// so core retry logic can lower maxTokens and continue in-session.
|
|
53
54
|
const AFFORDABILITY_RE = /requires more credits|can only afford|insufficient credits|not enough credits|fewer max_tokens/i;
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
// "Stream idle timeout" and "partial response received" are emitted by the SDK/harness
|
|
56
|
+
// for mid-stream disconnects. Both indicate a transient network-level interruption.
|
|
57
|
+
// See: https://github.com/gsd-build/gsd-2/issues/4558
|
|
58
|
+
const NETWORK_RE = /network|ECONNRESET|ETIMEDOUT|ECONNREFUSED|socket hang up|fetch failed|connection.*reset|dns|unexpected eof|stream idle timeout|partial response received/i;
|
|
59
|
+
// Context overflow errors (context window/length exceeded) should be treated as server-class
|
|
60
|
+
// transient errors so auto-mode can retry with reduced budget or fall back to a larger-context model.
|
|
61
|
+
// See: https://github.com/gsd-build/gsd-2/issues/4528
|
|
62
|
+
const SERVER_RE = /internal server error|500|502|503|overloaded|server_error|api_error|service.?unavailable|context (?:window|length) exceed|context window exceed/i;
|
|
56
63
|
// ECONNRESET/ECONNREFUSED are in NETWORK_RE (same-model retry first).
|
|
57
64
|
const CONNECTION_RE = /terminated|connection.?(?:refused|error)|other side closed|EPIPE|network.?(?:is\s+)?unavailable|stream_exhausted(?:_without_result)?/i;
|
|
58
65
|
// Catch-all for V8 JSON.parse errors: all modern variants end with "in JSON at position \d+".
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// GSD Exec History — read-side helpers for the exec sandbox.
|
|
2
|
+
//
|
|
3
|
+
// Pure I/O: scans `.gsd/exec/*.meta.json` under a base directory and
|
|
4
|
+
// returns lightweight records. Used by the gsd_exec_search tool and
|
|
5
|
+
// any future compaction-snapshot enrichment.
|
|
6
|
+
|
|
7
|
+
import { closeSync, openSync, readdirSync, readFileSync, readSync, statSync } from "node:fs";
|
|
8
|
+
import { join, resolve } from "node:path";
|
|
9
|
+
|
|
10
|
+
export interface ExecHistoryEntry {
|
|
11
|
+
id: string;
|
|
12
|
+
runtime: "bash" | "node" | "python" | string;
|
|
13
|
+
purpose: string | null;
|
|
14
|
+
started_at: string;
|
|
15
|
+
finished_at: string;
|
|
16
|
+
duration_ms: number;
|
|
17
|
+
exit_code: number | null;
|
|
18
|
+
signal: string | null;
|
|
19
|
+
timed_out: boolean;
|
|
20
|
+
stdout_bytes: number;
|
|
21
|
+
stderr_bytes: number;
|
|
22
|
+
stdout_truncated: boolean;
|
|
23
|
+
stderr_truncated: boolean;
|
|
24
|
+
stdout_path: string;
|
|
25
|
+
stderr_path: string;
|
|
26
|
+
meta_path: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ExecSearchOptions {
|
|
30
|
+
/** Case-insensitive needle matched against purpose. Empty string matches all. */
|
|
31
|
+
query?: string;
|
|
32
|
+
/** Restrict to this runtime. */
|
|
33
|
+
runtime?: ExecHistoryEntry["runtime"];
|
|
34
|
+
/** Include only entries with exit_code !== 0 || timed_out. */
|
|
35
|
+
failing_only?: boolean;
|
|
36
|
+
/** Return at most N entries, most recent first. Default 20, cap 200. */
|
|
37
|
+
limit?: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ExecSearchHit {
|
|
41
|
+
entry: ExecHistoryEntry;
|
|
42
|
+
/** Tail of stdout (first 300 chars) — cheap to read, useful for disambiguation. */
|
|
43
|
+
digest_preview?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function listMetaFiles(baseDir: string): string[] {
|
|
47
|
+
const dir = resolve(baseDir, ".gsd", "exec");
|
|
48
|
+
try {
|
|
49
|
+
return readdirSync(dir)
|
|
50
|
+
.filter((name) => name.endsWith(".meta.json"))
|
|
51
|
+
.map((name) => join(dir, name));
|
|
52
|
+
} catch {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function safeReadMeta(path: string): ExecHistoryEntry | null {
|
|
58
|
+
try {
|
|
59
|
+
const raw = readFileSync(path, "utf-8");
|
|
60
|
+
const parsed = JSON.parse(raw) as Partial<ExecHistoryEntry>;
|
|
61
|
+
if (typeof parsed.id !== "string" || typeof parsed.runtime !== "string") return null;
|
|
62
|
+
return {
|
|
63
|
+
id: parsed.id,
|
|
64
|
+
runtime: parsed.runtime,
|
|
65
|
+
purpose: typeof parsed.purpose === "string" ? parsed.purpose : null,
|
|
66
|
+
started_at: typeof parsed.started_at === "string" ? parsed.started_at : "",
|
|
67
|
+
finished_at: typeof parsed.finished_at === "string" ? parsed.finished_at : "",
|
|
68
|
+
duration_ms: typeof parsed.duration_ms === "number" ? parsed.duration_ms : 0,
|
|
69
|
+
exit_code: typeof parsed.exit_code === "number" ? parsed.exit_code : null,
|
|
70
|
+
signal: typeof parsed.signal === "string" ? parsed.signal : null,
|
|
71
|
+
timed_out: parsed.timed_out === true,
|
|
72
|
+
stdout_bytes: typeof parsed.stdout_bytes === "number" ? parsed.stdout_bytes : 0,
|
|
73
|
+
stderr_bytes: typeof parsed.stderr_bytes === "number" ? parsed.stderr_bytes : 0,
|
|
74
|
+
stdout_truncated: parsed.stdout_truncated === true,
|
|
75
|
+
stderr_truncated: parsed.stderr_truncated === true,
|
|
76
|
+
stdout_path: path.replace(/\.meta\.json$/, ".stdout"),
|
|
77
|
+
stderr_path: path.replace(/\.meta\.json$/, ".stderr"),
|
|
78
|
+
meta_path: path,
|
|
79
|
+
};
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function listExecHistory(baseDir: string): ExecHistoryEntry[] {
|
|
86
|
+
const metas = listMetaFiles(baseDir)
|
|
87
|
+
.map((path) => {
|
|
88
|
+
let mtime = 0;
|
|
89
|
+
try {
|
|
90
|
+
mtime = statSync(path).mtimeMs;
|
|
91
|
+
} catch {
|
|
92
|
+
/* ignore */
|
|
93
|
+
}
|
|
94
|
+
const entry = safeReadMeta(path);
|
|
95
|
+
return entry ? { entry, mtime } : null;
|
|
96
|
+
})
|
|
97
|
+
.filter((value): value is { entry: ExecHistoryEntry; mtime: number } => value !== null);
|
|
98
|
+
metas.sort((a, b) => b.mtime - a.mtime);
|
|
99
|
+
return metas.map((m) => m.entry);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function matchesFilters(entry: ExecHistoryEntry, opts: ExecSearchOptions): boolean {
|
|
103
|
+
if (opts.runtime && entry.runtime !== opts.runtime) return false;
|
|
104
|
+
if (opts.failing_only) {
|
|
105
|
+
const failed = entry.timed_out || (entry.exit_code !== 0 && entry.exit_code !== null);
|
|
106
|
+
if (!failed) return false;
|
|
107
|
+
}
|
|
108
|
+
const query = (opts.query ?? "").trim().toLowerCase();
|
|
109
|
+
if (!query) return true;
|
|
110
|
+
const haystack = `${entry.id} ${entry.purpose ?? ""}`.toLowerCase();
|
|
111
|
+
return haystack.includes(query);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function readDigestPreview(entry: ExecHistoryEntry, maxChars: number): string | undefined {
|
|
115
|
+
if (!entry.stdout_path || maxChars <= 0) return undefined;
|
|
116
|
+
try {
|
|
117
|
+
const size = statSync(entry.stdout_path).size;
|
|
118
|
+
if (size === 0) return undefined;
|
|
119
|
+
const readBytes = Math.min(size, maxChars * 4); // 4 bytes/char upper bound for UTF-8
|
|
120
|
+
const buf = Buffer.allocUnsafe(readBytes);
|
|
121
|
+
const fd = openSync(entry.stdout_path, "r");
|
|
122
|
+
try {
|
|
123
|
+
const bytesRead = readSync(fd, buf, 0, readBytes, Math.max(0, size - readBytes));
|
|
124
|
+
const text = buf.subarray(0, bytesRead).toString("utf-8");
|
|
125
|
+
const trimmed = text.trimEnd();
|
|
126
|
+
return trimmed.length <= maxChars ? trimmed : trimmed.slice(trimmed.length - maxChars);
|
|
127
|
+
} finally {
|
|
128
|
+
closeSync(fd);
|
|
129
|
+
}
|
|
130
|
+
} catch {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function searchExecHistory(
|
|
136
|
+
baseDir: string,
|
|
137
|
+
opts: ExecSearchOptions = {},
|
|
138
|
+
): ExecSearchHit[] {
|
|
139
|
+
const limit = clampLimit(opts.limit, 20, 200);
|
|
140
|
+
const entries = listExecHistory(baseDir);
|
|
141
|
+
const filtered = entries.filter((entry) => matchesFilters(entry, opts));
|
|
142
|
+
return filtered.slice(0, limit).map((entry) => ({
|
|
143
|
+
entry,
|
|
144
|
+
digest_preview: readDigestPreview(entry, 300),
|
|
145
|
+
}));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function clampLimit(value: unknown, fallback: number, max: number): number {
|
|
149
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
|
|
150
|
+
if (value < 1) return 1;
|
|
151
|
+
if (value > max) return max;
|
|
152
|
+
return Math.floor(value);
|
|
153
|
+
}
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
// GSD Exec Sandbox — tool-output sandboxing for sub-sessions.
|
|
2
|
+
//
|
|
3
|
+
// Runs a script in a subprocess and persists stdout/stderr to
|
|
4
|
+
// `.gsd/exec/<id>.{stdout,stderr,meta.json}`. Only a short digest is
|
|
5
|
+
// returned to the calling agent's context, keeping large outputs
|
|
6
|
+
// (e.g. Playwright snapshots, issue dumps) out of the window.
|
|
7
|
+
//
|
|
8
|
+
// Inspired by mksglu/context-mode (Elastic License 2.0). Independent
|
|
9
|
+
// implementation — no upstream code incorporated.
|
|
10
|
+
|
|
11
|
+
import { spawn } from "node:child_process";
|
|
12
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { randomUUID } from "node:crypto";
|
|
14
|
+
import { resolve } from "node:path";
|
|
15
|
+
|
|
16
|
+
export interface ExecSandboxRequest {
|
|
17
|
+
/** Interpreter to use. */
|
|
18
|
+
runtime: "bash" | "node" | "python";
|
|
19
|
+
/** Script body. Executed via the runtime's -c equivalent. */
|
|
20
|
+
script: string;
|
|
21
|
+
/** Optional purpose/label recorded in meta.json. */
|
|
22
|
+
purpose?: string;
|
|
23
|
+
/** Per-invocation timeout in ms. Clamped to `clamp_timeout_ms`. */
|
|
24
|
+
timeout_ms?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ExecSandboxOptions {
|
|
28
|
+
/** Project root. stdout/stderr persist under `<baseDir>/.gsd/exec/`. */
|
|
29
|
+
baseDir: string;
|
|
30
|
+
/** Absolute upper bound for the timeout. */
|
|
31
|
+
clamp_timeout_ms: number;
|
|
32
|
+
/** Default timeout if request omits one. */
|
|
33
|
+
default_timeout_ms: number;
|
|
34
|
+
/** Cap on persisted stdout bytes. Further output is truncated with a marker. */
|
|
35
|
+
stdout_cap_bytes: number;
|
|
36
|
+
/** Cap on persisted stderr bytes. */
|
|
37
|
+
stderr_cap_bytes: number;
|
|
38
|
+
/** Number of trailing stdout chars returned as the digest. */
|
|
39
|
+
digest_chars: number;
|
|
40
|
+
/** Env var allowlist (case-sensitive). PATH/HOME always forwarded. */
|
|
41
|
+
env_allowlist: readonly string[];
|
|
42
|
+
/** Optional override of process.env for tests. */
|
|
43
|
+
env?: NodeJS.ProcessEnv;
|
|
44
|
+
/** Optional override for the current time (tests). */
|
|
45
|
+
now?: () => Date;
|
|
46
|
+
/** Optional override for id generation (tests). */
|
|
47
|
+
generateId?: () => string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface ExecSandboxResult {
|
|
51
|
+
id: string;
|
|
52
|
+
runtime: ExecSandboxRequest["runtime"];
|
|
53
|
+
exit_code: number | null;
|
|
54
|
+
signal: NodeJS.Signals | null;
|
|
55
|
+
timed_out: boolean;
|
|
56
|
+
duration_ms: number;
|
|
57
|
+
stdout_bytes: number;
|
|
58
|
+
stderr_bytes: number;
|
|
59
|
+
stdout_truncated: boolean;
|
|
60
|
+
stderr_truncated: boolean;
|
|
61
|
+
stdout_path: string;
|
|
62
|
+
stderr_path: string;
|
|
63
|
+
meta_path: string;
|
|
64
|
+
digest: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const ALWAYS_FORWARD_ENV = ["PATH", "HOME"] as const;
|
|
68
|
+
|
|
69
|
+
export const EXEC_DEFAULTS = {
|
|
70
|
+
clampTimeoutMs: 600_000,
|
|
71
|
+
defaultTimeoutMs: 30_000,
|
|
72
|
+
stdoutCapBytes: 1_048_576,
|
|
73
|
+
stderrCapBytes: 262_144,
|
|
74
|
+
digestChars: 300,
|
|
75
|
+
envAllowlist: [
|
|
76
|
+
"LANG",
|
|
77
|
+
"LC_ALL",
|
|
78
|
+
"TERM",
|
|
79
|
+
"TZ",
|
|
80
|
+
"SHELL",
|
|
81
|
+
"USER",
|
|
82
|
+
"LOGNAME",
|
|
83
|
+
"TMPDIR",
|
|
84
|
+
"NODE_OPTIONS",
|
|
85
|
+
"PYTHONPATH",
|
|
86
|
+
"PYTHONIOENCODING",
|
|
87
|
+
] as const,
|
|
88
|
+
} as const;
|
|
89
|
+
|
|
90
|
+
function buildChildEnv(opts: ExecSandboxOptions): NodeJS.ProcessEnv {
|
|
91
|
+
const source = opts.env ?? process.env;
|
|
92
|
+
const out: NodeJS.ProcessEnv = {};
|
|
93
|
+
const allowed = new Set<string>([...ALWAYS_FORWARD_ENV, ...opts.env_allowlist]);
|
|
94
|
+
for (const key of allowed) {
|
|
95
|
+
const value = source[key];
|
|
96
|
+
if (typeof value === "string") out[key] = value;
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function clampTimeout(request: ExecSandboxRequest, opts: ExecSandboxOptions): number {
|
|
102
|
+
const requested = typeof request.timeout_ms === "number" && Number.isFinite(request.timeout_ms)
|
|
103
|
+
? Math.floor(request.timeout_ms)
|
|
104
|
+
: opts.default_timeout_ms;
|
|
105
|
+
if (requested < 1) return 1;
|
|
106
|
+
if (requested > opts.clamp_timeout_ms) return opts.clamp_timeout_ms;
|
|
107
|
+
return requested;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function resolveCommand(runtime: ExecSandboxRequest["runtime"]): { cmd: string; args: string[] } {
|
|
111
|
+
switch (runtime) {
|
|
112
|
+
case "bash":
|
|
113
|
+
return { cmd: "bash", args: ["-c"] };
|
|
114
|
+
case "node":
|
|
115
|
+
return { cmd: process.execPath, args: ["-e"] };
|
|
116
|
+
case "python":
|
|
117
|
+
return { cmd: "python3", args: ["-c"] };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function tail(buf: Buffer, chars: number): string {
|
|
122
|
+
if (chars <= 0) return "";
|
|
123
|
+
const text = buf.toString("utf-8");
|
|
124
|
+
return text.length <= chars ? text : text.slice(text.length - chars);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Run a script in a subprocess, capture stdout/stderr to files under
|
|
129
|
+
* `.gsd/exec/<id>.{stdout,stderr,meta.json}`, and return an `ExecSandboxResult`
|
|
130
|
+
* containing the digest plus metadata.
|
|
131
|
+
*
|
|
132
|
+
* Errors from spawn failures resolve (not reject) with `exit_code=null`.
|
|
133
|
+
* The function is pure with respect to its inputs — no global state beyond
|
|
134
|
+
* filesystem writes under `baseDir`.
|
|
135
|
+
*/
|
|
136
|
+
export function runExecSandbox(
|
|
137
|
+
request: ExecSandboxRequest,
|
|
138
|
+
opts: ExecSandboxOptions,
|
|
139
|
+
): Promise<ExecSandboxResult> {
|
|
140
|
+
return new Promise((resolveP) => {
|
|
141
|
+
const id = (opts.generateId ?? defaultGenerateId)();
|
|
142
|
+
const now = (opts.now ?? (() => new Date()))();
|
|
143
|
+
const execDir = resolve(opts.baseDir, ".gsd", "exec");
|
|
144
|
+
if (!existsSync(execDir)) mkdirSync(execDir, { recursive: true });
|
|
145
|
+
const stdoutPath = resolve(execDir, `${id}.stdout`);
|
|
146
|
+
const stderrPath = resolve(execDir, `${id}.stderr`);
|
|
147
|
+
const metaPath = resolve(execDir, `${id}.meta.json`);
|
|
148
|
+
|
|
149
|
+
const timeoutMs = clampTimeout(request, opts);
|
|
150
|
+
const { cmd, args } = resolveCommand(request.runtime);
|
|
151
|
+
const env = buildChildEnv(opts);
|
|
152
|
+
const useProcessGroup = process.platform !== "win32";
|
|
153
|
+
|
|
154
|
+
const started = Date.now();
|
|
155
|
+
let child;
|
|
156
|
+
try {
|
|
157
|
+
child = spawn(cmd, [...args, request.script], {
|
|
158
|
+
cwd: opts.baseDir,
|
|
159
|
+
env,
|
|
160
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
161
|
+
...(useProcessGroup ? { detached: true } : {}),
|
|
162
|
+
});
|
|
163
|
+
} catch (err) {
|
|
164
|
+
const duration = Date.now() - started;
|
|
165
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
166
|
+
writeFileSync(stdoutPath, "");
|
|
167
|
+
writeFileSync(stderrPath, `spawn error: ${message}\n`);
|
|
168
|
+
const result: ExecSandboxResult = {
|
|
169
|
+
id,
|
|
170
|
+
runtime: request.runtime,
|
|
171
|
+
exit_code: null,
|
|
172
|
+
signal: null,
|
|
173
|
+
timed_out: false,
|
|
174
|
+
duration_ms: duration,
|
|
175
|
+
stdout_bytes: 0,
|
|
176
|
+
stderr_bytes: Buffer.byteLength(`spawn error: ${message}\n`),
|
|
177
|
+
stdout_truncated: false,
|
|
178
|
+
stderr_truncated: false,
|
|
179
|
+
stdout_path: stdoutPath,
|
|
180
|
+
stderr_path: stderrPath,
|
|
181
|
+
meta_path: metaPath,
|
|
182
|
+
digest: `[spawn error: ${message}]`,
|
|
183
|
+
};
|
|
184
|
+
writeMeta(metaPath, result, request, now);
|
|
185
|
+
resolveP(result);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const stdoutChunks: Buffer[] = [];
|
|
190
|
+
const stderrChunks: Buffer[] = [];
|
|
191
|
+
let stdoutBytes = 0;
|
|
192
|
+
let stderrBytes = 0;
|
|
193
|
+
let stdoutTruncated = false;
|
|
194
|
+
let stderrTruncated = false;
|
|
195
|
+
|
|
196
|
+
child.stdout?.on("data", (chunk: Buffer) => {
|
|
197
|
+
const remaining = opts.stdout_cap_bytes - stdoutBytes;
|
|
198
|
+
if (remaining <= 0) {
|
|
199
|
+
stdoutTruncated = true;
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (chunk.length <= remaining) {
|
|
203
|
+
stdoutChunks.push(chunk);
|
|
204
|
+
stdoutBytes += chunk.length;
|
|
205
|
+
} else {
|
|
206
|
+
stdoutChunks.push(chunk.subarray(0, remaining));
|
|
207
|
+
stdoutBytes += remaining;
|
|
208
|
+
stdoutTruncated = true;
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
child.stderr?.on("data", (chunk: Buffer) => {
|
|
212
|
+
const remaining = opts.stderr_cap_bytes - stderrBytes;
|
|
213
|
+
if (remaining <= 0) {
|
|
214
|
+
stderrTruncated = true;
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (chunk.length <= remaining) {
|
|
218
|
+
stderrChunks.push(chunk);
|
|
219
|
+
stderrBytes += chunk.length;
|
|
220
|
+
} else {
|
|
221
|
+
stderrChunks.push(chunk.subarray(0, remaining));
|
|
222
|
+
stderrBytes += remaining;
|
|
223
|
+
stderrTruncated = true;
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
let timedOut = false;
|
|
228
|
+
const timer = setTimeout(() => {
|
|
229
|
+
timedOut = true;
|
|
230
|
+
if (useProcessGroup && child.pid != null) {
|
|
231
|
+
try {
|
|
232
|
+
process.kill(-child.pid, "SIGKILL");
|
|
233
|
+
} catch {
|
|
234
|
+
child.kill("SIGKILL");
|
|
235
|
+
}
|
|
236
|
+
} else {
|
|
237
|
+
child.kill("SIGKILL");
|
|
238
|
+
}
|
|
239
|
+
}, timeoutMs);
|
|
240
|
+
timer.unref?.();
|
|
241
|
+
|
|
242
|
+
const finalize = (exitCode: number | null, signal: NodeJS.Signals | null) => {
|
|
243
|
+
clearTimeout(timer);
|
|
244
|
+
const duration = Date.now() - started;
|
|
245
|
+
const stdoutBuf = Buffer.concat(stdoutChunks);
|
|
246
|
+
const stderrBuf = Buffer.concat(stderrChunks);
|
|
247
|
+
const stdoutSuffix = stdoutTruncated ? "\n[truncated: stdout cap reached]\n" : "";
|
|
248
|
+
const stderrSuffix = stderrTruncated ? "\n[truncated: stderr cap reached]\n" : "";
|
|
249
|
+
writeFileSync(stdoutPath, Buffer.concat([stdoutBuf, Buffer.from(stdoutSuffix, "utf-8")]));
|
|
250
|
+
writeFileSync(stderrPath, Buffer.concat([stderrBuf, Buffer.from(stderrSuffix, "utf-8")]));
|
|
251
|
+
|
|
252
|
+
const digestBody = tail(stdoutBuf, opts.digest_chars);
|
|
253
|
+
const digest =
|
|
254
|
+
digestBody.length > 0
|
|
255
|
+
? digestBody
|
|
256
|
+
: timedOut
|
|
257
|
+
? "[no stdout — timed out]"
|
|
258
|
+
: stderrBuf.length > 0
|
|
259
|
+
? `[no stdout — tail of stderr]\n${tail(stderrBuf, opts.digest_chars)}`
|
|
260
|
+
: "[no output]";
|
|
261
|
+
|
|
262
|
+
const result: ExecSandboxResult = {
|
|
263
|
+
id,
|
|
264
|
+
runtime: request.runtime,
|
|
265
|
+
exit_code: exitCode,
|
|
266
|
+
signal,
|
|
267
|
+
timed_out: timedOut,
|
|
268
|
+
duration_ms: duration,
|
|
269
|
+
stdout_bytes: stdoutBytes,
|
|
270
|
+
stderr_bytes: stderrBytes,
|
|
271
|
+
stdout_truncated: stdoutTruncated,
|
|
272
|
+
stderr_truncated: stderrTruncated,
|
|
273
|
+
stdout_path: stdoutPath,
|
|
274
|
+
stderr_path: stderrPath,
|
|
275
|
+
meta_path: metaPath,
|
|
276
|
+
digest,
|
|
277
|
+
};
|
|
278
|
+
writeMeta(metaPath, result, request, now);
|
|
279
|
+
resolveP(result);
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
child.on("error", (err) => {
|
|
283
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
284
|
+
const line = `child error: ${message}\n`;
|
|
285
|
+
const remaining = opts.stderr_cap_bytes - stderrBytes;
|
|
286
|
+
if (remaining > 0) {
|
|
287
|
+
const chunk = Buffer.from(line, "utf-8").subarray(0, remaining);
|
|
288
|
+
stderrChunks.push(chunk);
|
|
289
|
+
stderrBytes += chunk.length;
|
|
290
|
+
if (chunk.length < Buffer.byteLength(line, "utf-8")) stderrTruncated = true;
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
child.on("close", (code, signal) => finalize(code, signal));
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function defaultGenerateId(): string {
|
|
298
|
+
return randomUUID();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function writeMeta(
|
|
302
|
+
path: string,
|
|
303
|
+
result: ExecSandboxResult,
|
|
304
|
+
request: ExecSandboxRequest,
|
|
305
|
+
now: Date,
|
|
306
|
+
): void {
|
|
307
|
+
const meta = {
|
|
308
|
+
id: result.id,
|
|
309
|
+
runtime: result.runtime,
|
|
310
|
+
purpose: request.purpose ?? null,
|
|
311
|
+
script_chars: request.script.length,
|
|
312
|
+
started_at: now.toISOString(),
|
|
313
|
+
finished_at: new Date(now.getTime() + result.duration_ms).toISOString(),
|
|
314
|
+
exit_code: result.exit_code,
|
|
315
|
+
signal: result.signal,
|
|
316
|
+
timed_out: result.timed_out,
|
|
317
|
+
duration_ms: result.duration_ms,
|
|
318
|
+
stdout_bytes: result.stdout_bytes,
|
|
319
|
+
stderr_bytes: result.stderr_bytes,
|
|
320
|
+
stdout_truncated: result.stdout_truncated,
|
|
321
|
+
stderr_truncated: result.stderr_truncated,
|
|
322
|
+
stdout_path: result.stdout_path,
|
|
323
|
+
stderr_path: result.stderr_path,
|
|
324
|
+
};
|
|
325
|
+
writeFileSync(path, `${JSON.stringify(meta, null, 2)}\n`);
|
|
326
|
+
}
|