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.
- package/dist/mcp-server.d.ts +7 -0
- package/dist/mcp-server.js +35 -1
- package/dist/resource-loader.d.ts +1 -1
- package/dist/resource-loader.js +2 -8
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +66 -4
- package/dist/resources/extensions/gsd/auto/phases.js +4 -1
- package/dist/resources/extensions/gsd/auto/session.js +4 -0
- package/dist/resources/extensions/gsd/auto-model-selection.js +39 -13
- package/dist/resources/extensions/gsd/auto-start.js +39 -21
- package/dist/resources/extensions/gsd/auto.js +15 -12
- package/dist/resources/extensions/gsd/blocked-models.js +68 -0
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +76 -0
- 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 +35 -0
- package/dist/resources/extensions/gsd/compaction-snapshot.js +121 -0
- package/dist/resources/extensions/gsd/complexity-classifier.js +5 -3
- package/dist/resources/extensions/gsd/error-classifier.js +31 -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 +62 -4
- package/dist/resources/extensions/gsd/init-wizard.js +15 -1
- package/dist/resources/extensions/gsd/key-manager.js +6 -0
- package/dist/resources/extensions/gsd/pre-execution-checks.js +13 -3
- 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/prompt-loader.js +22 -7
- package/dist/resources/extensions/gsd/safety/file-change-validator.js +1 -1
- 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/extensions/search-the-web/command-search-provider.js +5 -4
- package/dist/resources/extensions/search-the-web/native-search.js +45 -13
- 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 +8 -8
- 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 +8 -8
- 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/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/src/workflow-tools.test.ts +146 -1
- package/packages/mcp-server/src/workflow-tools.ts +84 -43
- package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.js +60 -15
- package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
- package/packages/pi-ai/dist/providers/think-tag-parser.d.ts +17 -0
- package/packages/pi-ai/dist/providers/think-tag-parser.d.ts.map +1 -0
- package/packages/pi-ai/dist/providers/think-tag-parser.js +75 -0
- package/packages/pi-ai/dist/providers/think-tag-parser.js.map +1 -0
- package/packages/pi-ai/dist/providers/think-tag-parser.test.d.ts +2 -0
- package/packages/pi-ai/dist/providers/think-tag-parser.test.d.ts.map +1 -0
- package/packages/pi-ai/dist/providers/think-tag-parser.test.js +41 -0
- package/packages/pi-ai/dist/providers/think-tag-parser.test.js.map +1 -0
- package/packages/pi-ai/src/providers/openai-completions.ts +57 -16
- package/packages/pi-ai/src/providers/think-tag-parser.test.ts +44 -0
- package/packages/pi-ai/src/providers/think-tag-parser.ts +94 -0
- package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-coding-agent/dist/core/model-discovery.d.ts +3 -1
- package/packages/pi-coding-agent/dist/core/model-discovery.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-discovery.js +92 -12
- package/packages/pi-coding-agent/dist/core/model-discovery.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-discovery.test.js +16 -1
- package/packages/pi-coding-agent/dist/core/model-discovery.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js +61 -1
- package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts +5 -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 +76 -10
- 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/components/chat-frame.d.ts +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.js +5 -4
- package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js +13 -7
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.d.ts +7 -6
- package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js +29 -21
- package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
- package/packages/pi-coding-agent/src/core/model-discovery.test.ts +19 -0
- package/packages/pi-coding-agent/src/core/model-discovery.ts +99 -12
- package/packages/pi-coding-agent/src/core/model-registry-discovery.test.ts +75 -0
- package/packages/pi-coding-agent/src/core/model-registry.ts +86 -10
- 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/components/chat-frame.ts +6 -6
- package/packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts +16 -7
- package/packages/pi-coding-agent/src/modes/interactive/components/skill-invocation-message.ts +36 -22
- package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
- package/scripts/link-workspace-packages.cjs +1 -0
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +67 -4
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +137 -2
- package/src/resources/extensions/gsd/auto/loop-deps.ts +1 -0
- package/src/resources/extensions/gsd/auto/phases.ts +4 -0
- package/src/resources/extensions/gsd/auto/session.ts +7 -1
- package/src/resources/extensions/gsd/auto-model-selection.ts +50 -12
- package/src/resources/extensions/gsd/auto-start.ts +40 -22
- package/src/resources/extensions/gsd/auto.ts +15 -12
- package/src/resources/extensions/gsd/blocked-models.ts +98 -0
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +97 -0
- 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 +36 -0
- package/src/resources/extensions/gsd/compaction-snapshot.ts +165 -0
- package/src/resources/extensions/gsd/complexity-classifier.ts +5 -3
- package/src/resources/extensions/gsd/error-classifier.ts +36 -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 +68 -4
- package/src/resources/extensions/gsd/init-wizard.ts +15 -1
- package/src/resources/extensions/gsd/key-manager.ts +6 -0
- package/src/resources/extensions/gsd/pre-execution-checks.ts +13 -3
- package/src/resources/extensions/gsd/preferences-types.ts +38 -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/prompt-loader.ts +30 -7
- package/src/resources/extensions/gsd/safety/file-change-validator.ts +1 -1
- package/src/resources/extensions/gsd/tests/auto-model-selection.test.ts +12 -0
- package/src/resources/extensions/gsd/tests/auto-start-model-capture.test.ts +33 -3
- package/src/resources/extensions/gsd/tests/auto-thinking-restore.test.ts +38 -0
- package/src/resources/extensions/gsd/tests/blocked-models.test.ts +98 -0
- package/src/resources/extensions/gsd/tests/compaction-snapshot.test.ts +123 -0
- package/src/resources/extensions/gsd/tests/complexity-classifier.test.ts +3 -3
- 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 +20 -0
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +151 -0
- package/src/resources/extensions/gsd/tests/init-wizard.test.ts +27 -0
- package/src/resources/extensions/gsd/tests/isolation-none-branch-guard.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/key-manager.test.ts +7 -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 +19 -0
- package/src/resources/extensions/gsd/tests/preferences.test.ts +110 -0
- package/src/resources/extensions/gsd/tests/prompt-loader-extension-dir.test.ts +49 -0
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +91 -0
- package/src/resources/extensions/gsd/tests/save-gate-result-render.test.ts +95 -0
- package/src/resources/extensions/gsd/tests/zombie-gsd-state.test.ts +3 -1
- 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/extensions/search-the-web/command-search-provider.ts +5 -4
- package/src/resources/extensions/search-the-web/native-search.ts +48 -12
- /package/dist/web/standalone/.next/static/{pBwmOoye64ZrRp-_rf0v1 → n21VtX2hZlkpdEUO_nU4z}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{pBwmOoye64ZrRp-_rf0v1 → n21VtX2hZlkpdEUO_nU4z}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// GSD — Persistent per-project blocklist of provider/model pairs that the
|
|
2
|
+
// provider has rejected at request time for account entitlement reasons.
|
|
3
|
+
//
|
|
4
|
+
// Lives at `.gsd/runtime/blocked-models.json` so the block survives /gsd auto
|
|
5
|
+
// restarts. Auto-mode model selection skips blocked entries; agent-end
|
|
6
|
+
// recovery adds entries when a runtime rejection is classified as
|
|
7
|
+
// `unsupported-model`. See issue #4513.
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
10
|
+
import { gsdRoot } from "./paths.js";
|
|
11
|
+
import { withFileLockSync } from "./file-lock.js";
|
|
12
|
+
function blockedModelsPath(basePath) {
|
|
13
|
+
return join(gsdRoot(basePath), "runtime", "blocked-models.json");
|
|
14
|
+
}
|
|
15
|
+
function modelKey(provider, id) {
|
|
16
|
+
return `${provider.toLowerCase()}/${id.toLowerCase()}`;
|
|
17
|
+
}
|
|
18
|
+
function readFileSafe(path) {
|
|
19
|
+
if (!existsSync(path))
|
|
20
|
+
return { version: 1, blocked: [] };
|
|
21
|
+
try {
|
|
22
|
+
const raw = readFileSync(path, "utf-8");
|
|
23
|
+
const parsed = JSON.parse(raw);
|
|
24
|
+
if (!parsed || !Array.isArray(parsed.blocked)) {
|
|
25
|
+
return { version: 1, blocked: [] };
|
|
26
|
+
}
|
|
27
|
+
const blocked = parsed.blocked.filter((e) => !!e && typeof e.provider === "string" && typeof e.id === "string");
|
|
28
|
+
return { version: 1, blocked };
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// Corrupted JSON: treat as empty so a bad file never blocks dispatch.
|
|
32
|
+
return { version: 1, blocked: [] };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export function loadBlockedModels(basePath) {
|
|
36
|
+
return readFileSafe(blockedModelsPath(basePath)).blocked;
|
|
37
|
+
}
|
|
38
|
+
export function isModelBlocked(basePath, provider, id) {
|
|
39
|
+
if (!provider || !id)
|
|
40
|
+
return false;
|
|
41
|
+
const target = modelKey(provider, id);
|
|
42
|
+
return loadBlockedModels(basePath).some((e) => modelKey(e.provider, e.id) === target);
|
|
43
|
+
}
|
|
44
|
+
export function blockModel(basePath, provider, id, reason) {
|
|
45
|
+
const path = blockedModelsPath(basePath);
|
|
46
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
47
|
+
// Ensure the file exists before we try to lock it — proper-lockfile requires
|
|
48
|
+
// the target path to exist (file-lock.ts falls through to an unlocked call
|
|
49
|
+
// otherwise).
|
|
50
|
+
if (!existsSync(path)) {
|
|
51
|
+
writeFileSync(path, JSON.stringify({ version: 1, blocked: [] }, null, 2) + "\n", "utf-8");
|
|
52
|
+
}
|
|
53
|
+
withFileLockSync(path, () => {
|
|
54
|
+
const current = readFileSafe(path);
|
|
55
|
+
const target = modelKey(provider, id);
|
|
56
|
+
if (current.blocked.some((e) => modelKey(e.provider, e.id) === target)) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const next = {
|
|
60
|
+
version: 1,
|
|
61
|
+
blocked: [
|
|
62
|
+
...current.blocked,
|
|
63
|
+
{ provider, id, reason, blockedAt: Date.now() },
|
|
64
|
+
],
|
|
65
|
+
};
|
|
66
|
+
writeFileSync(path, JSON.stringify(next, null, 2) + "\n", "utf-8");
|
|
67
|
+
});
|
|
68
|
+
}
|
|
@@ -8,6 +8,7 @@ import { resolveModelId } from "../auto-model-selection.js";
|
|
|
8
8
|
import { clearDiscussionFlowState } from "./write-gate.js";
|
|
9
9
|
import { resumeAutoAfterProviderDelay } from "./provider-error-resume.js";
|
|
10
10
|
import { classifyError, createRetryState, resetRetryState, isTransient, } from "../error-classifier.js";
|
|
11
|
+
import { blockModel, isModelBlocked } from "../blocked-models.js";
|
|
11
12
|
const retryState = createRetryState();
|
|
12
13
|
const MAX_NETWORK_RETRIES = 2;
|
|
13
14
|
const MAX_TRANSIENT_AUTO_RESUMES = 8;
|
|
@@ -106,6 +107,81 @@ export async function handleAgentEnd(pi, event, ctx) {
|
|
|
106
107
|
const explicitRetryAfterMs = ("retryAfterMs" in lastMsg && typeof lastMsg.retryAfterMs === "number") ? lastMsg.retryAfterMs : undefined;
|
|
107
108
|
// ── 1. Classify using rawErrorMsg to avoid prose false-positives ────
|
|
108
109
|
const cls = classifyError(rawErrorMsg, explicitRetryAfterMs);
|
|
110
|
+
// ── 1a. Unsupported-model: provider rejected this model for the current
|
|
111
|
+
// account/plan at request time (#4513). Persist a block so the
|
|
112
|
+
// same dead model isn't reselected on the next /gsd auto restart,
|
|
113
|
+
// then try a fallback before pausing.
|
|
114
|
+
if (cls.kind === "unsupported-model") {
|
|
115
|
+
const dash = getAutoDashboardData();
|
|
116
|
+
const rejectedProvider = ctx.model?.provider;
|
|
117
|
+
const rejectedId = ctx.model?.id;
|
|
118
|
+
if (dash.basePath && rejectedProvider && rejectedId) {
|
|
119
|
+
try {
|
|
120
|
+
blockModel(dash.basePath, rejectedProvider, rejectedId, rawErrorMsg || "unsupported for account");
|
|
121
|
+
ctx.ui.notify(`Blocked ${rejectedProvider}/${rejectedId} for this project — provider rejected it for the current account.`, "warning");
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
const m = err instanceof Error ? err.message : String(err);
|
|
125
|
+
logWarning("bootstrap", `Failed to persist blocked model: ${m}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// Try configured fallback chain, skipping anything already blocked.
|
|
129
|
+
if (dash.currentUnit && dash.basePath) {
|
|
130
|
+
const modelConfig = resolveModelWithFallbacksForUnit(dash.currentUnit.type);
|
|
131
|
+
if (modelConfig && modelConfig.fallbacks.length > 0) {
|
|
132
|
+
const availableModels = ctx.modelRegistry.getAvailable();
|
|
133
|
+
let cursorModelId = ctx.model?.id;
|
|
134
|
+
while (true) {
|
|
135
|
+
const nextModelId = getNextFallbackModel(cursorModelId, modelConfig);
|
|
136
|
+
if (!nextModelId)
|
|
137
|
+
break;
|
|
138
|
+
const candidate = resolveModelId(nextModelId, availableModels, ctx.model?.provider);
|
|
139
|
+
if (candidate && !isModelBlocked(dash.basePath, candidate.provider, candidate.id)) {
|
|
140
|
+
const ok = await pi.setModel(candidate, { persist: false });
|
|
141
|
+
if (ok) {
|
|
142
|
+
setCurrentDispatchedModelId({ provider: candidate.provider, id: candidate.id });
|
|
143
|
+
ctx.ui.notify(`Switched to fallback ${candidate.provider}/${candidate.id} after account entitlement rejection.`, "warning");
|
|
144
|
+
pi.sendMessage({ customType: "gsd-auto-timeout-recovery", content: "Continue execution.", display: false }, { triggerTurn: true });
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
cursorModelId = nextModelId;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// Fallback chain exhausted — try the auto-mode start model if it isn't
|
|
152
|
+
// the same one we just blocked and isn't itself blocked.
|
|
153
|
+
const sessionModel = getAutoModeStartModel();
|
|
154
|
+
if (sessionModel &&
|
|
155
|
+
!(sessionModel.provider === rejectedProvider && sessionModel.id === rejectedId) &&
|
|
156
|
+
!isModelBlocked(dash.basePath, sessionModel.provider, sessionModel.id)) {
|
|
157
|
+
const startModel = ctx.modelRegistry
|
|
158
|
+
.getAvailable()
|
|
159
|
+
.find((m) => m.provider === sessionModel.provider && m.id === sessionModel.id);
|
|
160
|
+
if (startModel) {
|
|
161
|
+
const ok = await pi.setModel(startModel, { persist: false });
|
|
162
|
+
if (ok) {
|
|
163
|
+
setCurrentDispatchedModelId({ provider: startModel.provider, id: startModel.id });
|
|
164
|
+
ctx.ui.notify(`Restored auto-mode start model ${startModel.provider}/${startModel.id} after entitlement rejection.`, "warning");
|
|
165
|
+
pi.sendMessage({ customType: "gsd-auto-timeout-recovery", content: "Continue execution.", display: false }, { triggerTurn: true });
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// No usable fallback — pause with a clearly named message.
|
|
172
|
+
const blockedLabel = rejectedProvider && rejectedId ? `${rejectedProvider}/${rejectedId}` : "current model";
|
|
173
|
+
const pauseDetail = `Model ${blockedLabel} blocked for this account${errorDetail}. Configure a different model and restart /gsd auto.`;
|
|
174
|
+
await pauseAutoForProviderError(ctx.ui, pauseDetail, () => pauseAuto(ctx, pi, {
|
|
175
|
+
message: pauseDetail,
|
|
176
|
+
category: "provider",
|
|
177
|
+
isTransient: false,
|
|
178
|
+
}), {
|
|
179
|
+
isRateLimit: false,
|
|
180
|
+
isTransient: false,
|
|
181
|
+
retryAfterMs: 0,
|
|
182
|
+
});
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
109
185
|
// ── 1b. Defer to Core RetryHandler for most transient errors ────────
|
|
110
186
|
// Core retries transient failures in-session after this handler.
|
|
111
187
|
// Keep that behavior for non-rate-limit classes to avoid pause/retry races,
|
|
@@ -19,6 +19,18 @@ function registerAlias(pi, toolDef, aliasName, canonicalName) {
|
|
|
19
19
|
promptGuidelines: [`Alias for ${canonicalName} — prefer the canonical name.`],
|
|
20
20
|
});
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Read a tool result's structured payload, accommodating MCP's `details` →
|
|
24
|
+
* `structuredContent` rename (#4472, #4477). In-process executions still
|
|
25
|
+
* deliver the payload on `result.details`; MCP-routed executions deliver it
|
|
26
|
+
* on `result.structuredContent` (post `adaptExecutorResult` transform). All
|
|
27
|
+
* `renderResult` callbacks in this file route through this helper so a future
|
|
28
|
+
* field rename only needs to be applied in one place.
|
|
29
|
+
*/
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- result shape varies by tool
|
|
31
|
+
function readDetails(result) {
|
|
32
|
+
return result?.details ?? result?.structuredContent;
|
|
33
|
+
}
|
|
22
34
|
export function registerDbTools(pi) {
|
|
23
35
|
// ─── gsd_decision_save (formerly gsd_save_decision) ─────────────────────
|
|
24
36
|
const decisionSaveExecute = async (_toolCallId, params, _signal, _onUpdate, _ctx) => {
|
|
@@ -92,7 +104,7 @@ export function registerDbTools(pi) {
|
|
|
92
104
|
return new Text(text, 0, 0);
|
|
93
105
|
},
|
|
94
106
|
renderResult(result, _options, theme) {
|
|
95
|
-
const d = result
|
|
107
|
+
const d = readDetails(result);
|
|
96
108
|
if (result.isError || d?.error) {
|
|
97
109
|
return new Text(theme.fg("error", `Error: ${d?.error ?? "unknown"}`), 0, 0);
|
|
98
110
|
}
|
|
@@ -175,7 +187,7 @@ export function registerDbTools(pi) {
|
|
|
175
187
|
return new Text(text, 0, 0);
|
|
176
188
|
},
|
|
177
189
|
renderResult(result, _options, theme) {
|
|
178
|
-
const d = result
|
|
190
|
+
const d = readDetails(result);
|
|
179
191
|
if (result.isError || d?.error) {
|
|
180
192
|
return new Text(theme.fg("error", `Error: ${d?.error ?? "unknown"}`), 0, 0);
|
|
181
193
|
}
|
|
@@ -255,7 +267,7 @@ export function registerDbTools(pi) {
|
|
|
255
267
|
return new Text(text, 0, 0);
|
|
256
268
|
},
|
|
257
269
|
renderResult(result, _options, theme) {
|
|
258
|
-
const d = result
|
|
270
|
+
const d = readDetails(result);
|
|
259
271
|
if (result.isError || d?.error) {
|
|
260
272
|
return new Text(theme.fg("error", `Error: ${d?.error ?? "unknown"}`), 0, 0);
|
|
261
273
|
}
|
|
@@ -301,7 +313,7 @@ export function registerDbTools(pi) {
|
|
|
301
313
|
return new Text(text, 0, 0);
|
|
302
314
|
},
|
|
303
315
|
renderResult(result, _options, theme) {
|
|
304
|
-
const d = result
|
|
316
|
+
const d = readDetails(result);
|
|
305
317
|
if (result.isError || d?.error) {
|
|
306
318
|
return new Text(theme.fg("error", `Error: ${d?.error ?? "unknown"}`), 0, 0);
|
|
307
319
|
}
|
|
@@ -382,7 +394,7 @@ export function registerDbTools(pi) {
|
|
|
382
394
|
return new Text(theme.fg("toolTitle", theme.bold("milestone_generate_id")), 0, 0);
|
|
383
395
|
},
|
|
384
396
|
renderResult(result, _options, theme) {
|
|
385
|
-
const d = result
|
|
397
|
+
const d = readDetails(result);
|
|
386
398
|
if (result.isError || d?.error) {
|
|
387
399
|
return new Text(theme.fg("error", `Error: ${d?.error ?? "unknown"}`), 0, 0);
|
|
388
400
|
}
|
|
@@ -967,13 +979,31 @@ export function registerDbTools(pi) {
|
|
|
967
979
|
text += theme.fg("dim", ` → ${args.verdict ?? ""}`);
|
|
968
980
|
return new Text(text, 0, 0);
|
|
969
981
|
},
|
|
982
|
+
/**
|
|
983
|
+
* Render the save_gate_result tool output for the TUI.
|
|
984
|
+
*
|
|
985
|
+
* Prefers structured fields, but falls back to `content[0].text` when the
|
|
986
|
+
* structured payload is empty. Defensive: the structural fix on this
|
|
987
|
+
* branch plumbs `details` through MCP via `structuredContent`, but older
|
|
988
|
+
* hosts, a future handler that forgets `structuredContent`, or any drop
|
|
989
|
+
* of non-standard return fields would otherwise render as
|
|
990
|
+
* "undefined: undefined". Same fallback applies to error rendering, and
|
|
991
|
+
* we strip a leading `Error:` from the fallback text to avoid producing
|
|
992
|
+
* `Error: Error: ...`.
|
|
993
|
+
*/
|
|
970
994
|
renderResult(result, _options, theme) {
|
|
971
|
-
const d = result
|
|
995
|
+
const d = readDetails(result);
|
|
972
996
|
if (result.isError || d?.error) {
|
|
973
|
-
|
|
997
|
+
const rawMsg = d?.error ?? result.content?.[0]?.text ?? "unknown";
|
|
998
|
+
const msg = rawMsg.replace(/^\s*Error:\s*/i, "");
|
|
999
|
+
return new Text(theme.fg("error", `Error: ${msg}`), 0, 0);
|
|
1000
|
+
}
|
|
1001
|
+
if (!d?.gateId || !d?.verdict) {
|
|
1002
|
+
const text = result.content?.[0]?.text ?? "Gate result saved";
|
|
1003
|
+
return new Text(theme.fg("success", text), 0, 0);
|
|
974
1004
|
}
|
|
975
|
-
const color = d
|
|
976
|
-
return new Text(theme.fg(color, `${d
|
|
1005
|
+
const color = d.verdict === "flag" ? "warning" : "success";
|
|
1006
|
+
return new Text(theme.fg(color, `${d.gateId}: ${d.verdict}`), 0, 0);
|
|
977
1007
|
},
|
|
978
1008
|
};
|
|
979
1009
|
pi.registerTool(saveGateResultTool);
|
|
@@ -0,0 +1,93 @@
|
|
|
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
|
+
import { Type } from "@sinclair/typebox";
|
|
6
|
+
import { executeGsdExec } from "../tools/exec-tool.js";
|
|
7
|
+
import { executeExecSearch } from "../tools/exec-search-tool.js";
|
|
8
|
+
import { executeResume } from "../tools/resume-tool.js";
|
|
9
|
+
import { loadEffectiveGSDPreferences } from "../preferences.js";
|
|
10
|
+
import { logWarning } from "../workflow-logger.js";
|
|
11
|
+
export function registerExecTools(pi) {
|
|
12
|
+
pi.registerTool({
|
|
13
|
+
name: "gsd_exec",
|
|
14
|
+
label: "Exec (Sandboxed)",
|
|
15
|
+
description: "Run a short script (bash/node/python) in a subprocess. Full stdout/stderr persist to " +
|
|
16
|
+
".gsd/exec/<id>.{stdout,stderr,meta.json}; only a short digest returns in context. Use " +
|
|
17
|
+
"this instead of reading many files or emitting large tool outputs — e.g. have the script " +
|
|
18
|
+
"count/grep/summarize and log the finding. Enabled by default; opt out via " +
|
|
19
|
+
"preferences.context_mode.enabled=false.",
|
|
20
|
+
promptSnippet: "Run a bash/node/python script in a sandbox; full output is saved to disk and only a digest returns",
|
|
21
|
+
promptGuidelines: [
|
|
22
|
+
"Prefer gsd_exec for analyses that would otherwise read >3 files or produce large tool output.",
|
|
23
|
+
"Write scripts that log the finding (counts, matches, summaries) rather than raw dumps.",
|
|
24
|
+
"The digest is the last ~300 chars of stdout — size your log output accordingly.",
|
|
25
|
+
"Need the full output? Read the stdout_path returned in details (file on local disk).",
|
|
26
|
+
],
|
|
27
|
+
parameters: Type.Object({
|
|
28
|
+
runtime: Type.Union([Type.Literal("bash"), Type.Literal("node"), Type.Literal("python")], { description: "Interpreter: bash (-c), node (-e), or python3 (-c)." }),
|
|
29
|
+
script: Type.String({ description: "Script body. Keep output small (log the finding, not the data)." }),
|
|
30
|
+
purpose: Type.Optional(Type.String({ description: "Short label recorded in meta.json for later review." })),
|
|
31
|
+
timeout_ms: Type.Optional(Type.Number({
|
|
32
|
+
description: "Per-invocation timeout (ms). Capped at 600000. Default from preferences.",
|
|
33
|
+
minimum: 1_000,
|
|
34
|
+
maximum: 600_000,
|
|
35
|
+
})),
|
|
36
|
+
}),
|
|
37
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
38
|
+
let prefs = null;
|
|
39
|
+
try {
|
|
40
|
+
prefs = loadEffectiveGSDPreferences();
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
logWarning("tool", `gsd_exec could not load preferences: ${err instanceof Error ? err.message : String(err)}`);
|
|
44
|
+
}
|
|
45
|
+
return executeGsdExec(params, {
|
|
46
|
+
baseDir: process.cwd(),
|
|
47
|
+
preferences: prefs?.preferences ?? null,
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
pi.registerTool({
|
|
52
|
+
name: "gsd_exec_search",
|
|
53
|
+
label: "Search gsd_exec History",
|
|
54
|
+
description: "List prior gsd_exec runs (most recent first) from .gsd/exec/*.meta.json. Useful for " +
|
|
55
|
+
"rediscovering the stdout_path of an earlier run without re-executing it. Read-only.",
|
|
56
|
+
promptSnippet: "Search prior gsd_exec runs by substring, runtime, or failing-only filter",
|
|
57
|
+
promptGuidelines: [
|
|
58
|
+
"Use this before re-running an expensive analysis — the prior run's stdout file may still answer.",
|
|
59
|
+
"The preview shows the trailing ~300 chars of stdout; read stdout_path for the full transcript.",
|
|
60
|
+
],
|
|
61
|
+
parameters: Type.Object({
|
|
62
|
+
query: Type.Optional(Type.String({ description: "Substring matched against id and purpose (case-insensitive)." })),
|
|
63
|
+
runtime: Type.Optional(Type.Union([Type.Literal("bash"), Type.Literal("node"), Type.Literal("python")], {
|
|
64
|
+
description: "Restrict to one runtime.",
|
|
65
|
+
})),
|
|
66
|
+
failing_only: Type.Optional(Type.Boolean({ description: "Only non-zero exit codes and timeouts." })),
|
|
67
|
+
limit: Type.Optional(Type.Number({ description: "Max results (default 20, cap 200)", minimum: 1, maximum: 200 })),
|
|
68
|
+
}),
|
|
69
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
70
|
+
return executeExecSearch(params, {
|
|
71
|
+
baseDir: process.cwd(),
|
|
72
|
+
});
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
pi.registerTool({
|
|
76
|
+
name: "gsd_resume",
|
|
77
|
+
label: "Resume (Read Snapshot)",
|
|
78
|
+
description: "Return the contents of .gsd/last-snapshot.md — a ≤2 KB digest of top memories, recent " +
|
|
79
|
+
"gsd_exec runs, and active context, written automatically on session_before_compact. Use " +
|
|
80
|
+
"this after compaction or session resume to re-orient quickly.",
|
|
81
|
+
promptSnippet: "Read the pre-compaction snapshot to re-orient after context loss",
|
|
82
|
+
promptGuidelines: [
|
|
83
|
+
"Call this right after a session resumes if you feel you've lost durable context.",
|
|
84
|
+
"The snapshot is a summary — use memory_query or gsd_exec_search for detail.",
|
|
85
|
+
],
|
|
86
|
+
parameters: Type.Object({}),
|
|
87
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
88
|
+
return executeResume(params, {
|
|
89
|
+
baseDir: process.cwd(),
|
|
90
|
+
});
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
}
|
|
@@ -4,6 +4,7 @@ import { registerWorktreeCommand } from "../worktree-command.js";
|
|
|
4
4
|
import { loadEcosystemExtensions } from "../ecosystem/loader.js";
|
|
5
5
|
import { registerDbTools } from "./db-tools.js";
|
|
6
6
|
import { registerDynamicTools } from "./dynamic-tools.js";
|
|
7
|
+
import { registerExecTools } from "./exec-tools.js";
|
|
7
8
|
import { registerJournalTools } from "./journal-tools.js";
|
|
8
9
|
import { registerMemoryTools } from "./memory-tools.js";
|
|
9
10
|
import { registerQueryTools } from "./query-tools.js";
|
|
@@ -86,6 +87,7 @@ export function registerGsdExtension(pi) {
|
|
|
86
87
|
["journal-tools", () => registerJournalTools(pi)],
|
|
87
88
|
["query-tools", () => registerQueryTools(pi)],
|
|
88
89
|
["memory-tools", () => registerMemoryTools(pi)],
|
|
90
|
+
["exec-tools", () => registerExecTools(pi)],
|
|
89
91
|
["shortcuts", () => registerShortcuts(pi)],
|
|
90
92
|
["hooks", () => registerHooks(pi, ecosystemHandlers)],
|
|
91
93
|
["ecosystem", () => {
|
|
@@ -204,6 +204,41 @@ export function registerHooks(pi, ecosystemHandlers) {
|
|
|
204
204
|
nextAction: `Resume task ${state.activeTask.id}: ${state.activeTask.title}.`,
|
|
205
205
|
}));
|
|
206
206
|
});
|
|
207
|
+
// Context-mode snapshot: write .gsd/last-snapshot.md before compaction so
|
|
208
|
+
// agents can call gsd_resume (or Read the file) to re-orient. Opt-in via
|
|
209
|
+
// preferences.context_mode.enabled. Runs after the auto-cancel handler
|
|
210
|
+
// above — if that one returned cancel:true, pi still fires us but the
|
|
211
|
+
// compaction won't actually happen; the snapshot is still useful then,
|
|
212
|
+
// since auto may pause and resume later.
|
|
213
|
+
pi.on("session_before_compact", async () => {
|
|
214
|
+
try {
|
|
215
|
+
const { loadEffectiveGSDPreferences } = await import("../preferences.js");
|
|
216
|
+
const { isContextModeEnabled } = await import("../preferences-types.js");
|
|
217
|
+
const prefs = loadEffectiveGSDPreferences();
|
|
218
|
+
if (!isContextModeEnabled(prefs?.preferences))
|
|
219
|
+
return;
|
|
220
|
+
const { writeCompactionSnapshot } = await import("../compaction-snapshot.js");
|
|
221
|
+
const { ensureDbOpen } = await import("./dynamic-tools.js");
|
|
222
|
+
await ensureDbOpen();
|
|
223
|
+
const basePath = process.cwd();
|
|
224
|
+
let activeContext = null;
|
|
225
|
+
try {
|
|
226
|
+
const state = await deriveState(basePath);
|
|
227
|
+
if (state.activeMilestone && state.activeSlice && state.activeTask) {
|
|
228
|
+
activeContext =
|
|
229
|
+
`Active: ${state.activeMilestone.id} / ${state.activeSlice.id} / ${state.activeTask.id}` +
|
|
230
|
+
(state.activeTask.title ? ` — ${state.activeTask.title}` : "");
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
/* non-fatal */
|
|
235
|
+
}
|
|
236
|
+
writeCompactionSnapshot(basePath, { activeContext });
|
|
237
|
+
}
|
|
238
|
+
catch (err) {
|
|
239
|
+
safetyLogWarning("context-mode", `failed to write compaction snapshot: ${err instanceof Error ? err.message : String(err)}`);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
207
242
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
208
243
|
if (isParallelActive()) {
|
|
209
244
|
try {
|
|
@@ -0,0 +1,121 @@
|
|
|
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
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { resolve } from "node:path";
|
|
9
|
+
import { getActiveMemoriesRanked } from "./memory-store.js";
|
|
10
|
+
import { listExecHistory } from "./exec-history.js";
|
|
11
|
+
export const DEFAULT_SNAPSHOT_BYTES = 2048;
|
|
12
|
+
export const SNAPSHOT_FILENAME = "last-snapshot.md";
|
|
13
|
+
/**
|
|
14
|
+
* Build a priority-tiered markdown snapshot. Pure — no I/O. Tiers:
|
|
15
|
+
* 1. Active context (if any)
|
|
16
|
+
* 2. Top memories by rank
|
|
17
|
+
* 3. Recent exec runs (failures highlighted)
|
|
18
|
+
* The result is guaranteed to be <= opts.maxBytes (truncated with an
|
|
19
|
+
* ellipsis marker if necessary).
|
|
20
|
+
*/
|
|
21
|
+
export function buildSnapshot(sources, opts = {}) {
|
|
22
|
+
const maxBytes = opts.maxBytes ?? DEFAULT_SNAPSHOT_BYTES;
|
|
23
|
+
const maxMemories = opts.maxMemories ?? 6;
|
|
24
|
+
const maxExec = opts.maxExec ?? 5;
|
|
25
|
+
const lines = [];
|
|
26
|
+
lines.push(`# GSD context snapshot (${sources.generatedAt.toISOString()})`);
|
|
27
|
+
lines.push("");
|
|
28
|
+
if (sources.activeContext && sources.activeContext.trim().length > 0) {
|
|
29
|
+
lines.push("## Active context");
|
|
30
|
+
lines.push(sources.activeContext.trim());
|
|
31
|
+
lines.push("");
|
|
32
|
+
}
|
|
33
|
+
const memories = sources.memories.slice(0, maxMemories);
|
|
34
|
+
if (memories.length > 0) {
|
|
35
|
+
lines.push("## Top project memories");
|
|
36
|
+
for (const memory of memories) {
|
|
37
|
+
lines.push(`- [${memory.id}] (${memory.category}) ${memory.content.trim()}`);
|
|
38
|
+
}
|
|
39
|
+
lines.push("");
|
|
40
|
+
}
|
|
41
|
+
const exec = sources.execHistory.slice(0, maxExec);
|
|
42
|
+
if (exec.length > 0) {
|
|
43
|
+
lines.push("## Recent gsd_exec runs");
|
|
44
|
+
for (const entry of exec) {
|
|
45
|
+
const status = entry.timed_out
|
|
46
|
+
? "timeout"
|
|
47
|
+
: entry.exit_code === null
|
|
48
|
+
? "exit:null"
|
|
49
|
+
: `exit:${entry.exit_code}`;
|
|
50
|
+
const purpose = entry.purpose ? ` — ${entry.purpose}` : "";
|
|
51
|
+
lines.push(`- [${entry.id}] ${entry.runtime} ${status}${purpose}`);
|
|
52
|
+
}
|
|
53
|
+
lines.push("");
|
|
54
|
+
}
|
|
55
|
+
if (memories.length === 0 && exec.length === 0 && !sources.activeContext) {
|
|
56
|
+
lines.push("_No durable memories, active context, or exec history to surface._");
|
|
57
|
+
}
|
|
58
|
+
return enforceByteCap(lines.join("\n").trimEnd(), maxBytes);
|
|
59
|
+
}
|
|
60
|
+
function enforceByteCap(input, maxBytes) {
|
|
61
|
+
if (Buffer.byteLength(input, "utf-8") <= maxBytes)
|
|
62
|
+
return input;
|
|
63
|
+
const marker = "\n…[truncated]";
|
|
64
|
+
const markerBytes = Buffer.byteLength(marker, "utf-8");
|
|
65
|
+
const budget = Math.max(0, maxBytes - markerBytes);
|
|
66
|
+
// Walk backwards until the trimmed string fits. utf-8 is variable-width;
|
|
67
|
+
// naive char slicing is safe for ASCII but may split a multi-byte char.
|
|
68
|
+
// Guard by decoding the trimmed Buffer and relying on the replacement-char
|
|
69
|
+
// fallback in TextDecoder (implicit via toString).
|
|
70
|
+
const buf = Buffer.from(input, "utf-8").subarray(0, budget);
|
|
71
|
+
return `${buf.toString("utf-8")}${marker}`;
|
|
72
|
+
}
|
|
73
|
+
export function writeCompactionSnapshot(baseDir, opts = {}) {
|
|
74
|
+
const memories = safeGetMemories();
|
|
75
|
+
const execHistory = safeListExec(baseDir);
|
|
76
|
+
const content = buildSnapshot({
|
|
77
|
+
memories,
|
|
78
|
+
execHistory,
|
|
79
|
+
generatedAt: (opts.now ?? (() => new Date()))(),
|
|
80
|
+
activeContext: opts.activeContext ?? null,
|
|
81
|
+
}, opts);
|
|
82
|
+
const gsdDir = resolve(baseDir, ".gsd");
|
|
83
|
+
if (!existsSync(gsdDir))
|
|
84
|
+
mkdirSync(gsdDir, { recursive: true });
|
|
85
|
+
const path = resolve(gsdDir, SNAPSHOT_FILENAME);
|
|
86
|
+
const finalContent = `${content}\n`;
|
|
87
|
+
writeFileSync(path, finalContent, "utf-8");
|
|
88
|
+
return {
|
|
89
|
+
path,
|
|
90
|
+
bytes: Buffer.byteLength(finalContent, "utf-8"),
|
|
91
|
+
memories: memories.length,
|
|
92
|
+
execRuns: execHistory.length,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
export function readCompactionSnapshot(baseDir) {
|
|
96
|
+
const path = resolve(baseDir, ".gsd", SNAPSHOT_FILENAME);
|
|
97
|
+
if (!existsSync(path))
|
|
98
|
+
return null;
|
|
99
|
+
try {
|
|
100
|
+
return readFileSync(path, "utf-8");
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function safeGetMemories() {
|
|
107
|
+
try {
|
|
108
|
+
return getActiveMemoriesRanked(12);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function safeListExec(baseDir) {
|
|
115
|
+
try {
|
|
116
|
+
return listExecHistory(baseDir);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -8,10 +8,12 @@ import { getAdaptiveTierAdjustment } from "./routing-history.js";
|
|
|
8
8
|
import { parseUnitId } from "./unit-id.js";
|
|
9
9
|
// ─── Unit Type → Default Tier Mapping ────────────────────────────────────────
|
|
10
10
|
const UNIT_TYPE_TIERS = {
|
|
11
|
-
// Tier 1 — Light:
|
|
12
|
-
"complete-slice": "light",
|
|
11
|
+
// Tier 1 — Light: compact verification turns
|
|
13
12
|
"run-uat": "light",
|
|
14
|
-
// Tier 2 — Standard: research, routine discussion
|
|
13
|
+
// Tier 2 — Standard: research, routine discussion, slice completion
|
|
14
|
+
// complete-slice can carry large inlined context; avoid routing it to the
|
|
15
|
+
// cheapest "light" model by default (#4520).
|
|
16
|
+
"complete-slice": "standard",
|
|
15
17
|
"discuss-milestone": "standard",
|
|
16
18
|
"discuss-slice": "standard",
|
|
17
19
|
"research-milestone": "standard",
|
|
@@ -22,18 +22,35 @@ const PERMANENT_RE = /auth|unauthorized|forbidden|invalid.*key|invalid.*api|bill
|
|
|
22
22
|
// Include provider-specific quota-window phrasing like:
|
|
23
23
|
// - "You've hit your limit"
|
|
24
24
|
// - "usage limit" / "quota reached"
|
|
25
|
-
|
|
25
|
+
// - "out of extra usage"
|
|
26
|
+
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;
|
|
26
27
|
// OpenRouter affordability-style quota errors should be treated as transient
|
|
27
28
|
// so core retry logic can lower maxTokens and continue in-session.
|
|
28
29
|
const AFFORDABILITY_RE = /requires more credits|can only afford|insufficient credits|not enough credits|fewer max_tokens/i;
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
// "Stream idle timeout" and "partial response received" are emitted by the SDK/harness
|
|
31
|
+
// for mid-stream disconnects. Both indicate a transient network-level interruption.
|
|
32
|
+
// See: https://github.com/gsd-build/gsd-2/issues/4558
|
|
33
|
+
const NETWORK_RE = /network|ECONNRESET|ETIMEDOUT|ECONNREFUSED|socket hang up|fetch failed|connection.*reset|dns|unexpected eof|stream idle timeout|partial response received/i;
|
|
34
|
+
// Context overflow errors (context window/length exceeded) should be treated as server-class
|
|
35
|
+
// transient errors so auto-mode can retry with reduced budget or fall back to a larger-context model.
|
|
36
|
+
// See: https://github.com/gsd-build/gsd-2/issues/4528
|
|
37
|
+
const SERVER_RE = /internal server error|500|502|503|overloaded|server_error|api_error|service.?unavailable|context (?:window|length) exceed|context window exceed/i;
|
|
31
38
|
// ECONNRESET/ECONNREFUSED are in NETWORK_RE (same-model retry first).
|
|
32
39
|
const CONNECTION_RE = /terminated|connection.?(?:refused|error)|other side closed|EPIPE|network.?(?:is\s+)?unavailable|stream_exhausted(?:_without_result)?/i;
|
|
33
40
|
// Catch-all for V8 JSON.parse errors: all modern variants end with "in JSON at position \d+".
|
|
34
41
|
// This eliminates the need to enumerate every error message variant individually.
|
|
35
42
|
const STREAM_RE = /in JSON at position \d+|Unexpected end of JSON|SyntaxError.*JSON/i;
|
|
36
43
|
const RESET_DELAY_RE = /reset in (\d+)s/i;
|
|
44
|
+
// Provider-side model entitlement rejection: the SDK accepted the model switch,
|
|
45
|
+
// but the provider refused at request time because the current account/plan/tier
|
|
46
|
+
// cannot use that model. Must match all three of: a model/deployment token,
|
|
47
|
+
// a negative-entitlement indicator, and an account/plan/tier/subscription token.
|
|
48
|
+
// Requiring all three keeps generic "account suspended" errors in `permanent`
|
|
49
|
+
// (no model token) while catching the phrasings providers actually use.
|
|
50
|
+
// See issue #4513.
|
|
51
|
+
const UNSUPPORTED_MODEL_MODEL_RE = /\b(?:model|deployment)\b/i;
|
|
52
|
+
const UNSUPPORTED_MODEL_INDICATOR_RE = /\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;
|
|
53
|
+
const UNSUPPORTED_MODEL_SCOPE_RE = /\b(?:account|plan|tier|subscription)\b/i;
|
|
37
54
|
/**
|
|
38
55
|
* Classify an error message into one of the ErrorClass kinds.
|
|
39
56
|
*
|
|
@@ -49,6 +66,17 @@ const RESET_DELAY_RE = /reset in (\d+)s/i;
|
|
|
49
66
|
export function classifyError(errorMsg, retryAfterMs) {
|
|
50
67
|
const isPermanent = PERMANENT_RE.test(errorMsg);
|
|
51
68
|
const isRateLimit = RATE_LIMIT_RE.test(errorMsg) || AFFORDABILITY_RE.test(errorMsg);
|
|
69
|
+
const isUnsupportedModel = UNSUPPORTED_MODEL_MODEL_RE.test(errorMsg) &&
|
|
70
|
+
UNSUPPORTED_MODEL_INDICATOR_RE.test(errorMsg) &&
|
|
71
|
+
UNSUPPORTED_MODEL_SCOPE_RE.test(errorMsg);
|
|
72
|
+
// 0. Unsupported model (account/plan entitlement rejection) — checked before
|
|
73
|
+
// `permanent` because PERMANENT_RE also matches /account/i and would
|
|
74
|
+
// otherwise swallow these errors, blocking the blocklist-driven fallback.
|
|
75
|
+
// Rate limit still wins when both patterns appear (a throttled account is
|
|
76
|
+
// not an entitlement failure).
|
|
77
|
+
if (isUnsupportedModel && !isRateLimit) {
|
|
78
|
+
return { kind: "unsupported-model" };
|
|
79
|
+
}
|
|
52
80
|
// 1. Permanent — but rate limit takes precedence
|
|
53
81
|
if (isPermanent && !isRateLimit) {
|
|
54
82
|
return { kind: "permanent" };
|