gsd-pi 2.80.0-dev.2848d9009 → 2.80.0-dev.42bd34838

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 (241) hide show
  1. package/README.md +4 -2
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/gsd/auto/loop.js +32 -1
  4. package/dist/resources/extensions/gsd/auto/phases.js +29 -15
  5. package/dist/resources/extensions/gsd/auto/resolve.js +17 -0
  6. package/dist/resources/extensions/gsd/auto/run-unit.js +13 -1
  7. package/dist/resources/extensions/gsd/auto-prompts.js +13 -1
  8. package/dist/resources/extensions/gsd/auto-recovery.js +43 -1
  9. package/dist/resources/extensions/gsd/auto-supervisor.js +8 -1
  10. package/dist/resources/extensions/gsd/auto-timeout-recovery.js +2 -2
  11. package/dist/resources/extensions/gsd/auto.js +68 -4
  12. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +21 -2
  13. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +27 -20
  14. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +21 -0
  15. package/dist/resources/extensions/gsd/context-budget.js +37 -2
  16. package/dist/resources/extensions/gsd/crash-recovery.js +56 -10
  17. package/dist/resources/extensions/gsd/db/unit-dispatches.js +39 -0
  18. package/dist/resources/extensions/gsd/db-base-schema.js +4 -2
  19. package/dist/resources/extensions/gsd/db-migration-steps.js +6 -0
  20. package/dist/resources/extensions/gsd/git-service.js +36 -4
  21. package/dist/resources/extensions/gsd/gsd-db.js +46 -13
  22. package/dist/resources/extensions/gsd/guided-flow.js +52 -10
  23. package/dist/resources/extensions/gsd/memory-store.js +69 -12
  24. package/dist/resources/extensions/gsd/migrate/command.js +40 -1
  25. package/dist/resources/extensions/gsd/migration-auto-check.js +87 -0
  26. package/dist/resources/extensions/gsd/planning-path-scope.js +26 -0
  27. package/dist/resources/extensions/gsd/prompt-loader.js +28 -2
  28. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +14 -13
  29. package/dist/resources/extensions/gsd/prompts/parallel-research-slices.md +1 -1
  30. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  31. package/dist/resources/extensions/gsd/prompts/quick-task.md +1 -5
  32. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +2 -2
  33. package/dist/resources/extensions/gsd/quick.js +34 -2
  34. package/dist/resources/extensions/gsd/tools/context-mode-tool-result.js +15 -0
  35. package/dist/resources/extensions/gsd/tools/exec-search-tool.js +5 -0
  36. package/dist/resources/extensions/gsd/tools/exec-tool.js +3 -15
  37. package/dist/resources/extensions/gsd/tools/memory-tools.js +1 -0
  38. package/dist/resources/extensions/gsd/tools/plan-slice.js +9 -0
  39. package/dist/resources/extensions/gsd/tools/plan-task.js +9 -0
  40. package/dist/resources/extensions/gsd/tools/resume-tool.js +5 -0
  41. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +1 -1
  42. package/dist/resources/extensions/gsd/unit-context-composer.js +12 -3
  43. package/dist/resources/extensions/gsd/unit-runtime.js +22 -0
  44. package/dist/resources/extensions/gsd/worktree-resolver.js +33 -17
  45. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  46. package/dist/web/standalone/.next/BUILD_ID +1 -1
  47. package/dist/web/standalone/.next/app-path-routes-manifest.json +11 -11
  48. package/dist/web/standalone/.next/build-manifest.json +2 -2
  49. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  50. package/dist/web/standalone/.next/required-server-files.json +1 -1
  51. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  52. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/index.html +1 -1
  68. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  73. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  74. package/dist/web/standalone/.next/server/app-paths-manifest.json +11 -11
  75. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  76. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  77. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  78. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  79. package/dist/web/standalone/server.js +1 -1
  80. package/package.json +3 -3
  81. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  82. package/packages/mcp-server/dist/workflow-tools.js +22 -17
  83. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  84. package/packages/mcp-server/src/workflow-tools.test.ts +75 -2
  85. package/packages/mcp-server/src/workflow-tools.ts +30 -16
  86. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  87. package/packages/native/tsconfig.tsbuildinfo +1 -1
  88. package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js +32 -0
  89. package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js.map +1 -1
  90. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  91. package/packages/pi-coding-agent/dist/core/agent-session.js +8 -0
  92. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  93. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js +3 -1
  94. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -1
  95. package/packages/pi-coding-agent/dist/core/compaction/compaction.d.ts +11 -0
  96. package/packages/pi-coding-agent/dist/core/compaction/compaction.d.ts.map +1 -1
  97. package/packages/pi-coding-agent/dist/core/compaction/compaction.js +9 -0
  98. package/packages/pi-coding-agent/dist/core/compaction/compaction.js.map +1 -1
  99. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.d.ts +2 -0
  100. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.d.ts.map +1 -0
  101. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.js +103 -0
  102. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.js.map +1 -0
  103. package/packages/pi-coding-agent/dist/core/db-snapshot.d.ts +15 -0
  104. package/packages/pi-coding-agent/dist/core/db-snapshot.d.ts.map +1 -0
  105. package/packages/pi-coding-agent/dist/core/db-snapshot.js +66 -0
  106. package/packages/pi-coding-agent/dist/core/db-snapshot.js.map +1 -0
  107. package/packages/pi-coding-agent/dist/core/db-snapshot.test.d.ts +2 -0
  108. package/packages/pi-coding-agent/dist/core/db-snapshot.test.d.ts.map +1 -0
  109. package/packages/pi-coding-agent/dist/core/db-snapshot.test.js +24 -0
  110. package/packages/pi-coding-agent/dist/core/db-snapshot.test.js.map +1 -0
  111. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +1 -0
  112. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
  113. package/packages/pi-coding-agent/dist/core/extensions/runner.js +3 -0
  114. package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
  115. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js +2 -0
  116. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js.map +1 -1
  117. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +7 -0
  118. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  119. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  120. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +20 -0
  121. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  122. package/packages/pi-coding-agent/dist/core/settings-manager.js +25 -0
  123. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  124. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +1 -0
  125. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  126. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +3 -0
  127. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  128. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  129. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +13 -5
  130. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  131. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.js +53 -0
  132. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.js.map +1 -1
  133. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  134. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +3 -0
  135. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  136. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage-safety-guard.test.d.ts +2 -0
  137. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage-safety-guard.test.d.ts.map +1 -0
  138. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage-safety-guard.test.js +10 -0
  139. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage-safety-guard.test.js.map +1 -0
  140. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.d.ts.map +1 -1
  141. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.js +3 -2
  142. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.js.map +1 -1
  143. package/packages/pi-coding-agent/src/core/agent-session-abort-order.test.ts +36 -0
  144. package/packages/pi-coding-agent/src/core/agent-session.ts +8 -0
  145. package/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +3 -1
  146. package/packages/pi-coding-agent/src/core/compaction/compaction.ts +18 -0
  147. package/packages/pi-coding-agent/src/core/compaction-threshold.test.ts +121 -0
  148. package/packages/pi-coding-agent/src/core/db-snapshot.test.ts +32 -0
  149. package/packages/pi-coding-agent/src/core/db-snapshot.ts +66 -0
  150. package/packages/pi-coding-agent/src/core/extensions/runner.test.ts +2 -0
  151. package/packages/pi-coding-agent/src/core/extensions/runner.ts +3 -0
  152. package/packages/pi-coding-agent/src/core/extensions/types.ts +7 -0
  153. package/packages/pi-coding-agent/src/core/settings-manager.ts +39 -1
  154. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +4 -0
  155. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.test.ts +56 -0
  156. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +22 -7
  157. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +3 -0
  158. package/packages/pi-coding-agent/src/resources/extensions/memory/storage-safety-guard.test.ts +14 -0
  159. package/packages/pi-coding-agent/src/resources/extensions/memory/storage.ts +3 -2
  160. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  161. package/packages/pi-tui/dist/tui.d.ts.map +1 -1
  162. package/packages/pi-tui/dist/tui.js +18 -8
  163. package/packages/pi-tui/dist/tui.js.map +1 -1
  164. package/packages/pi-tui/src/tui.ts +20 -8
  165. package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
  166. package/src/resources/extensions/gsd/auto/loop.ts +50 -8
  167. package/src/resources/extensions/gsd/auto/phases.ts +35 -20
  168. package/src/resources/extensions/gsd/auto/resolve.ts +23 -1
  169. package/src/resources/extensions/gsd/auto/run-unit.ts +18 -1
  170. package/src/resources/extensions/gsd/auto-prompts.ts +17 -1
  171. package/src/resources/extensions/gsd/auto-recovery.ts +54 -0
  172. package/src/resources/extensions/gsd/auto-supervisor.ts +7 -0
  173. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +2 -2
  174. package/src/resources/extensions/gsd/auto.ts +80 -3
  175. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +21 -1
  176. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +27 -19
  177. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +22 -0
  178. package/src/resources/extensions/gsd/context-budget.ts +44 -2
  179. package/src/resources/extensions/gsd/crash-recovery.ts +67 -10
  180. package/src/resources/extensions/gsd/db/unit-dispatches.ts +41 -0
  181. package/src/resources/extensions/gsd/db-base-schema.ts +4 -2
  182. package/src/resources/extensions/gsd/db-migration-steps.ts +8 -0
  183. package/src/resources/extensions/gsd/git-service.ts +46 -8
  184. package/src/resources/extensions/gsd/gsd-db.ts +50 -13
  185. package/src/resources/extensions/gsd/guided-flow.ts +71 -10
  186. package/src/resources/extensions/gsd/memory-store.ts +77 -12
  187. package/src/resources/extensions/gsd/migrate/command.ts +47 -1
  188. package/src/resources/extensions/gsd/migration-auto-check.ts +129 -0
  189. package/src/resources/extensions/gsd/planning-path-scope.ts +35 -0
  190. package/src/resources/extensions/gsd/preferences-types.ts +1 -1
  191. package/src/resources/extensions/gsd/prompt-loader.ts +27 -2
  192. package/src/resources/extensions/gsd/prompts/complete-milestone.md +14 -13
  193. package/src/resources/extensions/gsd/prompts/parallel-research-slices.md +1 -1
  194. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  195. package/src/resources/extensions/gsd/prompts/quick-task.md +1 -5
  196. package/src/resources/extensions/gsd/prompts/validate-milestone.md +2 -2
  197. package/src/resources/extensions/gsd/quick.ts +37 -2
  198. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +71 -0
  199. package/src/resources/extensions/gsd/tests/auto-phases-lifecycle.test.ts +56 -13
  200. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +14 -1
  201. package/src/resources/extensions/gsd/tests/compaction-snapshot.test.ts +14 -1
  202. package/src/resources/extensions/gsd/tests/context-budget.test.ts +10 -1
  203. package/src/resources/extensions/gsd/tests/crash-handler-secondary.test.ts +55 -0
  204. package/src/resources/extensions/gsd/tests/crash-recovery-via-db.test.ts +22 -0
  205. package/src/resources/extensions/gsd/tests/dispatch-rule-coverage.test.ts +313 -0
  206. package/src/resources/extensions/gsd/tests/exec-history.test.ts +15 -0
  207. package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +65 -0
  208. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +54 -0
  209. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +234 -0
  210. package/src/resources/extensions/gsd/tests/memory-decay-factor.test.ts +90 -0
  211. package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +48 -0
  212. package/src/resources/extensions/gsd/tests/migration-auto-check.test.ts +127 -0
  213. package/src/resources/extensions/gsd/tests/plan-slice.test.ts +50 -0
  214. package/src/resources/extensions/gsd/tests/plan-task.test.ts +21 -0
  215. package/src/resources/extensions/gsd/tests/prompt-path-audit.test.ts +40 -0
  216. package/src/resources/extensions/gsd/tests/prompt-step-ordering.test.ts +19 -0
  217. package/src/resources/extensions/gsd/tests/quick-external-gsd.test.ts +40 -0
  218. package/src/resources/extensions/gsd/tests/schema-v27-v28-sequence.test.ts +156 -0
  219. package/src/resources/extensions/gsd/tests/signal-handlers.test.ts +27 -0
  220. package/src/resources/extensions/gsd/tests/smart-entry-complete.test.ts +20 -0
  221. package/src/resources/extensions/gsd/tests/stalled-tool-recovery.test.ts +49 -1
  222. package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +55 -0
  223. package/src/resources/extensions/gsd/tests/status-db-open.test.ts +9 -0
  224. package/src/resources/extensions/gsd/tests/unit-context-composer.test.ts +136 -4
  225. package/src/resources/extensions/gsd/tests/unit-dispatches.test.ts +30 -0
  226. package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +37 -0
  227. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +3 -0
  228. package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +63 -1
  229. package/src/resources/extensions/gsd/tools/context-mode-tool-result.ts +25 -0
  230. package/src/resources/extensions/gsd/tools/exec-search-tool.ts +7 -7
  231. package/src/resources/extensions/gsd/tools/exec-tool.ts +4 -23
  232. package/src/resources/extensions/gsd/tools/memory-tools.ts +1 -0
  233. package/src/resources/extensions/gsd/tools/plan-slice.ts +13 -0
  234. package/src/resources/extensions/gsd/tools/plan-task.ts +10 -0
  235. package/src/resources/extensions/gsd/tools/resume-tool.ts +7 -7
  236. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +1 -1
  237. package/src/resources/extensions/gsd/unit-context-composer.ts +19 -4
  238. package/src/resources/extensions/gsd/unit-runtime.ts +25 -0
  239. package/src/resources/extensions/gsd/worktree-resolver.ts +36 -15
  240. /package/dist/web/standalone/.next/static/{vaQ47E8aNlbbNEDoeME6G → tht0ltP8EIjsohPOA_WZf}/_buildManifest.js +0 -0
  241. /package/dist/web/standalone/.next/static/{vaQ47E8aNlbbNEDoeME6G → tht0ltP8EIjsohPOA_WZf}/_ssgManifest.js +0 -0
