gsd-pi 2.76.0-dev.82e249f7b → 2.76.0-dev.fe143342a

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.
Files changed (139) hide show
  1. package/dist/mcp-server.d.ts +7 -0
  2. package/dist/mcp-server.js +35 -1
  3. package/dist/resource-loader.d.ts +1 -1
  4. package/dist/resource-loader.js +2 -8
  5. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +66 -4
  6. package/dist/resources/extensions/gsd/auto-start.js +27 -14
  7. package/dist/resources/extensions/gsd/auto.js +11 -11
  8. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +39 -9
  9. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +93 -0
  10. package/dist/resources/extensions/gsd/bootstrap/register-extension.js +2 -0
  11. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +35 -0
  12. package/dist/resources/extensions/gsd/compaction-snapshot.js +121 -0
  13. package/dist/resources/extensions/gsd/error-classifier.js +10 -3
  14. package/dist/resources/extensions/gsd/exec-history.js +120 -0
  15. package/dist/resources/extensions/gsd/exec-sandbox.js +258 -0
  16. package/dist/resources/extensions/gsd/gsd-db.js +62 -4
  17. package/dist/resources/extensions/gsd/init-wizard.js +15 -1
  18. package/dist/resources/extensions/gsd/key-manager.js +6 -0
  19. package/dist/resources/extensions/gsd/pre-execution-checks.js +13 -3
  20. package/dist/resources/extensions/gsd/preferences-types.js +9 -0
  21. package/dist/resources/extensions/gsd/preferences-validation.js +83 -0
  22. package/dist/resources/extensions/gsd/preferences.js +17 -17
  23. package/dist/resources/extensions/gsd/safety/file-change-validator.js +1 -1
  24. package/dist/resources/extensions/gsd/tools/exec-search-tool.js +59 -0
  25. package/dist/resources/extensions/gsd/tools/exec-tool.js +126 -0
  26. package/dist/resources/extensions/gsd/tools/resume-tool.js +23 -0
  27. package/dist/resources/extensions/gsd/workflow-mcp.js +3 -0
  28. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  29. package/dist/web/standalone/.next/BUILD_ID +1 -1
  30. package/dist/web/standalone/.next/app-path-routes-manifest.json +9 -9
  31. package/dist/web/standalone/.next/build-manifest.json +2 -2
  32. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  33. package/dist/web/standalone/.next/required-server-files.json +1 -1
  34. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  35. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  43. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/index.html +1 -1
  51. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app-paths-manifest.json +9 -9
  58. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  59. package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
  60. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  61. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  62. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  63. package/dist/web/standalone/server.js +1 -1
  64. package/package.json +1 -1
  65. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  66. package/packages/mcp-server/dist/workflow-tools.js +64 -25
  67. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  68. package/packages/mcp-server/src/workflow-tools.test.ts +146 -1
  69. package/packages/mcp-server/src/workflow-tools.ts +84 -43
  70. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  71. package/packages/pi-coding-agent/dist/core/redact-secrets.d.ts +2 -0
  72. package/packages/pi-coding-agent/dist/core/redact-secrets.d.ts.map +1 -0
  73. package/packages/pi-coding-agent/dist/core/redact-secrets.js +49 -0
  74. package/packages/pi-coding-agent/dist/core/redact-secrets.js.map +1 -0
  75. package/packages/pi-coding-agent/dist/core/redact-secrets.test.d.ts +2 -0
  76. package/packages/pi-coding-agent/dist/core/redact-secrets.test.d.ts.map +1 -0
  77. package/packages/pi-coding-agent/dist/core/redact-secrets.test.js +67 -0
  78. package/packages/pi-coding-agent/dist/core/redact-secrets.test.js.map +1 -0
  79. package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
  80. package/packages/pi-coding-agent/dist/core/session-manager.js +9 -5
  81. package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
  82. package/packages/pi-coding-agent/dist/core/session-manager.test.js +25 -1
  83. package/packages/pi-coding-agent/dist/core/session-manager.test.js.map +1 -1
  84. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.d.ts +1 -1
  85. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.d.ts.map +1 -1
  86. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.js +5 -4
  87. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.js.map +1 -1
  88. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.d.ts +7 -6
  89. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
  90. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js +29 -21
  91. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
  92. package/packages/pi-coding-agent/src/core/redact-secrets.test.ts +86 -0
  93. package/packages/pi-coding-agent/src/core/redact-secrets.ts +58 -0
  94. package/packages/pi-coding-agent/src/core/session-manager.test.ts +36 -1
  95. package/packages/pi-coding-agent/src/core/session-manager.ts +9 -5
  96. package/packages/pi-coding-agent/src/modes/interactive/components/chat-frame.ts +6 -6
  97. package/packages/pi-coding-agent/src/modes/interactive/components/skill-invocation-message.ts +36 -22
  98. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  99. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +67 -4
  100. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +137 -2
  101. package/src/resources/extensions/gsd/auto-start.ts +28 -15
  102. package/src/resources/extensions/gsd/auto.ts +11 -11
  103. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +40 -9
  104. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +109 -0
  105. package/src/resources/extensions/gsd/bootstrap/register-extension.ts +2 -0
  106. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +36 -0
  107. package/src/resources/extensions/gsd/compaction-snapshot.ts +165 -0
  108. package/src/resources/extensions/gsd/error-classifier.ts +10 -3
  109. package/src/resources/extensions/gsd/exec-history.ts +153 -0
  110. package/src/resources/extensions/gsd/exec-sandbox.ts +326 -0
  111. package/src/resources/extensions/gsd/gsd-db.ts +68 -4
  112. package/src/resources/extensions/gsd/init-wizard.ts +15 -1
  113. package/src/resources/extensions/gsd/key-manager.ts +6 -0
  114. package/src/resources/extensions/gsd/pre-execution-checks.ts +13 -3
  115. package/src/resources/extensions/gsd/preferences-types.ts +38 -0
  116. package/src/resources/extensions/gsd/preferences-validation.ts +79 -0
  117. package/src/resources/extensions/gsd/preferences.ts +17 -17
  118. package/src/resources/extensions/gsd/safety/file-change-validator.ts +1 -1
  119. package/src/resources/extensions/gsd/tests/compaction-snapshot.test.ts +123 -0
  120. package/src/resources/extensions/gsd/tests/exec-history.test.ts +124 -0
  121. package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +210 -0
  122. package/src/resources/extensions/gsd/tests/file-change-validator.test.ts +20 -0
  123. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +151 -0
  124. package/src/resources/extensions/gsd/tests/init-wizard.test.ts +27 -0
  125. package/src/resources/extensions/gsd/tests/isolation-none-branch-guard.test.ts +1 -1
  126. package/src/resources/extensions/gsd/tests/key-manager.test.ts +7 -0
  127. package/src/resources/extensions/gsd/tests/pre-exec-backtick-strip.test.ts +14 -0
  128. package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +19 -0
  129. package/src/resources/extensions/gsd/tests/preferences.test.ts +110 -0
  130. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +48 -0
  131. package/src/resources/extensions/gsd/tests/save-gate-result-render.test.ts +95 -0
  132. package/src/resources/extensions/gsd/tests/zombie-gsd-state.test.ts +3 -1
  133. package/src/resources/extensions/gsd/tools/exec-search-tool.ts +81 -0
  134. package/src/resources/extensions/gsd/tools/exec-tool.ts +183 -0
  135. package/src/resources/extensions/gsd/tools/resume-tool.ts +40 -0
  136. package/src/resources/extensions/gsd/workflow-logger.ts +2 -1
  137. package/src/resources/extensions/gsd/workflow-mcp.ts +3 -0
  138. /package/dist/web/standalone/.next/static/{ecSsu49rxxcpbNmVP4mLD → n21VtX2hZlkpdEUO_nU4z}/_buildManifest.js +0 -0
  139. /package/dist/web/standalone/.next/static/{ecSsu49rxxcpbNmVP4mLD → n21VtX2hZlkpdEUO_nU4z}/_ssgManifest.js +0 -0
