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.
Files changed (244) hide show
  1. package/dist/resources/extensions/ask-user-questions.js +47 -3
  2. package/dist/resources/extensions/gsd/auto/loop.js +8 -1
  3. package/dist/resources/extensions/gsd/auto/phases.js +10 -3
  4. package/dist/resources/extensions/gsd/auto-post-unit.js +6 -4
  5. package/dist/resources/extensions/gsd/auto-start.js +11 -6
  6. package/dist/resources/extensions/gsd/auto-timers.js +8 -2
  7. package/dist/resources/extensions/gsd/auto-verification.js +14 -3
  8. package/dist/resources/extensions/gsd/auto-worktree.js +19 -0
  9. package/dist/resources/extensions/gsd/auto.js +24 -0
  10. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +4 -0
  11. package/dist/resources/extensions/gsd/bootstrap/tool-call-loop-guard.js +11 -1
  12. package/dist/resources/extensions/gsd/commands-handlers.js +18 -7
  13. package/dist/resources/extensions/gsd/db-writer.js +64 -28
  14. package/dist/resources/extensions/gsd/preferences-models.js +74 -0
  15. package/dist/resources/extensions/gsd/preferences-skills.js +6 -1
  16. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
  17. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
  18. package/dist/resources/extensions/gsd/skill-catalog.js +6 -4
  19. package/dist/resources/extensions/gsd/skill-discovery.js +24 -6
  20. package/dist/resources/extensions/gsd/skill-health.js +7 -3
  21. package/dist/resources/extensions/gsd/skill-telemetry.js +5 -2
  22. package/dist/resources/extensions/gsd/state.js +1 -0
  23. package/dist/resources/extensions/gsd/tools/complete-slice.js +3 -3
  24. package/dist/resources/extensions/gsd/workflow-logger.js +13 -8
  25. package/dist/resources/extensions/gsd/workflow-reconcile.js +3 -1
  26. package/dist/web/standalone/.next/BUILD_ID +1 -1
  27. package/dist/web/standalone/.next/app-path-routes-manifest.json +17 -17
  28. package/dist/web/standalone/.next/build-manifest.json +3 -3
  29. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  30. package/dist/web/standalone/.next/required-server-files.json +3 -3
  31. package/dist/web/standalone/.next/server/app/_global-error/page.js +3 -3
  32. package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  33. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  34. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_not-found/page.js +2 -2
  42. package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  43. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  44. package/dist/web/standalone/.next/server/app/_not-found.rsc +3 -3
  45. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  46. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +3 -3
  48. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
  52. package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
  53. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js +1 -1
  54. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
  55. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js +1 -1
  56. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
  57. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js +2 -2
  58. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
  59. package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
  60. package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
  61. package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
  62. package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
  63. package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
  64. package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
  65. package/dist/web/standalone/.next/server/app/api/dev-mode/route.js +1 -1
  66. package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
  67. package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
  68. package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
  69. package/dist/web/standalone/.next/server/app/api/experimental/route.js +2 -2
  70. package/dist/web/standalone/.next/server/app/api/experimental/route_client-reference-manifest.js +1 -1
  71. package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
  72. package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
  73. package/dist/web/standalone/.next/server/app/api/files/route.js +1 -1
  74. package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
  75. package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
  76. package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
  77. package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
  78. package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
  79. package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
  80. package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
  81. package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
  82. package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
  83. package/dist/web/standalone/.next/server/app/api/inspect/route.js +1 -1
  84. package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
  85. package/dist/web/standalone/.next/server/app/api/knowledge/route.js +1 -1
  86. package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
  87. package/dist/web/standalone/.next/server/app/api/live-state/route.js +1 -1
  88. package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
  89. package/dist/web/standalone/.next/server/app/api/onboarding/route.js +1 -1
  90. package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
  91. package/dist/web/standalone/.next/server/app/api/preferences/route.js +1 -1
  92. package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
  93. package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
  94. package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
  95. package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
  96. package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
  97. package/dist/web/standalone/.next/server/app/api/remote-questions/route.js +2 -2
  98. package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
  99. package/dist/web/standalone/.next/server/app/api/session/browser/route.js +1 -1
  100. package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
  101. package/dist/web/standalone/.next/server/app/api/session/command/route.js +1 -1
  102. package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
  103. package/dist/web/standalone/.next/server/app/api/session/events/route.js +2 -2
  104. package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
  105. package/dist/web/standalone/.next/server/app/api/session/manage/route.js +1 -1
  106. package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
  107. package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
  108. package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
  109. package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
  110. package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
  111. package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
  112. package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
  113. package/dist/web/standalone/.next/server/app/api/steer/route.js +1 -1
  114. package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
  115. package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
  116. package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
  117. package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +2 -2
  118. package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
  119. package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +2 -2
  120. package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
  121. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js +2 -2
  122. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
  123. package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js +4 -4
  124. package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
  125. package/dist/web/standalone/.next/server/app/api/terminal/upload/route.js +1 -1
  126. package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
  127. package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
  128. package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
  129. package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
  130. package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
  131. package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
  132. package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
  133. package/dist/web/standalone/.next/server/app/index.html +1 -1
  134. package/dist/web/standalone/.next/server/app/index.rsc +4 -4
  135. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  136. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +4 -4
  137. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  138. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +3 -3
  139. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  140. package/dist/web/standalone/.next/server/app/page.js +2 -2
  141. package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  142. package/dist/web/standalone/.next/server/app-paths-manifest.json +17 -17
  143. package/dist/web/standalone/.next/server/chunks/2229.js +1 -1
  144. package/dist/web/standalone/.next/server/chunks/7471.js +3 -3
  145. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  146. package/dist/web/standalone/.next/server/middleware.js +2 -2
  147. package/dist/web/standalone/.next/server/next-font-manifest.js +1 -1
  148. package/dist/web/standalone/.next/server/next-font-manifest.json +1 -1
  149. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  150. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  151. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  152. package/dist/web/standalone/.next/static/chunks/app/_not-found/{page-2f24283c162b6ab3.js → page-f2a7482d42a5614b.js} +1 -1
  153. package/dist/web/standalone/.next/static/chunks/app/{layout-9ecfd95f343793f0.js → layout-a16c7a7ecdf0c2cf.js} +1 -1
  154. package/dist/web/standalone/.next/static/chunks/app/page-0c485498795110d6.js +1 -0
  155. package/dist/web/standalone/.next/static/chunks/main-app-fdab67f7802d7832.js +1 -0
  156. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-459824ffb8c323dd.js +1 -0
  157. package/dist/web/standalone/node_modules/node-pty/build/Makefile +2 -2
  158. package/dist/web/standalone/node_modules/node-pty/build/Release/pty.node +0 -0
  159. package/dist/web/standalone/node_modules/node-pty/build/pty.target.mk +14 -14
  160. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api.target.mk +14 -14
  161. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_except.target.mk +14 -14
  162. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_maybe.target.mk +14 -14
  163. package/dist/web/standalone/server.js +1 -1
  164. package/package.json +1 -1
  165. package/packages/mcp-server/src/cli.ts +1 -1
  166. package/packages/mcp-server/src/index.ts +15 -1
  167. package/packages/mcp-server/src/readers/captures.ts +119 -0
  168. package/packages/mcp-server/src/readers/doctor-lite.ts +225 -0
  169. package/packages/mcp-server/src/readers/index.ts +16 -0
  170. package/packages/mcp-server/src/readers/knowledge.ts +111 -0
  171. package/packages/mcp-server/src/readers/metrics.ts +118 -0
  172. package/packages/mcp-server/src/readers/paths.ts +217 -0
  173. package/packages/mcp-server/src/readers/readers.test.ts +509 -0
  174. package/packages/mcp-server/src/readers/roadmap.ts +263 -0
  175. package/packages/mcp-server/src/readers/state.ts +223 -0
  176. package/packages/mcp-server/src/server.ts +134 -3
  177. package/packages/pi-ai/dist/utils/repair-tool-json.d.ts +26 -6
  178. package/packages/pi-ai/dist/utils/repair-tool-json.d.ts.map +1 -1
  179. package/packages/pi-ai/dist/utils/repair-tool-json.js +67 -9
  180. package/packages/pi-ai/dist/utils/repair-tool-json.js.map +1 -1
  181. package/packages/pi-ai/dist/utils/tests/repair-tool-json.test.js +73 -1
  182. package/packages/pi-ai/dist/utils/tests/repair-tool-json.test.js.map +1 -1
  183. package/packages/pi-ai/src/utils/repair-tool-json.ts +74 -10
  184. package/packages/pi-ai/src/utils/tests/repair-tool-json.test.ts +94 -1
  185. package/packages/pi-coding-agent/dist/core/agent-session-model-switch.test.d.ts +2 -0
  186. package/packages/pi-coding-agent/dist/core/agent-session-model-switch.test.d.ts.map +1 -0
  187. package/packages/pi-coding-agent/dist/core/agent-session-model-switch.test.js +16 -0
  188. package/packages/pi-coding-agent/dist/core/agent-session-model-switch.test.js.map +1 -0
  189. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  190. package/packages/pi-coding-agent/dist/core/agent-session.js +4 -0
  191. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  192. package/packages/pi-coding-agent/dist/core/retry-handler.d.ts +3 -0
  193. package/packages/pi-coding-agent/dist/core/retry-handler.d.ts.map +1 -1
  194. package/packages/pi-coding-agent/dist/core/retry-handler.js +48 -16
  195. package/packages/pi-coding-agent/dist/core/retry-handler.js.map +1 -1
  196. package/packages/pi-coding-agent/dist/core/retry-handler.test.js +20 -3
  197. package/packages/pi-coding-agent/dist/core/retry-handler.test.js.map +1 -1
  198. package/packages/pi-coding-agent/package.json +1 -1
  199. package/packages/pi-coding-agent/src/core/agent-session-model-switch.test.ts +21 -0
  200. package/packages/pi-coding-agent/src/core/agent-session.ts +4 -0
  201. package/packages/pi-coding-agent/src/core/retry-handler.test.ts +30 -3
  202. package/packages/pi-coding-agent/src/core/retry-handler.ts +49 -16
  203. package/pkg/package.json +1 -1
  204. package/src/resources/extensions/ask-user-questions.ts +60 -4
  205. package/src/resources/extensions/gsd/auto/loop.ts +8 -1
  206. package/src/resources/extensions/gsd/auto/phases.ts +8 -6
  207. package/src/resources/extensions/gsd/auto-post-unit.ts +6 -3
  208. package/src/resources/extensions/gsd/auto-start.ts +11 -6
  209. package/src/resources/extensions/gsd/auto-timers.ts +8 -2
  210. package/src/resources/extensions/gsd/auto-verification.ts +14 -3
  211. package/src/resources/extensions/gsd/auto-worktree.ts +18 -0
  212. package/src/resources/extensions/gsd/auto.ts +25 -0
  213. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +4 -0
  214. package/src/resources/extensions/gsd/bootstrap/tool-call-loop-guard.ts +13 -1
  215. package/src/resources/extensions/gsd/commands-handlers.ts +20 -7
  216. package/src/resources/extensions/gsd/db-writer.ts +67 -30
  217. package/src/resources/extensions/gsd/preferences-models.ts +78 -0
  218. package/src/resources/extensions/gsd/preferences-skills.ts +6 -1
  219. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
  220. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
  221. package/src/resources/extensions/gsd/skill-catalog.ts +6 -3
  222. package/src/resources/extensions/gsd/skill-discovery.ts +23 -6
  223. package/src/resources/extensions/gsd/skill-health.ts +7 -3
  224. package/src/resources/extensions/gsd/skill-telemetry.ts +5 -2
  225. package/src/resources/extensions/gsd/state.ts +1 -0
  226. package/src/resources/extensions/gsd/tests/ask-user-questions-dedup.test.ts +120 -0
  227. package/src/resources/extensions/gsd/tests/auto-start-model-capture.test.ts +22 -2
  228. package/src/resources/extensions/gsd/tests/auto-wrapup-inflight-guard.test.ts +107 -0
  229. package/src/resources/extensions/gsd/tests/claude-skill-dirs.test.ts +51 -0
  230. package/src/resources/extensions/gsd/tests/db-writer.test.ts +41 -0
  231. package/src/resources/extensions/gsd/tests/model-isolation.test.ts +75 -1
  232. package/src/resources/extensions/gsd/tests/steer-worktree-path.test.ts +108 -0
  233. package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +17 -4
  234. package/src/resources/extensions/gsd/tests/workflow-logger.test.ts +17 -41
  235. package/src/resources/extensions/gsd/tests/worktree-db-respawn-truncation.test.ts +81 -2
  236. package/src/resources/extensions/gsd/tools/complete-slice.ts +3 -5
  237. package/src/resources/extensions/gsd/workflow-logger.ts +13 -8
  238. package/src/resources/extensions/gsd/workflow-reconcile.ts +3 -1
  239. package/src/resources/extensions/shared/tests/ask-user-freetext.test.ts +6 -1
  240. package/dist/web/standalone/.next/static/chunks/app/page-62be3b5fa91e4c8f.js +0 -1
  241. package/dist/web/standalone/.next/static/chunks/main-app-d3d4c336195465f9.js +0 -1
  242. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-ab5a8926e07ec673.js +0 -1
  243. /package/dist/web/standalone/.next/static/{F4rzqt_3m83A68ZRiU12r → erQZ_8_1lkclnPJLJnCxG}/_buildManifest.js +0 -0
  244. /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
- setTimeout(() => {
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
- setTimeout(() => {
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
- setTimeout(() => {
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
- this._retryAbortController?.abort();
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
- setTimeout(() => {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glittercowboy/gsd",
3
- "version": "2.62.0",
3
+ "version": "2.62.1",
4
4
  "piConfig": {
5
5
  "name": "gsd",
6
6
  "configDir": ".gsd"
@@ -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) return { ...remoteResult, details: remoteResult.details as unknown };
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
- return {
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
- return {
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. Last: ${msg}`,
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 ?? "Pre-dispatch health check failed.",
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} but DB is unavailable skipping retry to avoid loop (#2517)`,
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
- const startModelSnapshot = ctx.model
159
- ? {
160
- provider: ctx.model.provider,
161
- id: ctx.model.id,
162
- }
163
- : null;
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: true },
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: true },
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 ${exhaustedAttempt > maxRetries ? exhaustedAttempt - 1 : exhaustedAttempt} retries — pausing for human review`,
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
- if (consecutiveCount > MAX_CONSECUTIVE_IDENTICAL_CALLS) {
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. */