@@ -3,21 +3,34 @@
3
3
  // Exposes the Context Mode runtime tools in-process. Default-on; opt out with
4
4
  // `context_mode.enabled: false` in preferences.
5
5
  import { Type } from "@sinclair/typebox";
6
+ async function loadContextModePreferences(baseDir) {
7
+ const [{ loadEffectiveGSDPreferences }, { logWarning }] = await Promise.all([
8
+ import("../preferences.js"),
9
+ import("../workflow-logger.js"),
10
+ ]);
11
+ try {
12
+ return loadEffectiveGSDPreferences(baseDir)?.preferences ?? null;
13
+ }
14
+ catch (err) {
15
+ logWarning("tool", `Context Mode tool could not load preferences: ${err instanceof Error ? err.message : String(err)}`);
16
+ return null;
17
+ }
18
+ }
6
19
  export function registerExecTools(pi) {
7
20
  pi.registerTool({
8
21
  name: "gsd_exec",
9
22
  label: "Exec (Sandboxed)",
10
- description: "Run a short script (bash/node/python) in a subprocess. Full stdout/stderr persist to " +
23
+ description: "Run a short script (bash/node/python) in a subprocess. Capped stdout/stderr and metadata persist to " +
11
24
  ".gsd/exec/<id>.{stdout,stderr,meta.json}; only a short digest returns in context. Use " +
12
25
  "this instead of reading many files or emitting large tool outputs — e.g. have the script " +
13
26
  "count/grep/summarize and log the finding. Enabled by default; opt out via " +
14
27
  "preferences.context_mode.enabled=false.",
15
- promptSnippet: "Run a bash/node/python script in a sandbox; full output is saved to disk and only a digest returns",
28
+ promptSnippet: "Run a bash/node/python script in a sandbox; capped output is saved to disk and only a digest returns",
16
29
  promptGuidelines: [
17
30
  "Prefer gsd_exec for analyses that would otherwise read >3 files or produce large tool output.",
18
31
  "Write scripts that log the finding (counts, matches, summaries) rather than raw dumps.",
19
32
  "The digest is the last ~300 chars of stdout — size your log output accordingly.",
20
- "Need the full output? Read the stdout_path returned in details (file on local disk).",
33
+ "Need persisted output? Read the stdout_path returned in details (file on local disk).",
21
34
  ],
22
35
  parameters: Type.Object({
23
36
  runtime: Type.Union([Type.Literal("bash"), Type.Literal("node"), Type.Literal("python")], { description: "Interpreter: bash (-c), node (-e), or python3 (-c)." }),
@@ -30,21 +43,11 @@ export function registerExecTools(pi) {
30
43
  })),
31
44
  }),
32
45
  async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
33
- const [{ executeGsdExec }, { loadEffectiveGSDPreferences }, { logWarning }] = await Promise.all([
34
- import("../tools/exec-tool.js"),
35
- import("../preferences.js"),
36
- import("../workflow-logger.js"),
37
- ]);
38
- let prefs = null;
39
- try {
40
- prefs = loadEffectiveGSDPreferences();
41
- }
42
- catch (err) {
43
- logWarning("tool", `gsd_exec could not load preferences: ${err instanceof Error ? err.message : String(err)}`);
44
- }
46
+ const { executeGsdExec } = await import("../tools/exec-tool.js");
47
+ const baseDir = process.cwd();
45
48
  return executeGsdExec(params, {
46
- baseDir: process.cwd(),
47
- preferences: prefs?.preferences ?? null,
49
+ baseDir,
50
+ preferences: await loadContextModePreferences(baseDir),
48
51
  });
49
52
  },
