gsd-pi 2.78.1-dev.e9d88a536 → 2.78.1-dev.eccf86e27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -7
- package/dist/help-text.js +1 -1
- package/dist/resource-loader.js +6 -1
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto/detect-stuck.js +41 -5
- package/dist/resources/extensions/gsd/auto/loop.js +235 -36
- package/dist/resources/extensions/gsd/auto/phases.js +14 -7
- package/dist/resources/extensions/gsd/auto/session.js +36 -0
- package/dist/resources/extensions/gsd/auto-dispatch.js +49 -4
- package/dist/resources/extensions/gsd/auto-post-unit.js +26 -12
- package/dist/resources/extensions/gsd/auto-worktree.js +185 -201
- package/dist/resources/extensions/gsd/auto.js +139 -49
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +1 -1
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +26 -20
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +67 -55
- package/dist/resources/extensions/gsd/crash-recovery.js +160 -47
- package/dist/resources/extensions/gsd/db/auto-workers.js +227 -0
- package/dist/resources/extensions/gsd/db/command-queue.js +105 -0
- package/dist/resources/extensions/gsd/db/milestone-leases.js +210 -0
- package/dist/resources/extensions/gsd/db/runtime-kv.js +91 -0
- package/dist/resources/extensions/gsd/db/unit-dispatches.js +322 -0
- package/dist/resources/extensions/gsd/db-writer.js +96 -16
- package/dist/resources/extensions/gsd/delegation-policy.js +155 -0
- package/dist/resources/extensions/gsd/docs/COORDINATION.md +42 -0
- package/dist/resources/extensions/gsd/doctor-proactive.js +4 -0
- package/dist/resources/extensions/gsd/doctor-runtime-checks.js +22 -6
- package/dist/resources/extensions/gsd/doctor.js +12 -2
- package/dist/resources/extensions/gsd/gsd-db.js +355 -3
- package/dist/resources/extensions/gsd/guided-flow-queue.js +1 -1
- package/dist/resources/extensions/gsd/guided-flow.js +116 -26
- package/dist/resources/extensions/gsd/interrupted-session.js +18 -15
- package/dist/resources/extensions/gsd/metrics.js +287 -1
- package/dist/resources/extensions/gsd/paths.js +79 -8
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +4 -4
- package/dist/resources/extensions/gsd/prompts/execute-task.md +3 -3
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +8 -1
- package/dist/resources/extensions/gsd/prompts/guided-discuss-project.md +22 -7
- package/dist/resources/extensions/gsd/prompts/guided-discuss-requirements.md +6 -2
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -1
- package/dist/resources/extensions/gsd/state.js +21 -6
- package/dist/resources/extensions/gsd/templates/project.md +10 -0
- package/dist/resources/extensions/gsd/workflow-mcp.js +2 -2
- package/dist/resources/extensions/gsd/workspace.js +59 -0
- package/dist/resources/extensions/gsd/worktree-resolver.js +79 -2
- package/dist/resources/extensions/gsd/write-intercept.js +3 -3
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +14 -14
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/required-server-files.json +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +14 -14
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/server.js +1 -1
- package/package.json +1 -1
- package/packages/mcp-server/README.md +2 -11
- package/packages/mcp-server/dist/remote-questions.d.ts +27 -0
- package/packages/mcp-server/dist/remote-questions.d.ts.map +1 -1
- package/packages/mcp-server/dist/remote-questions.js +28 -0
- package/packages/mcp-server/dist/remote-questions.js.map +1 -1
- package/packages/mcp-server/dist/server.d.ts +28 -0
- package/packages/mcp-server/dist/server.d.ts.map +1 -1
- package/packages/mcp-server/dist/server.js +94 -4
- package/packages/mcp-server/dist/server.js.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/src/mcp-server.test.ts +226 -0
- package/packages/mcp-server/src/remote-questions.test.ts +103 -0
- package/packages/mcp-server/src/remote-questions.ts +35 -0
- package/packages/mcp-server/src/server.ts +129 -6
- package/packages/mcp-server/src/workflow-tools.ts +1 -1
- package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
- package/src/resources/extensions/gsd/auto/detect-stuck.ts +37 -5
- package/src/resources/extensions/gsd/auto/loop.ts +263 -41
- package/src/resources/extensions/gsd/auto/phases.ts +15 -7
- package/src/resources/extensions/gsd/auto/session.ts +40 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +63 -4
- package/src/resources/extensions/gsd/auto-post-unit.ts +27 -12
- package/src/resources/extensions/gsd/auto-worktree.ts +218 -225
- package/src/resources/extensions/gsd/auto.ts +166 -43
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +1 -1
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +26 -21
- package/src/resources/extensions/gsd/bootstrap/tests/write-gate-basepath.test.ts +103 -0
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +80 -55
- package/src/resources/extensions/gsd/crash-recovery.ts +177 -43
- package/src/resources/extensions/gsd/db/auto-workers.ts +273 -0
- package/src/resources/extensions/gsd/db/command-queue.ts +149 -0
- package/src/resources/extensions/gsd/db/milestone-leases.ts +274 -0
- package/src/resources/extensions/gsd/db/runtime-kv.ts +127 -0
- package/src/resources/extensions/gsd/db/unit-dispatches.ts +446 -0
- package/src/resources/extensions/gsd/db-writer.ts +113 -17
- package/src/resources/extensions/gsd/delegation-policy.ts +197 -0
- package/src/resources/extensions/gsd/docs/COORDINATION.md +42 -0
- package/src/resources/extensions/gsd/doctor-proactive.ts +4 -0
- package/src/resources/extensions/gsd/doctor-runtime-checks.ts +24 -6
- package/src/resources/extensions/gsd/doctor.ts +10 -2
- package/src/resources/extensions/gsd/gsd-db.ts +354 -3
- package/src/resources/extensions/gsd/guided-flow-queue.ts +1 -1
- package/src/resources/extensions/gsd/guided-flow.ts +152 -26
- package/src/resources/extensions/gsd/interrupted-session.ts +19 -12
- package/src/resources/extensions/gsd/metrics.ts +321 -1
- package/src/resources/extensions/gsd/paths.ts +67 -8
- package/src/resources/extensions/gsd/prompts/complete-slice.md +4 -4
- package/src/resources/extensions/gsd/prompts/execute-task.md +3 -3
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +8 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-project.md +22 -7
- package/src/resources/extensions/gsd/prompts/guided-discuss-requirements.md +6 -2
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -1
- package/src/resources/extensions/gsd/state.ts +44 -6
- package/src/resources/extensions/gsd/templates/project.md +10 -0
- package/src/resources/extensions/gsd/tests/auto-discuss-milestone-deadlock-4973.test.ts +14 -14
- package/src/resources/extensions/gsd/tests/auto-loop-no-copy-artifacts.test.ts +72 -0
- package/src/resources/extensions/gsd/tests/auto-loop-symlink-worktree.test.ts +190 -0
- package/src/resources/extensions/gsd/tests/auto-session-scope.test.ts +331 -0
- package/src/resources/extensions/gsd/tests/auto-workers.test.ts +105 -0
- package/src/resources/extensions/gsd/tests/auto-worktree-registry.test.ts +176 -0
- package/src/resources/extensions/gsd/tests/command-queue.test.ts +141 -0
- package/src/resources/extensions/gsd/tests/crash-recovery-via-db.test.ts +203 -0
- package/src/resources/extensions/gsd/tests/crash-recovery.test.ts +169 -59
- package/src/resources/extensions/gsd/tests/db-writer-path-containment.test.ts +152 -0
- package/src/resources/extensions/gsd/tests/db-writer-root-artifact.test.ts +221 -0
- package/src/resources/extensions/gsd/tests/db-writer-scope.test.ts +230 -0
- package/src/resources/extensions/gsd/tests/delegation-policy.test.ts +151 -0
- package/src/resources/extensions/gsd/tests/detect-stuck-respects-retry.test.ts +173 -0
- package/src/resources/extensions/gsd/tests/dispatch-backgroundable-annotation.test.ts +55 -0
- package/src/resources/extensions/gsd/tests/draft-promotion.test.ts +3 -23
- package/src/resources/extensions/gsd/tests/gate-1b-orphan-discrimination.test.ts +193 -0
- package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound-corrections.test.ts +246 -0
- package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound.test.ts +218 -0
- package/src/resources/extensions/gsd/tests/gsd-db-failed-open-restore.test.ts +117 -0
- package/src/resources/extensions/gsd/tests/gsd-db-workspace-scope.test.ts +226 -0
- package/src/resources/extensions/gsd/tests/gsd-root-canonical.test.ts +66 -0
- package/src/resources/extensions/gsd/tests/gsd-root-home-guard.test.ts +68 -5
- package/src/resources/extensions/gsd/tests/guided-flow-prompt-consolidation.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/integration/auto-worktree.test.ts +22 -12
- package/src/resources/extensions/gsd/tests/integration/doctor-proactive.test.ts +24 -10
- package/src/resources/extensions/gsd/tests/integration/doctor-runtime.test.ts +35 -23
- package/src/resources/extensions/gsd/tests/integration/workspace-collapse-integration.test.ts +369 -0
- package/src/resources/extensions/gsd/tests/interrupted-session-auto.test.ts +72 -25
- package/src/resources/extensions/gsd/tests/interrupted-session-ui.test.ts +72 -25
- package/src/resources/extensions/gsd/tests/memory-pressure-stuck-state.test.ts +9 -6
- package/src/resources/extensions/gsd/tests/metrics-atomic-merge.test.ts +222 -0
- package/src/resources/extensions/gsd/tests/metrics-lock-hardening.test.ts +400 -0
- package/src/resources/extensions/gsd/tests/metrics-lock-not-acquired.test.ts +141 -0
- package/src/resources/extensions/gsd/tests/metrics-lock-retry-sleep.test.ts +287 -0
- package/src/resources/extensions/gsd/tests/metrics-prune-cache-invalidation.test.ts +149 -0
- package/src/resources/extensions/gsd/tests/metrics-scope.test.ts +378 -0
- package/src/resources/extensions/gsd/tests/milestone-leases.test.ts +152 -0
- package/src/resources/extensions/gsd/tests/originalbase-path-comparison.test.ts +329 -0
- package/src/resources/extensions/gsd/tests/parallel-milestone-isolation.test.ts +106 -0
- package/src/resources/extensions/gsd/tests/path-cache-decoupled.test.ts +209 -0
- package/src/resources/extensions/gsd/tests/path-normalization-unified.test.ts +175 -0
- package/src/resources/extensions/gsd/tests/paths-cache.test.ts +170 -0
- package/src/resources/extensions/gsd/tests/paused-session-via-db.test.ts +119 -0
- package/src/resources/extensions/gsd/tests/pending-autostart-scope.test.ts +120 -0
- package/src/resources/extensions/gsd/tests/pipeline-variant-dispatch.test.ts +58 -0
- package/src/resources/extensions/gsd/tests/preferences-worktree-sync.test.ts +3 -17
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +150 -7
- package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +138 -16
- package/src/resources/extensions/gsd/tests/resume-missing-worktree-warning.test.ts +209 -0
- package/src/resources/extensions/gsd/tests/runtime-kv.test.ts +120 -0
- package/src/resources/extensions/gsd/tests/skipped-validation-completion.test.ts +133 -28
- package/src/resources/extensions/gsd/tests/skipped-validation-db-atomicity.test.ts +17 -0
- package/src/resources/extensions/gsd/tests/stuck-state-via-db.test.ts +134 -0
- package/src/resources/extensions/gsd/tests/sync-layer-scope.test.ts +434 -0
- package/src/resources/extensions/gsd/tests/teardown-chdir-failure-clears-registry.test.ts +162 -0
- package/src/resources/extensions/gsd/tests/teardown-cleanup-parity.test.ts +98 -0
- package/src/resources/extensions/gsd/tests/teardown-failure-clears-registry.test.ts +186 -0
- package/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/unit-dispatches.test.ts +247 -0
- package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +41 -1
- package/src/resources/extensions/gsd/tests/validator-scope-parity.test.ts +239 -0
- package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +9 -15
- package/src/resources/extensions/gsd/tests/workspace.test.ts +196 -0
- package/src/resources/extensions/gsd/tests/write-gate-predicates.test.ts +35 -35
- package/src/resources/extensions/gsd/tests/write-gate.test.ts +94 -71
- package/src/resources/extensions/gsd/tests/write-intercept.test.ts +1 -1
- package/src/resources/extensions/gsd/workflow-mcp.ts +2 -2
- package/src/resources/extensions/gsd/workspace.ts +95 -0
- package/src/resources/extensions/gsd/worktree-resolver.ts +78 -2
- package/src/resources/extensions/gsd/write-intercept.ts +3 -3
- package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +0 -213
- package/src/resources/extensions/gsd/tests/auto-stale-lock-self-kill.test.ts +0 -87
- package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +0 -159
- /package/dist/web/standalone/.next/static/{oZGTPvJBQX_IDKKnuV8Bt → Y5UeGFkXTYM9WIQOWHkot}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{oZGTPvJBQX_IDKKnuV8Bt → Y5UeGFkXTYM9WIQOWHkot}/_ssgManifest.js +0 -0
|
@@ -63,20 +63,42 @@ const QUEUE_SAFE_TOOLS = new Set([
|
|
|
63
63
|
*/
|
|
64
64
|
const BASH_READ_ONLY_RE = /^\s*(cat|head|tail|less|more|wc|file|stat|du|df|which|type|echo|printf|ls|find|grep|rg|awk|sed\b(?!.*-i)|sort|uniq|diff|comm|tr|cut|tee\s+-a\s+\/dev\/null|git\s+(log|show|diff|status|branch|tag|remote|rev-parse|ls-files|blame|shortlog|describe|stash\s+list|config\s+--get|cat-file)|gh\s+(issue|pr|api|repo|release)\s+(view|list|diff|status|checks)|mkdir\s+-p\s+\.gsd|rtk\s|npm\s+run\s+(test|test:\w+|lint|lint:\w+|typecheck|type-check|type-check:\w+|check|verify|audit|outdated|format:check|ci|validate)\b|npm\s+(ls|list|info|view|show|outdated|audit|explain|doctor|ping|--version|-v)\b|npx\s|tsx\s|node\s+(--print|--version|-v\b)|python[23]?\s+(-c\s+'[^']*'|--version|-V\b|-m\s+(pip\s+show|pip\s+list|site))|pip[23]?\s+(show|list|freeze|check|index\s+versions)\b|jq\s|yq\s|curl\s+(-s\b|--silent\b)(?!\s+[^|>]*\s-[oO]\b)(?!\s+[^|>]*\s--output\b)[^|>]*$|openssl\s+(version|x509|s_client)|env\b|printenv\b|true\b|false\b)/;
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
interface InMemoryWriteGateState {
|
|
67
|
+
verifiedDepthMilestones: Set<string>;
|
|
68
|
+
verifiedApprovalGates: Set<string>;
|
|
69
|
+
activeQueuePhase: boolean;
|
|
70
|
+
pendingGateId: string | null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function createEmptyWriteGateState(): InMemoryWriteGateState {
|
|
74
|
+
return {
|
|
75
|
+
verifiedDepthMilestones: new Set<string>(),
|
|
76
|
+
verifiedApprovalGates: new Set<string>(),
|
|
77
|
+
activeQueuePhase: false,
|
|
78
|
+
pendingGateId: null,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const writeGateStatesByBasePath = new Map<string, InMemoryWriteGateState>();
|
|
83
|
+
|
|
84
|
+
function writeGateStateKey(basePath: string): string {
|
|
85
|
+
return resolve(basePath);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function getWriteGateState(basePath: string = process.cwd()): InMemoryWriteGateState {
|
|
89
|
+
const key = writeGateStateKey(basePath);
|
|
90
|
+
let state = writeGateStatesByBasePath.get(key);
|
|
91
|
+
if (!state) {
|
|
92
|
+
state = createEmptyWriteGateState();
|
|
93
|
+
writeGateStatesByBasePath.set(key, state);
|
|
94
|
+
}
|
|
95
|
+
return state;
|
|
96
|
+
}
|
|
69
97
|
|
|
70
98
|
/**
|
|
71
|
-
* Discussion gate enforcement state
|
|
72
|
-
*
|
|
73
|
-
* When ask_user_questions is called with a recognized gate question ID,
|
|
74
|
-
* we track the pending gate. Until the gate is confirmed (user selects the
|
|
75
|
-
* first/recommended option), all non-read-only tool calls are blocked.
|
|
76
|
-
* This mechanically prevents the model from rationalizing past failed or
|
|
77
|
-
* cancelled gate questions.
|
|
99
|
+
* Discussion gate enforcement state is scoped per basePath so multiple
|
|
100
|
+
* workspaces can coexist in the same process without sharing gate state.
|
|
78
101
|
*/
|
|
79
|
-
let pendingGateId: string | null = null;
|
|
80
102
|
|
|
81
103
|
/**
|
|
82
104
|
* Recognized gate question ID patterns.
|
|
@@ -119,25 +141,26 @@ function shouldPersistWriteGateSnapshot(env: NodeJS.ProcessEnv = process.env): b
|
|
|
119
141
|
return v !== "0" && v !== "false";
|
|
120
142
|
}
|
|
121
143
|
|
|
122
|
-
function writeGateSnapshotPath(basePath: string
|
|
144
|
+
function writeGateSnapshotPath(basePath: string): string {
|
|
123
145
|
return join(basePath, ".gsd", "runtime", "write-gate-state.json");
|
|
124
146
|
}
|
|
125
147
|
|
|
126
|
-
function currentWriteGateSnapshot(): WriteGateSnapshot {
|
|
148
|
+
function currentWriteGateSnapshot(basePath: string = process.cwd()): WriteGateSnapshot {
|
|
149
|
+
const state = getWriteGateState(basePath);
|
|
127
150
|
return {
|
|
128
|
-
verifiedDepthMilestones: [...verifiedDepthMilestones].sort(),
|
|
129
|
-
verifiedApprovalGates: [...verifiedApprovalGates].sort(),
|
|
130
|
-
activeQueuePhase,
|
|
131
|
-
pendingGateId,
|
|
151
|
+
verifiedDepthMilestones: [...state.verifiedDepthMilestones].sort(),
|
|
152
|
+
verifiedApprovalGates: [...state.verifiedApprovalGates].sort(),
|
|
153
|
+
activeQueuePhase: state.activeQueuePhase,
|
|
154
|
+
pendingGateId: state.pendingGateId,
|
|
132
155
|
};
|
|
133
156
|
}
|
|
134
157
|
|
|
135
|
-
function persistWriteGateSnapshot(basePath: string
|
|
158
|
+
function persistWriteGateSnapshot(basePath: string): void {
|
|
136
159
|
if (!shouldPersistWriteGateSnapshot()) return;
|
|
137
160
|
const path = writeGateSnapshotPath(basePath);
|
|
138
161
|
mkdirSync(join(basePath, ".gsd", "runtime"), { recursive: true });
|
|
139
162
|
const tempPath = `${path}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
140
|
-
writeFileSync(tempPath, JSON.stringify(currentWriteGateSnapshot(), null, 2), "utf-8");
|
|
163
|
+
writeFileSync(tempPath, JSON.stringify(currentWriteGateSnapshot(basePath), null, 2), "utf-8");
|
|
141
164
|
try {
|
|
142
165
|
renameSync(tempPath, path);
|
|
143
166
|
} catch (err: unknown) {
|
|
@@ -152,7 +175,7 @@ function persistWriteGateSnapshot(basePath: string = process.cwd()): void {
|
|
|
152
175
|
}
|
|
153
176
|
}
|
|
154
177
|
|
|
155
|
-
function clearPersistedWriteGateSnapshot(basePath: string
|
|
178
|
+
function clearPersistedWriteGateSnapshot(basePath: string): void {
|
|
156
179
|
if (!shouldPersistWriteGateSnapshot()) return;
|
|
157
180
|
const path = writeGateSnapshotPath(basePath);
|
|
158
181
|
try {
|
|
@@ -185,32 +208,35 @@ const EMPTY_SNAPSHOT: WriteGateSnapshot = {
|
|
|
185
208
|
pendingGateId: null,
|
|
186
209
|
};
|
|
187
210
|
|
|
188
|
-
export function loadWriteGateSnapshot(basePath: string
|
|
211
|
+
export function loadWriteGateSnapshot(basePath: string): WriteGateSnapshot {
|
|
189
212
|
const path = writeGateSnapshotPath(basePath);
|
|
190
213
|
if (!existsSync(path)) {
|
|
191
214
|
// When persist mode is active and the file has been deleted, treat it as a
|
|
192
215
|
// full state reset so deleting the file clears the HARD BLOCK gate.
|
|
193
216
|
// In non-persist mode the file is never written, so fall back to in-memory.
|
|
194
217
|
if (shouldPersistWriteGateSnapshot()) return EMPTY_SNAPSHOT;
|
|
195
|
-
return currentWriteGateSnapshot();
|
|
218
|
+
return currentWriteGateSnapshot(basePath);
|
|
196
219
|
}
|
|
197
220
|
try {
|
|
198
221
|
return normalizeWriteGateSnapshot(JSON.parse(readFileSync(path, "utf-8")));
|
|
199
222
|
} catch {
|
|
200
|
-
return currentWriteGateSnapshot();
|
|
223
|
+
return currentWriteGateSnapshot(basePath);
|
|
201
224
|
}
|
|
202
225
|
}
|
|
203
226
|
|
|
204
|
-
export function isDepthVerified(): boolean {
|
|
205
|
-
return verifiedDepthMilestones.size > 0;
|
|
227
|
+
export function isDepthVerified(basePath: string = process.cwd()): boolean {
|
|
228
|
+
return getWriteGateState(basePath).verifiedDepthMilestones.size > 0;
|
|
206
229
|
}
|
|
207
230
|
|
|
208
231
|
/**
|
|
209
232
|
* Check whether a specific milestone has passed depth verification.
|
|
210
233
|
*/
|
|
211
|
-
export function isMilestoneDepthVerified(
|
|
234
|
+
export function isMilestoneDepthVerified(
|
|
235
|
+
milestoneId: string | null | undefined,
|
|
236
|
+
basePath: string = process.cwd(),
|
|
237
|
+
): boolean {
|
|
212
238
|
if (!milestoneId) return false;
|
|
213
|
-
return verifiedDepthMilestones.has(milestoneId);
|
|
239
|
+
return getWriteGateState(basePath).verifiedDepthMilestones.has(milestoneId);
|
|
214
240
|
}
|
|
215
241
|
|
|
216
242
|
export function isMilestoneDepthVerifiedInSnapshot(
|
|
@@ -221,39 +247,37 @@ export function isMilestoneDepthVerifiedInSnapshot(
|
|
|
221
247
|
return snapshot.verifiedDepthMilestones.includes(milestoneId);
|
|
222
248
|
}
|
|
223
249
|
|
|
224
|
-
export function isQueuePhaseActive(): boolean {
|
|
225
|
-
return activeQueuePhase;
|
|
250
|
+
export function isQueuePhaseActive(basePath: string = process.cwd()): boolean {
|
|
251
|
+
return getWriteGateState(basePath).activeQueuePhase;
|
|
226
252
|
}
|
|
227
253
|
|
|
228
|
-
export function setQueuePhaseActive(active: boolean): void {
|
|
229
|
-
activeQueuePhase = active;
|
|
230
|
-
persistWriteGateSnapshot();
|
|
254
|
+
export function setQueuePhaseActive(active: boolean, basePath: string): void {
|
|
255
|
+
getWriteGateState(basePath).activeQueuePhase = active;
|
|
256
|
+
persistWriteGateSnapshot(basePath);
|
|
231
257
|
}
|
|
232
258
|
|
|
233
|
-
export function resetWriteGateState(): void {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
259
|
+
export function resetWriteGateState(basePath: string): void {
|
|
260
|
+
const state = getWriteGateState(basePath);
|
|
261
|
+
state.verifiedDepthMilestones.clear();
|
|
262
|
+
state.verifiedApprovalGates.clear();
|
|
263
|
+
state.pendingGateId = null;
|
|
264
|
+
persistWriteGateSnapshot(basePath);
|
|
238
265
|
}
|
|
239
266
|
|
|
240
|
-
export function clearDiscussionFlowState(): void {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
activeQueuePhase = false;
|
|
244
|
-
pendingGateId = null;
|
|
245
|
-
clearPersistedWriteGateSnapshot();
|
|
267
|
+
export function clearDiscussionFlowState(basePath: string): void {
|
|
268
|
+
writeGateStatesByBasePath.delete(writeGateStateKey(basePath));
|
|
269
|
+
clearPersistedWriteGateSnapshot(basePath);
|
|
246
270
|
}
|
|
247
271
|
|
|
248
272
|
export function markDepthVerified(milestoneId?: string | null, basePath: string = process.cwd()): void {
|
|
249
273
|
if (!milestoneId) return;
|
|
250
|
-
verifiedDepthMilestones.add(milestoneId);
|
|
274
|
+
getWriteGateState(basePath).verifiedDepthMilestones.add(milestoneId);
|
|
251
275
|
persistWriteGateSnapshot(basePath);
|
|
252
276
|
}
|
|
253
277
|
|
|
254
278
|
export function markApprovalGateVerified(gateId?: string | null, basePath: string = process.cwd()): void {
|
|
255
279
|
if (!gateId) return;
|
|
256
|
-
verifiedApprovalGates.add(gateId);
|
|
280
|
+
getWriteGateState(basePath).verifiedApprovalGates.add(gateId);
|
|
257
281
|
persistWriteGateSnapshot(basePath);
|
|
258
282
|
}
|
|
259
283
|
|
|
@@ -292,27 +316,28 @@ function extractContextMilestoneId(inputPath: string): string | null {
|
|
|
292
316
|
/**
|
|
293
317
|
* Mark a gate as pending (called when ask_user_questions is invoked with a gate ID).
|
|
294
318
|
*/
|
|
295
|
-
export function setPendingGate(gateId: string): void {
|
|
296
|
-
|
|
297
|
-
|
|
319
|
+
export function setPendingGate(gateId: string, basePath: string): void {
|
|
320
|
+
const state = getWriteGateState(basePath);
|
|
321
|
+
state.pendingGateId = gateId;
|
|
322
|
+
state.verifiedApprovalGates.delete(gateId);
|
|
298
323
|
const milestoneId = extractDepthVerificationMilestoneId(gateId);
|
|
299
|
-
if (milestoneId) verifiedDepthMilestones.delete(milestoneId);
|
|
300
|
-
persistWriteGateSnapshot();
|
|
324
|
+
if (milestoneId) state.verifiedDepthMilestones.delete(milestoneId);
|
|
325
|
+
persistWriteGateSnapshot(basePath);
|
|
301
326
|
}
|
|
302
327
|
|
|
303
328
|
/**
|
|
304
329
|
* Clear the pending gate (called when the user confirms).
|
|
305
330
|
*/
|
|
306
|
-
export function clearPendingGate(): void {
|
|
307
|
-
pendingGateId = null;
|
|
308
|
-
persistWriteGateSnapshot();
|
|
331
|
+
export function clearPendingGate(basePath: string): void {
|
|
332
|
+
getWriteGateState(basePath).pendingGateId = null;
|
|
333
|
+
persistWriteGateSnapshot(basePath);
|
|
309
334
|
}
|
|
310
335
|
|
|
311
336
|
/**
|
|
312
337
|
* Get the currently pending gate, if any.
|
|
313
338
|
*/
|
|
314
|
-
export function getPendingGate(): string | null {
|
|
315
|
-
return pendingGateId;
|
|
339
|
+
export function getPendingGate(basePath: string = process.cwd()): string | null {
|
|
340
|
+
return getWriteGateState(basePath).pendingGateId;
|
|
316
341
|
}
|
|
317
342
|
|
|
318
343
|
/**
|
|
@@ -1,21 +1,43 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* GSD Crash Recovery
|
|
2
|
+
* GSD Crash Recovery (Phase C pt 2 — DB-backed)
|
|
3
3
|
*
|
|
4
|
-
* Detects interrupted auto-mode sessions via
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Detects interrupted auto-mode sessions via the DB-backed workers +
|
|
5
|
+
* unit_dispatches + runtime_kv tables. The auto.lock file is gone; the
|
|
6
|
+
* `LockData` shape is preserved for backward compatibility with callers
|
|
7
|
+
* (auto.ts, doctor checks, interrupted-session.ts), but the contents are
|
|
8
|
+
* now synthesized from:
|
|
7
9
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
10
|
+
* - workers.pid / .started_at / .last_heartbeat_at → liveness + age
|
|
11
|
+
* - unit_dispatches.unit_type / .unit_id / .started_at → what was running
|
|
12
|
+
* - runtime_kv("worker", workerId, "session_file") → pi session JSONL path
|
|
13
|
+
*
|
|
14
|
+
* "Crashed" is detected via workers.status='active' + heartbeat past TTL,
|
|
15
|
+
* cross-checked with the OS PID via isLockProcessAlive(). When the DB is
|
|
16
|
+
* unavailable (fresh project before init), all readers return null and
|
|
17
|
+
* writers no-op — preserving the historical "no lock means no prior
|
|
18
|
+
* crash" semantics.
|
|
19
|
+
*
|
|
20
|
+
* The journal-based emitCrashRecoveredUnitEnd is unchanged from the file
|
|
21
|
+
* era — it queries the journal independently of the lock mechanism.
|
|
11
22
|
*/
|
|
12
23
|
|
|
24
|
+
import {
|
|
25
|
+
emitJournalEvent,
|
|
26
|
+
queryJournal,
|
|
27
|
+
} from "./journal.js";
|
|
13
28
|
import { readFileSync, unlinkSync, existsSync } from "node:fs";
|
|
14
29
|
import { join } from "node:path";
|
|
15
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
findStaleWorkerForProject,
|
|
32
|
+
getAllAutoWorkers,
|
|
33
|
+
type AutoWorkerRow,
|
|
34
|
+
} from "./db/auto-workers.js";
|
|
35
|
+
import { getLatestForUnit, type DispatchStatus } from "./db/unit-dispatches.js";
|
|
36
|
+
import { getRuntimeKv, setRuntimeKv, deleteRuntimeKv } from "./db/runtime-kv.js";
|
|
37
|
+
import { _getAdapter, isDbAvailable } from "./gsd-db.js";
|
|
38
|
+
import { gsdRoot, normalizeRealPath } from "./paths.js";
|
|
16
39
|
import { atomicWriteSync } from "./atomic-write.js";
|
|
17
40
|
import { effectiveLockFile } from "./session-lock.js";
|
|
18
|
-
import { emitJournalEvent, queryJournal } from "./journal.js";
|
|
19
41
|
|
|
20
42
|
export interface LockData {
|
|
21
43
|
pid: number;
|
|
@@ -27,11 +49,91 @@ export interface LockData {
|
|
|
27
49
|
sessionFile?: string;
|
|
28
50
|
}
|
|
29
51
|
|
|
52
|
+
const SESSION_FILE_KV_KEY = "session_file";
|
|
53
|
+
|
|
30
54
|
function lockPath(basePath: string): string {
|
|
31
55
|
return join(gsdRoot(basePath), effectiveLockFile());
|
|
32
56
|
}
|
|
33
57
|
|
|
34
|
-
|
|
58
|
+
function readLegacyLock(basePath: string): LockData | null {
|
|
59
|
+
try {
|
|
60
|
+
const p = lockPath(basePath);
|
|
61
|
+
if (!existsSync(p)) return null;
|
|
62
|
+
return JSON.parse(readFileSync(p, "utf-8")) as LockData;
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function findActiveWorkerForCurrentProcess(
|
|
69
|
+
projectRootRealpath: string,
|
|
70
|
+
): AutoWorkerRow | null {
|
|
71
|
+
if (!isDbAvailable()) return null;
|
|
72
|
+
const workers = getAllAutoWorkers();
|
|
73
|
+
for (const worker of workers) {
|
|
74
|
+
if (
|
|
75
|
+
worker.pid === process.pid
|
|
76
|
+
&& worker.project_root_realpath === projectRootRealpath
|
|
77
|
+
) {
|
|
78
|
+
return worker;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Look up the most recent dispatch row for a worker, regardless of status.
|
|
86
|
+
* Returns null if the worker has no dispatch history yet (e.g. crashed
|
|
87
|
+
* during bootstrap before claiming the first unit).
|
|
88
|
+
*/
|
|
89
|
+
function getLatestDispatchForWorker(workerId: string):
|
|
90
|
+
| { unit_type: string; unit_id: string; started_at: string; status: DispatchStatus }
|
|
91
|
+
| null {
|
|
92
|
+
if (!isDbAvailable()) return null;
|
|
93
|
+
const db = _getAdapter()!;
|
|
94
|
+
const row = db.prepare(
|
|
95
|
+
`SELECT unit_type, unit_id, started_at, status
|
|
96
|
+
FROM unit_dispatches
|
|
97
|
+
WHERE worker_id = :worker_id
|
|
98
|
+
ORDER BY id DESC
|
|
99
|
+
LIMIT 1`,
|
|
100
|
+
).get({ ":worker_id": workerId }) as
|
|
101
|
+
| { unit_type: string; unit_id: string; started_at: string; status: DispatchStatus }
|
|
102
|
+
| undefined;
|
|
103
|
+
return row ?? null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function workerToLockData(worker: AutoWorkerRow): LockData {
|
|
107
|
+
const dispatch = getLatestDispatchForWorker(worker.worker_id);
|
|
108
|
+
const sessionFile =
|
|
109
|
+
getRuntimeKv<string>("worker", worker.worker_id, SESSION_FILE_KV_KEY) ?? undefined;
|
|
110
|
+
return {
|
|
111
|
+
pid: worker.pid,
|
|
112
|
+
startedAt: worker.started_at,
|
|
113
|
+
// Pre-Phase-C-pt-2 default: when no dispatch row exists yet (bootstrap
|
|
114
|
+
// crash), report unitType="starting", unitId="bootstrap" — same shape
|
|
115
|
+
// the file-based writer used to produce.
|
|
116
|
+
unitType: dispatch?.unit_type ?? "starting",
|
|
117
|
+
unitId: dispatch?.unit_id ?? "bootstrap",
|
|
118
|
+
unitStartedAt: dispatch?.started_at ?? worker.started_at,
|
|
119
|
+
sessionFile,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Write or update the lock state for the current auto-mode session.
|
|
125
|
+
*
|
|
126
|
+
* Phase C pt 2: the only persistent state this function adds beyond what
|
|
127
|
+
* the workers + unit_dispatches tables already track is the pi session
|
|
128
|
+
* JSONL path, which lands in runtime_kv (worker scope, key
|
|
129
|
+
* "session_file"). The pid/startedAt/unitType/unitId/unitStartedAt are
|
|
130
|
+
* recorded by registerAutoWorker / heartbeatAutoWorker / recordDispatchClaim
|
|
131
|
+
* already.
|
|
132
|
+
*
|
|
133
|
+
* basePath is unused by the new implementation (kept as a parameter for
|
|
134
|
+
* back-compat with the 15+ call sites) — the worker is identified by
|
|
135
|
+
* pid + project_root_realpath in the workers table.
|
|
136
|
+
*/
|
|
35
137
|
export function writeLock(
|
|
36
138
|
basePath: string,
|
|
37
139
|
unitType: string,
|
|
@@ -47,51 +149,84 @@ export function writeLock(
|
|
|
47
149
|
unitStartedAt: new Date().toISOString(),
|
|
48
150
|
sessionFile,
|
|
49
151
|
};
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
152
|
+
atomicWriteSync(lockPath(basePath), JSON.stringify(data, null, 2));
|
|
153
|
+
} catch {
|
|
154
|
+
// Best-effort — never throw from the lock writer.
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!isDbAvailable() || !sessionFile) return;
|
|
158
|
+
try {
|
|
159
|
+
const projectRoot = normalizeRealPath(basePath);
|
|
160
|
+
const worker = findActiveWorkerForCurrentProcess(projectRoot);
|
|
161
|
+
if (!worker) return;
|
|
162
|
+
setRuntimeKv("worker", worker.worker_id, SESSION_FILE_KV_KEY, sessionFile);
|
|
163
|
+
} catch {
|
|
164
|
+
// Best-effort — never throw from the lock writer.
|
|
165
|
+
}
|
|
53
166
|
}
|
|
54
167
|
|
|
55
|
-
/**
|
|
168
|
+
/**
|
|
169
|
+
* Phase C pt 2: clearLock no longer deletes a file. The cleanup path
|
|
170
|
+
* (markWorkerStopping in stopAuto) flips the workers row to 'stopping'.
|
|
171
|
+
* This function additionally drops the session_file runtime_kv row for
|
|
172
|
+
* the current worker so a follow-up crash detection doesn't pick up a
|
|
173
|
+
* stale session-file pointer.
|
|
174
|
+
*/
|
|
56
175
|
export function clearLock(basePath: string): void {
|
|
57
176
|
try {
|
|
58
177
|
const p = lockPath(basePath);
|
|
59
178
|
if (existsSync(p)) unlinkSync(p);
|
|
60
|
-
} catch
|
|
179
|
+
} catch {
|
|
180
|
+
// Best-effort.
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!isDbAvailable()) return;
|
|
184
|
+
try {
|
|
185
|
+
const projectRoot = normalizeRealPath(basePath);
|
|
186
|
+
const worker = findActiveWorkerForCurrentProcess(projectRoot);
|
|
187
|
+
if (!worker) return;
|
|
188
|
+
deleteRuntimeKv("worker", worker.worker_id, SESSION_FILE_KV_KEY);
|
|
189
|
+
} catch {
|
|
190
|
+
// Best-effort.
|
|
191
|
+
}
|
|
61
192
|
}
|
|
62
193
|
|
|
63
|
-
/**
|
|
194
|
+
/**
|
|
195
|
+
* Detect a previous crashed auto-mode session.
|
|
196
|
+
*
|
|
197
|
+
* Phase C pt 2: synthesized from workers (status='active' + lapsed
|
|
198
|
+
* heartbeat) + unit_dispatches (most recent for that worker) +
|
|
199
|
+
* runtime_kv (session_file). Returns null when no stale worker exists
|
|
200
|
+
* or the DB is unavailable.
|
|
201
|
+
*/
|
|
64
202
|
export function readCrashLock(basePath: string): LockData | null {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
203
|
+
if (isDbAvailable()) {
|
|
204
|
+
try {
|
|
205
|
+
const projectRoot = normalizeRealPath(basePath);
|
|
206
|
+
const stale = findStaleWorkerForProject(projectRoot);
|
|
207
|
+
if (stale) return workerToLockData(stale);
|
|
208
|
+
} catch {
|
|
209
|
+
// Fall through to the legacy lock-file compatibility path.
|
|
210
|
+
}
|
|
73
211
|
}
|
|
212
|
+
return readLegacyLock(basePath);
|
|
74
213
|
}
|
|
75
214
|
|
|
76
215
|
/**
|
|
77
216
|
* Check whether the process that wrote the lock is still running.
|
|
78
217
|
* Uses `process.kill(pid, 0)` which sends no signal but checks liveness.
|
|
79
218
|
* Returns true if the PID matches our own — we are the lock holder (#2470).
|
|
219
|
+
*
|
|
220
|
+
* Unchanged from the file-based era — pure stateless OS check.
|
|
80
221
|
*/
|
|
81
222
|
export function isLockProcessAlive(lock: LockData): boolean {
|
|
82
223
|
const pid = lock.pid;
|
|
83
224
|
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
84
|
-
// Our own PID means WE hold this lock — we are alive. (#2470)
|
|
85
|
-
// Callers that need to distinguish "our lock" from "someone else's lock"
|
|
86
|
-
// (e.g. startAuto checking for a prior crashed session with a recycled PID)
|
|
87
|
-
// already guard with `crashLock.pid !== process.pid` before calling us.
|
|
88
225
|
if (pid === process.pid) return true;
|
|
89
226
|
try {
|
|
90
227
|
process.kill(pid, 0);
|
|
91
228
|
return true;
|
|
92
229
|
} catch (err) {
|
|
93
|
-
// EPERM means the process exists but we lack permission — treat as alive.
|
|
94
|
-
// ESRCH means the process does not exist — treat as dead (stale lock).
|
|
95
230
|
if ((err as NodeJS.ErrnoException).code === "EPERM") return true;
|
|
96
231
|
return false;
|
|
97
232
|
}
|
|
@@ -106,7 +241,6 @@ export function formatCrashInfo(lock: LockData): string {
|
|
|
106
241
|
` PID: ${lock.pid}`,
|
|
107
242
|
];
|
|
108
243
|
|
|
109
|
-
// Add recovery guidance based on what was happening when it crashed
|
|
110
244
|
if (lock.unitType === "starting" && lock.unitId === "bootstrap") {
|
|
111
245
|
lines.push(`No work was lost. Run /gsd auto to restart.`);
|
|
112
246
|
} else if (lock.unitType.includes("research") || lock.unitType.includes("plan")) {
|
|
@@ -122,22 +256,14 @@ export function formatCrashInfo(lock: LockData): string {
|
|
|
122
256
|
|
|
123
257
|
/**
|
|
124
258
|
* Emit a synthetic unit-end event for a unit that crashed without emitting its own.
|
|
125
|
-
*
|
|
126
|
-
* Queries the journal to find the most recent unit-start for the crashed unit.
|
|
127
|
-
* If a matching unit-end already exists (e.g. the hard timeout fired), this is a
|
|
128
|
-
* no-op. Called during crash recovery, before clearing the stale lock.
|
|
129
|
-
*
|
|
130
|
-
* Addresses the gap reported in #3348 where `unit-start` was emitted but no
|
|
131
|
-
* `unit-end` followed — side effects landed but the worker died before closeout.
|
|
259
|
+
* Unchanged from the file era — operates on the journal, not the lock.
|
|
132
260
|
*/
|
|
133
261
|
export function emitCrashRecoveredUnitEnd(basePath: string, lock: LockData): void {
|
|
134
|
-
// Skip bootstrap / starting pseudo-units — they have no meaningful unit-start event.
|
|
135
262
|
if (!lock.unitType || !lock.unitId || lock.unitType === "starting") return;
|
|
136
263
|
|
|
137
264
|
try {
|
|
138
265
|
const all = queryJournal(basePath);
|
|
139
266
|
|
|
140
|
-
// Find the most recent unit-start for this unitId
|
|
141
267
|
const starts = all.filter(
|
|
142
268
|
(e) => e.eventType === "unit-start" && e.data?.unitId === lock.unitId,
|
|
143
269
|
);
|
|
@@ -145,7 +271,6 @@ export function emitCrashRecoveredUnitEnd(basePath: string, lock: LockData): voi
|
|
|
145
271
|
|
|
146
272
|
const lastStart = starts[starts.length - 1];
|
|
147
273
|
|
|
148
|
-
// Check if a unit-end was already emitted (e.g. hard timeout fired after the crash)
|
|
149
274
|
const alreadyClosed = all.some(
|
|
150
275
|
(e) =>
|
|
151
276
|
e.eventType === "unit-end" &&
|
|
@@ -155,7 +280,6 @@ export function emitCrashRecoveredUnitEnd(basePath: string, lock: LockData): voi
|
|
|
155
280
|
);
|
|
156
281
|
if (alreadyClosed) return;
|
|
157
282
|
|
|
158
|
-
// Find the highest seq in this flow for monotonic ordering
|
|
159
283
|
const maxSeq = all
|
|
160
284
|
.filter((e) => e.flowId === lastStart.flowId)
|
|
161
285
|
.reduce((max, e) => Math.max(max, e.seq), lastStart.seq);
|
|
@@ -174,6 +298,16 @@ export function emitCrashRecoveredUnitEnd(basePath: string, lock: LockData): voi
|
|
|
174
298
|
causedBy: { flowId: lastStart.flowId, seq: lastStart.seq },
|
|
175
299
|
});
|
|
176
300
|
} catch {
|
|
177
|
-
// Never throw from crash recovery path
|
|
301
|
+
// Never throw from crash recovery path.
|
|
178
302
|
}
|
|
179
303
|
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Used by the doctor checks (doctor-runtime-checks.ts, doctor-proactive.ts)
|
|
307
|
+
* to enumerate stale workers across all projects this DB knows about.
|
|
308
|
+
* Phase C pt 2 export — surface for the same diagnostics that previously
|
|
309
|
+
* iterated `auto.lock` files.
|
|
310
|
+
*/
|
|
311
|
+
export function findStaleAutoWorker(basePath: string): LockData | null {
|
|
312
|
+
return readCrashLock(basePath);
|
|
313
|
+
}
|