gsd-pi 2.80.0-dev.0aaa4fc17 → 2.80.0-dev.1486c1899

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 (166) 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/phases.js +22 -15
  4. package/dist/resources/extensions/gsd/auto/resolve.js +17 -0
  5. package/dist/resources/extensions/gsd/auto/run-unit.js +13 -1
  6. package/dist/resources/extensions/gsd/auto-prompts.js +13 -1
  7. package/dist/resources/extensions/gsd/auto-recovery.js +43 -1
  8. package/dist/resources/extensions/gsd/auto-supervisor.js +8 -1
  9. package/dist/resources/extensions/gsd/auto-timeout-recovery.js +2 -2
  10. package/dist/resources/extensions/gsd/auto.js +52 -4
  11. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +21 -2
  12. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +27 -20
  13. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +21 -0
  14. package/dist/resources/extensions/gsd/db/unit-dispatches.js +39 -0
  15. package/dist/resources/extensions/gsd/guided-flow.js +19 -4
  16. package/dist/resources/extensions/gsd/prompts/parallel-research-slices.md +1 -1
  17. package/dist/resources/extensions/gsd/prompts/quick-task.md +1 -5
  18. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +2 -2
  19. package/dist/resources/extensions/gsd/quick.js +34 -2
  20. package/dist/resources/extensions/gsd/tools/context-mode-tool-result.js +15 -0
  21. package/dist/resources/extensions/gsd/tools/exec-search-tool.js +5 -0
  22. package/dist/resources/extensions/gsd/tools/exec-tool.js +3 -15
  23. package/dist/resources/extensions/gsd/tools/resume-tool.js +5 -0
  24. package/dist/resources/extensions/gsd/unit-context-composer.js +12 -3
  25. package/dist/resources/extensions/gsd/unit-runtime.js +11 -0
  26. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  27. package/dist/web/standalone/.next/BUILD_ID +1 -1
  28. package/dist/web/standalone/.next/app-path-routes-manifest.json +15 -15
  29. package/dist/web/standalone/.next/build-manifest.json +2 -2
  30. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  31. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  32. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  40. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/index.html +1 -1
  48. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app-paths-manifest.json +15 -15
  55. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  56. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  57. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  58. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  59. package/package.json +3 -3
  60. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  61. package/packages/mcp-server/dist/workflow-tools.js +22 -17
  62. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  63. package/packages/mcp-server/src/workflow-tools.test.ts +75 -2
  64. package/packages/mcp-server/src/workflow-tools.ts +30 -16
  65. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  66. package/packages/native/tsconfig.tsbuildinfo +1 -1
  67. package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js +32 -0
  68. package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js.map +1 -1
  69. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  70. package/packages/pi-coding-agent/dist/core/agent-session.js +8 -0
  71. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  72. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js +3 -1
  73. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -1
  74. package/packages/pi-coding-agent/dist/core/compaction/compaction.d.ts +11 -0
  75. package/packages/pi-coding-agent/dist/core/compaction/compaction.d.ts.map +1 -1
  76. package/packages/pi-coding-agent/dist/core/compaction/compaction.js +9 -0
  77. package/packages/pi-coding-agent/dist/core/compaction/compaction.js.map +1 -1
  78. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.d.ts +2 -0
  79. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.d.ts.map +1 -0
  80. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.js +103 -0
  81. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.js.map +1 -0
  82. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +1 -0
  83. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
  84. package/packages/pi-coding-agent/dist/core/extensions/runner.js +3 -0
  85. package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
  86. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js +2 -0
  87. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js.map +1 -1
  88. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +7 -0
  89. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  90. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  91. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +20 -0
  92. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  93. package/packages/pi-coding-agent/dist/core/settings-manager.js +25 -0
  94. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  95. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +1 -0
  96. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  97. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +3 -0
  98. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  99. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  100. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +13 -5
  101. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  102. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.js +53 -0
  103. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.js.map +1 -1
  104. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  105. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +3 -0
  106. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  107. package/packages/pi-coding-agent/src/core/agent-session-abort-order.test.ts +36 -0
  108. package/packages/pi-coding-agent/src/core/agent-session.ts +8 -0
  109. package/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +3 -1
  110. package/packages/pi-coding-agent/src/core/compaction/compaction.ts +18 -0
  111. package/packages/pi-coding-agent/src/core/compaction-threshold.test.ts +121 -0
  112. package/packages/pi-coding-agent/src/core/extensions/runner.test.ts +2 -0
  113. package/packages/pi-coding-agent/src/core/extensions/runner.ts +3 -0
  114. package/packages/pi-coding-agent/src/core/extensions/types.ts +7 -0
  115. package/packages/pi-coding-agent/src/core/settings-manager.ts +39 -1
  116. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +4 -0
  117. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.test.ts +56 -0
  118. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +22 -7
  119. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +3 -0
  120. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  121. package/packages/pi-tui/dist/tui.d.ts.map +1 -1
  122. package/packages/pi-tui/dist/tui.js +18 -8
  123. package/packages/pi-tui/dist/tui.js.map +1 -1
  124. package/packages/pi-tui/src/tui.ts +20 -8
  125. package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
  126. package/src/resources/extensions/gsd/auto/phases.ts +28 -20
  127. package/src/resources/extensions/gsd/auto/resolve.ts +23 -1
  128. package/src/resources/extensions/gsd/auto/run-unit.ts +18 -1
  129. package/src/resources/extensions/gsd/auto-prompts.ts +17 -1
  130. package/src/resources/extensions/gsd/auto-recovery.ts +54 -0
  131. package/src/resources/extensions/gsd/auto-supervisor.ts +7 -0
  132. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +2 -2
  133. package/src/resources/extensions/gsd/auto.ts +60 -3
  134. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +21 -1
  135. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +27 -19
  136. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +22 -0
  137. package/src/resources/extensions/gsd/db/unit-dispatches.ts +41 -0
  138. package/src/resources/extensions/gsd/guided-flow.ts +32 -4
  139. package/src/resources/extensions/gsd/preferences-types.ts +1 -1
  140. package/src/resources/extensions/gsd/prompts/parallel-research-slices.md +1 -1
  141. package/src/resources/extensions/gsd/prompts/quick-task.md +1 -5
  142. package/src/resources/extensions/gsd/prompts/validate-milestone.md +2 -2
  143. package/src/resources/extensions/gsd/quick.ts +37 -2
  144. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +71 -0
  145. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +14 -1
  146. package/src/resources/extensions/gsd/tests/compaction-snapshot.test.ts +11 -0
  147. package/src/resources/extensions/gsd/tests/exec-history.test.ts +15 -0
  148. package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +65 -0
  149. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +234 -0
  150. package/src/resources/extensions/gsd/tests/prompt-path-audit.test.ts +40 -0
  151. package/src/resources/extensions/gsd/tests/quick-external-gsd.test.ts +40 -0
  152. package/src/resources/extensions/gsd/tests/signal-handlers.test.ts +27 -0
  153. package/src/resources/extensions/gsd/tests/stalled-tool-recovery.test.ts +49 -1
  154. package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +55 -0
  155. package/src/resources/extensions/gsd/tests/status-db-open.test.ts +9 -0
  156. package/src/resources/extensions/gsd/tests/unit-context-composer.test.ts +136 -4
  157. package/src/resources/extensions/gsd/tests/unit-dispatches.test.ts +30 -0
  158. package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +30 -0
  159. package/src/resources/extensions/gsd/tools/context-mode-tool-result.ts +25 -0
  160. package/src/resources/extensions/gsd/tools/exec-search-tool.ts +7 -7
  161. package/src/resources/extensions/gsd/tools/exec-tool.ts +4 -23
  162. package/src/resources/extensions/gsd/tools/resume-tool.ts +7 -7
  163. package/src/resources/extensions/gsd/unit-context-composer.ts +19 -4
  164. package/src/resources/extensions/gsd/unit-runtime.ts +11 -0
  165. /package/dist/web/standalone/.next/static/{YRK7-Es93MwpIn3VL35K0 → O-2rnxSJki_W-2fkK3zIj}/_buildManifest.js +0 -0
  166. /package/dist/web/standalone/.next/static/{YRK7-Es93MwpIn3VL35K0 → O-2rnxSJki_W-2fkK3zIj}/_ssgManifest.js +0 -0
