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
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// gsd-2 + Non-correctness-critical key-value storage (Phase C — file-state migration)
|
|
2
|
+
//
|
|
3
|
+
// STRICT INVARIANT (re-stated from gsd-db.ts createRuntimeKvTableV25):
|
|
4
|
+
// runtime_kv is for SOFT state only. UI cursors, dashboard caches,
|
|
5
|
+
// last-seen-version markers, resume cursors, and similar values that
|
|
6
|
+
// can be lost without breaking auto-mode correctness.
|
|
7
|
+
//
|
|
8
|
+
// Anything that drives the auto-loop's control flow MUST get typed
|
|
9
|
+
// columns in unit_dispatches / workers / milestone_leases — never a
|
|
10
|
+
// bag of JSON in runtime_kv. The reviewer's smell test: if losing the
|
|
11
|
+
// row would cause the loop to reorder, double-execute, or stuck-loop,
|
|
12
|
+
// it does NOT belong here.
|
|
13
|
+
//
|
|
14
|
+
// Single-host invariant: SQLite WAL coordination, local disk only.
|
|
15
|
+
// See db/auto-workers.ts for the same constraint applied to coordination.
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
_getAdapter,
|
|
19
|
+
isDbAvailable,
|
|
20
|
+
transaction,
|
|
21
|
+
} from "../gsd-db.js";
|
|
22
|
+
|
|
23
|
+
export type RuntimeKvScope = "global" | "worker" | "milestone";
|
|
24
|
+
|
|
25
|
+
export interface RuntimeKvRow {
|
|
26
|
+
scope: RuntimeKvScope;
|
|
27
|
+
scope_id: string;
|
|
28
|
+
key: string;
|
|
29
|
+
value_json: string;
|
|
30
|
+
updated_at: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Set or update a runtime_kv row. The value is JSON-stringified before
|
|
35
|
+
* storage. Best-effort — silently no-ops when the DB is unavailable.
|
|
36
|
+
*/
|
|
37
|
+
export function setRuntimeKv(
|
|
38
|
+
scope: RuntimeKvScope,
|
|
39
|
+
scopeId: string,
|
|
40
|
+
key: string,
|
|
41
|
+
value: unknown,
|
|
42
|
+
): void {
|
|
43
|
+
if (!isDbAvailable()) return;
|
|
44
|
+
const now = new Date().toISOString();
|
|
45
|
+
const db = _getAdapter()!;
|
|
46
|
+
let valueJson: string;
|
|
47
|
+
try {
|
|
48
|
+
valueJson = JSON.stringify(value);
|
|
49
|
+
} catch {
|
|
50
|
+
valueJson = JSON.stringify(String(value));
|
|
51
|
+
}
|
|
52
|
+
if (valueJson === undefined) {
|
|
53
|
+
valueJson = JSON.stringify(null);
|
|
54
|
+
}
|
|
55
|
+
transaction(() => {
|
|
56
|
+
db.prepare(
|
|
57
|
+
`INSERT INTO runtime_kv (scope, scope_id, key, value_json, updated_at)
|
|
58
|
+
VALUES (:scope, :scope_id, :key, :value_json, :updated_at)
|
|
59
|
+
ON CONFLICT (scope, scope_id, key) DO UPDATE SET
|
|
60
|
+
value_json = excluded.value_json,
|
|
61
|
+
updated_at = excluded.updated_at`,
|
|
62
|
+
).run({
|
|
63
|
+
":scope": scope,
|
|
64
|
+
":scope_id": scopeId,
|
|
65
|
+
":key": key,
|
|
66
|
+
":value_json": valueJson,
|
|
67
|
+
":updated_at": now,
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Read a runtime_kv value, parsed from JSON. Returns null if the row
|
|
74
|
+
* doesn't exist or the DB is unavailable.
|
|
75
|
+
*/
|
|
76
|
+
export function getRuntimeKv<T = unknown>(
|
|
77
|
+
scope: RuntimeKvScope,
|
|
78
|
+
scopeId: string,
|
|
79
|
+
key: string,
|
|
80
|
+
): T | null {
|
|
81
|
+
if (!isDbAvailable()) return null;
|
|
82
|
+
const db = _getAdapter()!;
|
|
83
|
+
const row = db.prepare(
|
|
84
|
+
`SELECT value_json FROM runtime_kv
|
|
85
|
+
WHERE scope = :scope AND scope_id = :scope_id AND key = :key`,
|
|
86
|
+
).get({ ":scope": scope, ":scope_id": scopeId, ":key": key }) as { value_json: string } | undefined;
|
|
87
|
+
if (!row) return null;
|
|
88
|
+
try {
|
|
89
|
+
return JSON.parse(row.value_json) as T;
|
|
90
|
+
} catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Delete a runtime_kv row. Idempotent — silently no-ops when the row
|
|
97
|
+
* doesn't exist or the DB is unavailable.
|
|
98
|
+
*/
|
|
99
|
+
export function deleteRuntimeKv(
|
|
100
|
+
scope: RuntimeKvScope,
|
|
101
|
+
scopeId: string,
|
|
102
|
+
key: string,
|
|
103
|
+
): void {
|
|
104
|
+
if (!isDbAvailable()) return;
|
|
105
|
+
const db = _getAdapter()!;
|
|
106
|
+
db.prepare(
|
|
107
|
+
`DELETE FROM runtime_kv WHERE scope = :scope AND scope_id = :scope_id AND key = :key`,
|
|
108
|
+
).run({ ":scope": scope, ":scope_id": scopeId, ":key": key });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* List all rows within a (scope, scopeId) bucket. Useful for diagnostics
|
|
113
|
+
* and bulk migrations.
|
|
114
|
+
*/
|
|
115
|
+
export function listRuntimeKv(
|
|
116
|
+
scope: RuntimeKvScope,
|
|
117
|
+
scopeId: string,
|
|
118
|
+
): readonly RuntimeKvRow[] {
|
|
119
|
+
if (!isDbAvailable()) return [];
|
|
120
|
+
const db = _getAdapter()!;
|
|
121
|
+
return db.prepare(
|
|
122
|
+
`SELECT scope, scope_id, key, value_json, updated_at
|
|
123
|
+
FROM runtime_kv
|
|
124
|
+
WHERE scope = :scope AND scope_id = :scope_id
|
|
125
|
+
ORDER BY key`,
|
|
126
|
+
).all({ ":scope": scope, ":scope_id": scopeId }) as unknown as RuntimeKvRow[];
|
|
127
|
+
}
|
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
// gsd-2 + Unit dispatch ledger (DB-backed coordination, Phase B)
|
|
2
|
+
//
|
|
3
|
+
// Records every auto-mode unit dispatch (plan-slice, run-task, summarize, …)
|
|
4
|
+
// with worker_id, fencing token, status lifecycle, and retry metadata. The
|
|
5
|
+
// ledger is the substrate Phase C will consume to migrate stuck-state.json
|
|
6
|
+
// and paused-session.json out of the runtime/ directory.
|
|
7
|
+
//
|
|
8
|
+
// Codex review MEDIUM B2: partial unique index
|
|
9
|
+
// idx_unit_dispatches_active_per_unit ON unit_dispatches(unit_id)
|
|
10
|
+
// WHERE status IN ('claimed','running')
|
|
11
|
+
// enforces that two workers cannot simultaneously claim the same unit.
|
|
12
|
+
// recordDispatchClaim relies on the index to fail fast at INSERT time
|
|
13
|
+
// rather than racing in application code.
|
|
14
|
+
|
|
15
|
+
import { randomUUID } from "node:crypto";
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
_getAdapter,
|
|
19
|
+
isDbAvailable,
|
|
20
|
+
transaction,
|
|
21
|
+
insertAuditEvent,
|
|
22
|
+
} from "../gsd-db.js";
|
|
23
|
+
|
|
24
|
+
export type DispatchStatus =
|
|
25
|
+
| "pending"
|
|
26
|
+
| "claimed"
|
|
27
|
+
| "running"
|
|
28
|
+
| "completed"
|
|
29
|
+
| "failed"
|
|
30
|
+
| "stuck"
|
|
31
|
+
| "canceled"
|
|
32
|
+
| "paused";
|
|
33
|
+
|
|
34
|
+
export interface UnitDispatchRow {
|
|
35
|
+
id: number;
|
|
36
|
+
trace_id: string;
|
|
37
|
+
turn_id: string | null;
|
|
38
|
+
worker_id: string;
|
|
39
|
+
milestone_lease_token: number;
|
|
40
|
+
milestone_id: string;
|
|
41
|
+
slice_id: string | null;
|
|
42
|
+
task_id: string | null;
|
|
43
|
+
unit_type: string;
|
|
44
|
+
unit_id: string;
|
|
45
|
+
status: DispatchStatus;
|
|
46
|
+
attempt_n: number;
|
|
47
|
+
started_at: string;
|
|
48
|
+
ended_at: string | null;
|
|
49
|
+
exit_reason: string | null;
|
|
50
|
+
error_summary: string | null;
|
|
51
|
+
verification_evidence_id: number | null;
|
|
52
|
+
next_run_at: string | null;
|
|
53
|
+
retry_after_ms: number | null;
|
|
54
|
+
max_attempts: number;
|
|
55
|
+
last_error_code: string | null;
|
|
56
|
+
last_error_at: string | null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface RecordClaimInput {
|
|
60
|
+
traceId: string;
|
|
61
|
+
turnId?: string | null;
|
|
62
|
+
workerId: string;
|
|
63
|
+
milestoneLeaseToken: number;
|
|
64
|
+
milestoneId: string;
|
|
65
|
+
sliceId?: string | null;
|
|
66
|
+
taskId?: string | null;
|
|
67
|
+
unitType: string;
|
|
68
|
+
unitId: string;
|
|
69
|
+
/**
|
|
70
|
+
* Attempt number for this unit. Callers should compute this from the
|
|
71
|
+
* most recent prior dispatch for the same unit_id (use
|
|
72
|
+
* getRecentForUnit() then add 1). Defaults to 1 for fresh claims.
|
|
73
|
+
*/
|
|
74
|
+
attemptN?: number;
|
|
75
|
+
/** Per-attempt cap; defaults to 3. */
|
|
76
|
+
maxAttempts?: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export type RecordClaimResult =
|
|
80
|
+
| { ok: true; dispatchId: number }
|
|
81
|
+
| { ok: false; error: "already_active"; existingId: number; existingStatus: DispatchStatus; existingWorker: string }
|
|
82
|
+
| { ok: false; error: "stale_lease"; milestoneId: string; workerId: string; milestoneLeaseToken: number };
|
|
83
|
+
|
|
84
|
+
function isAlreadyActiveConstraintError(err: unknown): boolean {
|
|
85
|
+
const code =
|
|
86
|
+
err && typeof err === "object" && "code" in err
|
|
87
|
+
? String((err as { code?: unknown }).code ?? "")
|
|
88
|
+
: "";
|
|
89
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
90
|
+
if (/\bFOREIGN KEY\b/i.test(msg)) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (code === "SQLITE_CONSTRAINT" || code === "SQLITE_CONSTRAINT_UNIQUE") {
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return /\bUNIQUE\b|\bconstraint failed\b/i.test(msg);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Insert a new dispatch row in `claimed` state. Atomic guard against
|
|
103
|
+
* double-claim (B2): the partial unique index
|
|
104
|
+
* idx_unit_dispatches_active_per_unit refuses the INSERT if any row for
|
|
105
|
+
* the same unit_id already has status IN ('claimed','running').
|
|
106
|
+
*/
|
|
107
|
+
export function recordDispatchClaim(input: RecordClaimInput): RecordClaimResult {
|
|
108
|
+
if (!isDbAvailable()) {
|
|
109
|
+
throw new Error("recordDispatchClaim: DB unavailable");
|
|
110
|
+
}
|
|
111
|
+
const now = new Date().toISOString();
|
|
112
|
+
|
|
113
|
+
return transaction((): RecordClaimResult => {
|
|
114
|
+
const db = _getAdapter()!;
|
|
115
|
+
|
|
116
|
+
const lease = db.prepare(
|
|
117
|
+
`SELECT fencing_token
|
|
118
|
+
FROM milestone_leases
|
|
119
|
+
WHERE milestone_id = :milestone_id
|
|
120
|
+
AND worker_id = :worker_id
|
|
121
|
+
AND fencing_token = :token
|
|
122
|
+
AND status = 'held'`,
|
|
123
|
+
).get({
|
|
124
|
+
":milestone_id": input.milestoneId,
|
|
125
|
+
":worker_id": input.workerId,
|
|
126
|
+
":token": input.milestoneLeaseToken,
|
|
127
|
+
}) as { fencing_token: number } | undefined;
|
|
128
|
+
if (!lease) {
|
|
129
|
+
return {
|
|
130
|
+
ok: false,
|
|
131
|
+
error: "stale_lease",
|
|
132
|
+
milestoneId: input.milestoneId,
|
|
133
|
+
workerId: input.workerId,
|
|
134
|
+
milestoneLeaseToken: input.milestoneLeaseToken,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const result = db.prepare(
|
|
140
|
+
`INSERT INTO unit_dispatches (
|
|
141
|
+
trace_id, turn_id, worker_id, milestone_lease_token,
|
|
142
|
+
milestone_id, slice_id, task_id,
|
|
143
|
+
unit_type, unit_id, status, attempt_n,
|
|
144
|
+
started_at, max_attempts
|
|
145
|
+
) VALUES (
|
|
146
|
+
:trace_id, :turn_id, :worker_id, :milestone_lease_token,
|
|
147
|
+
:milestone_id, :slice_id, :task_id,
|
|
148
|
+
:unit_type, :unit_id, 'claimed', :attempt_n,
|
|
149
|
+
:started_at, :max_attempts
|
|
150
|
+
)`,
|
|
151
|
+
).run({
|
|
152
|
+
":trace_id": input.traceId,
|
|
153
|
+
":turn_id": input.turnId ?? null,
|
|
154
|
+
":worker_id": input.workerId,
|
|
155
|
+
":milestone_lease_token": input.milestoneLeaseToken,
|
|
156
|
+
":milestone_id": input.milestoneId,
|
|
157
|
+
":slice_id": input.sliceId ?? null,
|
|
158
|
+
":task_id": input.taskId ?? null,
|
|
159
|
+
":unit_type": input.unitType,
|
|
160
|
+
":unit_id": input.unitId,
|
|
161
|
+
":attempt_n": input.attemptN ?? 1,
|
|
162
|
+
":started_at": now,
|
|
163
|
+
":max_attempts": input.maxAttempts ?? 3,
|
|
164
|
+
});
|
|
165
|
+
const id = Number((result as { lastInsertRowid?: number | bigint }).lastInsertRowid ?? 0);
|
|
166
|
+
|
|
167
|
+
insertAuditEvent({
|
|
168
|
+
eventId: randomUUID(),
|
|
169
|
+
traceId: input.traceId,
|
|
170
|
+
turnId: input.turnId ?? undefined,
|
|
171
|
+
category: "orchestration",
|
|
172
|
+
type: "dispatch-claimed",
|
|
173
|
+
ts: now,
|
|
174
|
+
payload: {
|
|
175
|
+
dispatchId: id,
|
|
176
|
+
unitId: input.unitId,
|
|
177
|
+
unitType: input.unitType,
|
|
178
|
+
workerId: input.workerId,
|
|
179
|
+
attemptN: input.attemptN ?? 1,
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
return { ok: true, dispatchId: id };
|
|
184
|
+
} catch (err) {
|
|
185
|
+
if (!isAlreadyActiveConstraintError(err)) throw err;
|
|
186
|
+
|
|
187
|
+
// Partial unique index rejected the INSERT — surface the existing
|
|
188
|
+
// active dispatch so callers can decide what to do.
|
|
189
|
+
const existing = db.prepare(
|
|
190
|
+
`SELECT id, status, worker_id FROM unit_dispatches
|
|
191
|
+
WHERE unit_id = :unit_id AND status IN ('claimed','running')
|
|
192
|
+
ORDER BY id DESC LIMIT 1`,
|
|
193
|
+
).get({ ":unit_id": input.unitId }) as { id: number; status: DispatchStatus; worker_id: string } | undefined;
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
ok: false,
|
|
197
|
+
error: "already_active",
|
|
198
|
+
existingId: existing?.id ?? 0,
|
|
199
|
+
existingStatus: existing?.status ?? "claimed",
|
|
200
|
+
existingWorker: existing?.worker_id ?? "unknown",
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Transition a `claimed` dispatch into `running`. */
|
|
207
|
+
export function markRunning(dispatchId: number): void {
|
|
208
|
+
if (!isDbAvailable()) return;
|
|
209
|
+
const db = _getAdapter()!;
|
|
210
|
+
db.prepare(
|
|
211
|
+
`UPDATE unit_dispatches SET status = 'running'
|
|
212
|
+
WHERE id = :id AND status = 'claimed'`,
|
|
213
|
+
).run({ ":id": dispatchId });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export interface CompleteOpts {
|
|
217
|
+
verificationEvidenceId?: number | null;
|
|
218
|
+
exitReason?: string;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Transition a dispatch into `completed`. */
|
|
222
|
+
export function markCompleted(dispatchId: number, opts?: CompleteOpts): void {
|
|
223
|
+
if (!isDbAvailable()) return;
|
|
224
|
+
const now = new Date().toISOString();
|
|
225
|
+
const db = _getAdapter()!;
|
|
226
|
+
let changes = 0;
|
|
227
|
+
transaction(() => {
|
|
228
|
+
const result = db.prepare(
|
|
229
|
+
`UPDATE unit_dispatches
|
|
230
|
+
SET status = 'completed', ended_at = :ended_at,
|
|
231
|
+
exit_reason = :exit_reason,
|
|
232
|
+
verification_evidence_id = :evidence_id
|
|
233
|
+
WHERE id = :id
|
|
234
|
+
AND status IN ('claimed','running')`,
|
|
235
|
+
).run({
|
|
236
|
+
":id": dispatchId,
|
|
237
|
+
":ended_at": now,
|
|
238
|
+
":exit_reason": opts?.exitReason ?? null,
|
|
239
|
+
":evidence_id": opts?.verificationEvidenceId ?? null,
|
|
240
|
+
});
|
|
241
|
+
changes =
|
|
242
|
+
typeof (result as { changes?: unknown }).changes === "number"
|
|
243
|
+
? (result as { changes: number }).changes
|
|
244
|
+
: 0;
|
|
245
|
+
});
|
|
246
|
+
if (changes < 1) return;
|
|
247
|
+
insertAuditEvent({
|
|
248
|
+
eventId: randomUUID(),
|
|
249
|
+
traceId: dispatchId.toString(),
|
|
250
|
+
category: "orchestration",
|
|
251
|
+
type: "dispatch-completed",
|
|
252
|
+
ts: now,
|
|
253
|
+
payload: { dispatchId },
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export interface FailureOpts {
|
|
258
|
+
errorSummary: string;
|
|
259
|
+
errorCode?: string;
|
|
260
|
+
/** Backoff before next attempt (used by stuck-detector retry suppression). */
|
|
261
|
+
retryAfterMs?: number;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Transition a dispatch into `failed`, optionally scheduling a retry. */
|
|
265
|
+
export function markFailed(dispatchId: number, opts: FailureOpts): void {
|
|
266
|
+
if (!isDbAvailable()) return;
|
|
267
|
+
const now = new Date();
|
|
268
|
+
const nowIso = now.toISOString();
|
|
269
|
+
const nextRunIso = opts.retryAfterMs
|
|
270
|
+
? new Date(now.getTime() + opts.retryAfterMs).toISOString()
|
|
271
|
+
: null;
|
|
272
|
+
const db = _getAdapter()!;
|
|
273
|
+
let changes = 0;
|
|
274
|
+
transaction(() => {
|
|
275
|
+
const result = db.prepare(
|
|
276
|
+
`UPDATE unit_dispatches
|
|
277
|
+
SET status = 'failed', ended_at = :ended_at,
|
|
278
|
+
error_summary = :error_summary,
|
|
279
|
+
last_error_code = :last_error_code,
|
|
280
|
+
last_error_at = :last_error_at,
|
|
281
|
+
retry_after_ms = :retry_after_ms,
|
|
282
|
+
next_run_at = :next_run_at
|
|
283
|
+
WHERE id = :id
|
|
284
|
+
AND status IN ('claimed','running')`,
|
|
285
|
+
).run({
|
|
286
|
+
":id": dispatchId,
|
|
287
|
+
":ended_at": nowIso,
|
|
288
|
+
":error_summary": opts.errorSummary,
|
|
289
|
+
":last_error_code": opts.errorCode ?? null,
|
|
290
|
+
":last_error_at": nowIso,
|
|
291
|
+
":retry_after_ms": opts.retryAfterMs ?? null,
|
|
292
|
+
":next_run_at": nextRunIso,
|
|
293
|
+
});
|
|
294
|
+
changes =
|
|
295
|
+
typeof (result as { changes?: unknown }).changes === "number"
|
|
296
|
+
? (result as { changes: number }).changes
|
|
297
|
+
: 0;
|
|
298
|
+
});
|
|
299
|
+
if (changes < 1) return;
|
|
300
|
+
insertAuditEvent({
|
|
301
|
+
eventId: randomUUID(),
|
|
302
|
+
traceId: dispatchId.toString(),
|
|
303
|
+
category: "orchestration",
|
|
304
|
+
type: "dispatch-failed",
|
|
305
|
+
ts: nowIso,
|
|
306
|
+
payload: { dispatchId, errorSummary: opts.errorSummary, retryAfterMs: opts.retryAfterMs ?? null },
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Transition a dispatch into `stuck`. */
|
|
311
|
+
export function markStuck(dispatchId: number, reason: string): void {
|
|
312
|
+
if (!isDbAvailable()) return;
|
|
313
|
+
const now = new Date().toISOString();
|
|
314
|
+
const db = _getAdapter()!;
|
|
315
|
+
const result = transaction(() => {
|
|
316
|
+
return db.prepare(
|
|
317
|
+
`UPDATE unit_dispatches
|
|
318
|
+
SET status = 'stuck', ended_at = :ended_at, exit_reason = :reason
|
|
319
|
+
WHERE id = :id
|
|
320
|
+
AND status IN ('claimed','running')`,
|
|
321
|
+
).run({ ":id": dispatchId, ":ended_at": now, ":reason": reason });
|
|
322
|
+
});
|
|
323
|
+
const changes =
|
|
324
|
+
typeof (result as { changes?: unknown }).changes === "number"
|
|
325
|
+
? (result as { changes: number }).changes
|
|
326
|
+
: 0;
|
|
327
|
+
if (changes <= 0) return;
|
|
328
|
+
insertAuditEvent({
|
|
329
|
+
eventId: randomUUID(),
|
|
330
|
+
traceId: dispatchId.toString(),
|
|
331
|
+
category: "orchestration",
|
|
332
|
+
type: "dispatch-stuck",
|
|
333
|
+
ts: now,
|
|
334
|
+
payload: { dispatchId, reason },
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** Transition a dispatch into `paused`. */
|
|
339
|
+
export function markPaused(dispatchId: number): void {
|
|
340
|
+
if (!isDbAvailable()) return;
|
|
341
|
+
const now = new Date().toISOString();
|
|
342
|
+
const db = _getAdapter()!;
|
|
343
|
+
db.prepare(
|
|
344
|
+
`UPDATE unit_dispatches
|
|
345
|
+
SET status = 'paused', ended_at = :ended_at
|
|
346
|
+
WHERE id = :id AND status IN ('claimed','running')`,
|
|
347
|
+
).run({ ":id": dispatchId, ":ended_at": now });
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Transition a dispatch into `canceled`. */
|
|
351
|
+
export function markCanceled(dispatchId: number, reason: string): void {
|
|
352
|
+
if (!isDbAvailable()) return;
|
|
353
|
+
const now = new Date().toISOString();
|
|
354
|
+
const db = _getAdapter()!;
|
|
355
|
+
db.prepare(
|
|
356
|
+
`UPDATE unit_dispatches
|
|
357
|
+
SET status = 'canceled', ended_at = :ended_at, exit_reason = :reason
|
|
358
|
+
WHERE id = :id AND status IN ('pending','claimed','running')`,
|
|
359
|
+
).run({ ":id": dispatchId, ":ended_at": now, ":reason": reason });
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Fetch the most recent N dispatches for a unit. Used by recordDispatchClaim
|
|
364
|
+
* callers to compute attempt_n and by detect-stuck.ts (B3) to consult
|
|
365
|
+
* retry budget before tripping the stuck verdict.
|
|
366
|
+
*/
|
|
367
|
+
export function getRecentForUnit(unitId: string, limit = 10): UnitDispatchRow[] {
|
|
368
|
+
if (!isDbAvailable()) return [];
|
|
369
|
+
const db = _getAdapter()!;
|
|
370
|
+
return db.prepare(
|
|
371
|
+
`SELECT * FROM unit_dispatches WHERE unit_id = :unit_id ORDER BY id DESC LIMIT :limit`,
|
|
372
|
+
).all({ ":unit_id": unitId, ":limit": limit }) as unknown as UnitDispatchRow[];
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Fetch the latest dispatch for a unit, regardless of status. Returns null
|
|
377
|
+
* if the unit has never been dispatched.
|
|
378
|
+
*/
|
|
379
|
+
export function getLatestForUnit(unitId: string): UnitDispatchRow | null {
|
|
380
|
+
if (!isDbAvailable()) return null;
|
|
381
|
+
const db = _getAdapter()!;
|
|
382
|
+
const row = db.prepare(
|
|
383
|
+
`SELECT * FROM unit_dispatches WHERE unit_id = :unit_id ORDER BY id DESC LIMIT 1`,
|
|
384
|
+
).get({ ":unit_id": unitId }) as UnitDispatchRow | undefined;
|
|
385
|
+
return row ?? null;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Phase C — return the most recent unit_id values for a worker, oldest-first.
|
|
390
|
+
*
|
|
391
|
+
* Drop-in replacement for the persistence side of stuck-state.json's
|
|
392
|
+
* `recentUnits` field. The auto-loop uses this to seed loopState.recentUnits
|
|
393
|
+
* on session start so the stuck-detector window survives a session restart
|
|
394
|
+
* (#3704). Returned in oldest-first order to match the in-memory window
|
|
395
|
+
* shape that detect-stuck.ts expects.
|
|
396
|
+
*/
|
|
397
|
+
export function getRecentUnitKeysForWorker(
|
|
398
|
+
workerId: string,
|
|
399
|
+
limit = 20,
|
|
400
|
+
): Array<{ key: string }> {
|
|
401
|
+
if (!isDbAvailable()) return [];
|
|
402
|
+
const db = _getAdapter()!;
|
|
403
|
+
const rows = db.prepare(
|
|
404
|
+
`SELECT unit_id FROM unit_dispatches
|
|
405
|
+
WHERE worker_id = :worker_id
|
|
406
|
+
ORDER BY started_at DESC, id DESC
|
|
407
|
+
LIMIT :limit`,
|
|
408
|
+
).all({ ":worker_id": workerId, ":limit": limit }) as Array<{ unit_id: string }>;
|
|
409
|
+
// Reverse so callers consume oldest-first (sliding-window semantics).
|
|
410
|
+
return rows.reverse().map((r) => ({ key: r.unit_id }));
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export function getRecentUnitKeysForProjectRoot(
|
|
414
|
+
projectRootRealpath: string,
|
|
415
|
+
limit = 20,
|
|
416
|
+
): Array<{ key: string }> {
|
|
417
|
+
if (!isDbAvailable()) return [];
|
|
418
|
+
const db = _getAdapter()!;
|
|
419
|
+
const rows = db.prepare(
|
|
420
|
+
`SELECT ud.unit_id
|
|
421
|
+
FROM unit_dispatches ud
|
|
422
|
+
INNER JOIN workers w ON w.worker_id = ud.worker_id
|
|
423
|
+
WHERE w.project_root_realpath = :project_root_realpath
|
|
424
|
+
ORDER BY ud.started_at DESC, ud.id DESC
|
|
425
|
+
LIMIT :limit`,
|
|
426
|
+
).all({
|
|
427
|
+
":project_root_realpath": projectRootRealpath,
|
|
428
|
+
":limit": limit,
|
|
429
|
+
}) as Array<{ unit_id: string }>;
|
|
430
|
+
return rows.reverse().map((r) => ({ key: r.unit_id }));
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Fetch dispatches for a milestone filtered by status. Useful for janitors
|
|
435
|
+
* + dashboards.
|
|
436
|
+
*/
|
|
437
|
+
export function getDispatchesByStatus(
|
|
438
|
+
milestoneId: string,
|
|
439
|
+
status: DispatchStatus,
|
|
440
|
+
): UnitDispatchRow[] {
|
|
441
|
+
if (!isDbAvailable()) return [];
|
|
442
|
+
const db = _getAdapter()!;
|
|
443
|
+
return db.prepare(
|
|
444
|
+
`SELECT * FROM unit_dispatches WHERE milestone_id = :mid AND status = :status ORDER BY id`,
|
|
445
|
+
).all({ ":mid": milestoneId, ":status": status }) as unknown as UnitDispatchRow[];
|
|
446
|
+
}
|