gsd-pi 2.76.0-dev.b072ebb73 → 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 (200) 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/phases.js +4 -1
  7. package/dist/resources/extensions/gsd/auto/session.js +4 -0
  8. package/dist/resources/extensions/gsd/auto-model-selection.js +39 -13
  9. package/dist/resources/extensions/gsd/auto-start.js +39 -21
  10. package/dist/resources/extensions/gsd/auto.js +15 -12
  11. package/dist/resources/extensions/gsd/blocked-models.js +68 -0
  12. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +76 -0
  13. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +39 -9
  14. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +93 -0
  15. package/dist/resources/extensions/gsd/bootstrap/register-extension.js +2 -0
  16. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +35 -0
  17. package/dist/resources/extensions/gsd/compaction-snapshot.js +121 -0
  18. package/dist/resources/extensions/gsd/complexity-classifier.js +5 -3
  19. package/dist/resources/extensions/gsd/error-classifier.js +31 -3
  20. package/dist/resources/extensions/gsd/exec-history.js +120 -0
  21. package/dist/resources/extensions/gsd/exec-sandbox.js +258 -0
  22. package/dist/resources/extensions/gsd/gsd-db.js +62 -4
  23. package/dist/resources/extensions/gsd/init-wizard.js +15 -1
  24. package/dist/resources/extensions/gsd/key-manager.js +6 -0
  25. package/dist/resources/extensions/gsd/pre-execution-checks.js +13 -3
  26. package/dist/resources/extensions/gsd/preferences-types.js +9 -0
  27. package/dist/resources/extensions/gsd/preferences-validation.js +83 -0
  28. package/dist/resources/extensions/gsd/preferences.js +17 -17
  29. package/dist/resources/extensions/gsd/prompt-loader.js +22 -7
  30. package/dist/resources/extensions/gsd/safety/file-change-validator.js +1 -1
  31. package/dist/resources/extensions/gsd/tools/exec-search-tool.js +59 -0
  32. package/dist/resources/extensions/gsd/tools/exec-tool.js +126 -0
  33. package/dist/resources/extensions/gsd/tools/resume-tool.js +23 -0
  34. package/dist/resources/extensions/gsd/workflow-mcp.js +3 -0
  35. package/dist/resources/extensions/search-the-web/command-search-provider.js +5 -4
  36. package/dist/resources/extensions/search-the-web/native-search.js +45 -13
  37. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  38. package/dist/web/standalone/.next/BUILD_ID +1 -1
  39. package/dist/web/standalone/.next/app-path-routes-manifest.json +8 -8
  40. package/dist/web/standalone/.next/build-manifest.json +2 -2
  41. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  42. package/dist/web/standalone/.next/required-server-files.json +1 -1
  43. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  44. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  52. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/index.html +1 -1
  60. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app-paths-manifest.json +8 -8
  67. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  68. package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
  69. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  70. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  71. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  72. package/dist/web/standalone/server.js +1 -1
  73. package/package.json +1 -1
  74. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  75. package/packages/mcp-server/dist/workflow-tools.js +64 -25
  76. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  77. package/packages/mcp-server/src/workflow-tools.test.ts +146 -1
  78. package/packages/mcp-server/src/workflow-tools.ts +84 -43
  79. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  80. package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
  81. package/packages/pi-ai/dist/providers/openai-completions.js +60 -15
  82. package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
  83. package/packages/pi-ai/dist/providers/think-tag-parser.d.ts +17 -0
  84. package/packages/pi-ai/dist/providers/think-tag-parser.d.ts.map +1 -0
  85. package/packages/pi-ai/dist/providers/think-tag-parser.js +75 -0
  86. package/packages/pi-ai/dist/providers/think-tag-parser.js.map +1 -0
  87. package/packages/pi-ai/dist/providers/think-tag-parser.test.d.ts +2 -0
  88. package/packages/pi-ai/dist/providers/think-tag-parser.test.d.ts.map +1 -0
  89. package/packages/pi-ai/dist/providers/think-tag-parser.test.js +41 -0
  90. package/packages/pi-ai/dist/providers/think-tag-parser.test.js.map +1 -0
  91. package/packages/pi-ai/src/providers/openai-completions.ts +57 -16
  92. package/packages/pi-ai/src/providers/think-tag-parser.test.ts +44 -0
  93. package/packages/pi-ai/src/providers/think-tag-parser.ts +94 -0
  94. package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
  95. package/packages/pi-coding-agent/dist/core/model-discovery.d.ts +3 -1
  96. package/packages/pi-coding-agent/dist/core/model-discovery.d.ts.map +1 -1
  97. package/packages/pi-coding-agent/dist/core/model-discovery.js +92 -12
  98. package/packages/pi-coding-agent/dist/core/model-discovery.js.map +1 -1
  99. package/packages/pi-coding-agent/dist/core/model-discovery.test.js +16 -1
  100. package/packages/pi-coding-agent/dist/core/model-discovery.test.js.map +1 -1
  101. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js +61 -1
  102. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js.map +1 -1
  103. package/packages/pi-coding-agent/dist/core/model-registry.d.ts +5 -0
  104. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  105. package/packages/pi-coding-agent/dist/core/model-registry.js +76 -10
  106. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  107. package/packages/pi-coding-agent/dist/core/redact-secrets.d.ts +2 -0
  108. package/packages/pi-coding-agent/dist/core/redact-secrets.d.ts.map +1 -0
  109. package/packages/pi-coding-agent/dist/core/redact-secrets.js +49 -0
  110. package/packages/pi-coding-agent/dist/core/redact-secrets.js.map +1 -0
  111. package/packages/pi-coding-agent/dist/core/redact-secrets.test.d.ts +2 -0
  112. package/packages/pi-coding-agent/dist/core/redact-secrets.test.d.ts.map +1 -0
  113. package/packages/pi-coding-agent/dist/core/redact-secrets.test.js +67 -0
  114. package/packages/pi-coding-agent/dist/core/redact-secrets.test.js.map +1 -0
  115. package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
  116. package/packages/pi-coding-agent/dist/core/session-manager.js +9 -5
  117. package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
  118. package/packages/pi-coding-agent/dist/core/session-manager.test.js +25 -1
  119. package/packages/pi-coding-agent/dist/core/session-manager.test.js.map +1 -1
  120. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.d.ts +1 -1
  121. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.d.ts.map +1 -1
  122. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.js +5 -4
  123. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.js.map +1 -1
  124. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts.map +1 -1
  125. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js +13 -7
  126. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js.map +1 -1
  127. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.d.ts +7 -6
  128. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
  129. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js +29 -21
  130. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
  131. package/packages/pi-coding-agent/src/core/model-discovery.test.ts +19 -0
  132. package/packages/pi-coding-agent/src/core/model-discovery.ts +99 -12
  133. package/packages/pi-coding-agent/src/core/model-registry-discovery.test.ts +75 -0
  134. package/packages/pi-coding-agent/src/core/model-registry.ts +86 -10
  135. package/packages/pi-coding-agent/src/core/redact-secrets.test.ts +86 -0
  136. package/packages/pi-coding-agent/src/core/redact-secrets.ts +58 -0
  137. package/packages/pi-coding-agent/src/core/session-manager.test.ts +36 -1
  138. package/packages/pi-coding-agent/src/core/session-manager.ts +9 -5
  139. package/packages/pi-coding-agent/src/modes/interactive/components/chat-frame.ts +6 -6
  140. package/packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts +16 -7
  141. package/packages/pi-coding-agent/src/modes/interactive/components/skill-invocation-message.ts +36 -22
  142. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  143. package/scripts/link-workspace-packages.cjs +1 -0
  144. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +67 -4
  145. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +137 -2
  146. package/src/resources/extensions/gsd/auto/loop-deps.ts +1 -0
  147. package/src/resources/extensions/gsd/auto/phases.ts +4 -0
  148. package/src/resources/extensions/gsd/auto/session.ts +7 -1
  149. package/src/resources/extensions/gsd/auto-model-selection.ts +50 -12
  150. package/src/resources/extensions/gsd/auto-start.ts +40 -22
  151. package/src/resources/extensions/gsd/auto.ts +15 -12
  152. package/src/resources/extensions/gsd/blocked-models.ts +98 -0
  153. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +97 -0
  154. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +40 -9
  155. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +109 -0
  156. package/src/resources/extensions/gsd/bootstrap/register-extension.ts +2 -0
  157. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +36 -0
  158. package/src/resources/extensions/gsd/compaction-snapshot.ts +165 -0
  159. package/src/resources/extensions/gsd/complexity-classifier.ts +5 -3
  160. package/src/resources/extensions/gsd/error-classifier.ts +36 -3
  161. package/src/resources/extensions/gsd/exec-history.ts +153 -0
  162. package/src/resources/extensions/gsd/exec-sandbox.ts +326 -0
  163. package/src/resources/extensions/gsd/gsd-db.ts +68 -4
  164. package/src/resources/extensions/gsd/init-wizard.ts +15 -1
  165. package/src/resources/extensions/gsd/key-manager.ts +6 -0
  166. package/src/resources/extensions/gsd/pre-execution-checks.ts +13 -3
  167. package/src/resources/extensions/gsd/preferences-types.ts +38 -0
  168. package/src/resources/extensions/gsd/preferences-validation.ts +79 -0
  169. package/src/resources/extensions/gsd/preferences.ts +17 -17
  170. package/src/resources/extensions/gsd/prompt-loader.ts +30 -7
  171. package/src/resources/extensions/gsd/safety/file-change-validator.ts +1 -1
  172. package/src/resources/extensions/gsd/tests/auto-model-selection.test.ts +12 -0
  173. package/src/resources/extensions/gsd/tests/auto-start-model-capture.test.ts +33 -3
  174. package/src/resources/extensions/gsd/tests/auto-thinking-restore.test.ts +38 -0
  175. package/src/resources/extensions/gsd/tests/blocked-models.test.ts +98 -0
  176. package/src/resources/extensions/gsd/tests/compaction-snapshot.test.ts +123 -0
  177. package/src/resources/extensions/gsd/tests/complexity-classifier.test.ts +3 -3
  178. package/src/resources/extensions/gsd/tests/exec-history.test.ts +124 -0
  179. package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +210 -0
  180. package/src/resources/extensions/gsd/tests/file-change-validator.test.ts +20 -0
  181. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +151 -0
  182. package/src/resources/extensions/gsd/tests/init-wizard.test.ts +27 -0
  183. package/src/resources/extensions/gsd/tests/isolation-none-branch-guard.test.ts +1 -1
  184. package/src/resources/extensions/gsd/tests/key-manager.test.ts +7 -0
  185. package/src/resources/extensions/gsd/tests/pre-exec-backtick-strip.test.ts +14 -0
  186. package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +19 -0
  187. package/src/resources/extensions/gsd/tests/preferences.test.ts +110 -0
  188. package/src/resources/extensions/gsd/tests/prompt-loader-extension-dir.test.ts +49 -0
  189. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +91 -0
  190. package/src/resources/extensions/gsd/tests/save-gate-result-render.test.ts +95 -0
  191. package/src/resources/extensions/gsd/tests/zombie-gsd-state.test.ts +3 -1
  192. package/src/resources/extensions/gsd/tools/exec-search-tool.ts +81 -0
  193. package/src/resources/extensions/gsd/tools/exec-tool.ts +183 -0
  194. package/src/resources/extensions/gsd/tools/resume-tool.ts +40 -0
  195. package/src/resources/extensions/gsd/workflow-logger.ts +2 -1
  196. package/src/resources/extensions/gsd/workflow-mcp.ts +3 -0
  197. package/src/resources/extensions/search-the-web/command-search-provider.ts +5 -4
  198. package/src/resources/extensions/search-the-web/native-search.ts +48 -12
  199. /package/dist/web/standalone/.next/static/{pBwmOoye64ZrRp-_rf0v1 → n21VtX2hZlkpdEUO_nU4z}/_buildManifest.js +0 -0
  200. /package/dist/web/standalone/.next/static/{pBwmOoye64ZrRp-_rf0v1 → n21VtX2hZlkpdEUO_nU4z}/_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