package/README.md CHANGED
@@ -259,6 +259,7 @@ Full documentation is in the [`docs/`](./docs/) directory:
259
259
 
260
260
  - **[Architecture](./docs/dev/architecture.md)** — system design and dispatch pipeline
261
261
  - **[CI/CD Pipeline](./docs/dev/ci-cd-pipeline.md)** — three-stage promotion pipeline (Dev → Test → Prod)
262
+ - **[E2E Testing](./tests/e2e/README.md)** — real-process CLI/runtime coverage and CI runner expectations
262
263
  - **[Pipeline Simplification (ADR-003)](./docs/dev/ADR-003-pipeline-simplification.md)** — merged research into planning, mechanical completion
263
264
  - **[VS Code Extension](./vscode-extension/README.md)** — chat participant, sidebar dashboard, RPC integration
264
265
 
@@ -361,7 +362,7 @@ The database is authoritative for milestones, slices, tasks, requirements, decis
361
362
 
362
363
  2. **Context pre-loading** — The dispatch prompt includes inlined task plans, slice plans, prior task summaries, dependency summaries, roadmap excerpts, and decisions register. The LLM starts with everything it needs instead of spending tool calls reading files.
363
364
 
364
- 3. **Context Mode** — Context Mode is enabled by default and gives every auto-mode unit guidance for preserving context. Agents are steered toward `gsd_exec` for noisy scans, builds, tests, and diagnostics; full stdout/stderr is saved under `.gsd/exec/` while only a short digest enters the conversation. `gsd_exec_search` lets agents reuse prior runs instead of repeating expensive checks, and `gsd_resume` reads `.gsd/last-snapshot.md` after compaction or resume. Opt out with `context_mode.enabled: false`; tune sandbox timeout/output caps with `context_mode.exec_timeout_ms`, `context_mode.exec_stdout_cap_bytes`, and `context_mode.exec_digest_chars`.
365
+ 3. **Context Mode** — Context Mode is enabled by default and gives eligible auto-mode units guidance for preserving context. Agents are steered toward `gsd_exec` for noisy scans, builds, tests, and diagnostics; capped stdout/stderr and metadata are saved under `.gsd/exec/` while only a short digest enters the conversation. `gsd_exec_search` lets agents reuse prior runs instead of repeating expensive checks, and `gsd_resume` reads a prior compaction snapshot from `.gsd/last-snapshot.md` when one exists. Opt out with `context_mode.enabled: false` to disable Context Mode guidance, snapshot injection, `gsd_exec`, `gsd_exec_search`, and `gsd_resume`; tune sandbox timeout/output caps and environment forwarding with `context_mode.exec_timeout_ms`, `context_mode.exec_stdout_cap_bytes`, `context_mode.exec_digest_chars`, and `context_mode.exec_env_allowlist`.
365
366
 