50
53
  });
@@ -56,7 +59,7 @@ export function registerExecTools(pi) {
56
59
  promptSnippet: "Search prior gsd_exec runs by substring, runtime, or failing-only filter",
57
60
  promptGuidelines: [
58
61
  "Use this before re-running an expensive analysis — the prior run's stdout file may still answer.",
59
- "The preview shows the trailing ~300 chars of stdout; read stdout_path for the full transcript.",
62
+ "The preview shows the trailing ~300 chars of stdout; read stdout_path for persisted output.",
60
63
  ],
61
64
  parameters: Type.Object({
62
65
  query: Type.Optional(Type.String({ description: "Substring matched against id and purpose (case-insensitive)." })),
@@ -68,8 +71,10 @@ export function registerExecTools(pi) {
68
71
  }),
69
72
  async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
70
73
  const { executeExecSearch } = await import("../tools/exec-search-tool.js");
74
+ const baseDir = process.cwd();
71
75
  return executeExecSearch(params, {
72
- baseDir: process.cwd(),
76
+ baseDir,
77
+ preferences: await loadContextModePreferences(baseDir),
73
78
  });
74
79
  },
75
80
  });
@@ -87,8 +92,10 @@ export function registerExecTools(pi) {
87
92
  parameters: Type.Object({}),
88
93
  async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
89
94
  const { executeResume } = await import("../tools/resume-tool.js");
95
+ const baseDir = process.cwd();
90
96
  return executeResume(params, {
91
- baseDir: process.cwd(),
97
+ baseDir,
98
+ preferences: await loadContextModePreferences(baseDir),
92
99
  });
93
100
  },
