gsd-pi 2.62.0 → 2.62.1-dev.1ae2b74
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/resources/extensions/ask-user-questions.js +47 -3
- package/dist/resources/extensions/gsd/auto/loop.js +8 -1
- package/dist/resources/extensions/gsd/auto/phases.js +10 -3
- package/dist/resources/extensions/gsd/auto-post-unit.js +6 -4
- package/dist/resources/extensions/gsd/auto-start.js +11 -6
- package/dist/resources/extensions/gsd/auto-timers.js +8 -2
- package/dist/resources/extensions/gsd/auto-verification.js +14 -3
- package/dist/resources/extensions/gsd/auto-worktree.js +19 -0
- package/dist/resources/extensions/gsd/auto.js +24 -0
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +4 -0
- package/dist/resources/extensions/gsd/bootstrap/tool-call-loop-guard.js +11 -1
- package/dist/resources/extensions/gsd/commands-handlers.js +18 -7
- package/dist/resources/extensions/gsd/db-writer.js +64 -28
- package/dist/resources/extensions/gsd/preferences-models.js +74 -0
- package/dist/resources/extensions/gsd/preferences-skills.js +6 -1
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
- package/dist/resources/extensions/gsd/skill-catalog.js +6 -4
- package/dist/resources/extensions/gsd/skill-discovery.js +24 -6
- package/dist/resources/extensions/gsd/skill-health.js +7 -3
- package/dist/resources/extensions/gsd/skill-telemetry.js +5 -2
- package/dist/resources/extensions/gsd/state.js +1 -0
- package/dist/resources/extensions/gsd/tools/complete-slice.js +3 -3
- package/dist/resources/extensions/gsd/workflow-logger.js +13 -8
- package/dist/resources/extensions/gsd/workflow-reconcile.js +3 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +17 -17
- package/dist/web/standalone/.next/build-manifest.json +3 -3
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/required-server-files.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error/page.js +3 -3
- package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
- 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/page.js +2 -2
- package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +3 -3
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
- 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 +3 -3
- 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/api/boot/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/dev-mode/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/experimental/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/experimental/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/files/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/inspect/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/knowledge/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/live-state/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/onboarding/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/preferences/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/remote-questions/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/browser/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/command/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/events/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/manage/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/steer/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js +4 -4
- package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/upload/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +4 -4
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +4 -4
- 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 +3 -3
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/page.js +2 -2
- package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +17 -17
- package/dist/web/standalone/.next/server/chunks/2229.js +1 -1
- package/dist/web/standalone/.next/server/chunks/7471.js +3 -3
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware.js +2 -2
- package/dist/web/standalone/.next/server/next-font-manifest.js +1 -1
- package/dist/web/standalone/.next/server/next-font-manifest.json +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +2 -2
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/.next/static/chunks/app/_not-found/{page-2f24283c162b6ab3.js → page-f2a7482d42a5614b.js} +1 -1
- package/dist/web/standalone/.next/static/chunks/app/{layout-9ecfd95f343793f0.js → layout-a16c7a7ecdf0c2cf.js} +1 -1
- package/dist/web/standalone/.next/static/chunks/app/page-0c485498795110d6.js +1 -0
- package/dist/web/standalone/.next/static/chunks/main-app-fdab67f7802d7832.js +1 -0
- package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-459824ffb8c323dd.js +1 -0
- package/dist/web/standalone/node_modules/node-pty/build/Makefile +2 -2
- package/dist/web/standalone/node_modules/node-pty/build/Release/pty.node +0 -0
- package/dist/web/standalone/node_modules/node-pty/build/pty.target.mk +14 -14
- package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api.target.mk +14 -14
- package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_except.target.mk +14 -14
- package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_maybe.target.mk +14 -14
- package/dist/web/standalone/server.js +1 -1
- package/package.json +1 -1
- package/packages/mcp-server/src/cli.ts +1 -1
- package/packages/mcp-server/src/index.ts +15 -1
- package/packages/mcp-server/src/readers/captures.ts +119 -0
- package/packages/mcp-server/src/readers/doctor-lite.ts +225 -0
- package/packages/mcp-server/src/readers/index.ts +16 -0
- package/packages/mcp-server/src/readers/knowledge.ts +111 -0
- package/packages/mcp-server/src/readers/metrics.ts +118 -0
- package/packages/mcp-server/src/readers/paths.ts +217 -0
- package/packages/mcp-server/src/readers/readers.test.ts +509 -0
- package/packages/mcp-server/src/readers/roadmap.ts +263 -0
- package/packages/mcp-server/src/readers/state.ts +223 -0
- package/packages/mcp-server/src/server.ts +134 -3
- package/packages/pi-ai/dist/utils/repair-tool-json.d.ts +26 -6
- package/packages/pi-ai/dist/utils/repair-tool-json.d.ts.map +1 -1
- package/packages/pi-ai/dist/utils/repair-tool-json.js +67 -9
- package/packages/pi-ai/dist/utils/repair-tool-json.js.map +1 -1
- package/packages/pi-ai/dist/utils/tests/repair-tool-json.test.js +73 -1
- package/packages/pi-ai/dist/utils/tests/repair-tool-json.test.js.map +1 -1
- package/packages/pi-ai/src/utils/repair-tool-json.ts +74 -10
- package/packages/pi-ai/src/utils/tests/repair-tool-json.test.ts +94 -1
- package/packages/pi-coding-agent/dist/core/agent-session-model-switch.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/agent-session-model-switch.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/agent-session-model-switch.test.js +16 -0
- package/packages/pi-coding-agent/dist/core/agent-session-model-switch.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +4 -0
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/retry-handler.d.ts +3 -0
- package/packages/pi-coding-agent/dist/core/retry-handler.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/retry-handler.js +48 -16
- package/packages/pi-coding-agent/dist/core/retry-handler.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/retry-handler.test.js +20 -3
- package/packages/pi-coding-agent/dist/core/retry-handler.test.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/agent-session-model-switch.test.ts +21 -0
- package/packages/pi-coding-agent/src/core/agent-session.ts +4 -0
- package/packages/pi-coding-agent/src/core/retry-handler.test.ts +30 -3
- package/packages/pi-coding-agent/src/core/retry-handler.ts +49 -16
- package/pkg/package.json +1 -1
- package/src/resources/extensions/ask-user-questions.ts +60 -4
- package/src/resources/extensions/gsd/auto/loop.ts +8 -1
- package/src/resources/extensions/gsd/auto/phases.ts +8 -6
- package/src/resources/extensions/gsd/auto-post-unit.ts +6 -3
- package/src/resources/extensions/gsd/auto-start.ts +11 -6
- package/src/resources/extensions/gsd/auto-timers.ts +8 -2
- package/src/resources/extensions/gsd/auto-verification.ts +14 -3
- package/src/resources/extensions/gsd/auto-worktree.ts +18 -0
- package/src/resources/extensions/gsd/auto.ts +25 -0
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +4 -0
- package/src/resources/extensions/gsd/bootstrap/tool-call-loop-guard.ts +13 -1
- package/src/resources/extensions/gsd/commands-handlers.ts +20 -7
- package/src/resources/extensions/gsd/db-writer.ts +67 -30
- package/src/resources/extensions/gsd/preferences-models.ts +78 -0
- package/src/resources/extensions/gsd/preferences-skills.ts +6 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
- package/src/resources/extensions/gsd/skill-catalog.ts +6 -3
- package/src/resources/extensions/gsd/skill-discovery.ts +23 -6
- package/src/resources/extensions/gsd/skill-health.ts +7 -3
- package/src/resources/extensions/gsd/skill-telemetry.ts +5 -2
- package/src/resources/extensions/gsd/state.ts +1 -0
- package/src/resources/extensions/gsd/tests/ask-user-questions-dedup.test.ts +120 -0
- package/src/resources/extensions/gsd/tests/auto-start-model-capture.test.ts +22 -2
- package/src/resources/extensions/gsd/tests/auto-wrapup-inflight-guard.test.ts +107 -0
- package/src/resources/extensions/gsd/tests/claude-skill-dirs.test.ts +51 -0
- package/src/resources/extensions/gsd/tests/db-writer.test.ts +41 -0
- package/src/resources/extensions/gsd/tests/model-isolation.test.ts +75 -1
- package/src/resources/extensions/gsd/tests/steer-worktree-path.test.ts +108 -0
- package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +17 -4
- package/src/resources/extensions/gsd/tests/workflow-logger.test.ts +17 -41
- package/src/resources/extensions/gsd/tests/worktree-db-respawn-truncation.test.ts +81 -2
- package/src/resources/extensions/gsd/tools/complete-slice.ts +3 -5
- package/src/resources/extensions/gsd/workflow-logger.ts +13 -8
- package/src/resources/extensions/gsd/workflow-reconcile.ts +3 -1
- package/src/resources/extensions/shared/tests/ask-user-freetext.test.ts +6 -1
- package/dist/web/standalone/.next/static/chunks/app/page-62be3b5fa91e4c8f.js +0 -1
- package/dist/web/standalone/.next/static/chunks/main-app-d3d4c336195465f9.js +0 -1
- package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-ab5a8926e07ec673.js +0 -1
- /package/dist/web/standalone/.next/static/{F4rzqt_3m83A68ZRiU12r → erQZ_8_1lkclnPJLJnCxG}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{F4rzqt_3m83A68ZRiU12r → erQZ_8_1lkclnPJLJnCxG}/_ssgManifest.js +0 -0
|
@@ -37,6 +37,8 @@ export class RetryHandler {
|
|
|
37
37
|
private _retryAttempt = 0;
|
|
38
38
|
private _retryPromise: Promise<void> | undefined = undefined;
|
|
39
39
|
private _retryResolve: (() => void) | undefined = undefined;
|
|
40
|
+
private _retryGeneration = 0;
|
|
41
|
+
private _continueTimeout: ReturnType<typeof setTimeout> | undefined = undefined;
|
|
40
42
|
|
|
41
43
|
constructor(private readonly _deps: RetryHandlerDeps) {}
|
|
42
44
|
|
|
@@ -134,6 +136,7 @@ export class RetryHandler {
|
|
|
134
136
|
}
|
|
135
137
|
|
|
136
138
|
// Try credential fallback before counting against retry budget.
|
|
139
|
+
const retryGeneration = this._retryGeneration;
|
|
137
140
|
if (this._deps.getModel() && message.errorMessage) {
|
|
138
141
|
const errorType = this._classifyErrorType(message.errorMessage);
|
|
139
142
|
const isCredentialError = errorType === "rate_limit" || errorType === "quota_exhausted";
|
|
@@ -157,9 +160,7 @@ export class RetryHandler {
|
|
|
157
160
|
});
|
|
158
161
|
|
|
159
162
|
// Retry immediately with the next credential - don't increment _retryAttempt
|
|
160
|
-
|
|
161
|
-
this._deps.agent.continue().catch(() => {});
|
|
162
|
-
}, 0);
|
|
163
|
+
this._scheduleContinue(retryGeneration);
|
|
163
164
|
|
|
164
165
|
return true;
|
|
165
166
|
}
|
|
@@ -193,9 +194,7 @@ export class RetryHandler {
|
|
|
193
194
|
});
|
|
194
195
|
|
|
195
196
|
// Retry immediately with fallback provider - don't increment _retryAttempt
|
|
196
|
-
|
|
197
|
-
this._deps.agent.continue().catch(() => {});
|
|
198
|
-
}, 0);
|
|
197
|
+
this._scheduleContinue(retryGeneration);
|
|
199
198
|
|
|
200
199
|
return true;
|
|
201
200
|
}
|
|
@@ -203,7 +202,7 @@ export class RetryHandler {
|
|
|
203
202
|
// No fallback available either
|
|
204
203
|
if (errorType === "quota_exhausted") {
|
|
205
204
|
// Try long-context model downgrade ([1m] → base) before giving up
|
|
206
|
-
const downgraded = this._tryLongContextDowngrade(message);
|
|
205
|
+
const downgraded = this._tryLongContextDowngrade(message, retryGeneration);
|
|
207
206
|
if (downgraded) return true;
|
|
208
207
|
|
|
209
208
|
this._deps.emit({
|
|
@@ -274,7 +273,12 @@ export class RetryHandler {
|
|
|
274
273
|
try {
|
|
275
274
|
await sleep(delayMs, this._retryAbortController.signal);
|
|
276
275
|
} catch {
|
|
277
|
-
// Aborted during sleep
|
|
276
|
+
// Aborted during sleep. If the retry generation already advanced, this
|
|
277
|
+
// cancellation was handled externally (e.g. explicit model switch).
|
|
278
|
+
if (retryGeneration !== this._retryGeneration) {
|
|
279
|
+
this._retryAbortController = undefined;
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
278
282
|
const attempt = this._retryAttempt;
|
|
279
283
|
this._retryAttempt = 0;
|
|
280
284
|
this._retryAbortController = undefined;
|
|
@@ -290,16 +294,36 @@ export class RetryHandler {
|
|
|
290
294
|
this._retryAbortController = undefined;
|
|
291
295
|
|
|
292
296
|
// Retry via continue() - use setTimeout to break out of event handler chain
|
|
293
|
-
|
|
294
|
-
this._deps.agent.continue().catch(() => {});
|
|
295
|
-
}, 0);
|
|
297
|
+
this._scheduleContinue(retryGeneration);
|
|
296
298
|
|
|
297
299
|
return true;
|
|
298
300
|
}
|
|
299
301
|
|
|
300
302
|
/** Cancel in-progress retry */
|
|
301
303
|
abortRetry(): void {
|
|
302
|
-
|
|
304
|
+
const hadRetry =
|
|
305
|
+
this._retryPromise !== undefined
|
|
306
|
+
|| this._retryAbortController !== undefined
|
|
307
|
+
|| this._continueTimeout !== undefined;
|
|
308
|
+
if (!hadRetry) return;
|
|
309
|
+
|
|
310
|
+
const attempt = this._retryAttempt > 0 ? this._retryAttempt : 1;
|
|
311
|
+
this._retryGeneration++;
|
|
312
|
+
if (this._continueTimeout) {
|
|
313
|
+
clearTimeout(this._continueTimeout);
|
|
314
|
+
this._continueTimeout = undefined;
|
|
315
|
+
}
|
|
316
|
+
if (this._retryAbortController) {
|
|
317
|
+
this._retryAbortController.abort();
|
|
318
|
+
this._retryAbortController = undefined;
|
|
319
|
+
}
|
|
320
|
+
this._retryAttempt = 0;
|
|
321
|
+
this._deps.emit({
|
|
322
|
+
type: "auto_retry_end",
|
|
323
|
+
success: false,
|
|
324
|
+
attempt,
|
|
325
|
+
finalError: "Retry cancelled",
|
|
326
|
+
});
|
|
303
327
|
this._resolveRetry();
|
|
304
328
|
}
|
|
305
329
|
|
|
@@ -330,6 +354,17 @@ export class RetryHandler {
|
|
|
330
354
|
}
|
|
331
355
|
}
|
|
332
356
|
|
|
357
|
+
private _scheduleContinue(retryGeneration: number): void {
|
|
358
|
+
if (this._continueTimeout) {
|
|
359
|
+
clearTimeout(this._continueTimeout);
|
|
360
|
+
}
|
|
361
|
+
this._continueTimeout = setTimeout(() => {
|
|
362
|
+
this._continueTimeout = undefined;
|
|
363
|
+
if (retryGeneration !== this._retryGeneration) return;
|
|
364
|
+
this._deps.agent.continue().catch(() => {});
|
|
365
|
+
}, 0);
|
|
366
|
+
}
|
|
367
|
+
|
|
333
368
|
private _findLastAssistantInMessages(
|
|
334
369
|
messages: Array<{ role: string } & Record<string, any>>,
|
|
335
370
|
): AssistantMessage | undefined {
|
|
@@ -361,7 +396,7 @@ export class RetryHandler {
|
|
|
361
396
|
* base model (claude-opus-4-6) when the account lacks the long-context billing
|
|
362
397
|
* entitlement. Returns true if the downgrade was initiated.
|
|
363
398
|
*/
|
|
364
|
-
private _tryLongContextDowngrade(message: AssistantMessage): boolean {
|
|
399
|
+
private _tryLongContextDowngrade(message: AssistantMessage, retryGeneration: number): boolean {
|
|
365
400
|
const currentModel = this._deps.getModel();
|
|
366
401
|
if (!currentModel) return false;
|
|
367
402
|
|
|
@@ -393,9 +428,7 @@ export class RetryHandler {
|
|
|
393
428
|
errorMessage: `${message.errorMessage} (long context downgrade)`,
|
|
394
429
|
});
|
|
395
430
|
|
|
396
|
-
|
|
397
|
-
this._deps.agent.continue().catch(() => {});
|
|
398
|
-
}, 0);
|
|
431
|
+
this._scheduleContinue(retryGeneration);
|
|
399
432
|
|
|
400
433
|
return true;
|
|
401
434
|
}
|
package/pkg/package.json
CHANGED
|
@@ -72,6 +72,41 @@ const AskUserQuestionsParams = Type.Object({
|
|
|
72
72
|
}),
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
+
// ─── Per-turn deduplication ──────────────────────────────────────────────────
|
|
76
|
+
// Prevents duplicate question dispatches (especially to remote channels like
|
|
77
|
+
// Discord) when the LLM calls ask_user_questions multiple times with the same
|
|
78
|
+
// questions in a single turn. Keyed by full canonicalized payload (id, header,
|
|
79
|
+
// question, options, allowMultiple) — not just IDs — so that calls with the
|
|
80
|
+
// same IDs but different text/options are treated as distinct.
|
|
81
|
+
|
|
82
|
+
import { createHash } from "node:crypto";
|
|
83
|
+
|
|
84
|
+
interface CachedResult {
|
|
85
|
+
content: { type: "text"; text: string }[];
|
|
86
|
+
details: AskUserQuestionsDetails;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const turnCache = new Map<string, CachedResult>();
|
|
90
|
+
|
|
91
|
+
/** @internal Exported for testing only. */
|
|
92
|
+
export function questionSignature(questions: Question[]): string {
|
|
93
|
+
const canonical = questions
|
|
94
|
+
.map((q) => ({
|
|
95
|
+
id: q.id,
|
|
96
|
+
header: q.header,
|
|
97
|
+
question: q.question,
|
|
98
|
+
options: (q.options || []).map((o) => ({ label: o.label, description: o.description })),
|
|
99
|
+
allowMultiple: !!q.allowMultiple,
|
|
100
|
+
}))
|
|
101
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
102
|
+
return createHash("sha256").update(JSON.stringify(canonical)).digest("hex").slice(0, 16);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Reset the dedup cache. Called on session boundaries. */
|
|
106
|
+
export function resetAskUserQuestionsCache(): void {
|
|
107
|
+
turnCache.clear();
|
|
108
|
+
}
|
|
109
|
+
|
|
75
110
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
76
111
|
|
|
77
112
|
const OTHER_OPTION_LABEL = "None of the above";
|
|
@@ -121,6 +156,16 @@ export default function AskUserQuestions(pi: ExtensionAPI) {
|
|
|
121
156
|
parameters: AskUserQuestionsParams,
|
|
122
157
|
|
|
123
158
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
159
|
+
// ── Per-turn dedup: return cached result for identical question sets ──
|
|
160
|
+
const sig = questionSignature(params.questions);
|
|
161
|
+
const cached = turnCache.get(sig);
|
|
162
|
+
if (cached) {
|
|
163
|
+
return {
|
|
164
|
+
content: [{ type: "text" as const, text: cached.content[0].text + "\n(Returned cached answer — this question set was already asked this turn.)" }],
|
|
165
|
+
details: cached.details,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
124
169
|
// Validation
|
|
125
170
|
if (params.questions.length === 0 || params.questions.length > 3) {
|
|
126
171
|
return errorResult("Error: questions must contain 1-3 items", params.questions);
|
|
@@ -140,7 +185,14 @@ export default function AskUserQuestions(pi: ExtensionAPI) {
|
|
|
140
185
|
// this is a no-op when the user has not set up Slack/Discord/Telegram.
|
|
141
186
|
const { tryRemoteQuestions } = await import("./remote-questions/manager.js");
|
|
142
187
|
const remoteResult = await tryRemoteQuestions(params.questions, signal);
|
|
143
|
-
if (remoteResult)
|
|
188
|
+
if (remoteResult) {
|
|
189
|
+
// Cache successful remote results to prevent duplicate Discord dispatches
|
|
190
|
+
const remoteDetails = remoteResult.details as Record<string, unknown> | undefined;
|
|
191
|
+
if (remoteDetails && !remoteDetails.timed_out && !remoteDetails.error) {
|
|
192
|
+
turnCache.set(sig, remoteResult as unknown as CachedResult);
|
|
193
|
+
}
|
|
194
|
+
return { ...remoteResult, details: remoteResult.details as unknown };
|
|
195
|
+
}
|
|
144
196
|
|
|
145
197
|
if (!ctx.hasUI) {
|
|
146
198
|
return errorResult("Error: UI not available (non-interactive mode)", params.questions);
|
|
@@ -197,7 +249,7 @@ export default function AskUserQuestions(pi: ExtensionAPI) {
|
|
|
197
249
|
]),
|
|
198
250
|
),
|
|
199
251
|
};
|
|
200
|
-
|
|
252
|
+
const fallbackResult = {
|
|
201
253
|
content: [{ type: "text" as const, text: JSON.stringify({ answers }) }],
|
|
202
254
|
details: {
|
|
203
255
|
questions: params.questions,
|
|
@@ -205,6 +257,8 @@ export default function AskUserQuestions(pi: ExtensionAPI) {
|
|
|
205
257
|
cancelled: false,
|
|
206
258
|
} satisfies LocalResultDetails,
|
|
207
259
|
};
|
|
260
|
+
turnCache.set(sig, fallbackResult);
|
|
261
|
+
return fallbackResult;
|
|
208
262
|
}
|
|
209
263
|
|
|
210
264
|
// Check if cancelled (empty answers = user exited)
|
|
@@ -216,10 +270,12 @@ export default function AskUserQuestions(pi: ExtensionAPI) {
|
|
|
216
270
|
};
|
|
217
271
|
}
|
|
218
272
|
|
|
219
|
-
|
|
220
|
-
content: [{ type: "text", text: formatForLLM(result) }],
|
|
273
|
+
const successResult = {
|
|
274
|
+
content: [{ type: "text" as const, text: formatForLLM(result) }],
|
|
221
275
|
details: { questions: params.questions, response: result, cancelled: false } satisfies LocalResultDetails,
|
|
222
276
|
};
|
|
277
|
+
turnCache.set(sig, successResult);
|
|
278
|
+
return successResult;
|
|
223
279
|
},
|
|
224
280
|
|
|
225
281
|
// ─── Rendering ────────────────────────────────────────────────────────
|
|
@@ -48,6 +48,7 @@ export async function autoLoop(
|
|
|
48
48
|
let iteration = 0;
|
|
49
49
|
const loopState: LoopState = { recentUnits: [], stuckRecoveryAttempts: 0 };
|
|
50
50
|
let consecutiveErrors = 0;
|
|
51
|
+
const recentErrorMessages: string[] = [];
|
|
51
52
|
|
|
52
53
|
while (s.active) {
|
|
53
54
|
iteration++;
|
|
@@ -202,6 +203,7 @@ export async function autoLoop(
|
|
|
202
203
|
|
|
203
204
|
deps.clearUnitTimeout();
|
|
204
205
|
consecutiveErrors = 0;
|
|
206
|
+
recentErrorMessages.length = 0;
|
|
205
207
|
deps.emitJournalEvent({ ts: new Date().toISOString(), flowId, seq: nextSeq(), eventType: "iteration-end", data: { iteration } });
|
|
206
208
|
debugLog("autoLoop", { phase: "iteration-complete", iteration });
|
|
207
209
|
continue;
|
|
@@ -250,6 +252,7 @@ export async function autoLoop(
|
|
|
250
252
|
if (finalizeResult.action === "continue") continue;
|
|
251
253
|
|
|
252
254
|
consecutiveErrors = 0; // Iteration completed successfully
|
|
255
|
+
recentErrorMessages.length = 0;
|
|
253
256
|
deps.emitJournalEvent({ ts: new Date().toISOString(), flowId, seq: nextSeq(), eventType: "iteration-end", data: { iteration } });
|
|
254
257
|
debugLog("autoLoop", { phase: "iteration-complete", iteration });
|
|
255
258
|
} catch (loopErr) {
|
|
@@ -280,6 +283,7 @@ export async function autoLoop(
|
|
|
280
283
|
}
|
|
281
284
|
|
|
282
285
|
consecutiveErrors++;
|
|
286
|
+
recentErrorMessages.push(msg.length > 120 ? msg.slice(0, 120) + "..." : msg);
|
|
283
287
|
debugLog("autoLoop", {
|
|
284
288
|
phase: "iteration-error",
|
|
285
289
|
iteration,
|
|
@@ -289,8 +293,11 @@ export async function autoLoop(
|
|
|
289
293
|
|
|
290
294
|
if (consecutiveErrors >= 3) {
|
|
291
295
|
// 3+ consecutive: hard stop — something is fundamentally broken
|
|
296
|
+
const errorHistory = recentErrorMessages
|
|
297
|
+
.map((m, i) => ` ${i + 1}. ${m}`)
|
|
298
|
+
.join("\n");
|
|
292
299
|
ctx.ui.notify(
|
|
293
|
-
`Auto-mode stopped: ${consecutiveErrors} consecutive iteration failures
|
|
300
|
+
`Auto-mode stopped: ${consecutiveErrors} consecutive iteration failures:\n${errorHistory}`,
|
|
294
301
|
"error",
|
|
295
302
|
);
|
|
296
303
|
await deps.stopAuto(
|
|
@@ -31,7 +31,7 @@ import { existsSync, cpSync } from "node:fs";
|
|
|
31
31
|
import { logWarning, logError } from "../workflow-logger.js";
|
|
32
32
|
import { gsdRoot } from "../paths.js";
|
|
33
33
|
import { atomicWriteSync } from "../atomic-write.js";
|
|
34
|
-
import { verifyExpectedArtifact } from "../auto-recovery.js";
|
|
34
|
+
import { verifyExpectedArtifact, diagnoseExpectedArtifact, buildLoopRemediationSteps } from "../auto-recovery.js";
|
|
35
35
|
import { writeUnitRuntimeRecord } from "../unit-runtime.js";
|
|
36
36
|
|
|
37
37
|
// ─── generateMilestoneReport ──────────────────────────────────────────────────
|
|
@@ -182,7 +182,7 @@ export async function runPreDispatch(
|
|
|
182
182
|
}
|
|
183
183
|
if (!healthGate.proceed) {
|
|
184
184
|
ctx.ui.notify(
|
|
185
|
-
healthGate.reason
|
|
185
|
+
healthGate.reason || "Pre-dispatch health check failed — run /gsd doctor for details.",
|
|
186
186
|
"error",
|
|
187
187
|
);
|
|
188
188
|
await deps.pauseAuto(ctx, pi);
|
|
@@ -628,15 +628,17 @@ export async function runDispatch(
|
|
|
628
628
|
unitId,
|
|
629
629
|
reason: stuckSignal.reason,
|
|
630
630
|
});
|
|
631
|
+
const stuckDiag = diagnoseExpectedArtifact(unitType, unitId, s.basePath);
|
|
632
|
+
const stuckRemediation = buildLoopRemediationSteps(unitType, unitId, s.basePath);
|
|
633
|
+
const stuckParts = [`Stuck on ${unitType} ${unitId} — ${stuckSignal.reason}.`];
|
|
634
|
+
if (stuckDiag) stuckParts.push(`Expected: ${stuckDiag}`);
|
|
635
|
+
if (stuckRemediation) stuckParts.push(`To recover:\n${stuckRemediation}`);
|
|
636
|
+
ctx.ui.notify(stuckParts.join(" "), "error");
|
|
631
637
|
await deps.stopAuto(
|
|
632
638
|
ctx,
|
|
633
639
|
pi,
|
|
634
640
|
`Stuck: ${stuckSignal.reason}`,
|
|
635
641
|
);
|
|
636
|
-
ctx.ui.notify(
|
|
637
|
-
`Stuck on ${unitType} ${unitId} — ${stuckSignal.reason}. The expected artifact was not written.`,
|
|
638
|
-
"error",
|
|
639
|
-
);
|
|
640
642
|
return { action: "break", reason: "stuck-detected" };
|
|
641
643
|
}
|
|
642
644
|
} else {
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
import {
|
|
34
34
|
verifyExpectedArtifact,
|
|
35
35
|
resolveExpectedArtifactPath,
|
|
36
|
+
diagnoseExpectedArtifact,
|
|
36
37
|
} from "./auto-recovery.js";
|
|
37
38
|
import { regenerateIfMissing } from "./workflow-projections.js";
|
|
38
39
|
import { syncStateToProjectRoot } from "./auto-worktree.js";
|
|
@@ -476,8 +477,9 @@ export async function postUnitPreVerification(pctx: PostUnitContext, opts?: PreV
|
|
|
476
477
|
// db_unavailable so the artifact was never written. Retrying would
|
|
477
478
|
// produce an infinite re-dispatch loop (#2517).
|
|
478
479
|
debugLog("postUnit", { phase: "artifact-verify-skip-db-unavailable", unitType: s.currentUnit.type, unitId: s.currentUnit.id });
|
|
480
|
+
const dbSkipDiag = diagnoseExpectedArtifact(s.currentUnit.type, s.currentUnit.id, s.basePath);
|
|
479
481
|
ctx.ui.notify(
|
|
480
|
-
`Artifact missing for ${s.currentUnit.type} ${s.currentUnit.id}
|
|
482
|
+
`Artifact missing for ${s.currentUnit.type} ${s.currentUnit.id} — DB unavailable, skipping retry.${dbSkipDiag ? ` Expected: ${dbSkipDiag}` : ""}`,
|
|
481
483
|
"error",
|
|
482
484
|
);
|
|
483
485
|
} else if (!triggerArtifactVerified) {
|
|
@@ -486,14 +488,15 @@ export async function postUnitPreVerification(pctx: PostUnitContext, opts?: PreV
|
|
|
486
488
|
const retryKey = `${s.currentUnit.type}:${s.currentUnit.id}`;
|
|
487
489
|
const attempt = (s.verificationRetryCount.get(retryKey) ?? 0) + 1;
|
|
488
490
|
s.verificationRetryCount.set(retryKey, attempt);
|
|
491
|
+
const retryDiag = diagnoseExpectedArtifact(s.currentUnit.type, s.currentUnit.id, s.basePath);
|
|
489
492
|
s.pendingVerificationRetry = {
|
|
490
493
|
unitId: s.currentUnit.id,
|
|
491
|
-
failureContext: `Artifact verification failed: expected artifact for ${s.currentUnit.type} "${s.currentUnit.id}" was not found on disk after unit execution (attempt ${attempt})
|
|
494
|
+
failureContext: `Artifact verification failed: expected artifact for ${s.currentUnit.type} "${s.currentUnit.id}" was not found on disk after unit execution (attempt ${attempt}).${retryDiag ? ` Expected: ${retryDiag}` : ""}`,
|
|
492
495
|
attempt,
|
|
493
496
|
};
|
|
494
497
|
debugLog("postUnit", { phase: "artifact-verify-retry", unitType: s.currentUnit.type, unitId: s.currentUnit.id, attempt });
|
|
495
498
|
ctx.ui.notify(
|
|
496
|
-
`Artifact missing for ${s.currentUnit.type} ${s.currentUnit.id} — retrying (attempt ${attempt})`,
|
|
499
|
+
`Artifact missing for ${s.currentUnit.type} ${s.currentUnit.id} — retrying (attempt ${attempt}).${retryDiag ? ` Expected: ${retryDiag}` : ""}`,
|
|
497
500
|
"warning",
|
|
498
501
|
);
|
|
499
502
|
return "retry";
|
|
@@ -80,6 +80,7 @@ import { join } from "node:path";
|
|
|
80
80
|
import { sep as pathSep } from "node:path";
|
|
81
81
|
|
|
82
82
|
import { resolveProjectRootDbPath } from "./bootstrap/dynamic-tools.js";
|
|
83
|
+
import { resolveDefaultSessionModel } from "./preferences-models.js";
|
|
83
84
|
import type { WorktreeResolver } from "./worktree-resolver.js";
|
|
84
85
|
|
|
85
86
|
export interface BootstrapDeps {
|
|
@@ -155,12 +156,16 @@ export async function bootstrapAutoSession(
|
|
|
155
156
|
|
|
156
157
|
// Capture the user's session model before guided-flow dispatch can apply a
|
|
157
158
|
// phase-specific planning model for a discuss turn (#2829).
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
159
|
+
//
|
|
160
|
+
// GSD PREFERENCES.md takes priority over the session model from settings.json
|
|
161
|
+
// (#3517). The session model (ctx.model) comes from findInitialModel() which
|
|
162
|
+
// reads defaultProvider/defaultModel from ~/.gsd/agent/settings.json. When
|
|
163
|
+
// the user has explicit model preferences in PREFERENCES.md, those should win.
|
|
164
|
+
const preferredModel = resolveDefaultSessionModel(ctx.model?.provider);
|
|
165
|
+
const startModelSnapshot = preferredModel
|
|
166
|
+
?? (ctx.model
|
|
167
|
+
? { provider: ctx.model.provider, id: ctx.model.id }
|
|
168
|
+
: null);
|
|
164
169
|
|
|
165
170
|
try {
|
|
166
171
|
// Validate GSD_PROJECT_ID early so the user gets immediate feedback
|
|
@@ -122,6 +122,10 @@ export function startUnitSupervision(sctx: SupervisionContext): void {
|
|
|
122
122
|
phase: "wrapup-warning-sent",
|
|
123
123
|
wrapupWarningSent: true,
|
|
124
124
|
});
|
|
125
|
+
// Only trigger a new turn if no tools are currently in flight.
|
|
126
|
+
// Triggering during active tool calls causes tool results to be skipped
|
|
127
|
+
// with "Skipped due to queued user message", leading to provider errors (#3512).
|
|
128
|
+
const softTrigger = getInFlightToolCount() === 0;
|
|
125
129
|
pi.sendMessage(
|
|
126
130
|
{
|
|
127
131
|
customType: "gsd-auto-wrapup",
|
|
@@ -136,7 +140,7 @@ export function startUnitSupervision(sctx: SupervisionContext): void {
|
|
|
136
140
|
"4. leave precise resume notes if anything remains unfinished",
|
|
137
141
|
].join("\n"),
|
|
138
142
|
},
|
|
139
|
-
{ triggerTurn:
|
|
143
|
+
{ triggerTurn: softTrigger },
|
|
140
144
|
);
|
|
141
145
|
}, softTimeoutMs);
|
|
142
146
|
|
|
@@ -293,6 +297,8 @@ export function startUnitSupervision(sctx: SupervisionContext): void {
|
|
|
293
297
|
);
|
|
294
298
|
}
|
|
295
299
|
|
|
300
|
+
// Only trigger a new turn if no tools are currently in flight (#3512).
|
|
301
|
+
const contextTrigger = getInFlightToolCount() === 0;
|
|
296
302
|
pi.sendMessage(
|
|
297
303
|
{
|
|
298
304
|
customType: "gsd-auto-wrapup",
|
|
@@ -308,7 +314,7 @@ export function startUnitSupervision(sctx: SupervisionContext): void {
|
|
|
308
314
|
"Do NOT start new sub-tasks or investigations.",
|
|
309
315
|
].join("\n"),
|
|
310
316
|
},
|
|
311
|
-
{ triggerTurn:
|
|
317
|
+
{ triggerTurn: contextTrigger },
|
|
312
318
|
);
|
|
313
319
|
|
|
314
320
|
if (s.continueHereHandle) {
|
|
@@ -196,19 +196,30 @@ export async function runPostUnitVerification(
|
|
|
196
196
|
failureContext: formatFailureContext(result),
|
|
197
197
|
attempt: nextAttempt,
|
|
198
198
|
};
|
|
199
|
+
const failedCmds = result.checks
|
|
200
|
+
.filter((c) => c.exitCode !== 0)
|
|
201
|
+
.map((c) => c.command);
|
|
202
|
+
const cmdSummary = failedCmds.length <= 3
|
|
203
|
+
? failedCmds.join(", ")
|
|
204
|
+
: `${failedCmds.slice(0, 3).join(", ")}... and ${failedCmds.length - 3} more`;
|
|
199
205
|
ctx.ui.notify(
|
|
200
|
-
`Verification failed — auto-fix attempt ${nextAttempt}/${maxRetries}`,
|
|
206
|
+
`Verification failed (${cmdSummary}) — auto-fix attempt ${nextAttempt}/${maxRetries}`,
|
|
201
207
|
"warning",
|
|
202
208
|
);
|
|
203
209
|
// Return "retry" — the autoLoop while loop will re-iterate with the retry context
|
|
204
210
|
return "retry";
|
|
205
211
|
} else {
|
|
206
212
|
// Gate failed, retries exhausted
|
|
207
|
-
const exhaustedAttempt = attempt + 1;
|
|
208
213
|
s.verificationRetryCount.delete(s.currentUnit.id);
|
|
209
214
|
s.pendingVerificationRetry = null;
|
|
215
|
+
const exhaustedFails = result.checks
|
|
216
|
+
.filter((c) => c.exitCode !== 0)
|
|
217
|
+
.map((c) => c.command);
|
|
218
|
+
const exhaustedSummary = exhaustedFails.length <= 3
|
|
219
|
+
? exhaustedFails.join(", ")
|
|
220
|
+
: `${exhaustedFails.slice(0, 3).join(", ")}... and ${exhaustedFails.length - 3} more`;
|
|
210
221
|
ctx.ui.notify(
|
|
211
|
-
`Verification gate FAILED after ${
|
|
222
|
+
`Verification gate FAILED after ${attempt} ${attempt === 1 ? "retry" : "retries"} (${exhaustedSummary}) — pausing for human review`,
|
|
212
223
|
"error",
|
|
213
224
|
);
|
|
214
225
|
await pauseAuto(ctx, pi);
|
|
@@ -314,10 +314,28 @@ export function syncProjectRootToWorktree(
|
|
|
314
314
|
// openDatabase re-creates it, causing "no such table" failures (#2815).
|
|
315
315
|
try {
|
|
316
316
|
const wtDb = join(wtGsd, "gsd.db");
|
|
317
|
+
let deleteSidecars = false;
|
|
317
318
|
if (existsSync(wtDb)) {
|
|
318
319
|
const size = statSync(wtDb).size;
|
|
319
320
|
if (size === 0) {
|
|
320
321
|
unlinkSync(wtDb);
|
|
322
|
+
deleteSidecars = true;
|
|
323
|
+
}
|
|
324
|
+
} else {
|
|
325
|
+
// Main DB already missing — sidecars are orphaned from a previous
|
|
326
|
+
// partial cleanup and must still be removed.
|
|
327
|
+
deleteSidecars = true;
|
|
328
|
+
}
|
|
329
|
+
// Always clean up WAL/SHM sidecar files when the main DB was deleted
|
|
330
|
+
// or is already missing. Orphaned WAL/SHM files cause SQLite WAL
|
|
331
|
+
// recovery on next open, which triggers a CPU spin on Node 24's
|
|
332
|
+
// node:sqlite DatabaseSync implementation (#2478).
|
|
333
|
+
if (deleteSidecars) {
|
|
334
|
+
for (const suffix of ["-wal", "-shm"]) {
|
|
335
|
+
const f = wtDb + suffix;
|
|
336
|
+
if (existsSync(f)) {
|
|
337
|
+
unlinkSync(f);
|
|
338
|
+
}
|
|
321
339
|
}
|
|
322
340
|
}
|
|
323
341
|
} catch (err) {
|
|
@@ -606,6 +606,18 @@ export async function stopAuto(
|
|
|
606
606
|
debugLog("stop-cleanup-locks", { error: e instanceof Error ? e.message : String(e) });
|
|
607
607
|
}
|
|
608
608
|
|
|
609
|
+
// ── Step 1b: Flush queued follow-up messages (#3512) ──
|
|
610
|
+
// Late async notifications (async_job_result, gsd-auto-wrapup) can trigger
|
|
611
|
+
// extra LLM turns after stop. Flush them the same way run-unit.ts does.
|
|
612
|
+
try {
|
|
613
|
+
const cmdCtxAny = s.cmdCtx as Record<string, unknown> | null;
|
|
614
|
+
if (typeof cmdCtxAny?.clearQueue === "function") {
|
|
615
|
+
(cmdCtxAny.clearQueue as () => unknown)();
|
|
616
|
+
}
|
|
617
|
+
} catch (e) {
|
|
618
|
+
debugLog("stop-cleanup-queue", { error: e instanceof Error ? e.message : String(e) });
|
|
619
|
+
}
|
|
620
|
+
|
|
609
621
|
// ── Step 2: Skill state ──
|
|
610
622
|
try {
|
|
611
623
|
clearSkillSnapshot();
|
|
@@ -834,6 +846,19 @@ export async function pauseAuto(
|
|
|
834
846
|
): Promise<void> {
|
|
835
847
|
if (!s.active) return;
|
|
836
848
|
clearUnitTimeout();
|
|
849
|
+
|
|
850
|
+
// Flush queued follow-up messages (#3512).
|
|
851
|
+
// Late async notifications (async_job_result, gsd-auto-wrapup) can trigger
|
|
852
|
+
// extra LLM turns after pause. Flush them the same way run-unit.ts does.
|
|
853
|
+
try {
|
|
854
|
+
const cmdCtxAny = s.cmdCtx as Record<string, unknown> | null;
|
|
855
|
+
if (typeof cmdCtxAny?.clearQueue === "function") {
|
|
856
|
+
(cmdCtxAny.clearQueue as () => unknown)();
|
|
857
|
+
}
|
|
858
|
+
} catch (e) {
|
|
859
|
+
debugLog("pause-cleanup-queue", { error: e instanceof Error ? e.message : String(e) });
|
|
860
|
+
}
|
|
861
|
+
|
|
837
862
|
// Unblock any pending unit promise so the auto-loop is not orphaned.
|
|
838
863
|
// Pass errorContext so runUnitPhase can distinguish user-initiated pause
|
|
839
864
|
// from provider-error pause and avoid hard-stopping (#2762).
|
|
@@ -17,6 +17,7 @@ import { getAutoDashboardData, isAutoActive, isAutoPaused, markToolEnd, markTool
|
|
|
17
17
|
import { isParallelActive, shutdownParallel } from "../parallel-orchestrator.js";
|
|
18
18
|
import { checkToolCallLoop, resetToolCallLoopGuard } from "./tool-call-loop-guard.js";
|
|
19
19
|
import { saveActivityLog } from "../activity-log.js";
|
|
20
|
+
import { resetAskUserQuestionsCache } from "../../ask-user-questions.js";
|
|
20
21
|
|
|
21
22
|
// Skip the welcome screen on the very first session_start — cli.ts already
|
|
22
23
|
// printed it before the TUI launched. Only re-print on /clear (subsequent sessions).
|
|
@@ -31,6 +32,7 @@ export function registerHooks(pi: ExtensionAPI): void {
|
|
|
31
32
|
pi.on("session_start", async (_event, ctx) => {
|
|
32
33
|
resetWriteGateState();
|
|
33
34
|
resetToolCallLoopGuard();
|
|
35
|
+
resetAskUserQuestionsCache();
|
|
34
36
|
await syncServiceTierStatus(ctx);
|
|
35
37
|
|
|
36
38
|
// Apply show_token_cost preference (#1515)
|
|
@@ -67,6 +69,7 @@ export function registerHooks(pi: ExtensionAPI): void {
|
|
|
67
69
|
pi.on("session_switch", async (_event, ctx) => {
|
|
68
70
|
resetWriteGateState();
|
|
69
71
|
resetToolCallLoopGuard();
|
|
72
|
+
resetAskUserQuestionsCache();
|
|
70
73
|
clearDiscussionFlowState();
|
|
71
74
|
await syncServiceTierStatus(ctx);
|
|
72
75
|
loadToolApiKeys();
|
|
@@ -78,6 +81,7 @@ export function registerHooks(pi: ExtensionAPI): void {
|
|
|
78
81
|
|
|
79
82
|
pi.on("agent_end", async (event, ctx: ExtensionContext) => {
|
|
80
83
|
resetToolCallLoopGuard();
|
|
84
|
+
resetAskUserQuestionsCache();
|
|
81
85
|
await handleAgentEnd(pi, event, ctx);
|
|
82
86
|
});
|
|
83
87
|
|
|
@@ -16,8 +16,13 @@ import { createHash } from "node:crypto";
|
|
|
16
16
|
|
|
17
17
|
const MAX_CONSECUTIVE_IDENTICAL_CALLS = 4;
|
|
18
18
|
|
|
19
|
+
/** Interactive/user-facing tools where even 1 duplicate is confusing. */
|
|
20
|
+
const STRICT_LOOP_TOOLS = new Set(["ask_user_questions"]);
|
|
21
|
+
const MAX_CONSECUTIVE_STRICT = 1;
|
|
22
|
+
|
|
19
23
|
let consecutiveCount = 0;
|
|
20
24
|
let lastSignature = "";
|
|
25
|
+
let lastToolName = "";
|
|
21
26
|
let enabled = true;
|
|
22
27
|
|
|
23
28
|
/** Hash tool name + args into a compact signature for comparison. */
|
|
@@ -55,9 +60,14 @@ export function checkToolCallLoop(
|
|
|
55
60
|
} else {
|
|
56
61
|
consecutiveCount = 1;
|
|
57
62
|
lastSignature = sig;
|
|
63
|
+
lastToolName = toolName;
|
|
58
64
|
}
|
|
59
65
|
|
|
60
|
-
|
|
66
|
+
const threshold = STRICT_LOOP_TOOLS.has(toolName)
|
|
67
|
+
? MAX_CONSECUTIVE_STRICT
|
|
68
|
+
: MAX_CONSECUTIVE_IDENTICAL_CALLS;
|
|
69
|
+
|
|
70
|
+
if (consecutiveCount > threshold) {
|
|
61
71
|
return {
|
|
62
72
|
block: true,
|
|
63
73
|
reason:
|
|
@@ -75,6 +85,7 @@ export function checkToolCallLoop(
|
|
|
75
85
|
export function resetToolCallLoopGuard(): void {
|
|
76
86
|
consecutiveCount = 0;
|
|
77
87
|
lastSignature = "";
|
|
88
|
+
lastToolName = "";
|
|
78
89
|
enabled = true;
|
|
79
90
|
}
|
|
80
91
|
|
|
@@ -83,6 +94,7 @@ export function disableToolCallLoopGuard(): void {
|
|
|
83
94
|
enabled = false;
|
|
84
95
|
consecutiveCount = 0;
|
|
85
96
|
lastSignature = "";
|
|
97
|
+
lastToolName = "";
|
|
86
98
|
}
|
|
87
99
|
|
|
88
100
|
/** Get current consecutive count for diagnostics. */
|