@@ -0,0 +1,109 @@
1
+ // GSD2 — Exec (context-mode) tool registration.
2
+ //
3
+ // Exposes the `gsd_exec` tool over MCP. Opt-in: disabled unless
4
+ // `context_mode.enabled: true` is set in preferences.
5
+
6
+ import { Type } from "@sinclair/typebox";
7
+ import type { ExtensionAPI } from "@gsd/pi-coding-agent";
8
+
9
+ import { executeGsdExec } from "../tools/exec-tool.js";
10
+ import { executeExecSearch } from "../tools/exec-search-tool.js";
11
+ import { executeResume } from "../tools/resume-tool.js";
12
+ import { loadEffectiveGSDPreferences } from "../preferences.js";
13
+ import { logWarning } from "../workflow-logger.js";
14
+
15
+ export function registerExecTools(pi: ExtensionAPI): void {
16
+ pi.registerTool({
17
+ name: "gsd_exec",
18
+ label: "Exec (Sandboxed)",
19
+ description:
20
+ "Run a short script (bash/node/python) in a subprocess. Full stdout/stderr persist to " +
21
+ ".gsd/exec/<id>.{stdout,stderr,meta.json}; only a short digest returns in context. Use " +
22
+ "this instead of reading many files or emitting large tool outputs — e.g. have the script " +
23
+ "count/grep/summarize and log the finding. Enabled by default; opt out via " +
24
+ "preferences.context_mode.enabled=false.",
25
+ promptSnippet:
26
+ "Run a bash/node/python script in a sandbox; full output is saved to disk and only a digest returns",
27
+ promptGuidelines: [
28
+ "Prefer gsd_exec for analyses that would otherwise read >3 files or produce large tool output.",
29
+ "Write scripts that log the finding (counts, matches, summaries) rather than raw dumps.",
30
+ "The digest is the last ~300 chars of stdout — size your log output accordingly.",
31
+ "Need the full output? Read the stdout_path returned in details (file on local disk).",
32
+ ],
33
+ parameters: Type.Object({
34
+ runtime: Type.Union(
35
+ [Type.Literal("bash"), Type.Literal("node"), Type.Literal("python")],
36
+ { description: "Interpreter: bash (-c), node (-e), or python3 (-c)." },
37
+ ),
38
+ script: Type.String({ description: "Script body. Keep output small (log the finding, not the data)." }),
39
+ purpose: Type.Optional(Type.String({ description: "Short label recorded in meta.json for later review." })),
40
+ timeout_ms: Type.Optional(
41
+ Type.Number({
42
+ description: "Per-invocation timeout (ms). Capped at 600000. Default from preferences.",
43
+ minimum: 1_000,
44
+ maximum: 600_000,
45
+ }),
46
+ ),
47
+ }),
48
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
49
+ let prefs: Awaited<ReturnType<typeof loadEffectiveGSDPreferences>> | null = null;
50
+ try {
51
+ prefs = loadEffectiveGSDPreferences();
52
+ } catch (err) {
53
+ logWarning("tool", `gsd_exec could not load preferences: ${err instanceof Error ? err.message : String(err)}`);
54
+ }
55
+ return executeGsdExec(params as Parameters<typeof executeGsdExec>[0], {
56
+ baseDir: process.cwd(),
57
+ preferences: prefs?.preferences ?? null,
58
+ });
59
+ },
60
+ });
61
+
62
+ pi.registerTool({
63
+ name: "gsd_exec_search",
64
+ label: "Search gsd_exec History",
65
+ description:
66
+ "List prior gsd_exec runs (most recent first) from .gsd/exec/*.meta.json. Useful for " +
67
+ "rediscovering the stdout_path of an earlier run without re-executing it. Read-only.",
68
+ promptSnippet: "Search prior gsd_exec runs by substring, runtime, or failing-only filter",
69
+ promptGuidelines: [
70
+ "Use this before re-running an expensive analysis — the prior run's stdout file may still answer.",
71
+ "The preview shows the trailing ~300 chars of stdout; read stdout_path for the full transcript.",
72
+ ],
73
+ parameters: Type.Object({
74
+ query: Type.Optional(Type.String({ description: "Substring matched against id and purpose (case-insensitive)." })),
75
+ runtime: Type.Optional(
76
+ Type.Union([Type.Literal("bash"), Type.Literal("node"), Type.Literal("python")], {
77
+ description: "Restrict to one runtime.",
78
+ }),
79
+ ),
80
+ failing_only: Type.Optional(Type.Boolean({ description: "Only non-zero exit codes and timeouts." })),
81
+ limit: Type.Optional(Type.Number({ description: "Max results (default 20, cap 200)", minimum: 1, maximum: 200 })),
82
+ }),
83
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
84
+ return executeExecSearch(params as Parameters<typeof executeExecSearch>[0], {
85
+ baseDir: process.cwd(),
86
+ });
87
+ },
88
+ });
89
+
90
+ pi.registerTool({
91
+ name: "gsd_resume",
92
+ label: "Resume (Read Snapshot)",
93
+ description:
94
+ "Return the contents of .gsd/last-snapshot.md — a ≤2 KB digest of top memories, recent " +
95
+ "gsd_exec runs, and active context, written automatically on session_before_compact. Use " +
96
+ "this after compaction or session resume to re-orient quickly.",
97
+ promptSnippet: "Read the pre-compaction snapshot to re-orient after context loss",
98
+ promptGuidelines: [
99
+ "Call this right after a session resumes if you feel you've lost durable context.",
100
+ "The snapshot is a summary — use memory_query or gsd_exec_search for detail.",
101
+ ],
102
+ parameters: Type.Object({}),
103
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
104
+ return executeResume(params as Parameters<typeof executeResume>[0], {
105
+ baseDir: process.cwd(),
106
+ });
107
+ },
108
+ });
109
+ }
@@ -8,6 +8,7 @@ import type { GSDEcosystemBeforeAgentStartHandler } from "../ecosystem/gsd-exten
8
8
  import { loadEcosystemExtensions } from "../ecosystem/loader.js";