94
101
  });
@@ -52,6 +52,25 @@ async function applyDisabledModelProviderPolicy(ctx) {
52
52
  // Non-fatal: keep default provider visibility if preferences cannot be loaded.
53
53
  }
54
54
  }
55
+ /**
56
+ * Bridge `context_management.compaction_threshold_percent` from GSD preferences
57
+ * into the agent's runtime compaction settings (#5475). The preference is
58
+ * validated to (0.5, 0.95) at load time, but defense-in-depth normalization
59
+ * here protects against a stale or hand-edited prefs file. Calling with
60
+ * `undefined` clears any prior override so a removed preference does not leak.
61
+ */
62
+ async function applyCompactionThresholdOverride(ctx) {
63
+ try {
64
+ const { loadEffectiveGSDPreferences } = await import("../preferences.js");
65
+ const prefs = loadEffectiveGSDPreferences();
66
+ const raw = prefs?.preferences.context_management?.compaction_threshold_percent;
67
+ const value = typeof raw === "number" && Number.isFinite(raw) && raw > 0 && raw < 1 ? raw : undefined;
68
+ ctx.setCompactionThresholdOverride(value);
69
+ }
70
+ catch {
71
+ // Non-fatal: leave any existing override in place.
72
+ }
73
+ }
55
74
  export function resolveNotificationStoreBasePath(cwd = process.cwd()) {
56
75
  return resolveWorktreeProjectRoot(cwd);
57
76
  }
