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
|
@@ -33,6 +33,31 @@ const AskUserQuestionsParams = Type.Object({
|
|
|
33
33
|
description: "Questions to show the user. Prefer 1 and do not exceed 3.",
|
|
34
34
|
}),
|
|
35
35
|
});
|
|
36
|
+
// ─── Per-turn deduplication ──────────────────────────────────────────────────
|
|
37
|
+
// Prevents duplicate question dispatches (especially to remote channels like
|
|
38
|
+
// Discord) when the LLM calls ask_user_questions multiple times with the same
|
|
39
|
+
// questions in a single turn. Keyed by full canonicalized payload (id, header,
|
|
40
|
+
// question, options, allowMultiple) — not just IDs — so that calls with the
|
|
41
|
+
// same IDs but different text/options are treated as distinct.
|
|
42
|
+
import { createHash } from "node:crypto";
|
|
43
|
+
const turnCache = new Map();
|
|
44
|
+
/** @internal Exported for testing only. */
|
|
45
|
+
export function questionSignature(questions) {
|
|
46
|
+
const canonical = questions
|
|
47
|
+
.map((q) => ({
|
|
48
|
+
id: q.id,
|
|
49
|
+
header: q.header,
|
|
50
|
+
question: q.question,
|
|
51
|
+
options: (q.options || []).map((o) => ({ label: o.label, description: o.description })),
|
|
52
|
+
allowMultiple: !!q.allowMultiple,
|
|
53
|
+
}))
|
|
54
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
55
|
+
return createHash("sha256").update(JSON.stringify(canonical)).digest("hex").slice(0, 16);
|
|
56
|
+
}
|
|
57
|
+
/** Reset the dedup cache. Called on session boundaries. */
|
|
58
|
+
export function resetAskUserQuestionsCache() {
|
|
59
|
+
turnCache.clear();
|
|
60
|
+
}
|
|
36
61
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
37
62
|
const OTHER_OPTION_LABEL = "None of the above";
|
|
38
63
|
function errorResult(message, questions = []) {
|
|
@@ -73,6 +98,15 @@ export default function AskUserQuestions(pi) {
|
|
|
73
98
|
],
|
|
74
99
|
parameters: AskUserQuestionsParams,
|
|
75
100
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
101
|
+
// ── Per-turn dedup: return cached result for identical question sets ──
|
|
102
|
+
const sig = questionSignature(params.questions);
|
|
103
|
+
const cached = turnCache.get(sig);
|
|
104
|
+
if (cached) {
|
|
105
|
+
return {
|
|
106
|
+
content: [{ type: "text", text: cached.content[0].text + "\n(Returned cached answer — this question set was already asked this turn.)" }],
|
|
107
|
+
details: cached.details,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
76
110
|
// Validation
|
|
77
111
|
if (params.questions.length === 0 || params.questions.length > 3) {
|
|
78
112
|
return errorResult("Error: questions must contain 1-3 items", params.questions);
|
|
@@ -87,8 +121,14 @@ export default function AskUserQuestions(pi) {
|
|
|
87
121
|
// this is a no-op when the user has not set up Slack/Discord/Telegram.
|
|
88
122
|
const { tryRemoteQuestions } = await import("./remote-questions/manager.js");
|
|
89
123
|
const remoteResult = await tryRemoteQuestions(params.questions, signal);
|
|
90
|
-
if (remoteResult)
|
|
124
|
+
if (remoteResult) {
|
|
125
|
+
// Cache successful remote results to prevent duplicate Discord dispatches
|
|
126
|
+
const remoteDetails = remoteResult.details;
|
|
127
|
+
if (remoteDetails && !remoteDetails.timed_out && !remoteDetails.error) {
|
|
128
|
+
turnCache.set(sig, remoteResult);
|
|
129
|
+
}
|
|
91
130
|
return { ...remoteResult, details: remoteResult.details };
|
|
131
|
+
}
|
|
92
132
|
if (!ctx.hasUI) {
|
|
93
133
|
return errorResult("Error: UI not available (non-interactive mode)", params.questions);
|
|
94
134
|
}
|
|
@@ -131,7 +171,7 @@ export default function AskUserQuestions(pi) {
|
|
|
131
171
|
{ selected: a.answers.length === 1 ? a.answers[0] : a.answers, notes: "" },
|
|
132
172
|
])),
|
|
133
173
|
};
|
|
134
|
-
|
|
174
|
+
const fallbackResult = {
|
|
135
175
|
content: [{ type: "text", text: JSON.stringify({ answers }) }],
|
|
136
176
|
details: {
|
|
137
177
|
questions: params.questions,
|
|
@@ -139,6 +179,8 @@ export default function AskUserQuestions(pi) {
|
|
|
139
179
|
cancelled: false,
|
|
140
180
|
},
|
|
141
181
|
};
|
|
182
|
+
turnCache.set(sig, fallbackResult);
|
|
183
|
+
return fallbackResult;
|
|
142
184
|
}
|
|
143
185
|
// Check if cancelled (empty answers = user exited)
|
|
144
186
|
const hasAnswers = Object.keys(result.answers).length > 0;
|
|
@@ -148,10 +190,12 @@ export default function AskUserQuestions(pi) {
|
|
|
148
190
|
details: { questions: params.questions, response: null, cancelled: true },
|
|
149
191
|
};
|
|
150
192
|
}
|
|
151
|
-
|
|
193
|
+
const successResult = {
|
|
152
194
|
content: [{ type: "text", text: formatForLLM(result) }],
|
|
153
195
|
details: { questions: params.questions, response: result, cancelled: false },
|
|
154
196
|
};
|
|
197
|
+
turnCache.set(sig, successResult);
|
|
198
|
+
return successResult;
|
|
155
199
|
},
|
|
156
200
|
// ─── Rendering ────────────────────────────────────────────────────────
|
|
157
201
|
renderCall(args, theme) {
|
|
@@ -26,6 +26,7 @@ export async function autoLoop(ctx, pi, s, deps) {
|
|
|
26
26
|
let iteration = 0;
|
|
27
27
|
const loopState = { recentUnits: [], stuckRecoveryAttempts: 0 };
|
|
28
28
|
let consecutiveErrors = 0;
|
|
29
|
+
const recentErrorMessages = [];
|
|
29
30
|
while (s.active) {
|
|
30
31
|
iteration++;
|
|
31
32
|
debugLog("autoLoop", { phase: "loop-top", iteration });
|
|
@@ -157,6 +158,7 @@ export async function autoLoop(ctx, pi, s, deps) {
|
|
|
157
158
|
});
|
|
158
159
|
deps.clearUnitTimeout();
|
|
159
160
|
consecutiveErrors = 0;
|
|
161
|
+
recentErrorMessages.length = 0;
|
|
160
162
|
deps.emitJournalEvent({ ts: new Date().toISOString(), flowId, seq: nextSeq(), eventType: "iteration-end", data: { iteration } });
|
|
161
163
|
debugLog("autoLoop", { phase: "iteration-complete", iteration });
|
|
162
164
|
continue;
|
|
@@ -206,6 +208,7 @@ export async function autoLoop(ctx, pi, s, deps) {
|
|
|
206
208
|
if (finalizeResult.action === "continue")
|
|
207
209
|
continue;
|
|
208
210
|
consecutiveErrors = 0; // Iteration completed successfully
|
|
211
|
+
recentErrorMessages.length = 0;
|
|
209
212
|
deps.emitJournalEvent({ ts: new Date().toISOString(), flowId, seq: nextSeq(), eventType: "iteration-end", data: { iteration } });
|
|
210
213
|
debugLog("autoLoop", { phase: "iteration-complete", iteration });
|
|
211
214
|
}
|
|
@@ -228,6 +231,7 @@ export async function autoLoop(ctx, pi, s, deps) {
|
|
|
228
231
|
break;
|
|
229
232
|
}
|
|
230
233
|
consecutiveErrors++;
|
|
234
|
+
recentErrorMessages.push(msg.length > 120 ? msg.slice(0, 120) + "..." : msg);
|
|
231
235
|
debugLog("autoLoop", {
|
|
232
236
|
phase: "iteration-error",
|
|
233
237
|
iteration,
|
|
@@ -236,7 +240,10 @@ export async function autoLoop(ctx, pi, s, deps) {
|
|
|
236
240
|
});
|
|
237
241
|
if (consecutiveErrors >= 3) {
|
|
238
242
|
// 3+ consecutive: hard stop — something is fundamentally broken
|
|
239
|
-
|
|
243
|
+
const errorHistory = recentErrorMessages
|
|
244
|
+
.map((m, i) => ` ${i + 1}. ${m}`)
|
|
245
|
+
.join("\n");
|
|
246
|
+
ctx.ui.notify(`Auto-mode stopped: ${consecutiveErrors} consecutive iteration failures:\n${errorHistory}`, "error");
|
|
240
247
|
await deps.stopAuto(ctx, pi, `${consecutiveErrors} consecutive iteration failures`);
|
|
241
248
|
break;
|
|
242
249
|
}
|
|
@@ -18,7 +18,7 @@ import { existsSync, cpSync } from "node:fs";
|
|
|
18
18
|
import { logWarning, logError } from "../workflow-logger.js";
|
|
19
19
|
import { gsdRoot } from "../paths.js";
|
|
20
20
|
import { atomicWriteSync } from "../atomic-write.js";
|
|
21
|
-
import { verifyExpectedArtifact } from "../auto-recovery.js";
|
|
21
|
+
import { verifyExpectedArtifact, diagnoseExpectedArtifact, buildLoopRemediationSteps } from "../auto-recovery.js";
|
|
22
22
|
import { writeUnitRuntimeRecord } from "../unit-runtime.js";
|
|
23
23
|
// ─── generateMilestoneReport ──────────────────────────────────────────────────
|
|
24
24
|
/**
|
|
@@ -116,7 +116,7 @@ export async function runPreDispatch(ic, loopState) {
|
|
|
116
116
|
ctx.ui.notify(`Pre-dispatch: ${healthGate.fixesApplied.join(", ")}`, "info");
|
|
117
117
|
}
|
|
118
118
|
if (!healthGate.proceed) {
|
|
119
|
-
ctx.ui.notify(healthGate.reason
|
|
119
|
+
ctx.ui.notify(healthGate.reason || "Pre-dispatch health check failed — run /gsd doctor for details.", "error");
|
|
120
120
|
await deps.pauseAuto(ctx, pi);
|
|
121
121
|
debugLog("autoLoop", { phase: "exit", reason: "health-gate-failed" });
|
|
122
122
|
return { action: "break", reason: "health-gate-failed" };
|
|
@@ -431,8 +431,15 @@ export async function runDispatch(ic, preData, loopState) {
|
|
|
431
431
|
unitId,
|
|
432
432
|
reason: stuckSignal.reason,
|
|
433
433
|
});
|
|
434
|
+
const stuckDiag = diagnoseExpectedArtifact(unitType, unitId, s.basePath);
|
|
435
|
+
const stuckRemediation = buildLoopRemediationSteps(unitType, unitId, s.basePath);
|
|
436
|
+
const stuckParts = [`Stuck on ${unitType} ${unitId} — ${stuckSignal.reason}.`];
|
|
437
|
+
if (stuckDiag)
|
|
438
|
+
stuckParts.push(`Expected: ${stuckDiag}`);
|
|
439
|
+
if (stuckRemediation)
|
|
440
|
+
stuckParts.push(`To recover:\n${stuckRemediation}`);
|
|
441
|
+
ctx.ui.notify(stuckParts.join(" "), "error");
|
|
434
442
|
await deps.stopAuto(ctx, pi, `Stuck: ${stuckSignal.reason}`);
|
|
435
|
-
ctx.ui.notify(`Stuck on ${unitType} ${unitId} — ${stuckSignal.reason}. The expected artifact was not written.`, "error");
|
|
436
443
|
return { action: "break", reason: "stuck-detected" };
|
|
437
444
|
}
|
|
438
445
|
}
|
|
@@ -19,7 +19,7 @@ import { invalidateAllCaches } from "./cache.js";
|
|
|
19
19
|
import { parseUnitId } from "./unit-id.js";
|
|
20
20
|
import { closeoutUnit } from "./auto-unit-closeout.js";
|
|
21
21
|
import { autoCommitCurrentBranch, } from "./worktree.js";
|
|
22
|
-
import { verifyExpectedArtifact, resolveExpectedArtifactPath, } from "./auto-recovery.js";
|
|
22
|
+
import { verifyExpectedArtifact, resolveExpectedArtifactPath, diagnoseExpectedArtifact, } from "./auto-recovery.js";
|
|
23
23
|
import { regenerateIfMissing } from "./workflow-projections.js";
|
|
24
24
|
import { syncStateToProjectRoot } from "./auto-worktree.js";
|
|
25
25
|
import { isDbAvailable, getTask, getSlice, getMilestone, updateTaskStatus, _getAdapter } from "./gsd-db.js";
|
|
@@ -383,7 +383,8 @@ export async function postUnitPreVerification(pctx, opts) {
|
|
|
383
383
|
// db_unavailable so the artifact was never written. Retrying would
|
|
384
384
|
// produce an infinite re-dispatch loop (#2517).
|
|
385
385
|
debugLog("postUnit", { phase: "artifact-verify-skip-db-unavailable", unitType: s.currentUnit.type, unitId: s.currentUnit.id });
|
|
386
|
-
|
|
386
|
+
const dbSkipDiag = diagnoseExpectedArtifact(s.currentUnit.type, s.currentUnit.id, s.basePath);
|
|
387
|
+
ctx.ui.notify(`Artifact missing for ${s.currentUnit.type} ${s.currentUnit.id} — DB unavailable, skipping retry.${dbSkipDiag ? ` Expected: ${dbSkipDiag}` : ""}`, "error");
|
|
387
388
|
}
|
|
388
389
|
else if (!triggerArtifactVerified) {
|
|
389
390
|
const hasExpectedArtifact = resolveExpectedArtifactPath(s.currentUnit.type, s.currentUnit.id, s.basePath) !== null;
|
|
@@ -391,13 +392,14 @@ export async function postUnitPreVerification(pctx, opts) {
|
|
|
391
392
|
const retryKey = `${s.currentUnit.type}:${s.currentUnit.id}`;
|
|
392
393
|
const attempt = (s.verificationRetryCount.get(retryKey) ?? 0) + 1;
|
|
393
394
|
s.verificationRetryCount.set(retryKey, attempt);
|
|
395
|
+
const retryDiag = diagnoseExpectedArtifact(s.currentUnit.type, s.currentUnit.id, s.basePath);
|
|
394
396
|
s.pendingVerificationRetry = {
|
|
395
397
|
unitId: s.currentUnit.id,
|
|
396
|
-
failureContext: `Artifact verification failed: expected artifact for ${s.currentUnit.type} "${s.currentUnit.id}" was not found on disk after unit execution (attempt ${attempt})
|
|
398
|
+
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}` : ""}`,
|
|
397
399
|
attempt,
|
|
398
400
|
};
|
|
399
401
|
debugLog("postUnit", { phase: "artifact-verify-retry", unitType: s.currentUnit.type, unitId: s.currentUnit.id, attempt });
|
|
400
|
-
ctx.ui.notify(`Artifact missing for ${s.currentUnit.type} ${s.currentUnit.id} — retrying (attempt ${attempt})`, "warning");
|
|
402
|
+
ctx.ui.notify(`Artifact missing for ${s.currentUnit.type} ${s.currentUnit.id} — retrying (attempt ${attempt}).${retryDiag ? ` Expected: ${retryDiag}` : ""}`, "warning");
|
|
401
403
|
return "retry";
|
|
402
404
|
}
|
|
403
405
|
}
|
|
@@ -39,6 +39,7 @@ import { existsSync, mkdirSync, readdirSync, statSync, unlinkSync, } from "node:
|
|
|
39
39
|
import { join } from "node:path";
|
|
40
40
|
import { sep as pathSep } from "node:path";
|
|
41
41
|
import { resolveProjectRootDbPath } from "./bootstrap/dynamic-tools.js";
|
|
42
|
+
import { resolveDefaultSessionModel } from "./preferences-models.js";
|
|
42
43
|
/**
|
|
43
44
|
* Bootstrap a fresh auto-mode session. Handles everything from git init
|
|
44
45
|
* through secrets collection, returning when ready for the first
|
|
@@ -89,12 +90,16 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
|
|
|
89
90
|
}
|
|
90
91
|
// Capture the user's session model before guided-flow dispatch can apply a
|
|
91
92
|
// phase-specific planning model for a discuss turn (#2829).
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
93
|
+
//
|
|
94
|
+
// GSD PREFERENCES.md takes priority over the session model from settings.json
|
|
95
|
+
// (#3517). The session model (ctx.model) comes from findInitialModel() which
|
|
96
|
+
// reads defaultProvider/defaultModel from ~/.gsd/agent/settings.json. When
|
|
97
|
+
// the user has explicit model preferences in PREFERENCES.md, those should win.
|
|
98
|
+
const preferredModel = resolveDefaultSessionModel(ctx.model?.provider);
|
|
99
|
+
const startModelSnapshot = preferredModel
|
|
100
|
+
?? (ctx.model
|
|
101
|
+
? { provider: ctx.model.provider, id: ctx.model.id }
|
|
102
|
+
: null);
|
|
98
103
|
try {
|
|
99
104
|
// Validate GSD_PROJECT_ID early so the user gets immediate feedback
|
|
100
105
|
const customProjectId = process.env.GSD_PROJECT_ID;
|
|
@@ -92,6 +92,10 @@ export function startUnitSupervision(sctx) {
|
|
|
92
92
|
phase: "wrapup-warning-sent",
|
|
93
93
|
wrapupWarningSent: true,
|
|
94
94
|
});
|
|
95
|
+
// Only trigger a new turn if no tools are currently in flight.
|
|
96
|
+
// Triggering during active tool calls causes tool results to be skipped
|
|
97
|
+
// with "Skipped due to queued user message", leading to provider errors (#3512).
|
|
98
|
+
const softTrigger = getInFlightToolCount() === 0;
|
|
95
99
|
pi.sendMessage({
|
|
96
100
|
customType: "gsd-auto-wrapup",
|
|
97
101
|
display: s.verbose,
|
|
@@ -104,7 +108,7 @@ export function startUnitSupervision(sctx) {
|
|
|
104
108
|
"3. mark task or slice state on disk correctly",
|
|
105
109
|
"4. leave precise resume notes if anything remains unfinished",
|
|
106
110
|
].join("\n"),
|
|
107
|
-
}, { triggerTurn:
|
|
111
|
+
}, { triggerTurn: softTrigger });
|
|
108
112
|
}, softTimeoutMs);
|
|
109
113
|
// ── 2. Idle watchdog ──
|
|
110
114
|
s.idleWatchdogHandle = setInterval(async () => {
|
|
@@ -245,6 +249,8 @@ export function startUnitSupervision(sctx) {
|
|
|
245
249
|
if (s.verbose) {
|
|
246
250
|
ctx.ui.notify(`Context at ${contextUsage.percent}% (threshold: ${continueHereThreshold}%) — sending wrap-up signal.`, "info");
|
|
247
251
|
}
|
|
252
|
+
// Only trigger a new turn if no tools are currently in flight (#3512).
|
|
253
|
+
const contextTrigger = getInFlightToolCount() === 0;
|
|
248
254
|
pi.sendMessage({
|
|
249
255
|
customType: "gsd-auto-wrapup",
|
|
250
256
|
display: s.verbose,
|
|
@@ -258,7 +264,7 @@ export function startUnitSupervision(sctx) {
|
|
|
258
264
|
"4. Leave precise resume notes if anything remains unfinished",
|
|
259
265
|
"Do NOT start new sub-tasks or investigations.",
|
|
260
266
|
].join("\n"),
|
|
261
|
-
}, { triggerTurn:
|
|
267
|
+
}, { triggerTurn: contextTrigger });
|
|
262
268
|
if (s.continueHereHandle) {
|
|
263
269
|
clearInterval(s.continueHereHandle);
|
|
264
270
|
s.continueHereHandle = null;
|
|
@@ -142,16 +142,27 @@ export async function runPostUnitVerification(vctx, pauseAuto) {
|
|
|
142
142
|
failureContext: formatFailureContext(result),
|
|
143
143
|
attempt: nextAttempt,
|
|
144
144
|
};
|
|
145
|
-
|
|
145
|
+
const failedCmds = result.checks
|
|
146
|
+
.filter((c) => c.exitCode !== 0)
|
|
147
|
+
.map((c) => c.command);
|
|
148
|
+
const cmdSummary = failedCmds.length <= 3
|
|
149
|
+
? failedCmds.join(", ")
|
|
150
|
+
: `${failedCmds.slice(0, 3).join(", ")}... and ${failedCmds.length - 3} more`;
|
|
151
|
+
ctx.ui.notify(`Verification failed (${cmdSummary}) — auto-fix attempt ${nextAttempt}/${maxRetries}`, "warning");
|
|
146
152
|
// Return "retry" — the autoLoop while loop will re-iterate with the retry context
|
|
147
153
|
return "retry";
|
|
148
154
|
}
|
|
149
155
|
else {
|
|
150
156
|
// Gate failed, retries exhausted
|
|
151
|
-
const exhaustedAttempt = attempt + 1;
|
|
152
157
|
s.verificationRetryCount.delete(s.currentUnit.id);
|
|
153
158
|
s.pendingVerificationRetry = null;
|
|
154
|
-
|
|
159
|
+
const exhaustedFails = result.checks
|
|
160
|
+
.filter((c) => c.exitCode !== 0)
|
|
161
|
+
.map((c) => c.command);
|
|
162
|
+
const exhaustedSummary = exhaustedFails.length <= 3
|
|
163
|
+
? exhaustedFails.join(", ")
|
|
164
|
+
: `${exhaustedFails.slice(0, 3).join(", ")}... and ${exhaustedFails.length - 3} more`;
|
|
165
|
+
ctx.ui.notify(`Verification gate FAILED after ${attempt} ${attempt === 1 ? "retry" : "retries"} (${exhaustedSummary}) — pausing for human review`, "error");
|
|
155
166
|
await pauseAuto(ctx, pi);
|
|
156
167
|
return "pause";
|
|
157
168
|
}
|
|
@@ -234,10 +234,29 @@ export function syncProjectRootToWorktree(projectRoot, worktreePath_, milestoneI
|
|
|
234
234
|
// openDatabase re-creates it, causing "no such table" failures (#2815).
|
|
235
235
|
try {
|
|
236
236
|
const wtDb = join(wtGsd, "gsd.db");
|
|
237
|
+
let deleteSidecars = false;
|
|
237
238
|
if (existsSync(wtDb)) {
|
|
238
239
|
const size = statSync(wtDb).size;
|
|
239
240
|
if (size === 0) {
|
|
240
241
|
unlinkSync(wtDb);
|
|
242
|
+
deleteSidecars = true;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
// Main DB already missing — sidecars are orphaned from a previous
|
|
247
|
+
// partial cleanup and must still be removed.
|
|
248
|
+
deleteSidecars = true;
|
|
249
|
+
}
|
|
250
|
+
// Always clean up WAL/SHM sidecar files when the main DB was deleted
|
|
251
|
+
// or is already missing. Orphaned WAL/SHM files cause SQLite WAL
|
|
252
|
+
// recovery on next open, which triggers a CPU spin on Node 24's
|
|
253
|
+
// node:sqlite DatabaseSync implementation (#2478).
|
|
254
|
+
if (deleteSidecars) {
|
|
255
|
+
for (const suffix of ["-wal", "-shm"]) {
|
|
256
|
+
const f = wtDb + suffix;
|
|
257
|
+
if (existsSync(f)) {
|
|
258
|
+
unlinkSync(f);
|
|
259
|
+
}
|
|
241
260
|
}
|
|
242
261
|
}
|
|
243
262
|
}
|
|
@@ -377,6 +377,18 @@ export async function stopAuto(ctx, pi, reason) {
|
|
|
377
377
|
catch (e) {
|
|
378
378
|
debugLog("stop-cleanup-locks", { error: e instanceof Error ? e.message : String(e) });
|
|
379
379
|
}
|
|
380
|
+
// ── Step 1b: Flush queued follow-up messages (#3512) ──
|
|
381
|
+
// Late async notifications (async_job_result, gsd-auto-wrapup) can trigger
|
|
382
|
+
// extra LLM turns after stop. Flush them the same way run-unit.ts does.
|
|
383
|
+
try {
|
|
384
|
+
const cmdCtxAny = s.cmdCtx;
|
|
385
|
+
if (typeof cmdCtxAny?.clearQueue === "function") {
|
|
386
|
+
cmdCtxAny.clearQueue();
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
catch (e) {
|
|
390
|
+
debugLog("stop-cleanup-queue", { error: e instanceof Error ? e.message : String(e) });
|
|
391
|
+
}
|
|
380
392
|
// ── Step 2: Skill state ──
|
|
381
393
|
try {
|
|
382
394
|
clearSkillSnapshot();
|
|
@@ -589,6 +601,18 @@ export async function pauseAuto(ctx, _pi, _errorContext) {
|
|
|
589
601
|
if (!s.active)
|
|
590
602
|
return;
|
|
591
603
|
clearUnitTimeout();
|
|
604
|
+
// Flush queued follow-up messages (#3512).
|
|
605
|
+
// Late async notifications (async_job_result, gsd-auto-wrapup) can trigger
|
|
606
|
+
// extra LLM turns after pause. Flush them the same way run-unit.ts does.
|
|
607
|
+
try {
|
|
608
|
+
const cmdCtxAny = s.cmdCtx;
|
|
609
|
+
if (typeof cmdCtxAny?.clearQueue === "function") {
|
|
610
|
+
cmdCtxAny.clearQueue();
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
catch (e) {
|
|
614
|
+
debugLog("pause-cleanup-queue", { error: e instanceof Error ? e.message : String(e) });
|
|
615
|
+
}
|
|
592
616
|
// Unblock any pending unit promise so the auto-loop is not orphaned.
|
|
593
617
|
// Pass errorContext so runUnitPhase can distinguish user-initiated pause
|
|
594
618
|
// from provider-error pause and avoid hard-stopping (#2762).
|
|
@@ -14,6 +14,7 @@ import { getAutoDashboardData, isAutoActive, isAutoPaused, markToolEnd, markTool
|
|
|
14
14
|
import { isParallelActive, shutdownParallel } from "../parallel-orchestrator.js";
|
|
15
15
|
import { checkToolCallLoop, resetToolCallLoopGuard } from "./tool-call-loop-guard.js";
|
|
16
16
|
import { saveActivityLog } from "../activity-log.js";
|
|
17
|
+
import { resetAskUserQuestionsCache } from "../../ask-user-questions.js";
|
|
17
18
|
// Skip the welcome screen on the very first session_start — cli.ts already
|
|
18
19
|
// printed it before the TUI launched. Only re-print on /clear (subsequent sessions).
|
|
19
20
|
let isFirstSession = true;
|
|
@@ -25,6 +26,7 @@ export function registerHooks(pi) {
|
|
|
25
26
|
pi.on("session_start", async (_event, ctx) => {
|
|
26
27
|
resetWriteGateState();
|
|
27
28
|
resetToolCallLoopGuard();
|
|
29
|
+
resetAskUserQuestionsCache();
|
|
28
30
|
await syncServiceTierStatus(ctx);
|
|
29
31
|
// Apply show_token_cost preference (#1515)
|
|
30
32
|
try {
|
|
@@ -60,6 +62,7 @@ export function registerHooks(pi) {
|
|
|
60
62
|
pi.on("session_switch", async (_event, ctx) => {
|
|
61
63
|
resetWriteGateState();
|
|
62
64
|
resetToolCallLoopGuard();
|
|
65
|
+
resetAskUserQuestionsCache();
|
|
63
66
|
clearDiscussionFlowState();
|
|
64
67
|
await syncServiceTierStatus(ctx);
|
|
65
68
|
loadToolApiKeys();
|
|
@@ -69,6 +72,7 @@ export function registerHooks(pi) {
|
|
|
69
72
|
});
|
|
70
73
|
pi.on("agent_end", async (event, ctx) => {
|
|
71
74
|
resetToolCallLoopGuard();
|
|
75
|
+
resetAskUserQuestionsCache();
|
|
72
76
|
await handleAgentEnd(pi, event, ctx);
|
|
73
77
|
});
|
|
74
78
|
// Squash-merge quick-task branch back to the original branch after the
|
|
@@ -13,8 +13,12 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { createHash } from "node:crypto";
|
|
15
15
|
const MAX_CONSECUTIVE_IDENTICAL_CALLS = 4;
|
|
16
|
+
/** Interactive/user-facing tools where even 1 duplicate is confusing. */
|
|
17
|
+
const STRICT_LOOP_TOOLS = new Set(["ask_user_questions"]);
|
|
18
|
+
const MAX_CONSECUTIVE_STRICT = 1;
|
|
16
19
|
let consecutiveCount = 0;
|
|
17
20
|
let lastSignature = "";
|
|
21
|
+
let lastToolName = "";
|
|
18
22
|
let enabled = true;
|
|
19
23
|
/** Hash tool name + args into a compact signature for comparison. */
|
|
20
24
|
function hashToolCall(toolName, args) {
|
|
@@ -45,8 +49,12 @@ export function checkToolCallLoop(toolName, args) {
|
|
|
45
49
|
else {
|
|
46
50
|
consecutiveCount = 1;
|
|
47
51
|
lastSignature = sig;
|
|
52
|
+
lastToolName = toolName;
|
|
48
53
|
}
|
|
49
|
-
|
|
54
|
+
const threshold = STRICT_LOOP_TOOLS.has(toolName)
|
|
55
|
+
? MAX_CONSECUTIVE_STRICT
|
|
56
|
+
: MAX_CONSECUTIVE_IDENTICAL_CALLS;
|
|
57
|
+
if (consecutiveCount > threshold) {
|
|
50
58
|
return {
|
|
51
59
|
block: true,
|
|
52
60
|
reason: `Tool loop detected: ${toolName} called ${consecutiveCount} times ` +
|
|
@@ -61,6 +69,7 @@ export function checkToolCallLoop(toolName, args) {
|
|
|
61
69
|
export function resetToolCallLoopGuard() {
|
|
62
70
|
consecutiveCount = 0;
|
|
63
71
|
lastSignature = "";
|
|
72
|
+
lastToolName = "";
|
|
64
73
|
enabled = true;
|
|
65
74
|
}
|
|
66
75
|
/** Disable the guard (e.g. during shutdown). */
|
|
@@ -68,6 +77,7 @@ export function disableToolCallLoopGuard() {
|
|
|
68
77
|
enabled = false;
|
|
69
78
|
consecutiveCount = 0;
|
|
70
79
|
lastSignature = "";
|
|
80
|
+
lastToolName = "";
|
|
71
81
|
}
|
|
72
82
|
/** Get current consecutive count for diagnostics. */
|
|
73
83
|
export function getToolCallLoopCount() {
|
|
@@ -11,7 +11,8 @@ import { gsdRoot } from "./paths.js";
|
|
|
11
11
|
import { appendCapture, hasPendingCaptures, loadPendingCaptures } from "./captures.js";
|
|
12
12
|
import { appendOverride, appendKnowledge } from "./files.js";
|
|
13
13
|
import { formatDoctorIssuesForPrompt, formatDoctorReport, formatDoctorReportJson, runGSDDoctor, selectDoctorScope, filterDoctorIssues, } from "./doctor.js";
|
|
14
|
-
import { isAutoActive } from "./auto.js";
|
|
14
|
+
import { isAutoActive, checkRemoteAutoSession } from "./auto.js";
|
|
15
|
+
import { getAutoWorktreePath } from "./auto-worktree.js";
|
|
15
16
|
import { projectRoot } from "./commands/context.js";
|
|
16
17
|
import { loadPrompt } from "./prompt-loader.js";
|
|
17
18
|
export function dispatchDoctorHeal(pi, scope, reportText, structuredIssues) {
|
|
@@ -172,7 +173,17 @@ export async function handleSteer(change, ctx, pi) {
|
|
|
172
173
|
const sid = state.activeSlice?.id ?? "none";
|
|
173
174
|
const tid = state.activeTask?.id ?? "none";
|
|
174
175
|
const appliedAt = `${mid}/${sid}/${tid}`;
|
|
175
|
-
|
|
176
|
+
// Resolve the correct target path: only route to a worktree when auto-mode
|
|
177
|
+
// is actively running there (in-process or remote). A worktree directory may
|
|
178
|
+
// exist from a previous session without being the active runtime path —
|
|
179
|
+
// writing there without a live session would silently drop the override.
|
|
180
|
+
const autoRunning = isAutoActive() || checkRemoteAutoSession(basePath).running;
|
|
181
|
+
const wtPath = autoRunning && mid !== "none"
|
|
182
|
+
? getAutoWorktreePath(basePath, mid)
|
|
183
|
+
: null;
|
|
184
|
+
const targetPath = wtPath ?? basePath;
|
|
185
|
+
await appendOverride(targetPath, change, appliedAt);
|
|
186
|
+
const overrideLoc = wtPath ? "worktree `.gsd/OVERRIDES.md`" : "`.gsd/OVERRIDES.md`";
|
|
176
187
|
if (isAutoActive()) {
|
|
177
188
|
pi.sendMessage({
|
|
178
189
|
customType: "gsd-hard-steer",
|
|
@@ -181,14 +192,14 @@ export async function handleSteer(change, ctx, pi) {
|
|
|
181
192
|
"",
|
|
182
193
|
`**Override:** ${change}`,
|
|
183
194
|
"",
|
|
184
|
-
|
|
195
|
+
`This override has been saved to ${overrideLoc} and will be injected into all future task prompts.`,
|
|
185
196
|
"A document rewrite unit will run before the next task to propagate this change across all active plan documents.",
|
|
186
197
|
"",
|
|
187
198
|
"If you are mid-task, finish your current work respecting this override. The next dispatched unit will be a document rewrite.",
|
|
188
199
|
].join("\n"),
|
|
189
200
|
display: false,
|
|
190
201
|
}, { triggerTurn: true });
|
|
191
|
-
ctx.ui.notify(`Override registered: "${change}". Will be applied before next task dispatch.`, "info");
|
|
202
|
+
ctx.ui.notify(`Override registered (${overrideLoc}): "${change}". Will be applied before next task dispatch.`, "info");
|
|
192
203
|
}
|
|
193
204
|
else {
|
|
194
205
|
pi.sendMessage({
|
|
@@ -198,13 +209,13 @@ export async function handleSteer(change, ctx, pi) {
|
|
|
198
209
|
"",
|
|
199
210
|
`**Override:** ${change}`,
|
|
200
211
|
"",
|
|
201
|
-
|
|
202
|
-
|
|
212
|
+
`This override has been saved to ${overrideLoc}.`,
|
|
213
|
+
`Before continuing, read ${overrideLoc} and update the current plan documents to reflect this change.`,
|
|
203
214
|
"Focus on: active slice plan, incomplete task plans, and DECISIONS.md.",
|
|
204
215
|
].join("\n"),
|
|
205
216
|
display: false,
|
|
206
217
|
}, { triggerTurn: true });
|
|
207
|
-
ctx.ui.notify(`Override registered: "${change}". Update plan documents to reflect this change.`, "info");
|
|
218
|
+
ctx.ui.notify(`Override registered (${overrideLoc}): "${change}". Update plan documents to reflect this change.`, "info");
|
|
208
219
|
}
|
|
209
220
|
}
|
|
210
221
|
export async function handleKnowledge(args, ctx) {
|
|
@@ -11,7 +11,7 @@ import { resolve } from 'node:path';
|
|
|
11
11
|
import { readFileSync, existsSync, statSync } from 'node:fs';
|
|
12
12
|
import { resolveGsdRootFile } from './paths.js';
|
|
13
13
|
import { saveFile } from './files.js';
|
|
14
|
-
import { GSDError, GSD_IO_ERROR } from './errors.js';
|
|
14
|
+
import { GSDError, GSD_STALE_STATE, GSD_IO_ERROR } from './errors.js';
|
|
15
15
|
import { logWarning, logError } from './workflow-logger.js';
|
|
16
16
|
import { invalidateStateCache } from './state.js';
|
|
17
17
|
import { clearPathCache } from './paths.js';
|
|
@@ -234,27 +234,44 @@ export async function nextRequirementId() {
|
|
|
234
234
|
/**
|
|
235
235
|
* Save a new requirement to DB and regenerate REQUIREMENTS.md.
|
|
236
236
|
* Auto-assigns the next ID via nextRequirementId().
|
|
237
|
+
*
|
|
238
|
+
* The ID computation and insert are wrapped in a single transaction
|
|
239
|
+
* to prevent parallel race conditions (same pattern as saveDecisionToDb).
|
|
240
|
+
*
|
|
237
241
|
* Returns the assigned ID.
|
|
238
242
|
*/
|
|
239
243
|
export async function saveRequirementToDb(fields, basePath) {
|
|
240
244
|
try {
|
|
241
245
|
const db = await import('./gsd-db.js');
|
|
242
|
-
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
246
|
+
// Atomic ID assignment + insert inside a transaction.
|
|
247
|
+
const id = db.transaction(() => {
|
|
248
|
+
const adapter = db._getAdapter();
|
|
249
|
+
if (!adapter)
|
|
250
|
+
throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
|
|
251
|
+
const row = adapter
|
|
252
|
+
.prepare('SELECT MAX(CAST(SUBSTR(id, 2) AS INTEGER)) as max_num FROM requirements')
|
|
253
|
+
.get();
|
|
254
|
+
const maxNum = row ? row['max_num'] : null;
|
|
255
|
+
const nextId = (maxNum == null || isNaN(maxNum))
|
|
256
|
+
? 'R001'
|
|
257
|
+
: `R${String(maxNum + 1).padStart(3, '0')}`;
|
|
258
|
+
const requirement = {
|
|
259
|
+
id: nextId,
|
|
260
|
+
class: fields.class,
|
|
261
|
+
status: fields.status ?? 'active',
|
|
262
|
+
description: fields.description,
|
|
263
|
+
why: fields.why,
|
|
264
|
+
source: fields.source,
|
|
265
|
+
primary_owner: fields.primary_owner ?? '',
|
|
266
|
+
supporting_slices: fields.supporting_slices ?? '',
|
|
267
|
+
validation: fields.validation ?? '',
|
|
268
|
+
notes: fields.notes ?? '',
|
|
269
|
+
full_content: '',
|
|
270
|
+
superseded_by: null,
|
|
271
|
+
};
|
|
272
|
+
db.upsertRequirement(requirement);
|
|
273
|
+
return nextId;
|
|
274
|
+
});
|
|
258
275
|
// Fetch all requirements for full file regeneration
|
|
259
276
|
const adapter = db._getAdapter();
|
|
260
277
|
let allRequirements = [];
|
|
@@ -300,22 +317,41 @@ export async function saveRequirementToDb(fields, basePath) {
|
|
|
300
317
|
/**
|
|
301
318
|
* Save a new decision to DB and regenerate DECISIONS.md.
|
|
302
319
|
* Auto-assigns the next ID via nextDecisionId().
|
|
320
|
+
*
|
|
321
|
+
* The ID computation (SELECT MAX) and insert are wrapped in a single
|
|
322
|
+
* transaction to prevent parallel tool calls from computing the same ID
|
|
323
|
+
* and silently overwriting each other (#3326, #3339, #3459).
|
|
324
|
+
*
|
|
303
325
|
* Returns the assigned ID.
|
|
304
326
|
*/
|
|
305
327
|
export async function saveDecisionToDb(fields, basePath) {
|
|
306
328
|
try {
|
|
307
329
|
const db = await import('./gsd-db.js');
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
330
|
+
// Atomic ID assignment + insert inside a transaction to prevent
|
|
331
|
+
// parallel calls from racing on the same MAX(id) value.
|
|
332
|
+
const id = db.transaction(() => {
|
|
333
|
+
const adapter = db._getAdapter();
|
|
334
|
+
if (!adapter)
|
|
335
|
+
throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
|
|
336
|
+
const row = adapter
|
|
337
|
+
.prepare('SELECT MAX(CAST(SUBSTR(id, 2) AS INTEGER)) as max_num FROM decisions')
|
|
338
|
+
.get();
|
|
339
|
+
const maxNum = row ? row['max_num'] : null;
|
|
340
|
+
const nextId = (maxNum == null || isNaN(maxNum))
|
|
341
|
+
? 'D001'
|
|
342
|
+
: `D${String(maxNum + 1).padStart(3, '0')}`;
|
|
343
|
+
db.upsertDecision({
|
|
344
|
+
id: nextId,
|
|
345
|
+
when_context: fields.when_context ?? '',
|
|
346
|
+
scope: fields.scope,
|
|
347
|
+
decision: fields.decision,
|
|
348
|
+
choice: fields.choice,
|
|
349
|
+
rationale: fields.rationale,
|
|
350
|
+
revisable: fields.revisable ?? 'Yes',
|
|
351
|
+
made_by: fields.made_by ?? 'agent',
|
|
352
|
+
superseded_by: null,
|
|
353
|
+
});
|
|
354
|
+
return nextId;
|
|
319
355
|
});
|
|
320
356
|
// Fetch all decisions (including superseded for the full register)
|
|
321
357
|
const adapter = db._getAdapter();
|