366
367
  4. **Git isolation** — When `git.isolation` is set to `worktree` or `branch`, each milestone runs on its own `milestone/<MID>` branch (in a worktree or in-place). All slice work commits sequentially — no branch switching, no merge conflicts. When the milestone completes, it's squash-merged to main as one clean commit. The default is `none` (work on the current branch), configurable via preferences. If `worktree` is configured in a repo with no committed `HEAD`, GSD temporarily behaves as `none` until the first commit exists because git worktrees need a committed start point.
367
368
 
@@ -659,10 +660,11 @@ auto_report: true
659
660
  | `unique_milestone_ids` | Uses unique milestone names to avoid clashes when working in teams of people |
660
661
  | `git.isolation` | `none` (default), `worktree`, or `branch` — enable worktree or branch isolation for milestone work. `worktree` requires a committed `HEAD`; zero-commit repos temporarily run as `none` |
661
662
  | `git.manage_gitignore` | Set `false` to prevent GSD from modifying `.gitignore` |
662
- | `context_mode.enabled` | Context Mode is default-on; set `false` to disable `gsd_exec`, exec history guidance, and resume snapshots |
663
+ | `context_mode.enabled` | Context Mode is default-on; set `false` to disable prompt guidance, snapshot injection, `gsd_exec`, `gsd_exec_search`, and `gsd_resume` |
663
664
  | `context_mode.exec_timeout_ms` | Timeout for sandboxed `gsd_exec` runs (default: 30000) |
664
665
  | `context_mode.exec_stdout_cap_bytes` | Persisted stdout cap for `gsd_exec` output (default: 1048576) |
665
666
  | `context_mode.exec_digest_chars` | Trailing stdout characters returned to the agent context (default: 300) |
667
+ | `context_mode.exec_env_allowlist` | Environment variables forwarded to sandboxed `gsd_exec` runs in addition to `PATH` and `HOME` |
666
668
  | `verification_commands` | Array of shell commands to run after task execution (e.g., `["npm run lint", "npm run test"]`) |
667
669
  | `verification_auto_fix` | Auto-retry on verification failures (default: true) |
668
670
  | `verification_max_retries` | Max retries for verification failures (default: 2) |
@@ -1 +1 @@
1
- 64e3cdf0430b5181
1
+ 1788ab029536c80a
@@ -23,12 +23,12 @@ import { existsSync, cpSync } from "node:fs";
23
23
  import { logWarning, logError, _resetLogs, drainLogs, drainAndSummarize, formatForNotification, hasAnyIssues, } from "../workflow-logger.js";
24
24
  import { gsdRoot } from "../paths.js";
25
25
  import { atomicWriteSync } from "../atomic-write.js";
26
- import { verifyExpectedArtifact, diagnoseExpectedArtifact, buildLoopRemediationSteps } from "../auto-recovery.js";
26
+ import { verifyExpectedArtifact, diagnoseExpectedArtifact, buildLoopRemediationSteps, refreshRecoveryDbForArtifact } from "../auto-recovery.js";
27
27
  import { writeUnitRuntimeRecord } from "../unit-runtime.js";
28
28
  import { withTimeout, FINALIZE_PRE_TIMEOUT_MS, FINALIZE_POST_TIMEOUT_MS } from "./finalize-timeout.js";
29
29
  import { getEligibleSlices } from "../slice-parallel-eligibility.js";
30
30
  import { startSliceParallel } from "../slice-parallel-orchestrator.js";
31
- import { isDbAvailable, getMilestoneSlices, refreshOpenDatabaseFromDisk } from "../gsd-db.js";
31
+ import { isDbAvailable, getMilestoneSlices } from "../gsd-db.js";
32
32
  import { ensurePlanV2Graph, isEmptyPlanV2GraphResult, isMissingFinalizedContextResult } from "../uok/plan-v2.js";