@@ -101,6 +120,7 @@ export function registerHooks(pi, ecosystemHandlers) {
101
120
  await resetAskUserQuestionsTurnCache();
102
121
  await syncServiceTierStatus(ctx);
103
122
  await applyDisabledModelProviderPolicy(ctx);
123
+ await applyCompactionThresholdOverride(ctx);
104
124
  // Skip MCP auto-prep when running inside an auto-worktree (see session_switch below).
105
125
  const { isInAutoWorktree } = await import("../auto-worktree.js");
106
126
  if (!isInAutoWorktree(process.cwd())) {
@@ -149,6 +169,7 @@ export function registerHooks(pi, ecosystemHandlers) {
149
169
  clearDiscussionFlowState(process.cwd());
150
170
  await syncServiceTierStatus(ctx);
151
171
  await applyDisabledModelProviderPolicy(ctx);
172
+ await applyCompactionThresholdOverride(ctx);
152
173
  // Skip MCP auto-prep when running inside an auto-worktree. The worktree
153
174
  // already has .mcp.json from createAutoWorktree, and re-running the writer
154
175
  // post-chdir rewrites the file mid-run (non-idempotent due to cwd-relative
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * @see D001 (module location), D002 (200K fallback), D003 (section-boundary truncation)
9
9
  */
10
- import { getCharsPerToken } from "./token-counter.js";
10
+ import { getCharsPerToken, isAccurateCountingAvailable, countTokensSync, } from "./token-counter.js";
11
11
  // ─── Budget ratio constants ──────────────────────────────────────────────────
12
12
  // Percentages of total context window allocated to each budget category.
13
13
  // These are applied after tokens→chars conversion.
@@ -23,6 +23,22 @@ const CHARS_PER_TOKEN = 4;
23
23
  const DEFAULT_CONTEXT_WINDOW = 200_000;
24
24
  /** Conservative effective context for Claude Code subscription routing (#4676) */
25
25
  const CLAUDE_CODE_EFFECTIVE_CONTEXT_WINDOW = 200_000;
26
+ /**
27
+ * Cached empirical chars-per-token from a tiktoken probe, keyed by provider.
28
+ * countTokensSync's fallback path is provider-aware, so we cache per-provider
29
+ * to preserve that distinction once the encoder warms. The cl100k_base encoder
30
+ * itself gives a stable ratio for ASCII English so a single probe per provider
31
+ * key is sufficient. Empty map means "not yet probed" or "encoder unavailable".
32
+ */
33
+ const _empiricalCharsPerTokenByProvider = new Map();
34
+ /**
35
+ * Test hook — clears the empirical chars-per-token cache so test cases that
36
+ * assert against the static char-ratio fallback aren't polluted by a prior
37
+ * tiktoken-warmed run in the same process. Production code must not call this.
38
+ */
39
+ export function _resetEmpiricalCacheForTest() {
40
+ _empiricalCharsPerTokenByProvider.clear();
41
+ }
26
42
  /** Percentage of context consumed before suggesting a continue-here checkpoint */
27
43
  const CONTINUE_THRESHOLD_PERCENT = 70;
28
44
  // ─── Task count bounds ───────────────────────────────────────────────────────
@@ -46,7 +62,26 @@ const TASK_COUNT_TIERS = [
46
62
  export function computeBudgets(contextWindow, provider) {
47
63
  const effectiveWindow = contextWindow > 0 ? contextWindow : DEFAULT_CONTEXT_WINDOW;
48
64
  const charsPerToken = provider ? getCharsPerToken(provider) : CHARS_PER_TOKEN;
49
- const totalChars = effectiveWindow * charsPerToken;
65
+ // Prefer the tiktoken encoder for total-char estimation when it has been
66
+ // warmed (initTokenCounter resolved). The cl100k_base ratio is stable for
67
+ // ASCII English, so probe once per provider and cache — computeBudgets is
68
+ // called multiple times per prompt build and the probe encode is otherwise
69
+ // wasted work.
70
+ let totalChars;
71
+ if (isAccurateCountingAvailable()) {
72
+ const providerKey = provider ?? "__default__";
73
+ let empirical = _empiricalCharsPerTokenByProvider.get(providerKey);
74
+ if (empirical === undefined) {
75
+ const probe = "the quick brown fox jumps over the lazy dog ".repeat(64);
76
+ const probeTokens = countTokensSync(probe, provider);
77
+ empirical = probeTokens > 0 ? probe.length / probeTokens : charsPerToken;
78
+ _empiricalCharsPerTokenByProvider.set(providerKey, empirical);
79
+ }
80
+ totalChars = effectiveWindow * empirical;
81
+ }
82
+ else {
83
+ totalChars = effectiveWindow * charsPerToken;
84
+ }
50
85
  return {
51
86
  summaryBudgetChars: Math.floor(totalChars * SUMMARY_RATIO),
52
87
  inlineContextBudgetChars: Math.floor(totalChars * INLINE_CONTEXT_RATIO),
@@ -29,6 +29,7 @@ import { _getAdapter, isDbAvailable } from "./gsd-db.js";
29
29
  import { gsdRoot, normalizeRealPath } from "./paths.js";
30
30
  import { atomicWriteSync } from "./atomic-write.js";
31
31
  import { effectiveLockFile } from "./session-lock.js";
32
+ import { isInFlightRuntimePhase, listUnitRuntimeRecords } from "./unit-runtime.js";
32
33
  const SESSION_FILE_KV_KEY = "session_file";
33
34
  function lockPath(basePath) {
34
35
  return join(gsdRoot(basePath), effectiveLockFile());
@@ -72,9 +73,37 @@ function getLatestDispatchForWorker(workerId) {
72
73
  LIMIT 1`).get({ ":worker_id": workerId });
73
74
  return row ?? null;
74
75
  }
75
- function workerToLockData(worker) {
76
+ function latestInFlightRuntimeRecord(basePath) {
77
+ const records = listUnitRuntimeRecords(basePath).filter((record) => isInFlightRuntimePhase(record.phase));
78
+ if (records.length === 0)
79
+ return null;
80
+ return records.sort((a, b) => {
81
+ const bTime = b.updatedAt || b.startedAt || 0;
82
+ const aTime = a.updatedAt || a.startedAt || 0;
83
+ return bTime - aTime;
84
+ })[0] ?? null;
85
+ }
86
+ function runtimeRecordToLockData(worker, record, sessionFile) {
87
+ const startedAt = Number.isFinite(record.startedAt)
88
+ ? new Date(record.startedAt).toISOString()
89
+ : worker.started_at;
90
+ return {
91
+ pid: worker.pid,
92
+ startedAt: worker.started_at,
93
+ unitType: record.unitType,
94
+ unitId: record.unitId,
95
+ unitStartedAt: startedAt,
96
+ sessionFile,
97
+ };
98
+ }
99
+ function workerToLockData(basePath, worker) {
76
100
  const dispatch = getLatestDispatchForWorker(worker.worker_id);
77
101
  const sessionFile = getRuntimeKv("worker", worker.worker_id, SESSION_FILE_KV_KEY) ?? undefined;
102
+ if (!dispatch) {
103
+ const runtimeRecord = latestInFlightRuntimeRecord(basePath);
104
+ if (runtimeRecord)
105
+ return runtimeRecordToLockData(worker, runtimeRecord, sessionFile);
106
+ }
78
107
  return {
79
108
  pid: worker.pid,
80
109
  startedAt: worker.started_at,
@@ -172,7 +201,7 @@ export function readCrashLock(basePath) {
172
201
  const projectRoot = normalizeRealPath(basePath);
173
202
  const stale = findStaleWorkerForProject(projectRoot);
174
203
  if (stale)
175
- return workerToLockData(stale);
204
+ return workerToLockData(basePath, stale);
176
205
  }
177
206
  catch {
178
207
  // Fall through to the legacy lock-file compatibility path.
@@ -232,18 +261,32 @@ export function formatCrashInfo(lock) {
232
261
  export function emitCrashRecoveredUnitEnd(basePath, lock) {
233
262
  if (!lock.unitType || !lock.unitId || lock.unitType === "starting")
234
263
  return;
264
+ emitOpenUnitEndForUnit(basePath, lock.unitType, lock.unitId, "crash-recovered");
265
+ }
266
+ export function emitOpenUnitEndForUnit(basePath, unitType, unitId, status, errorContext) {
235
267
  try {
236
268
  const all = queryJournal(basePath);
237
- const starts = all.filter((e) => e.eventType === "unit-start" && e.data?.unitId === lock.unitId);
269
+ const starts = all.filter((e) => e.eventType === "unit-start" &&
270
+ e.data?.unitType === unitType &&
271
+ e.data?.unitId === unitId);
238
272
  if (starts.length === 0)
239
- return;
240
- const lastStart = starts[starts.length - 1];
273
+ return false;
274
+ const lastStart = [...starts].reverse().find((start) => {
275
+ return !all.some((e) => e.eventType === "unit-end" &&
276
+ e.data?.unitType === unitType &&
277
+ e.data?.unitId === unitId &&
278
+ e.causedBy?.flowId === start.flowId &&
279
+ e.causedBy?.seq === start.seq);
280
+ });
281
+ if (!lastStart)
282
+ return false;
241
283
  const alreadyClosed = all.some((e) => e.eventType === "unit-end" &&
242
- e.data?.unitId === lock.unitId &&
284
+ e.data?.unitType === unitType &&
285
+ e.data?.unitId === unitId &&
243
286
  e.causedBy?.flowId === lastStart.flowId &&
244
287
  e.causedBy?.seq === lastStart.seq);
245
288
  if (alreadyClosed)
246
- return;
289
+ return false;
247
290
  const maxSeq = all
248
291
  .filter((e) => e.flowId === lastStart.flowId)
249
292
  .reduce((max, e) => Math.max(max, e.seq), lastStart.seq);
@@ -253,16 +296,19 @@ export function emitCrashRecoveredUnitEnd(basePath, lock) {
253
296
  seq: maxSeq + 1,
254
297
  eventType: "unit-end",
255
298
  data: {
256
- unitType: lock.unitType,
257
- unitId: lock.unitId,
258
- status: "crash-recovered",
299
+ unitType,
300
+ unitId,
301
+ status,
259
302
  artifactVerified: false,
303
+ ...(errorContext ? { errorContext } : {}),
260
304
  },
261
305
  causedBy: { flowId: lastStart.flowId, seq: lastStart.seq },
262
306
  });
307
+ return true;
263
308
  }
264
309
  catch {
265
310
  // Never throw from crash recovery path.
311
+ return false;
266
312
  }
267
313
  }
268
314
  /**
@@ -253,6 +253,45 @@ export function markCanceled(dispatchId, reason) {
253
253
  SET status = 'canceled', ended_at = :ended_at, exit_reason = :reason
254
254
  WHERE id = :id AND status IN ('pending','claimed','running')`).run({ ":id": dispatchId, ":ended_at": now, ":reason": reason });
255
255
  }
256
+ /**
257
+ * Best-effort signal/crash cleanup: cancel the latest active dispatch owned by
258
+ * a worker when the process is exiting before the normal loop can settle it.
259
+ */
260
+ export function markLatestActiveForWorkerCanceled(workerId, reason) {
261
+ if (!isDbAvailable())
262
+ return false;
263
+ const now = new Date().toISOString();
264
+ const db = _getAdapter();
265
+ const result = transaction(() => {
266
+ return db.prepare(`UPDATE unit_dispatches
267
+ SET status = 'canceled', ended_at = :ended_at, exit_reason = :reason
268
+ WHERE id = (
269
+ SELECT id FROM unit_dispatches
270
+ WHERE worker_id = :worker_id
271
+ AND status IN ('pending','claimed','running')
272
+ ORDER BY id DESC
273
+ LIMIT 1
274
+ )`).run({
275
+ ":ended_at": now,
276
+ ":reason": reason,
277
+ ":worker_id": workerId,
278
+ });
279
+ });
280
+ const changes = typeof result.changes === "number"
281
+ ? result.changes
282
+ : 0;
283
+ if (changes <= 0)
284
+ return false;
285
+ insertAuditEvent({
286
+ eventId: randomUUID(),
287
+ traceId: workerId,
288
+ category: "orchestration",
289
+ type: "dispatch-canceled",
290
+ ts: now,
291
+ payload: { workerId, reason },
292
+ });
293
+ return true;
294
+ }
256
295
  /**
257
296
  * Fetch the most recent N dispatches for a unit. Used by recordDispatchClaim
258
297
  * callers to compute attempt_n and by detect-stuck.ts (B3) to consult
@@ -46,7 +46,8 @@ export function createBaseSchemaObjects(db, hooks) {
46
46
  slice_id TEXT DEFAULT NULL,
47
47
  task_id TEXT DEFAULT NULL,
48
48
  full_content TEXT NOT NULL DEFAULT '',
49
- imported_at TEXT NOT NULL DEFAULT ''
49
+ imported_at TEXT NOT NULL DEFAULT '',
50
+ content_hash TEXT DEFAULT NULL
50
51
  )
51
52
  `);
52
53
  db.exec(`
@@ -64,7 +65,8 @@ export function createBaseSchemaObjects(db, hooks) {
64
65
  hit_count INTEGER NOT NULL DEFAULT 0,
65
66
  scope TEXT NOT NULL DEFAULT 'project',
66
67
  tags TEXT NOT NULL DEFAULT '[]',
67
- structured_fields TEXT DEFAULT NULL
68
+ structured_fields TEXT DEFAULT NULL,
69
+ last_hit_at TEXT DEFAULT NULL
68
70
  )
69
71
  `);
70
72
  db.exec(`
@@ -379,6 +379,12 @@ export function applyMigrationV26MilestoneCommitAttributions(db) {
379
379
  `);
380
380
  db.exec("CREATE INDEX IF NOT EXISTS idx_milestone_commit_attr_milestone ON milestone_commit_attributions(milestone_id)");
381
381
  }
382
+ export function applyMigrationV27ArtifactHash(db) {
383
+ ensureColumn(db, "artifacts", "content_hash", "ALTER TABLE artifacts ADD COLUMN content_hash TEXT DEFAULT NULL");
384
+ }
385
+ export function applyMigrationV28MemoryLastHitAt(db) {
386
+ ensureColumn(db, "memories", "last_hit_at", "ALTER TABLE memories ADD COLUMN last_hit_at TEXT DEFAULT NULL");
387
+ }
382
388
  export function applyMigrationV22QualityGateRepair(db, hooks) {
383
389
  const qgInfo = db.prepare("PRAGMA table_info(quality_gates)").all();
384
390
  const taskIdCol = qgInfo.find((r) => r["name"] === "task_id");
@@ -13,6 +13,7 @@ import { isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
13
13
  import { gsdRoot } from "./paths.js";
14
14
  import { GIT_NO_PROMPT_ENV } from "./git-constants.js";
15
15
  import { loadEffectiveGSDPreferences } from "./preferences.js";
16
+ import { logWarning } from "./workflow-logger.js";
16
17
  import { detectWorktreeName, } from "./worktree.js";
17
18
  import { SLICE_BRANCH_RE, QUICK_BRANCH_RE, WORKFLOW_BRANCH_RE } from "./branch-patterns.js";
18
19
  import { nativeGetCurrentBranch, nativeDetectMainBranch, nativeBranchExists, nativeHasChanges, nativeAddAllWithExclusions, nativeHasStagedChanges, nativeCommit, nativeRmCached, nativeUpdateRef, nativeAddPaths, nativeResetSoft, nativeCommitSubject, _resetHasChangesCache, } from "./native-git-bridge.js";
@@ -535,14 +536,45 @@ export class GitServiceImpl {
535
536
  if (keyFiles.length === 0)
536
537
  return false;
537
538
  const allExclusions = [...RUNTIME_EXCLUSION_PATHS, ...extraExclusions];
538
- const paths = Array.from(new Set(keyFiles
539
+ const normalized = keyFiles
539
540
  .map(file => normalizeRepoRelativePath(this.basePath, file))
540
541
  .filter((file) => file !== null)
541
- .filter(file => !isExcludedScopedPath(file, allExclusions))));
542
+ .filter(file => !isExcludedScopedPath(file, allExclusions));
543
+ // Drop entries that don't exist on disk. The LLM occasionally lists files
544
+ // it intended to write but didn't (or names them with wrong casing/path).
545
+ // Pre-`b304f738b` `git add -A` swallowed these silently; the scoped
546
+ // pathspec form passes each path explicitly, so a single bad entry made
547
+ // the whole commit fail (see #5500). Filter so valid paths still commit.
548
+ const missing = [];
549
+ const existing = [];
550
+ for (const path of normalized) {
551
+ if (existsSync(join(this.basePath, path))) {
552
+ existing.push(path);
553
+ }
554
+ else {
555
+ missing.push(path);
556
+ }
557
+ }
558
+ if (missing.length > 0) {
559
+ logWarning("engine", `scoped stage: dropping ${missing.length} non-existent keyFile(s) from task commit: ${missing.join(", ")}`, { file: "git-service.ts" });
560
+ }
561
+ const paths = Array.from(new Set(existing));
542
562
  if (paths.length === 0)
543
563
  return false;
544
- nativeAddPaths(this.basePath, paths);
545
- return true;
564
+ try {
565
+ nativeAddPaths(this.basePath, paths);
566
+ return true;
567
+ }
568
+ catch (err) {
569
+ // Defense-in-depth: even after existence filtering, libgit2/git can
570
+ // still reject paths (gitignore matches, case-only differences on
571
+ // case-insensitive FS, submodule boundaries). Returning false lets
572
+ // autoCommit fall through to smartStage so the commit still goes out
573
+ // — restoring the resilience the unscoped path used to provide.
574
+ const msg = err instanceof Error ? err.message : String(err);
575
+ logWarning("engine", `scoped stage failed (${msg}); falling back to smartStage`, { file: "git-service.ts" });
576
+ return false;
577
+ }
546
578
  }
547
579
  /** Tracks whether runtime file cleanup has run this session. */
548
580
  _runtimeFilesCleanedUp = false;
@@ -22,6 +22,7 @@
22
22
  // intentionally independent store for cross-worktree claim races and is
23
23
  // excluded from this invariant.
24
24
  import { createRequire } from "node:module";
25
+ import { createHash } from "node:crypto";
25
26
  import { existsSync, copyFileSync, mkdirSync, realpathSync } from "node:fs";
26
27
  import { dirname } from "node:path";
27
28
  import { GSDError, GSD_STALE_STATE } from "./errors.js";
@@ -36,7 +37,7 @@ import { rowToActiveDecision, rowToActiveRequirement, rowToDecision, rowToRequir
36
37
  import { rowToGate } from "./db-gate-rows.js";
37
38
  import { rowToArtifact, rowToMilestone } from "./db-milestone-artifact-rows.js";
38
39
  import { backupDatabaseBeforeMigration } from "./db-migration-backup.js";
39
- import { applyMigrationV2Artifacts, applyMigrationV3Memories, applyMigrationV4DecisionMadeBy, applyMigrationV5HierarchyTables, applyMigrationV6SliceSummaries, applyMigrationV7Dependencies, applyMigrationV8PlanningFields, applyMigrationV9Ordering, applyMigrationV10ReplanTrigger, applyMigrationV11TaskPlanning, applyMigrationV12QualityGates, applyMigrationV13HotPathIndexes, applyMigrationV14SliceDependencies, applyMigrationV15AuditTables, applyMigrationV16EscalationSource, applyMigrationV17TaskEscalation, applyMigrationV18MemorySources, applyMigrationV19MemoryFts, applyMigrationV20MemoryRelations, applyMigrationV21StructuredMemories, applyMigrationV22QualityGateRepair, applyMigrationV23MilestoneQueue, applyMigrationV26MilestoneCommitAttributions, } from "./db-migration-steps.js";
40
+ import { applyMigrationV2Artifacts, applyMigrationV3Memories, applyMigrationV4DecisionMadeBy, applyMigrationV5HierarchyTables, applyMigrationV6SliceSummaries, applyMigrationV7Dependencies, applyMigrationV8PlanningFields, applyMigrationV9Ordering, applyMigrationV10ReplanTrigger, applyMigrationV11TaskPlanning, applyMigrationV12QualityGates, applyMigrationV13HotPathIndexes, applyMigrationV14SliceDependencies, applyMigrationV15AuditTables, applyMigrationV16EscalationSource, applyMigrationV17TaskEscalation, applyMigrationV18MemorySources, applyMigrationV19MemoryFts, applyMigrationV20MemoryRelations, applyMigrationV21StructuredMemories, applyMigrationV22QualityGateRepair, applyMigrationV23MilestoneQueue, applyMigrationV26MilestoneCommitAttributions, applyMigrationV27ArtifactHash, applyMigrationV28MemoryLastHitAt, } from "./db-migration-steps.js";
40
41
  import { isMemoriesFtsAvailableSchema, tryCreateMemoriesFtsSchema } from "./db-memory-fts-schema.js";
41
42
  import { createDbOpenState } from "./db-open-state.js";
42
43
  import { createRuntimeKvTableV25 } from "./db-runtime-kv-schema.js";
@@ -52,7 +53,7 @@ const providerLoader = createSqliteProviderLoader({
52
53
  nodeVersion: process.versions.node,
53
54
  writeStderr: (message) => process.stderr.write(message),
54
55
  });
55
- export const SCHEMA_VERSION = 26;
56
+ export const SCHEMA_VERSION = 28;
56
57
  function initSchema(db, fileBacked) {
57
58
  if (fileBacked)
58
59
  db.exec("PRAGMA journal_mode=WAL");
@@ -250,6 +251,14 @@ function migrateSchema(db) {
250
251
  applyMigrationV26MilestoneCommitAttributions(db);
251
252
  recordSchemaVersion(db, 26);
252
253
  }
254
+ if (currentVersion < 27) {
255
+ applyMigrationV27ArtifactHash(db);
256
+ recordSchemaVersion(db, 27);
257
+ }
258
+ if (currentVersion < 28) {
259
+ applyMigrationV28MemoryLastHitAt(db);
260
+ recordSchemaVersion(db, 28);
261
+ }
253
262
  db.exec("COMMIT");
254
263
  }
255
264
  catch (err) {
@@ -821,8 +830,9 @@ export function clearArtifacts() {
821
830
  export function insertArtifact(a) {
822
831
  if (!currentDb)
823
832
  throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
824
- currentDb.prepare(`INSERT OR REPLACE INTO artifacts (path, artifact_type, milestone_id, slice_id, task_id, full_content, imported_at)
825
- VALUES (:path, :artifact_type, :milestone_id, :slice_id, :task_id, :full_content, :imported_at)`).run({
833
+ const contentHash = createHash("sha256").update(a.full_content).digest("hex");
834
+ currentDb.prepare(`INSERT OR REPLACE INTO artifacts (path, artifact_type, milestone_id, slice_id, task_id, full_content, imported_at, content_hash)
835
+ VALUES (:path, :artifact_type, :milestone_id, :slice_id, :task_id, :full_content, :imported_at, :content_hash)`).run({
826
836
  ":path": a.path,
827
837
  ":artifact_type": a.artifact_type,
828
838
  ":milestone_id": a.milestone_id,
@@ -830,6 +840,7 @@ export function insertArtifact(a) {
830
840
  ":task_id": a.task_id,
831
841
  ":full_content": a.full_content,
832
842
  ":imported_at": new Date().toISOString(),
843
+ ":content_hash": contentHash,
833
844
  });
834
845
  }
835
846
  export function insertMilestone(m) {
@@ -1521,6 +1532,13 @@ export function reconcileWorktreeDb(mainDbPath, worktreeDbPath) {
1521
1532
  const hasEscalationAwaiting = wtTaskInfo.some((col) => col["name"] === "escalation_awaiting_review");
1522
1533
  const hasEscalationArtifact = wtTaskInfo.some((col) => col["name"] === "escalation_artifact_path");
1523
1534
  const hasEscalationOverride = wtTaskInfo.some((col) => col["name"] === "escalation_override_applied_at");
1535
+ const wtArtifactInfo = adapter.prepare("PRAGMA wt.table_info('artifacts')").all();
1536
+ const hasArtifactContentHash = wtArtifactInfo.some((col) => col["name"] === "content_hash");
1537
+ const wtMemoryInfo = adapter.prepare("PRAGMA wt.table_info('memories')").all();
1538
+ const hasMemoryScope = wtMemoryInfo.some((col) => col["name"] === "scope");
1539
+ const hasMemoryTags = wtMemoryInfo.some((col) => col["name"] === "tags");
1540
+ const hasMemoryStructuredFields = wtMemoryInfo.some((col) => col["name"] === "structured_fields");
1541
+ const hasMemoryLastHitAt = wtMemoryInfo.some((col) => col["name"] === "last_hit_at");
1524
1542
  const decConf = adapter.prepare(`SELECT m.id FROM decisions m INNER JOIN wt.decisions w ON m.id = w.id WHERE m.decision != w.decision OR m.choice != w.choice OR m.rationale != w.rationale OR ${hasMadeBy ? "m.made_by != w.made_by" : "'agent' != 'agent'"} OR m.superseded_by IS NOT w.superseded_by`).all();
1525
1543
  for (const row of decConf)
1526
1544
  conflicts.push(`decision ${row["id"]}: modified in both`);
@@ -1553,12 +1571,17 @@ export function reconcileWorktreeDb(mainDbPath, worktreeDbPath) {
1553
1571
  supporting_slices, validation, notes, full_content, superseded_by
1554
1572
  FROM wt.requirements
1555
1573
  `).run());
1574
+ // V27: preserve content_hash. If the worktree predates V27 (no column),
1575
+ // fall back to the main DB's existing hash so reconcile doesn't null
1576
+ // out integrity fingerprints on artifacts that were unchanged in wt.
1556
1577
  merged.artifacts = countChanges(adapter.prepare(`
1557
1578
  INSERT OR REPLACE INTO artifacts (
1558
- path, artifact_type, milestone_id, slice_id, task_id, full_content, imported_at
1579
+ path, artifact_type, milestone_id, slice_id, task_id, full_content, imported_at, content_hash
1559
1580
  )
1560
- SELECT path, artifact_type, milestone_id, slice_id, task_id, full_content, imported_at
1561
- FROM wt.artifacts
1581
+ SELECT w.path, w.artifact_type, w.milestone_id, w.slice_id, w.task_id, w.full_content, w.imported_at,
1582
+ ${hasArtifactContentHash ? "w.content_hash" : "m.content_hash"}
1583
+ FROM wt.artifacts w
1584
+ LEFT JOIN artifacts m ON m.path = w.path
1562
1585
  `).run());
1563
1586
  // Merge milestones — worktree may have updated status/planning fields.
1564
1587
  // Never downgrade status: complete > active > pre-planning (#4372).
@@ -1656,15 +1679,25 @@ export function reconcileWorktreeDb(mainDbPath, worktreeDbPath) {
1656
1679
  FROM wt.tasks w
1657
1680
  LEFT JOIN tasks m ON m.milestone_id = w.milestone_id AND m.slice_id = w.slice_id AND m.id = w.id
1658
1681
  `).run());
1659
- // Merge memories — keep worktree-learned insights
1682
+ // Merge memories — keep worktree-learned insights.
1683
+ // V18 (scope, tags), V21 (structured_fields), V28 (last_hit_at): for each
1684
+ // column the wt may not yet have (older worktree DB), fall back to the
1685
+ // main DB's existing value via LEFT JOIN so reconcile never silently
1686
+ // resets these fields to defaults on rows that already had them.
1660
1687
  merged.memories = countChanges(adapter.prepare(`
1661
1688
  INSERT OR REPLACE INTO memories (
1662
1689
  seq, id, category, content, confidence, source_unit_type, source_unit_id,
1663
- created_at, updated_at, superseded_by, hit_count
1690
+ created_at, updated_at, superseded_by, hit_count,
1691
+ scope, tags, structured_fields, last_hit_at
1664
1692
  )
1665
- SELECT seq, id, category, content, confidence, source_unit_type, source_unit_id,
1666
- created_at, updated_at, superseded_by, hit_count
1667
- FROM wt.memories
1693
+ SELECT w.seq, w.id, w.category, w.content, w.confidence, w.source_unit_type, w.source_unit_id,
1694
+ w.created_at, w.updated_at, w.superseded_by, w.hit_count,
1695
+ ${hasMemoryScope ? "w.scope" : "COALESCE(m.scope, 'project')"},
1696
+ ${hasMemoryTags ? "w.tags" : "COALESCE(m.tags, '[]')"},
1697
+ ${hasMemoryStructuredFields ? "w.structured_fields" : "m.structured_fields"},
1698
+ ${hasMemoryLastHitAt ? "w.last_hit_at" : "m.last_hit_at"}
1699
+ FROM wt.memories w
1700
+ LEFT JOIN memories m ON m.id = w.id
1668
1701
  `).run());
1669
1702
  // Merge verification evidence — append-only, use INSERT OR IGNORE to avoid duplicates
1670
1703
  merged.verification_evidence = countChanges(adapter.prepare(`
@@ -2423,7 +2456,7 @@ export function updateMemoryContentRow(id, content, confidence, updatedAt) {
2423
2456
  export function incrementMemoryHitCount(id, updatedAt) {
2424
2457
  if (!currentDb)
2425
2458
  throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
2426
- currentDb.prepare("UPDATE memories SET hit_count = hit_count + 1, updated_at = :updated_at WHERE id = :id").run({ ":updated_at": updatedAt, ":id": id });
2459
+ currentDb.prepare("UPDATE memories SET hit_count = hit_count + 1, updated_at = :updated_at, last_hit_at = :last_hit_at WHERE id = :id").run({ ":updated_at": updatedAt, ":last_hit_at": updatedAt, ":id": id });
2427
2460
  }
2428
2461
  export function supersedeMemoryRow(oldId, newId, updatedAt) {
2429
2462
  if (!currentDb)