+ }
@@ -32,11 +32,13 @@ export interface TaskMetadata {
32
32
  // ─── Unit Type → Default Tier Mapping ────────────────────────────────────────
33
33
 
34
34
  const UNIT_TYPE_TIERS: Record<string, ComplexityTier> = {
35
- // Tier 1 — Light: structured summaries, completion, UAT
36
- "complete-slice": "light",
35
+ // Tier 1 — Light: compact verification turns
37
36
  "run-uat": "light",
38
37
 
39
- // Tier 2 — Standard: research, routine discussion
38
+ // Tier 2 — Standard: research, routine discussion, slice completion
39
+ // complete-slice can carry large inlined context; avoid routing it to the
40
+ // cheapest "light" model by default (#4520).
41
+ "complete-slice": "standard",
40
42
  "discuss-milestone": "standard",
41
43
  "discuss-slice": "standard",
42
44
  "research-milestone": "standard",
@@ -19,6 +19,7 @@ export type ErrorClass =
19
19
  | { kind: "stream"; retryAfterMs: number }
20
20
  | { kind: "connection"; retryAfterMs: number }
21
21
  | { kind: "model-error" }
22
+ | { kind: "unsupported-model" }
22
23
  | { kind: "permanent" }
23
24
  | { kind: "unknown" };
24
25
 
@@ -46,12 +47,19 @@ const PERMANENT_RE = /auth|unauthorized|forbidden|invalid.*key|invalid.*api|bill
46
47
  // Include provider-specific quota-window phrasing like:
47
48
  // - "You've hit your limit"
48
49
  // - "usage limit" / "quota reached"
49
- 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;
50
52
  // OpenRouter affordability-style quota errors should be treated as transient
51
53
  // so core retry logic can lower maxTokens and continue in-session.
52
54
  const AFFORDABILITY_RE = /requires more credits|can only afford|insufficient credits|not enough credits|fewer max_tokens/i;
53
- const NETWORK_RE = /network|ECONNRESET|ETIMEDOUT|ECONNREFUSED|socket hang up|fetch failed|connection.*reset|dns|unexpected eof/i;
54
- 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;
55
63
  // ECONNRESET/ECONNREFUSED are in NETWORK_RE (same-model retry first).
56
64
  const CONNECTION_RE = /terminated|connection.?(?:refused|error)|other side closed|EPIPE|network.?(?:is\s+)?unavailable|stream_exhausted(?:_without_result)?/i;
57
65
  // Catch-all for V8 JSON.parse errors: all modern variants end with "in JSON at position \d+".
@@ -59,6 +67,18 @@ const CONNECTION_RE = /terminated|connection.?(?:refused|error)|other side close
59
67
  const STREAM_RE = /in JSON at position \d+|Unexpected end of JSON|SyntaxError.*JSON/i;
60
68
  const RESET_DELAY_RE = /reset in (\d+)s/i;
61
69
 
70
+ // Provider-side model entitlement rejection: the SDK accepted the model switch,
71
+ // but the provider refused at request time because the current account/plan/tier
72
+ // cannot use that model. Must match all three of: a model/deployment token,
73
+ // a negative-entitlement indicator, and an account/plan/tier/subscription token.
74
+ // Requiring all three keeps generic "account suspended" errors in `permanent`
75
+ // (no model token) while catching the phrasings providers actually use.
76
+ // See issue #4513.
77
+ const UNSUPPORTED_MODEL_MODEL_RE = /\b(?:model|deployment)\b/i;
78
+ const UNSUPPORTED_MODEL_INDICATOR_RE =
79
+ /\bnot support(?:ed|s)?\b|\bunsupported\b|\bnot available\b|\bunavailable\b|\bno access\b|\bdoes(?:n['’]t| not) (?:have access|support)\b|\bnot authori[sz]ed\b/i;
80
+ const UNSUPPORTED_MODEL_SCOPE_RE = /\b(?:account|plan|tier|subscription)\b/i;
81
+
62
82
  /**
63
83
  * Classify an error message into one of the ErrorClass kinds.
64
84
  *
@@ -74,6 +94,19 @@ const RESET_DELAY_RE = /reset in (\d+)s/i;
74
94
  export function classifyError(errorMsg: string, retryAfterMs?: number): ErrorClass {
75
95
  const isPermanent = PERMANENT_RE.test(errorMsg);
76
96
  const isRateLimit = RATE_LIMIT_RE.test(errorMsg) || AFFORDABILITY_RE.test(errorMsg);
97
+ const isUnsupportedModel =
98
+ UNSUPPORTED_MODEL_MODEL_RE.test(errorMsg) &&
99
+ UNSUPPORTED_MODEL_INDICATOR_RE.test(errorMsg) &&
100
+ UNSUPPORTED_MODEL_SCOPE_RE.test(errorMsg);
101
+
102
+ // 0. Unsupported model (account/plan entitlement rejection) — checked before
103
+ // `permanent` because PERMANENT_RE also matches /account/i and would
104
+ // otherwise swallow these errors, blocking the blocklist-driven fallback.
105
+ // Rate limit still wins when both patterns appear (a throttled account is
106
+ // not an entitlement failure).
107
+ if (isUnsupportedModel && !isRateLimit) {
108
+ return { kind: "unsupported-model" };
109
+ }
77
110
 
78
111
  // 1. Permanent — but rate limit takes precedence
79
112
  if (isPermanent && !isRateLimit) {
@@ -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
+ }