33
33
  import { resolveUokFlags } from "../uok/flags.js";
34
34
  import { UokGateRunner } from "../uok/gate-runner.js";
@@ -42,13 +42,6 @@ import { getWorkflowTransportSupportError, getRequiredWorkflowToolsForAutoUnit,
42
42
  function isSamePathLocal(a, b) {
43
43
  return normalizeWorktreePathForCompare(a) === normalizeWorktreePathForCompare(b);
44
44
  }
45
- function refreshPlanSliceRecoveryDbIfNeeded(unitType) {
46
- if (unitType !== "plan-slice")
47
- return true;
48
- if (!isDbAvailable())
49
- return true;
50
- return refreshOpenDatabaseFromDisk();
51
- }
52
45
  // ─── Session timeout auto-resume state ────────────────────────────────────────
53
46
  let consecutiveSessionTimeouts = 0;
54
47
  const MAX_SESSION_TIMEOUT_AUTO_RESUMES = 3;
@@ -795,11 +788,18 @@ export async function runDispatch(ic, preData, loopState) {
795
788
  level: 1,
796
789
  action: "artifact-found",
797
790
  });
798
- ctx.ui.notify(`Stuck recovery: artifact for ${unitType} ${unitId} found on disk. Invalidating caches.`, "info");
799
- if (!refreshPlanSliceRecoveryDbIfNeeded(unitType)) {
800
- ctx.ui.notify(`Stuck recovery found ${unitType} ${unitId} artifacts, but the DB refresh failed. Keeping stuck state for retry.`, "warning");
791
+ const recoveryDb = refreshRecoveryDbForArtifact(unitType, unitId);
792
+ if (!recoveryDb.ok) {
793
+ ctx.ui.notify(recoveryDb.fatal
794
+ ? `${recoveryDb.message} Pausing auto-mode for manual recovery.`
795
+ : `${recoveryDb.message} Keeping stuck state for retry.`, "warning");
796
+ if (recoveryDb.fatal) {
797
+ await deps.pauseAuto(ctx, pi);
798
+ return { action: "break", reason: recoveryDb.reason };
799
+ }
801
800
  return { action: "continue" };
802
801
  }
802
+ ctx.ui.notify(`Stuck recovery: artifact for ${unitType} ${unitId} found on disk. Invalidating caches.`, "info");
803
803
  deps.invalidateAllCaches();
804
804
  loopState.recentUnits.length = 0;
805
805
  loopState.stuckRecoveryAttempts = 0;
@@ -818,13 +818,20 @@ export async function runDispatch(ic, preData, loopState) {
818
818
  level: 2,
819
819
  action: "artifact-found",
820
820
  });
821
- ctx.ui.notify(`Stuck recovery: artifact for ${unitType} ${unitId} found on disk after cache invalidation. Continuing.`, "info");
822
- if (refreshPlanSliceRecoveryDbIfNeeded(unitType)) {
821
+ const recoveryDb = refreshRecoveryDbForArtifact(unitType, unitId);
822
+ if (recoveryDb.ok) {
823
+ ctx.ui.notify(`Stuck recovery: artifact for ${unitType} ${unitId} found on disk after cache invalidation. Continuing.`, "info");
823
824
  loopState.recentUnits.length = 0;
824
825
  loopState.stuckRecoveryAttempts = 0;
825
826
  return { action: "continue" };
826
827
  }
827
- ctx.ui.notify(`Stuck recovery found ${unitType} ${unitId} artifacts, but the DB refresh failed. Stopping for manual recovery.`, "warning");
828
+ ctx.ui.notify(recoveryDb.fatal
829
+ ? `${recoveryDb.message} Pausing auto-mode for manual recovery.`
830
+ : `${recoveryDb.message} Stopping for manual recovery.`, "warning");
831
+ if (recoveryDb.fatal) {
832
+ await deps.pauseAuto(ctx, pi);
833
+ return { action: "break", reason: recoveryDb.reason };
834
+ }
828
835
  }