9
9
  import { registerDbTools } from "./db-tools.js";
10
10
  import { registerDynamicTools } from "./dynamic-tools.js";
11
+ import { registerExecTools } from "./exec-tools.js";
11
12
  import { registerJournalTools } from "./journal-tools.js";
12
13
  import { registerMemoryTools } from "./memory-tools.js";
13
14
  import { registerQueryTools } from "./query-tools.js";
@@ -100,6 +101,7 @@ export function registerGsdExtension(pi: ExtensionAPI): void {
100
101
  ["journal-tools", () => registerJournalTools(pi)],
101
102
  ["query-tools", () => registerQueryTools(pi)],
102
103
  ["memory-tools", () => registerMemoryTools(pi)],
104
+ ["exec-tools", () => registerExecTools(pi)],
103
105
  ["shortcuts", () => registerShortcuts(pi)],
104
106
  ["hooks", () => registerHooks(pi, ecosystemHandlers)],
105
107
  ["ecosystem", () => {
@@ -225,6 +225,42 @@ export function registerHooks(
225
225
  }));
226
226
  });
227
227
 
228
+ // Context-mode snapshot: write .gsd/last-snapshot.md before compaction so
229
+ // agents can call gsd_resume (or Read the file) to re-orient. Opt-in via
230
+ // preferences.context_mode.enabled. Runs after the auto-cancel handler
231
+ // above — if that one returned cancel:true, pi still fires us but the
232
+ // compaction won't actually happen; the snapshot is still useful then,
233
+ // since auto may pause and resume later.
234
+ pi.on("session_before_compact", async () => {
235
+ try {
236
+ const { loadEffectiveGSDPreferences } = await import("../preferences.js");
237
+ const { isContextModeEnabled } = await import("../preferences-types.js");
238
+ const prefs = loadEffectiveGSDPreferences();
239
+ if (!isContextModeEnabled(prefs?.preferences)) return;
240
+ const { writeCompactionSnapshot } = await import("../compaction-snapshot.js");
241
+ const { ensureDbOpen } = await import("./dynamic-tools.js");
242
+ await ensureDbOpen();
243
+ const basePath = process.cwd();
244
+ let activeContext: string | null = null;
245
+ try {
246
+ const state = await deriveState(basePath);
247
+ if (state.activeMilestone && state.activeSlice && state.activeTask) {
248
+ activeContext =
249
+ `Active: ${state.activeMilestone.id} / ${state.activeSlice.id} / ${state.activeTask.id}` +
250
+ (state.activeTask.title ? ` — ${state.activeTask.title}` : "");
251
+ }
252
+ } catch {
253
+ /* non-fatal */
254
+ }
255
+ writeCompactionSnapshot(basePath, { activeContext });
256
+ } catch (err) {
257
+ safetyLogWarning(
258
+ "context-mode",
259
+ `failed to write compaction snapshot: ${err instanceof Error ? err.message : String(err)}`,
260
+ );
261
+ }
262
+ });
263
+
228
264
  pi.on("session_shutdown", async (_event, ctx: ExtensionContext) => {
229
265
  if (isParallelActive()) {
230
266
  try {
@@ -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
- const RATE_LIMIT_RE = /rate.?limit|too many requests|429|hit your limit|usage limit|quota (?:reached|hit)|limit.*resets?/i;
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
- const NETWORK_RE = /network|ECONNRESET|ETIMEDOUT|ECONNREFUSED|socket hang up|fetch failed|connection.*reset|dns|unexpected eof/i;
55
- const SERVER_RE = /internal server error|500|502|503|overloaded|server_error|api_error|service.?unavailable/i;
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
+ }