829
836
  debugLog("autoLoop", {
830
837
  phase: "stuck-detected",
@@ -17,6 +17,7 @@ import { bumpTurnGeneration } from "./turn-epoch.js";
17
17
  // scoped pendingResolve + pendingAgentEndQueue pattern.
18
18
  let _currentResolve = null;
19
19
  let _sessionSwitchInFlight = false;
20
+ let _pendingSwitchCancellation = null;
20
21
  // ─── Setters (needed for cross-module mutation) ─────────────────────────────
21
22
  export function _setCurrentResolve(fn) {
22
23
  _currentResolve = fn;
@@ -27,6 +28,11 @@ export function _setSessionSwitchInFlight(v) {
27
28
  export function _clearCurrentResolve() {
28
29
  _currentResolve = null;
29
30
  }
31
+ export function _consumePendingSwitchCancellation() {
32
+ const pending = _pendingSwitchCancellation;
33
+ _pendingSwitchCancellation = null;
34
+ return pending;
35
+ }
30
36
  // ─── resolveAgentEnd ─────────────────────────────────────────────────────────
31
37
  /**
32
38
  * Called from the agent_end event handler in index.ts to resolve the
@@ -90,8 +96,18 @@ export function resolveAgentEndCancelled(errorContext) {
90
96
  debugLog("resolveAgentEndCancelled", { status: "resolving-cancelled" });
91
97
  const r = _currentResolve;
92
98
  _currentResolve = null;
99
+ _pendingSwitchCancellation = null;
93
100
  r({ status: "cancelled", ...(errorContext ? { errorContext } : {}) });
101
+ return true;
102
+ }
103
+ if (_sessionSwitchInFlight) {
104
+ bumpTurnGeneration(`cancelled-during-switch:${errorContext?.category ?? "unknown"}`);
105
+ _pendingSwitchCancellation = errorContext ? { errorContext } : {};
106
+ debugLog("resolveAgentEndCancelled", { status: "queued-during-switch" });
107
+ return false;
94
108
  }
109
+ debugLog("resolveAgentEndCancelled", { status: "no-pending-resolve" });
110
+ return false;
95
111
  }
96
112
  // ─── resetPendingResolve (test helper) ───────────────────────────────────────
97
113
  /**
@@ -101,6 +117,7 @@ export function resolveAgentEndCancelled(errorContext) {
101
117
  export function _resetPendingResolve() {
102
118
  _currentResolve = null;
103
119
  _sessionSwitchInFlight = false;
120
+ _pendingSwitchCancellation = null;
104
121
  }
105
122
  export function _hasPendingResolveForTest() {
106
123
  return _currentResolve !== null;
@@ -1,6 +1,6 @@
1
1
  // GSD-2 + src/resources/extensions/gsd/auto/run-unit.ts - Runs one GSD auto-mode unit from session creation through agent completion.
2
2
  import { NEW_SESSION_TIMEOUT_MS } from "./session.js";
3
- import { _clearCurrentResolve, _setCurrentResolve, _setSessionSwitchInFlight } from "./resolve.js";
3
+ import { _clearCurrentResolve, _consumePendingSwitchCancellation, _setCurrentResolve, _setSessionSwitchInFlight, } from "./resolve.js";
4
4
  import { getCurrentTurnGeneration, runWithTurnGeneration, } from "./turn-epoch.js";
5
5
  import { debugLog } from "../debug-logger.js";
6
6
  import { logWarning } from "../workflow-logger.js";
@@ -71,6 +71,7 @@ export async function runUnit(ctx, pi, s, unitType, unitId, prompt) {
71
71
  catch (sessionErr) {
72
72
  if (sessionTimeoutHandle)
73
73
  clearTimeout(sessionTimeoutHandle);
74
+ _consumePendingSwitchCancellation();
74
75
  const msg = sessionErr instanceof Error ? sessionErr.message : String(sessionErr);
75
76
  debugLog("runUnit", {
76
77
  phase: "session-error",
@@ -83,15 +84,18 @@ export async function runUnit(ctx, pi, s, unitType, unitId, prompt) {
83
84
  if (sessionTimeoutHandle)
84
85
  clearTimeout(sessionTimeoutHandle);
85
86
  if (sessionResult.cancelled) {
87
+ _consumePendingSwitchCancellation();
86
88
  debugLog("runUnit-session-timeout", { unitType, unitId });
87
89
  return { status: "cancelled", errorContext: { message: "Session creation timed out", category: "timeout", isTransient: true } };
88
90
  }
89
91
  if (!s.active) {
92
+ _consumePendingSwitchCancellation();
90
93
  return { status: "cancelled" };
91
94
  }
92
95
  if (s.currentUnitModel && typeof pi.setModel === "function") {
93
96
  const restored = await pi.setModel(s.currentUnitModel, { persist: false });
94
97
  if (!restored) {
98
+ _consumePendingSwitchCancellation();
95
99
  const message = `Failed to restore configured model ${s.currentUnitModel.provider}/${s.currentUnitModel.id} after session creation`;
96
100
  ctx.ui.notify(`${message}. Cancelling unit before dispatch.`, "warning");
97
101
  return {
@@ -111,6 +115,14 @@ export async function runUnit(ctx, pi, s, unitType, unitId, prompt) {
111
115
  const unitPromise = new Promise((resolve) => {
112
116
  _setCurrentResolve(resolve);
113
117
  });
118
+ const pendingSwitchCancellation = _consumePendingSwitchCancellation();
119
+ if (pendingSwitchCancellation) {
120
+ _clearCurrentResolve();
121
+ return {
122
+ status: "cancelled",
123
+ ...(pendingSwitchCancellation.errorContext ? { errorContext: pendingSwitchCancellation.errorContext } : {}),
124
+ };
125
+ }
114
126
  // ── Provider request-readiness pre-check (#4555) ──
115
127
  // Verify the provider can accept requests before dispatching. If the token
116
128
  // has expired since bootstrap, return cancelled immediately so the unit is
@@ -21,6 +21,7 @@ import { assertGateCoverage, getGatesForTurn, } from "./gate-registry.js";
21
21
  import { formatDecisionsCompact, formatRequirementsCompact } from "./structured-data-formatter.js";
22
22
  import { readPhaseAnchor, formatAnchorForPrompt } from "./phase-anchor.js";
23
23
  import { composeContextModeInstructions, composeInlinedContext } from "./unit-context-composer.js";
24
+ import { readCompactionSnapshot } from "./compaction-snapshot.js";
24
25
  import { logWarning } from "./workflow-logger.js";
25
26
  import { inlineGraphSubgraph } from "./graph-context.js";
26
27
  import { buildExtractionStepsBlock } from "./commands-extract-learnings.js";
@@ -178,8 +179,19 @@ function renderContextModeForPrompt(unitType, base, renderMode = "standalone") {
178
179
  renderMode,
179
180
  });
180
181
  }
181
- function prependContextModeToBlock(unitType, base, block, renderMode = "standalone") {
182
+ function renderContextModeBlockForPrompt(unitType, base, renderMode = "standalone") {
182
183
  const contextMode = renderContextModeForPrompt(unitType, base, renderMode);
184
+ if (!contextMode)
185
+ return "";
186
+ if (renderMode === "nested")
187
+ return contextMode;
188
+ const snapshot = readCompactionSnapshot(base);
189
+ if (!snapshot?.trim())
190
+ return contextMode;
191
+ return `${contextMode}\n\n## Context Snapshot\nSource: \`.gsd/last-snapshot.md\`\n\n${snapshot.trimEnd()}`;
192
+ }
193
+ function prependContextModeToBlock(unitType, base, block, renderMode = "standalone") {
194
+ const contextMode = renderContextModeBlockForPrompt(unitType, base, renderMode);
183
195
  if (!contextMode)
184
196
  return block;
185
197
  if (!block.trim())
@@ -30,7 +30,49 @@ import { getProjectResearchStatus } from "./project-research-policy.js";
30
30
  // Re-export so existing consumers of auto-recovery.ts keep working.
31
31
  export { resolveExpectedArtifactPath, diagnoseExpectedArtifact };
32
32
  export { classifyMilestoneSummaryContent, } from "./milestone-summary-classifier.js";
33
- // ─── Artifact Resolution & Verification ───────────────────────────────────────
33
+ export function refreshRecoveryDbForArtifact(unitType, unitId) {
34
+ if (unitType !== "plan-slice" && unitType !== "execute-task")
35
+ return { ok: true };
36
+ if (!isDbAvailable())
37
+ return { ok: true };
38
+ if (!refreshOpenDatabaseFromDisk()) {
39
+ return {
40
+ ok: false,
41
+ fatal: unitType === "execute-task",
42
+ reason: `${unitType}-db-refresh-failed`,
43
+ message: `Stuck recovery found ${unitType} ${unitId} artifacts, but the DB refresh failed.`,
44
+ };
45
+ }
46
+ if (unitType !== "execute-task")
47
+ return { ok: true };
48
+ const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
49
+ if (!mid || !sid || !tid) {
50
+ return {
51
+ ok: false,
52
+ fatal: true,
53
+ reason: "execute-task-invalid-unit-id",
54
+ message: `Stuck recovery found execute-task ${unitId} artifacts, but the unit id could not be parsed for DB verification.`,
55
+ };
56
+ }
57
+ const task = getTask(mid, sid, tid);
58
+ if (!task) {
59
+ return {
60
+ ok: false,
61
+ fatal: true,
62
+ reason: "execute-task-artifact-db-missing",
63
+ message: `Stuck recovery found execute-task ${unitId} artifacts, but no matching DB task row exists after refresh.`,
64
+ };
65
+ }
66
+ if (!isClosedStatus(task.status)) {
67
+ return {
68
+ ok: false,
69
+ fatal: true,
70
+ reason: "execute-task-artifact-db-mismatch",
71
+ message: `Stuck recovery found execute-task ${unitId} artifacts, but the DB task status is still '${task.status}' after refresh.`,
72
+ };
73
+ }
74
+ return { ok: true };
75
+ }
34
76
  function hasCapturedWorkflowPrefs(base) {
35
77
  const prefsPath = resolveExpectedArtifactPath("workflow-preferences", "WORKFLOW-PREFS", base);
36
78
  if (!prefsPath || !existsSync(prefsPath))
@@ -24,7 +24,7 @@ let _currentSigtermHandler = null;
24
24
  *
25
25
  * Returns the new handler so the caller can store and deregister it later.
26
26
  */
27
- export function registerSigtermHandler(currentBasePath, previousHandler) {
27
+ export function registerSigtermHandler(currentBasePath, previousHandler, onSignalCleanup) {
28
28
  // Remove the explicitly-passed previous handler
29
29
  if (previousHandler) {
30
30
  for (const sig of CLEANUP_SIGNALS)
@@ -37,6 +37,13 @@ export function registerSigtermHandler(currentBasePath, previousHandler) {
37
37
  process.off(sig, _currentSigtermHandler);
38
38
  }
39
39
  const handler = () => {
40
+ try {
41
+ onSignalCleanup?.();
42
+ }
43
+ catch {
44
+ void 0;
45
+ // Signal cleanup is best-effort; lock cleanup and process exit still run.
46
+ }
40
47
  clearLock(currentBasePath);
41
48
  releaseSessionLock(currentBasePath);
42
49
  process.exit(0);
@@ -35,13 +35,13 @@ export async function recoverTimedOutUnit(ctx, pi, unitType, unitId, reason, rct
35
35
  writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnitStartedAt, {
36
36
  recovery: status,
37
37
  });
38
- const durableComplete = status.summaryExists && status.taskChecked && status.nextActionAdvanced;
38
+ const durableComplete = status.dbComplete || (status.summaryExists && status.taskChecked && status.nextActionAdvanced);
39
39
  if (durableComplete) {
40
40
  writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnitStartedAt, {
41
41
  phase: "finalized",
42
42
  recovery: status,
43
43
  });
44
- ctx.ui.notify(`${reason === "idle" ? "Idle" : "Timeout"} recovery: ${unitType} ${unitId} already completed on disk. Continuing auto-mode. (attempt ${attemptNumber})`, "info");
44
+ ctx.ui.notify(`${reason === "idle" ? "Idle" : "Timeout"} recovery: ${unitType} ${unitId} already completed. Continuing auto-mode. (attempt ${attemptNumber})`, "info");
45
45
  unitRecoveryCount.delete(recoveryKey);
46
46
  bumpAndResolveSynthetic(`timeout-recovery:${reason}:${unitType}/${unitId}`);
47
47
  return "recovered";
@@ -20,7 +20,7 @@ import { gsdRoot, resolveMilestoneFile, resolveMilestonePath, resolveDir, milest
20
20
  import { invalidateAllCaches } from "./cache.js";
21
21
  import { clearActivityLogState } from "./activity-log.js";
22
22
  import { synthesizeCrashRecovery, getDeepDiagnostic, readActiveMilestoneId, } from "./session-forensics.js";
23
- import { writeLock, clearLock, readCrashLock, isLockProcessAlive, formatCrashInfo, emitCrashRecoveredUnitEnd, } from "./crash-recovery.js";
23
+ import { writeLock, clearLock, readCrashLock, isLockProcessAlive, formatCrashInfo, emitCrashRecoveredUnitEnd, emitOpenUnitEndForUnit, } from "./crash-recovery.js";
24
24
  import { acquireSessionLock, getSessionLockStatus, releaseSessionLock, updateSessionLock, } from "./session-lock.js";
25
25
  import { resolveAutoSupervisorConfig, loadEffectiveGSDPreferences, getIsolationMode, } from "./preferences.js";
26
26
  import { sendDesktopNotification } from "./notifications.js";
@@ -61,6 +61,8 @@ import { isClosedStatus } from "./status-guards.js";
61
61
  import { updateProgressWidget as _updateProgressWidget, updateSliceProgressCache, clearSliceProgressCache, } from "./auto-dashboard.js";
62
62
  import { registerSigtermHandler as _registerSigtermHandler, deregisterSigtermHandler as _deregisterSigtermHandler, } from "./auto-supervisor.js";
63
63
  import { isDbAvailable, getMilestone } from "./gsd-db.js";
64
+ import { markLatestActiveForWorkerCanceled } from "./db/unit-dispatches.js";
65
+ import { writeUnitRuntimeRecord } from "./unit-runtime.js";
64
66
  import { countPendingCaptures } from "./captures.js";
65
67
  import { CMUX_CHANNELS } from "../shared/cmux-events.js";
66
68
  import { ensureDbOpen } from "./bootstrap/dynamic-tools.js";
@@ -112,11 +114,12 @@ const STATE_REBUILD_MIN_INTERVAL_MS = 30_000;
112
114
  * the DB is unavailable (e.g. fresh project before init) we skip registration
113
115
  * silently rather than blocking session start.
114
116
  */
115
- function registerAutoWorkerForSession(session) {
117
+ function registerAutoWorkerForSession(session, projectRootOverride) {
116
118
  if (session.workerId)
117
119
  return; // already registered (e.g. resume re-runs)
118
120
  try {
119
- const projectRootRealpath = normalizeRealPath(session.scope?.workspace.projectRoot
121
+ const projectRootRealpath = normalizeRealPath(projectRootOverride
122
+ ?? session.scope?.workspace.projectRoot
120
123
  ?? (session.originalBasePath || session.basePath));
121
124
  session.workerId = registerAutoWorker({ projectRootRealpath });
122
125
  }
@@ -272,9 +275,50 @@ export function shouldUseWorktreeIsolation(basePath) {
272
275
  */
273
276
  // Re-export budget utilities for external consumers
274
277
  export { getBudgetAlertLevel, getNewBudgetAlertLevel, getBudgetEnforcementAction, } from "./auto-budget.js";
278
+ function closeOutSignalInterruptedUnit(currentBasePath) {
279
+ const currentUnit = s.currentUnit;
280
+ if (!currentUnit)
281
+ return;
282
+ const reason = "Auto-mode process received a termination signal";
283
+ const errorContext = {
284
+ message: reason,
285
+ category: "aborted",
286
+ isTransient: false,
287
+ };
288
+ const basePath = s.basePath || currentBasePath;
289
+ try {
290
+ emitOpenUnitEndForUnit(basePath, currentUnit.type, currentUnit.id, "cancelled", errorContext);
291
+ }
292
+ catch (err) {
293
+ logWarning("engine", `signal unit-end cleanup failed: ${getErrorMessage(err)}`, { file: "auto.ts" });
294
+ }
295
+ try {
296
+ writeUnitRuntimeRecord(basePath, currentUnit.type, currentUnit.id, currentUnit.startedAt, {
297
+ phase: "crashed",
298
+ lastProgressAt: Date.now(),
299
+ lastProgressKind: "signal",
300
+ });
301
+ }
302
+ catch (err) {
303
+ logWarning("engine", `signal runtime cleanup failed: ${getErrorMessage(err)}`, { file: "auto.ts" });
304
+ }
305
+ try {
306
+ if (s.workerId)
307
+ markLatestActiveForWorkerCanceled(s.workerId, "signal-exit");
308
+ }
309
+ catch (err) {
310
+ logWarning("engine", `signal dispatch cleanup failed: ${getErrorMessage(err)}`, { file: "auto.ts" });
311
+ }
312
+ try {
313
+ resolveAgentEndCancelled(errorContext);
314
+ }
315
+ catch (err) {
316
+ logWarning("engine", `signal resolve cleanup failed: ${getErrorMessage(err)}`, { file: "auto.ts" });
317
+ }
318
+ }
275
319
  /** Wrapper: register SIGTERM handler and store reference. */
276
320
  function registerSigtermHandler(currentBasePath) {
277
- s.sigtermHandler = _registerSigtermHandler(currentBasePath, s.sigtermHandler);
321
+ s.sigtermHandler = _registerSigtermHandler(currentBasePath, s.sigtermHandler, () => closeOutSignalInterruptedUnit(currentBasePath));
278
322
  }
279
323
  /** Wrapper: deregister SIGTERM handler and clear reference. */
280
324
  function deregisterSigtermHandler() {
@@ -1631,6 +1675,10 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
1631
1675
  lockBase,
1632
1676
  buildResolver,
1633
1677
  };
1678
+ // Register the worker before bootstrap enters a milestone worktree.
1679
+ // This ensures enterMilestone can claim a lease and seed dispatch claims
1680
+ // for crash-recovery fidelity (#5405).
1681
+ registerAutoWorkerForSession(s, base);
1634
1682
  const ready = await bootstrapAutoSession(s, ctx, pi, base, verboseMode, requestedStepMode, bootstrapDeps, freshStartAssessment);
1635
1683
  if (!ready)
1636
1684
  return;
@@ -118,9 +118,28 @@ export async function handleAgentEnd(pi, event, ctx) {
118
118
  return;
119
119
  if (!isAutoActive())
120
120
  return;
121
- if (isSessionSwitchInFlight())
122
- return;
123
121
  const lastMsg = event.messages[event.messages.length - 1];
122
+ if (isSessionSwitchInFlight()) {
123
+ if (lastMsg && "stopReason" in lastMsg && lastMsg.stopReason === "error") {
124
+ const rawErrorMsg = ("errorMessage" in lastMsg && lastMsg.errorMessage) ? String(lastMsg.errorMessage) : "";
125
+ if (isUserInitiatedAbortMessage(rawErrorMsg)) {
126
+ resolveAgentEndCancelled({
127
+ message: rawErrorMsg,
128
+ category: "aborted",
129
+ isTransient: false,
130
+ });
131
+ }
132
+ }
133
+ else if (lastMsg && "stopReason" in lastMsg && lastMsg.stopReason === "aborted") {
134
+ const content = "content" in lastMsg ? lastMsg.content : undefined;
135
+ const hasEmptyContent = Array.isArray(content) && content.length === 0;
136
+ const hasErrorMessage = "errorMessage" in lastMsg && !!lastMsg.errorMessage;
137
+ if (!hasEmptyContent || hasErrorMessage) {
138
+ resolveAgentEndCancelled(_buildAbortedPauseContext(lastMsg));
139
+ }
140
+ }
141
+ return;
142
+ }
124
143
  if (lastMsg && "stopReason" in lastMsg && lastMsg.stopReason === "aborted") {
125
144
  // Empty content with aborted stopReason is a non-fatal agent stop (the LLM
126
145
  // chose to end without producing output). Only pause on genuine fatal aborts
@@ -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
@@ -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