agent-dealer 0.2.0 → 1.0.0
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/bundle/server/dist/adapters/agent-deck-bind.js +443 -0
- package/bundle/server/dist/adapters/agent-deck-bind.test.js +454 -0
- package/bundle/server/dist/adapters/agent-deck.js +38 -50
- package/bundle/server/dist/adapters/agent-deck.test.js +63 -0
- package/bundle/server/dist/adapters/agent-health.js +230 -33
- package/bundle/server/dist/adapters/agent-health.test.js +237 -0
- package/bundle/server/dist/adapters/authority-lifecycle.js +289 -0
- package/bundle/server/dist/adapters/codex-scoped-config.js +103 -0
- package/bundle/server/dist/adapters/execution-authority.js +103 -0
- package/bundle/server/dist/adapters/execution-authority.test.js +166 -0
- package/bundle/server/dist/adapters/external.js +0 -1
- package/bundle/server/dist/adapters/git-worktree.js +514 -0
- package/bundle/server/dist/adapters/git-worktree.test.js +502 -0
- package/bundle/server/dist/adapters/github.js +170 -0
- package/bundle/server/dist/adapters/github.test.js +205 -0
- package/bundle/server/dist/adapters/linear-inbox.js +199 -30
- package/bundle/server/dist/adapters/linear-inbox.test.js +35 -0
- package/bundle/server/dist/adapters/linear-sync.js +3 -101
- package/bundle/server/dist/adapters/managed-repo.js +168 -0
- package/bundle/server/dist/adapters/managed-repo.test.js +63 -0
- package/bundle/server/dist/adapters/outbound-delivery.js +83 -0
- package/bundle/server/dist/adapters/outbound-delivery.test.js +53 -0
- package/bundle/server/dist/adapters/reflect-authority.js +43 -0
- package/bundle/server/dist/cli-env.js +26 -0
- package/bundle/server/dist/cli-env.test.js +14 -1
- package/bundle/server/dist/coordinator/abort.integration.test.js +74 -0
- package/bundle/server/dist/coordinator/admission.js +330 -0
- package/bundle/server/dist/coordinator/admission.test.js +338 -0
- package/bundle/server/dist/coordinator/args.js +128 -0
- package/bundle/server/dist/coordinator/args.test.js +132 -0
- package/bundle/server/dist/coordinator/authority-recovery.integration.test.js +661 -0
- package/bundle/server/dist/coordinator/auto-merge.integration.test.js +363 -0
- package/bundle/server/dist/coordinator/auto-merge.js +286 -0
- package/bundle/server/dist/coordinator/auto-merge.timeout.test.js +14 -0
- package/bundle/server/dist/coordinator/branch-progress.js +81 -0
- package/bundle/server/dist/coordinator/clock-jump.js +98 -0
- package/bundle/server/dist/coordinator/commands.js +1088 -0
- package/bundle/server/dist/coordinator/commands.test.js +554 -0
- package/bundle/server/dist/coordinator/deck-outage-config.js +32 -0
- package/bundle/server/dist/coordinator/deck-outage.integration.test.js +200 -0
- package/bundle/server/dist/coordinator/dependencies.js +242 -0
- package/bundle/server/dist/coordinator/dependency-readiness.integration.test.js +535 -0
- package/bundle/server/dist/coordinator/developer-effect.js +959 -0
- package/bundle/server/dist/coordinator/developer-effect.test.js +1143 -0
- package/bundle/server/dist/coordinator/dispatcher.js +58 -0
- package/bundle/server/dist/coordinator/dispatcher.test.js +81 -0
- package/bundle/server/dist/coordinator/effect-registry.js +17 -0
- package/bundle/server/dist/coordinator/execution-env.integration.test.js +122 -0
- package/bundle/server/dist/coordinator/failure-reason.js +273 -0
- package/bundle/server/dist/coordinator/failure-reason.test.js +234 -0
- package/bundle/server/dist/coordinator/guidance.js +33 -0
- package/bundle/server/dist/coordinator/guidance.test.js +110 -0
- package/bundle/server/dist/coordinator/human-resolution.js +88 -0
- package/bundle/server/dist/coordinator/human-resolution.test.js +72 -0
- package/bundle/server/dist/coordinator/intent-forecast.js +13 -0
- package/bundle/server/dist/coordinator/intent-forecast.test.js +34 -0
- package/bundle/server/dist/coordinator/latest-failure.js +100 -0
- package/bundle/server/dist/coordinator/latest-failure.test.js +125 -0
- package/bundle/server/dist/coordinator/metrics.js +35 -0
- package/bundle/server/dist/coordinator/metrics.test.js +52 -0
- package/bundle/server/dist/coordinator/permissions.js +127 -0
- package/bundle/server/dist/coordinator/permissions.test.js +184 -0
- package/bundle/server/dist/coordinator/presumed-dead-budget.integration.test.js +146 -0
- package/bundle/server/dist/coordinator/process-liveness.js +191 -0
- package/bundle/server/dist/coordinator/profile-snapshot.js +20 -0
- package/bundle/server/dist/coordinator/profile-snapshot.test.js +118 -0
- package/bundle/server/dist/coordinator/projection.js +147 -0
- package/bundle/server/dist/coordinator/projection.test.js +121 -0
- package/bundle/server/dist/coordinator/prompts.js +164 -0
- package/bundle/server/dist/coordinator/prompts.test.js +177 -0
- package/bundle/server/dist/coordinator/queue-by-default.integration.test.js +271 -0
- package/bundle/server/dist/coordinator/reclaim-republish.integration.test.js +372 -0
- package/bundle/server/dist/coordinator/recovery.js +466 -0
- package/bundle/server/dist/coordinator/recovery.test.js +164 -0
- package/bundle/server/dist/coordinator/reflect-trigger.js +218 -0
- package/bundle/server/dist/coordinator/reflect-trigger.test.js +324 -0
- package/bundle/server/dist/coordinator/repair-cycle.integration.test.js +234 -0
- package/bundle/server/dist/coordinator/restart-recovery.integration.test.js +363 -0
- package/bundle/server/dist/coordinator/reviewer-effect.js +507 -0
- package/bundle/server/dist/coordinator/reviewer-effect.test.js +612 -0
- package/bundle/server/dist/coordinator/reviewer-result.js +38 -0
- package/bundle/server/dist/coordinator/reviewer-result.test.js +45 -0
- package/bundle/server/dist/coordinator/routing.js +160 -0
- package/bundle/server/dist/coordinator/routing.test.js +336 -0
- package/bundle/server/dist/coordinator/session-lifecycle.integration.test.js +127 -0
- package/bundle/server/dist/coordinator/session-lifecycle.js +280 -0
- package/bundle/server/dist/coordinator/session-lifecycle.test.js +121 -0
- package/bundle/server/dist/coordinator/session-progress.js +351 -0
- package/bundle/server/dist/coordinator/session-progress.test.js +190 -0
- package/bundle/server/dist/coordinator/session-timeouts.js +33 -0
- package/bundle/server/dist/coordinator/sleep-recovery.integration.test.js +233 -0
- package/bundle/server/dist/coordinator/spawn.js +49 -0
- package/bundle/server/dist/coordinator/sync-issue-base-branch.js +18 -0
- package/bundle/server/dist/coordinator/usage-cap-config.js +10 -0
- package/bundle/server/dist/coordinator/usage-cap-defer.js +189 -0
- package/bundle/server/dist/coordinator/usage-cap.integration.test.js +193 -0
- package/bundle/server/dist/coordinator/usage.js +40 -0
- package/bundle/server/dist/coordinator/usage.test.js +69 -0
- package/bundle/server/dist/coordinator/verification-receipt.js +333 -0
- package/bundle/server/dist/coordinator/verification-receipt.test.js +382 -0
- package/bundle/server/dist/coordinator/worker-loop.js +363 -0
- package/bundle/server/dist/coordinator/worker-loop.test.js +327 -0
- package/bundle/server/dist/coordinator/workflow-instances.test.js +45 -0
- package/bundle/server/dist/coordinator/worktree-owner-liveness.js +82 -0
- package/bundle/server/dist/coordinator/worktree-owner-liveness.test.js +199 -0
- package/bundle/server/dist/db/artifacts-issue-columns.test.js +18 -0
- package/bundle/server/dist/db/authority-attempts-stale-at-migration.test.js +65 -0
- package/bundle/server/dist/db/idempotency-index-migration.test.js +57 -0
- package/bundle/server/dist/db/index.js +359 -7
- package/bundle/server/dist/db/linear-inbox-unlock.test.js +67 -0
- package/bundle/server/dist/db/migrate-to-issues.js +824 -0
- package/bundle/server/dist/db/migrate-to-issues.test.js +842 -0
- package/bundle/server/dist/db/schema.sql +311 -1
- package/bundle/server/dist/db/schema.test.js +66 -0
- package/bundle/server/dist/db/worker-session-pid-migration.test.js +112 -0
- package/bundle/server/dist/dev-review-cli-happy-path.integration.test.js +338 -0
- package/bundle/server/dist/direct-start-liveness.integration.test.js +350 -0
- package/bundle/server/dist/index.js +58 -5
- package/bundle/server/dist/paths.js +40 -4
- package/bundle/server/dist/paths.test.js +42 -0
- package/bundle/server/dist/pid-marker-claim-child.js +18 -0
- package/bundle/server/dist/pid-marker-hold-arbiter-child.js +24 -0
- package/bundle/server/dist/pid-marker.js +141 -0
- package/bundle/server/dist/pid-marker.test.js +208 -0
- package/bundle/server/dist/queue/approve-deliver.js +128 -28
- package/bundle/server/dist/queue/reflect-schedule.js +25 -0
- package/bundle/server/dist/queue/result-qa.js +1 -1
- package/bundle/server/dist/queue/send-gate.test.js +172 -36
- package/bundle/server/dist/repository/agents.js +55 -18
- package/bundle/server/dist/repository/artifacts-for-issue.js +42 -0
- package/bundle/server/dist/repository/artifacts-for-issue.test.js +48 -0
- package/bundle/server/dist/repository/artifacts.js +47 -0
- package/bundle/server/dist/repository/artifacts.test.js +48 -0
- package/bundle/server/dist/repository/authority-attempts.js +189 -0
- package/bundle/server/dist/repository/findings.js +70 -0
- package/bundle/server/dist/repository/findings.test.js +68 -0
- package/bundle/server/dist/repository/human-actions.js +142 -0
- package/bundle/server/dist/repository/human-actions.test.js +123 -0
- package/bundle/server/dist/repository/intake-settings.js +5 -59
- package/bundle/server/dist/repository/issues.js +305 -0
- package/bundle/server/dist/repository/issues.test.js +69 -0
- package/bundle/server/dist/repository/outbound-drafts.js +28 -0
- package/bundle/server/dist/repository/queue-entries.js +162 -0
- package/bundle/server/dist/repository/review-publications.js +153 -0
- package/bundle/server/dist/repository/review-publications.test.js +78 -0
- package/bundle/server/dist/repository/runs-external-id.test.js +5 -3
- package/bundle/server/dist/repository/runs.js +4 -211
- package/bundle/server/dist/repository/runtime-availability.js +54 -0
- package/bundle/server/dist/repository/usage-events.js +60 -0
- package/bundle/server/dist/repository/usage-events.test.js +56 -0
- package/bundle/server/dist/repository/work-items.js +305 -0
- package/bundle/server/dist/repository/work-items.test.js +142 -0
- package/bundle/server/dist/repository/worker-sessions.js +219 -0
- package/bundle/server/dist/repository/worker-sessions.test.js +60 -0
- package/bundle/server/dist/repository/workflow-events.js +193 -0
- package/bundle/server/dist/repository/workflow-events.test.js +89 -0
- package/bundle/server/dist/resolve-tsx-bin.js +19 -0
- package/bundle/server/dist/routes/human-actions.js +68 -0
- package/bundle/server/dist/routes/human-actions.test.js +284 -0
- package/bundle/server/dist/routes/index.js +29 -496
- package/bundle/server/dist/routes/issues.js +240 -0
- package/bundle/server/dist/routes/issues.test.js +451 -0
- package/bundle/server/dist/routes/queue.js +41 -0
- package/bundle/server/dist/routes/queue.test.js +78 -0
- package/bundle/server/dist/runners/claude-args.js +18 -30
- package/bundle/server/dist/runners/claude.js +19 -163
- package/bundle/server/dist/runners/claude.test.js +15 -70
- package/bundle/server/dist/runners/codex-args.js +59 -0
- package/bundle/server/dist/runners/codex-args.test.js +72 -0
- package/bundle/server/dist/runners/codex-jsonl.js +150 -0
- package/bundle/server/dist/runners/codex-jsonl.test.js +36 -0
- package/bundle/server/dist/runners/codex-resume.test.js +19 -0
- package/bundle/server/dist/runners/codex.js +46 -0
- package/bundle/server/dist/runners/models-codex.test.js +14 -0
- package/bundle/server/dist/runners/models.js +53 -1
- package/bundle/server/dist/runners/persist.js +4 -1
- package/bundle/server/dist/runners/process-registry.js +11 -0
- package/bundle/server/dist/runners/prompts.js +13 -280
- package/bundle/server/dist/runners/prompts.test.js +22 -153
- package/bundle/server/dist/runners/qa.js +17 -1
- package/bundle/server/dist/runners/reflect.js +4 -16
- package/bundle/server/dist/runners/spawn-cli.js +197 -0
- package/bundle/server/dist/runners/spawn-cli.test.js +130 -0
- package/bundle/server/dist/runners/stream-json.js +0 -141
- package/bundle/server/dist/runners/usage-cap.js +242 -0
- package/bundle/server/dist/runners/usage-cap.test.js +97 -0
- package/bundle/server/dist/server-liveness.js +37 -0
- package/bundle/server/dist/server-liveness.test.js +82 -0
- package/bundle/server/dist/static-ui.test.js +44 -0
- package/bundle/server/package.json +4 -2
- package/bundle/server/static-ui/assets/index-BTtPGYpY.js +60 -0
- package/bundle/server/static-ui/assets/index-BxTx4b1d.css +1 -0
- package/bundle/server/static-ui/index.html +2 -2
- package/bundle/shared/dist/agents.d.ts +262 -133
- package/bundle/shared/dist/agents.js +85 -19
- package/bundle/shared/dist/cursor-auth-health.d.ts +12 -0
- package/bundle/shared/dist/cursor-auth-health.js +36 -0
- package/bundle/shared/dist/cursor-auth-health.test.d.ts +1 -0
- package/bundle/shared/dist/cursor-auth-health.test.js +40 -0
- package/bundle/shared/dist/findings.d.ts +46 -0
- package/bundle/shared/dist/findings.js +17 -0
- package/bundle/shared/dist/findings.test.d.ts +1 -0
- package/bundle/shared/dist/findings.test.js +20 -0
- package/bundle/shared/dist/github-repo.d.ts +28 -0
- package/bundle/shared/dist/github-repo.js +98 -0
- package/bundle/shared/dist/github-repo.test.d.ts +1 -0
- package/bundle/shared/dist/github-repo.test.js +32 -0
- package/bundle/shared/dist/human-actions.d.ts +66 -0
- package/bundle/shared/dist/human-actions.js +51 -0
- package/bundle/shared/dist/human-actions.test.d.ts +1 -0
- package/bundle/shared/dist/human-actions.test.js +57 -0
- package/bundle/shared/dist/index.d.ts +567 -757
- package/bundle/shared/dist/index.js +22 -36
- package/bundle/shared/dist/issues.d.ts +385 -0
- package/bundle/shared/dist/issues.js +169 -0
- package/bundle/shared/dist/issues.test.d.ts +1 -0
- package/bundle/shared/dist/issues.test.js +72 -0
- package/bundle/shared/dist/outbound-draft.d.ts +6 -0
- package/bundle/shared/dist/outbound-draft.js +4 -0
- package/bundle/shared/dist/plan-triage.d.ts +6 -6
- package/bundle/shared/dist/profile-snapshot.d.ts +154 -0
- package/bundle/shared/dist/profile-snapshot.js +145 -0
- package/bundle/shared/dist/profile-snapshot.test.d.ts +1 -0
- package/bundle/shared/dist/profile-snapshot.test.js +67 -0
- package/bundle/shared/dist/queue-entries.d.ts +89 -0
- package/bundle/shared/dist/queue-entries.js +19 -0
- package/bundle/shared/dist/result-qa.d.ts +2 -2
- package/bundle/shared/dist/runtime-auth-health.d.ts +66 -0
- package/bundle/shared/dist/runtime-auth-health.js +186 -0
- package/bundle/shared/dist/runtime-auth-health.test.d.ts +1 -0
- package/bundle/shared/dist/runtime-auth-health.test.js +200 -0
- package/bundle/shared/dist/runtime-availability.d.ts +51 -0
- package/bundle/shared/dist/runtime-availability.js +13 -0
- package/bundle/shared/dist/runtime.d.ts +1 -1
- package/bundle/shared/dist/runtime.js +1 -1
- package/bundle/shared/dist/usage-events.d.ts +36 -0
- package/bundle/shared/dist/usage-events.js +13 -0
- package/bundle/shared/dist/worker-sessions.d.ts +140 -0
- package/bundle/shared/dist/worker-sessions.js +70 -0
- package/bundle/shared/dist/worker-sessions.test.d.ts +1 -0
- package/bundle/shared/dist/worker-sessions.test.js +39 -0
- package/bundle/shared/dist/workflow.d.ts +78 -0
- package/bundle/shared/dist/workflow.js +59 -0
- package/bundle/shared/dist/workflow.test.d.ts +1 -0
- package/bundle/shared/dist/workflow.test.js +61 -0
- package/bundle/shared/package.json +1 -1
- package/dist/action.d.ts +1 -0
- package/dist/action.js +50 -0
- package/dist/action.test.d.ts +1 -0
- package/dist/action.test.js +70 -0
- package/dist/agent.d.ts +1 -0
- package/dist/agent.js +17 -0
- package/dist/agent.test.d.ts +1 -0
- package/dist/agent.test.js +19 -0
- package/dist/cli-check.d.ts +7 -0
- package/dist/cli-check.js +40 -0
- package/dist/doctor.js +59 -1
- package/dist/http.d.ts +5 -0
- package/dist/http.js +19 -0
- package/dist/index.js +28 -0
- package/dist/issue.d.ts +35 -0
- package/dist/issue.js +143 -0
- package/dist/issue.test.d.ts +1 -0
- package/dist/issue.test.js +240 -0
- package/dist/lifecycle.contract.test.d.ts +1 -0
- package/dist/lifecycle.contract.test.js +135 -0
- package/dist/queue.d.ts +11 -0
- package/dist/queue.js +60 -0
- package/dist/queue.test.d.ts +1 -0
- package/dist/queue.test.js +46 -0
- package/dist/test-fetch-stub.d.ts +6 -0
- package/dist/test-fetch-stub.js +31 -0
- package/package.json +2 -1
- package/templates/prod.env.example +1 -1
- package/bundle/server/static-ui/assets/index-B_7S7xJb.js +0 -79
- package/bundle/server/static-ui/assets/index-QYv3zmE6.css +0 -1
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Regression test for the bug `npm run dev` hit against any database that ran migrate()
|
|
2
|
+
// between NOT-93 (introduced `authority_attempts`) and NOT-91 (added its `stale_at`
|
|
3
|
+
// column + an index on it): schema.sql's own `CREATE INDEX idx_authority_attempts_stale
|
|
4
|
+
// ON authority_attempts(status, stale_at)` threw "no such column: stale_at" against the
|
|
5
|
+
// pre-existing table, aborting migrate() before it ever reached the incremental-ALTER
|
|
6
|
+
// section below it — see db/index.ts's pre-schema.sql guard.
|
|
7
|
+
import { test } from "node:test";
|
|
8
|
+
import assert from "node:assert/strict";
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import os from "node:os";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
process.env.AGENT_DEALER_HOME = fs.mkdtempSync(path.join(os.tmpdir(), "dealer-authattempt-migrate-"));
|
|
13
|
+
const { migrate, getDb } = await import("./index.js");
|
|
14
|
+
test("migrate() upgrades a pre-NOT-91 authority_attempts table that has no stale_at column", () => {
|
|
15
|
+
migrate();
|
|
16
|
+
const db = getDb();
|
|
17
|
+
// Recreate the pre-NOT-91 world: the table and its two original indexes, but without
|
|
18
|
+
// stale_at or the index on it — exactly what a database that last ran migrate() between
|
|
19
|
+
// NOT-93 and NOT-91 landing would have.
|
|
20
|
+
db.exec(`
|
|
21
|
+
DROP INDEX IF EXISTS idx_authority_attempts_owner;
|
|
22
|
+
DROP INDEX IF EXISTS idx_authority_attempts_stale;
|
|
23
|
+
DROP INDEX IF EXISTS idx_authority_attempts_status;
|
|
24
|
+
DROP TABLE authority_attempts;
|
|
25
|
+
CREATE TABLE authority_attempts (
|
|
26
|
+
id TEXT PRIMARY KEY,
|
|
27
|
+
owner_kind TEXT NOT NULL,
|
|
28
|
+
owner_id TEXT NOT NULL,
|
|
29
|
+
idempotency_key TEXT NOT NULL,
|
|
30
|
+
authority_id TEXT,
|
|
31
|
+
deck_id TEXT NOT NULL,
|
|
32
|
+
run_id TEXT NOT NULL,
|
|
33
|
+
attempt_id TEXT NOT NULL,
|
|
34
|
+
ttl_ms INTEGER NOT NULL,
|
|
35
|
+
tool_scope_hint_json TEXT,
|
|
36
|
+
status TEXT NOT NULL,
|
|
37
|
+
expires_at TEXT,
|
|
38
|
+
created_at TEXT NOT NULL,
|
|
39
|
+
updated_at TEXT NOT NULL
|
|
40
|
+
);
|
|
41
|
+
CREATE INDEX idx_authority_attempts_owner ON authority_attempts(owner_kind, owner_id);
|
|
42
|
+
CREATE INDEX idx_authority_attempts_status ON authority_attempts(status);
|
|
43
|
+
`);
|
|
44
|
+
const preFixCols = db.prepare("PRAGMA table_info(authority_attempts)").all();
|
|
45
|
+
assert.ok(!preFixCols.some((c) => c.name === "stale_at"), "test setup must reproduce the pre-fix table exactly");
|
|
46
|
+
const now = new Date().toISOString();
|
|
47
|
+
db.prepare(`INSERT INTO authority_attempts (id, owner_kind, owner_id, idempotency_key, deck_id, run_id, attempt_id, ttl_ms, status, created_at, updated_at)
|
|
48
|
+
VALUES ('a1', 'developer', 'issue-1:developer', 'a1:1', 'deck-1', 'run-1', 'a1', 60000, 'acquiring', ?, ?)`).run(now, now);
|
|
49
|
+
// This is the actual regression: pre-fix, this throws "no such column: stale_at" from
|
|
50
|
+
// schema.sql's own CREATE INDEX, before ever reaching an incremental ALTER.
|
|
51
|
+
assert.doesNotThrow(() => migrate());
|
|
52
|
+
const cols = db.prepare("PRAGMA table_info(authority_attempts)").all();
|
|
53
|
+
assert.ok(cols.some((c) => c.name === "stale_at"), "stale_at must exist after migrate()");
|
|
54
|
+
// The pre-existing row survived the migration untouched.
|
|
55
|
+
const row = db.prepare("SELECT stale_at FROM authority_attempts WHERE id = 'a1'").get();
|
|
56
|
+
assert.equal(row.stale_at, null);
|
|
57
|
+
// The stale-tracking index now exists and is actually usable.
|
|
58
|
+
db.exec("UPDATE authority_attempts SET stale_at = '2026-01-01T00:00:00.000Z' WHERE id = 'a1'");
|
|
59
|
+
const staleRows = db
|
|
60
|
+
.prepare("SELECT id FROM authority_attempts WHERE status = 'acquiring' AND stale_at IS NOT NULL")
|
|
61
|
+
.all();
|
|
62
|
+
assert.deepEqual(staleRows.map((r) => r.id), ["a1"]);
|
|
63
|
+
// A second migrate() is a no-op (column already present) and still starts cleanly.
|
|
64
|
+
assert.doesNotThrow(() => migrate());
|
|
65
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
process.env.AGENT_DEALER_HOME = fs.mkdtempSync(path.join(os.tmpdir(), "dealer-idem-migrate-"));
|
|
7
|
+
const { migrate, getDb } = await import("./index.js");
|
|
8
|
+
const { BUILTIN_AGENT_CLAUDE_ID, BUILTIN_AGENT_CURSOR_ID } = await import("@agent-dealer/shared");
|
|
9
|
+
const { createIssue } = await import("../repository/issues.js");
|
|
10
|
+
test("migrate() upgrades a pre-fix non-unique idempotency index that already holds duplicates", () => {
|
|
11
|
+
migrate();
|
|
12
|
+
const db = getDb();
|
|
13
|
+
const issue = createIssue({
|
|
14
|
+
title: "T",
|
|
15
|
+
repo: "/r",
|
|
16
|
+
baseBranch: "main",
|
|
17
|
+
developerAgentId: BUILTIN_AGENT_CLAUDE_ID,
|
|
18
|
+
reviewerAgentId: BUILTIN_AGENT_CURSOR_ID,
|
|
19
|
+
maxReviewRounds: 3,
|
|
20
|
+
maxInfraAttempts: 3,
|
|
21
|
+
source: "manual",
|
|
22
|
+
});
|
|
23
|
+
// Recreate the pre-fix world: non-unique index + duplicate provider keys.
|
|
24
|
+
db.exec("DROP INDEX idx_workflow_events_idempotency");
|
|
25
|
+
db.exec("CREATE INDEX idx_workflow_events_idempotency ON workflow_events(idempotency_key) WHERE idempotency_key IS NOT NULL");
|
|
26
|
+
const insert = db.prepare(`INSERT INTO workflow_events (id, issue_id, type, actor_type, stage, idempotency_key, causation_event_id, ts)
|
|
27
|
+
VALUES (?, ?, 'pull_request.updated', 'system', 'developing', ?, ?, ?)`);
|
|
28
|
+
insert.run("e1", issue.id, "dup-key", null, "2026-01-01T00:00:00.000Z");
|
|
29
|
+
insert.run("e2", issue.id, "dup-key", null, "2026-01-01T00:00:01.000Z");
|
|
30
|
+
insert.run("e3", issue.id, "dup-key", null, "2026-01-01T00:00:02.000Z");
|
|
31
|
+
insert.run("e4", issue.id, "other-key", null, "2026-01-01T00:00:03.000Z");
|
|
32
|
+
// A downstream event whose recorded cause is a duplicate that will be discarded.
|
|
33
|
+
insert.run("e5", issue.id, "later-key", "e2", "2026-01-01T00:00:04.000Z");
|
|
34
|
+
// A duplicate row that itself points at another duplicate.
|
|
35
|
+
insert.run("e6", issue.id, "dup-key", "e3", "2026-01-01T00:00:05.000Z");
|
|
36
|
+
assert.doesNotThrow(() => migrate());
|
|
37
|
+
// The first event recorded for the repeated key is kept; later copies dropped.
|
|
38
|
+
const kept = db
|
|
39
|
+
.prepare("SELECT id FROM workflow_events WHERE idempotency_key = 'dup-key' ORDER BY id")
|
|
40
|
+
.all();
|
|
41
|
+
assert.deepStrictEqual(kept.map((r) => r.id), ["e1"]);
|
|
42
|
+
assert.equal(db.prepare("SELECT COUNT(*) c FROM workflow_events WHERE idempotency_key = 'other-key'").get().c, 1);
|
|
43
|
+
// Causality is remapped to the canonical event, not left dangling or dropped.
|
|
44
|
+
assert.equal(db.prepare("SELECT causation_event_id x FROM workflow_events WHERE id = 'e5'").get().x, "e1");
|
|
45
|
+
// Every causation reference still resolves to a surviving event.
|
|
46
|
+
assert.equal(db
|
|
47
|
+
.prepare(`SELECT COUNT(*) c FROM workflow_events e
|
|
48
|
+
WHERE e.causation_event_id IS NOT NULL
|
|
49
|
+
AND NOT EXISTS (SELECT 1 FROM workflow_events p WHERE p.id = e.causation_event_id)`)
|
|
50
|
+
.get().c, 0);
|
|
51
|
+
// The index is now unique and actually enforced.
|
|
52
|
+
const idx = db.prepare("PRAGMA index_list(workflow_events)").all().find((i) => i.name === "idx_workflow_events_idempotency");
|
|
53
|
+
assert.equal(idx?.unique, 1);
|
|
54
|
+
assert.throws(() => insert.run("e7", issue.id, "dup-key", null, "2026-01-01T00:00:06.000Z"), /UNIQUE constraint failed: workflow_events\.idempotency_key/);
|
|
55
|
+
// A second migrate() is a no-op (index already unique) and still starts cleanly.
|
|
56
|
+
assert.doesNotThrow(() => migrate());
|
|
57
|
+
});
|
|
@@ -4,7 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import Database from "better-sqlite3";
|
|
5
5
|
import { readFileSync } from "node:fs";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
-
import { BUILTIN_AGENT_CLAUDE_ID, BUILTIN_AGENT_CURSOR_ID, CURSOR_DEFAULT_MODEL, } from "@agent-dealer/shared";
|
|
7
|
+
import { BUILTIN_AGENT_CLAUDE_ID, BUILTIN_AGENT_CURSOR_ID, BUILTIN_AGENT_CODEX_ID, CURSOR_DEFAULT_MODEL, } from "@agent-dealer/shared";
|
|
8
8
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
9
|
export function getDataDir() {
|
|
10
10
|
const home = process.env.AGENT_DEALER_HOME ?? path.join(os.homedir(), ".agent-dealer");
|
|
@@ -23,8 +23,35 @@ export function getDb() {
|
|
|
23
23
|
}
|
|
24
24
|
return dbInstance;
|
|
25
25
|
}
|
|
26
|
+
/** Tests only — drops the cached connection so a later getDb()/migrate() reopens against
|
|
27
|
+
* whatever AGENT_DEALER_HOME is set to next, instead of silently reusing a stale handle
|
|
28
|
+
* to the previous test's database. */
|
|
29
|
+
export function closeDb() {
|
|
30
|
+
dbInstance?.close();
|
|
31
|
+
dbInstance = null;
|
|
32
|
+
}
|
|
26
33
|
export function migrate() {
|
|
27
34
|
const db = getDb();
|
|
35
|
+
// NOT-91 added `stale_at` (and an index on it) to `authority_attempts`, but that table
|
|
36
|
+
// itself was introduced earlier (NOT-93). Every other incremental ALTER in this function
|
|
37
|
+
// runs AFTER the schema.sql exec below because schema.sql's CREATE TABLE/INDEX
|
|
38
|
+
// statements are themselves idempotent (IF NOT EXISTS) against an already-current table.
|
|
39
|
+
// This one column can't wait that long: schema.sql's own
|
|
40
|
+
// `CREATE INDEX idx_authority_attempts_stale ON authority_attempts(status, stale_at)`
|
|
41
|
+
// throws "no such column: stale_at" against a pre-NOT-91 `authority_attempts` table
|
|
42
|
+
// before the exec below ever reaches the incremental-ALTER section — aborting migrate()
|
|
43
|
+
// (and every table's schema updates after it) for any database that ran migrate()
|
|
44
|
+
// between NOT-93 and NOT-91 landing. Must run first, and only if the table already
|
|
45
|
+
// exists — a brand-new database gets the column from schema.sql's CREATE TABLE directly.
|
|
46
|
+
const authorityAttemptsExists = db
|
|
47
|
+
.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'authority_attempts'")
|
|
48
|
+
.get();
|
|
49
|
+
if (authorityAttemptsExists) {
|
|
50
|
+
const authorityAttemptCols = db.prepare("PRAGMA table_info(authority_attempts)").all();
|
|
51
|
+
if (!authorityAttemptCols.some((c) => c.name === "stale_at")) {
|
|
52
|
+
db.exec("ALTER TABLE authority_attempts ADD COLUMN stale_at TEXT");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
28
55
|
const schema = readFileSync(path.join(__dirname, "schema.sql"), "utf8");
|
|
29
56
|
db.exec(schema);
|
|
30
57
|
const runCols = db.prepare("PRAGMA table_info(runs)").all();
|
|
@@ -55,12 +82,333 @@ export function migrate() {
|
|
|
55
82
|
db.exec("ALTER TABLE runs ADD COLUMN plan_model TEXT");
|
|
56
83
|
db.exec("ALTER TABLE runs ADD COLUMN execute_model TEXT");
|
|
57
84
|
}
|
|
85
|
+
// NOT-60: role-neutral agent-profile columns + the frozen per-session execution
|
|
86
|
+
// snapshot. Additive ALTERs (SQLite has no ADD COLUMN IF NOT EXISTS), guarded by a
|
|
87
|
+
// PRAGMA check so re-running migrate() is a no-op.
|
|
88
|
+
const agentCols4 = db.prepare("PRAGMA table_info(agents)").all();
|
|
89
|
+
if (!agentCols4.some((c) => c.name === "default_model")) {
|
|
90
|
+
db.exec("ALTER TABLE agents ADD COLUMN default_model TEXT");
|
|
91
|
+
db.exec("ALTER TABLE agents ADD COLUMN default_budget_json TEXT");
|
|
92
|
+
db.exec("ALTER TABLE agents ADD COLUMN purpose TEXT");
|
|
93
|
+
db.exec("ALTER TABLE agents ADD COLUMN playbook_ids_json TEXT");
|
|
94
|
+
db.exec("ALTER TABLE agents ADD COLUMN external_memory_refs_json TEXT");
|
|
95
|
+
db.exec("ALTER TABLE agents ADD COLUMN permission_policy_json TEXT");
|
|
96
|
+
// Backfill the role-neutral defaults from the legacy phase columns — execute first,
|
|
97
|
+
// then plan (design §`agents`). The old columns stay legacy-readable for one release.
|
|
98
|
+
db.exec(`
|
|
99
|
+
UPDATE agents SET
|
|
100
|
+
default_model = COALESCE(default_execute_model, default_plan_model),
|
|
101
|
+
default_budget_json = COALESCE(default_execute_budget_json, default_plan_budget_json)
|
|
102
|
+
WHERE default_model IS NULL AND default_budget_json IS NULL
|
|
103
|
+
`);
|
|
104
|
+
}
|
|
105
|
+
// NOT-81: reasoning effort on the agent profile (frozen into the session snapshot).
|
|
106
|
+
const agentCols5 = db.prepare("PRAGMA table_info(agents)").all();
|
|
107
|
+
if (!agentCols5.some((c) => c.name === "default_effort")) {
|
|
108
|
+
db.exec("ALTER TABLE agents ADD COLUMN default_effort TEXT");
|
|
109
|
+
}
|
|
110
|
+
const workerSessionCols = db.prepare("PRAGMA table_info(worker_sessions)").all();
|
|
111
|
+
if (!workerSessionCols.some((c) => c.name === "profile_snapshot_json")) {
|
|
112
|
+
db.exec("ALTER TABLE worker_sessions ADD COLUMN profile_snapshot_json TEXT");
|
|
113
|
+
}
|
|
114
|
+
// NOT-124: spawned-CLI liveness evidence. Existing rows stay NULL, which recovery reads
|
|
115
|
+
// as "no evidence" and falls back to the timestamp-only reclaim it has always done.
|
|
116
|
+
//
|
|
117
|
+
// Each column is guarded on its OWN presence, not on the pair. Every `ALTER TABLE` here
|
|
118
|
+
// commits separately, so a process killed between two of them leaves the table half
|
|
119
|
+
// upgraded — and a guard keyed on the first column would then skip the block forever
|
|
120
|
+
// while every worker_sessions INSERT still names the second, failing permanently. Checked
|
|
121
|
+
// independently, the same pass that could have produced that state also repairs it.
|
|
122
|
+
if (!workerSessionCols.some((c) => c.name === "process_pid")) {
|
|
123
|
+
db.exec("ALTER TABLE worker_sessions ADD COLUMN process_pid INTEGER");
|
|
124
|
+
}
|
|
125
|
+
if (!workerSessionCols.some((c) => c.name === "process_owner")) {
|
|
126
|
+
db.exec("ALTER TABLE worker_sessions ADD COLUMN process_owner TEXT");
|
|
127
|
+
}
|
|
128
|
+
// NOT-131: portable liveness evidence. Added NULL on existing rows, which processLiveness
|
|
129
|
+
// reads as "unknown" — a session in flight across the upgrade degrades to timestamp-only
|
|
130
|
+
// reclaim rather than being mistaken for alive.
|
|
131
|
+
if (!workerSessionCols.some((c) => c.name === "process_started_at")) {
|
|
132
|
+
db.exec("ALTER TABLE worker_sessions ADD COLUMN process_started_at TEXT");
|
|
133
|
+
}
|
|
134
|
+
// Tighten the workflow-event idempotency index to UNIQUE for DBs created before the
|
|
135
|
+
// constraint (schema.sql's IF NOT EXISTS won't upgrade an existing non-unique index).
|
|
136
|
+
// A pre-fix DB may already hold duplicate keys — the old index was non-unique and
|
|
137
|
+
// appendWorkflowEvent() inserted unconditionally — so dedupe first, then swap the
|
|
138
|
+
// index, all inside one transaction so a failure never leaves the table indexless.
|
|
139
|
+
const wfIdemIdx = db.prepare("PRAGMA index_list(workflow_events)").all();
|
|
140
|
+
const nonUnique = wfIdemIdx.find((i) => i.name === "idx_workflow_events_idempotency" && i.unique === 0);
|
|
141
|
+
if (nonUnique) {
|
|
142
|
+
db.transaction(() => {
|
|
143
|
+
// Preservation policy: for each repeated provider key keep the first event
|
|
144
|
+
// recorded for it (lowest rowid = earliest insert) and drop the later copies —
|
|
145
|
+
// a duplicate delivery should collapse to its original, matching the runtime
|
|
146
|
+
// ON CONFLICT DO NOTHING behaviour.
|
|
147
|
+
//
|
|
148
|
+
// causation_event_id is a self-FK, so first repoint any reference that points at
|
|
149
|
+
// a discarded duplicate to the canonical (kept) event for that key — otherwise
|
|
150
|
+
// the DELETE below fails with FOREIGN KEY constraint failed and rolls back.
|
|
151
|
+
db.exec(`
|
|
152
|
+
WITH canon AS (
|
|
153
|
+
SELECT idempotency_key, MIN(rowid) AS keep_rowid
|
|
154
|
+
FROM workflow_events
|
|
155
|
+
WHERE idempotency_key IS NOT NULL
|
|
156
|
+
GROUP BY idempotency_key
|
|
157
|
+
),
|
|
158
|
+
remap AS (
|
|
159
|
+
SELECT e.id AS dup_id, k.id AS canon_id
|
|
160
|
+
FROM workflow_events e
|
|
161
|
+
JOIN canon c ON c.idempotency_key = e.idempotency_key
|
|
162
|
+
JOIN workflow_events k ON k.rowid = c.keep_rowid
|
|
163
|
+
WHERE e.idempotency_key IS NOT NULL AND e.rowid <> c.keep_rowid
|
|
164
|
+
)
|
|
165
|
+
UPDATE workflow_events
|
|
166
|
+
SET causation_event_id = remap.canon_id
|
|
167
|
+
FROM remap
|
|
168
|
+
WHERE workflow_events.causation_event_id = remap.dup_id
|
|
169
|
+
`);
|
|
170
|
+
db.exec(`
|
|
171
|
+
DELETE FROM workflow_events
|
|
172
|
+
WHERE idempotency_key IS NOT NULL
|
|
173
|
+
AND rowid NOT IN (
|
|
174
|
+
SELECT MIN(rowid) FROM workflow_events
|
|
175
|
+
WHERE idempotency_key IS NOT NULL
|
|
176
|
+
GROUP BY idempotency_key
|
|
177
|
+
)
|
|
178
|
+
`);
|
|
179
|
+
db.exec("DROP INDEX idx_workflow_events_idempotency");
|
|
180
|
+
db.exec("CREATE UNIQUE INDEX idx_workflow_events_idempotency ON workflow_events(idempotency_key) WHERE idempotency_key IS NOT NULL");
|
|
181
|
+
})();
|
|
182
|
+
}
|
|
183
|
+
const artifactCols = db.prepare("PRAGMA table_info(artifacts)").all();
|
|
184
|
+
if (!artifactCols.some((c) => c.name === "issue_id")) {
|
|
185
|
+
// Additive per spec §"artifacts": issue_id/worker_session_id are nullable here since
|
|
186
|
+
// existing rows predate the issue model — every row the migration or new coordinator
|
|
187
|
+
// writes going forward populates issue_id.
|
|
188
|
+
db.exec("ALTER TABLE artifacts ADD COLUMN issue_id TEXT REFERENCES issues(id)");
|
|
189
|
+
db.exec("ALTER TABLE artifacts ADD COLUMN worker_session_id TEXT REFERENCES worker_sessions(id)");
|
|
190
|
+
}
|
|
191
|
+
// NOT-57 Task 1 amendment: issue-linked artifacts have no run, so run_id must be
|
|
192
|
+
// nullable — but SQLite can't ALTER a column's NOT NULL away, so rebuild the table for
|
|
193
|
+
// any database created before schema.sql dropped the constraint (fresh databases from
|
|
194
|
+
// the updated schema.sql never hit this branch).
|
|
195
|
+
const artifactRunIdCol = db.prepare("PRAGMA table_info(artifacts)").all().find((c) => c.name === "run_id");
|
|
196
|
+
if (artifactRunIdCol?.notnull === 1) {
|
|
197
|
+
db.exec(`
|
|
198
|
+
CREATE TABLE artifacts_new (
|
|
199
|
+
id TEXT PRIMARY KEY,
|
|
200
|
+
run_id TEXT REFERENCES runs(id),
|
|
201
|
+
kind TEXT NOT NULL,
|
|
202
|
+
content_json TEXT,
|
|
203
|
+
blob_path TEXT,
|
|
204
|
+
author TEXT NOT NULL,
|
|
205
|
+
created_at TEXT NOT NULL,
|
|
206
|
+
issue_id TEXT REFERENCES issues(id),
|
|
207
|
+
worker_session_id TEXT REFERENCES worker_sessions(id)
|
|
208
|
+
);
|
|
209
|
+
INSERT INTO artifacts_new (id, run_id, kind, content_json, blob_path, author, created_at, issue_id, worker_session_id)
|
|
210
|
+
SELECT id, run_id, kind, content_json, blob_path, author, created_at, issue_id, worker_session_id FROM artifacts;
|
|
211
|
+
DROP TABLE artifacts;
|
|
212
|
+
ALTER TABLE artifacts_new RENAME TO artifacts;
|
|
213
|
+
CREATE INDEX IF NOT EXISTS idx_artifacts_run ON artifacts(run_id);
|
|
214
|
+
`);
|
|
215
|
+
}
|
|
216
|
+
// NOT-63: infra-attempt budget, separate from the review-round budget above — a flaky
|
|
217
|
+
// session/git/gh/Agent Deck/publish failure must not spend the same counter as a
|
|
218
|
+
// reviewer's genuine `changes_requested`.
|
|
219
|
+
const issueCols = db.prepare("PRAGMA table_info(issues)").all();
|
|
220
|
+
if (!issueCols.some((c) => c.name === "max_infra_attempts")) {
|
|
221
|
+
db.exec("ALTER TABLE issues ADD COLUMN max_infra_attempts INTEGER NOT NULL DEFAULT 3");
|
|
222
|
+
db.exec("ALTER TABLE issues ADD COLUMN infra_attempts INTEGER NOT NULL DEFAULT 0");
|
|
223
|
+
}
|
|
224
|
+
// NOT-102: per-issue auto-merge after reviewer approve (kick UI defaults on; existing
|
|
225
|
+
// rows stay off so in-flight workflows keep the human final_review path).
|
|
226
|
+
const issueColsForAutoMerge = db.prepare("PRAGMA table_info(issues)").all();
|
|
227
|
+
if (!issueColsForAutoMerge.some((c) => c.name === "auto_merge")) {
|
|
228
|
+
db.exec("ALTER TABLE issues ADD COLUMN auto_merge INTEGER NOT NULL DEFAULT 0");
|
|
229
|
+
}
|
|
230
|
+
// NOT-93: Deck's own correlation id for the INTERACTION_REQUIRED response that raised a
|
|
231
|
+
// deck_interaction_required action — lets a repeated signal for the same request dedupe
|
|
232
|
+
// to one open action instead of piling up duplicates.
|
|
233
|
+
const humanActionCols = db.prepare("PRAGMA table_info(human_actions)").all();
|
|
234
|
+
if (!humanActionCols.some((c) => c.name === "request_id")) {
|
|
235
|
+
db.exec("ALTER TABLE human_actions ADD COLUMN request_id TEXT");
|
|
236
|
+
}
|
|
237
|
+
// NOT-95: a Run-scoped action (outbound-draft delivery parking) has no Issue, so issue_id
|
|
238
|
+
// must become nullable — SQLite can't ALTER a column's NOT NULL away, so rebuild the table
|
|
239
|
+
// for any database created before schema.sql dropped the constraint (fresh databases from
|
|
240
|
+
// the updated schema.sql never hit this branch). Same recipe as the artifacts_new rebuild
|
|
241
|
+
// above.
|
|
242
|
+
const humanActionIssueIdCol = db.prepare("PRAGMA table_info(human_actions)").all().find((c) => c.name === "issue_id");
|
|
243
|
+
if (humanActionIssueIdCol?.notnull === 1) {
|
|
244
|
+
db.exec(`
|
|
245
|
+
CREATE TABLE human_actions_new (
|
|
246
|
+
id TEXT PRIMARY KEY,
|
|
247
|
+
issue_id TEXT REFERENCES issues(id),
|
|
248
|
+
run_id TEXT REFERENCES runs(id),
|
|
249
|
+
workflow_instance_id TEXT REFERENCES workflow_instances(id),
|
|
250
|
+
action_type TEXT NOT NULL,
|
|
251
|
+
reason TEXT NOT NULL,
|
|
252
|
+
question TEXT NOT NULL,
|
|
253
|
+
evidence_json TEXT,
|
|
254
|
+
response_options_json TEXT,
|
|
255
|
+
continuation_preview_json TEXT,
|
|
256
|
+
request_id TEXT,
|
|
257
|
+
status TEXT NOT NULL,
|
|
258
|
+
resolution_json TEXT,
|
|
259
|
+
resolved_by TEXT,
|
|
260
|
+
requested_at TEXT NOT NULL,
|
|
261
|
+
resolved_at TEXT
|
|
262
|
+
);
|
|
263
|
+
INSERT INTO human_actions_new (
|
|
264
|
+
id, issue_id, workflow_instance_id, action_type, reason, question, evidence_json,
|
|
265
|
+
response_options_json, continuation_preview_json, request_id, status, resolution_json,
|
|
266
|
+
resolved_by, requested_at, resolved_at
|
|
267
|
+
)
|
|
268
|
+
SELECT
|
|
269
|
+
id, issue_id, workflow_instance_id, action_type, reason, question, evidence_json,
|
|
270
|
+
response_options_json, continuation_preview_json, request_id, status, resolution_json,
|
|
271
|
+
resolved_by, requested_at, resolved_at
|
|
272
|
+
FROM human_actions;
|
|
273
|
+
DROP TABLE human_actions;
|
|
274
|
+
ALTER TABLE human_actions_new RENAME TO human_actions;
|
|
275
|
+
CREATE INDEX IF NOT EXISTS idx_human_actions_issue ON human_actions(issue_id);
|
|
276
|
+
CREATE INDEX IF NOT EXISTS idx_human_actions_status ON human_actions(status);
|
|
277
|
+
`);
|
|
278
|
+
}
|
|
279
|
+
else if (!humanActionCols.some((c) => c.name === "run_id")) {
|
|
280
|
+
// Rebuild already happened in a prior migrate() run (or this is a fresh schema.sql
|
|
281
|
+
// database missing only this additive column for some other reason) — plain add.
|
|
282
|
+
db.exec("ALTER TABLE human_actions ADD COLUMN run_id TEXT REFERENCES runs(id)");
|
|
283
|
+
}
|
|
284
|
+
// Always last, once run_id is guaranteed to exist (fresh schema.sql create, the rebuild
|
|
285
|
+
// above, or the plain ALTER above) — see schema.sql's comment on why this index isn't
|
|
286
|
+
// declared there.
|
|
287
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_human_actions_run ON human_actions(run_id)");
|
|
288
|
+
// NOT-91: findOpenHumanActionByRequestId's dedupe was a plain read-then-create check —
|
|
289
|
+
// two concurrent duplicate INTERACTION_REQUIRED deliveries could both see "no open
|
|
290
|
+
// action" and both insert. Tighten to a real constraint enforced via createHumanAction's
|
|
291
|
+
// `ON CONFLICT ... DO NOTHING` (repository/human-actions.ts). Must run AFTER the NOT-95
|
|
292
|
+
// rebuild above: that rebuild drops and recreates `human_actions` (to make issue_id
|
|
293
|
+
// nullable) without carrying over any index created before it, so creating this index
|
|
294
|
+
// earlier in the same migrate() call — as a prior version of this migration did — got it
|
|
295
|
+
// silently dropped by the very next block, and only a second restart (skipping the rebuild
|
|
296
|
+
// block on its now-nullable issue_id) actually created it (review-caught bug). Re-querying
|
|
297
|
+
// PRAGMA index_list here, after the rebuild, is what makes this idempotent no matter which
|
|
298
|
+
// branch above ran. A pre-fix database may already hold a duplicate open pair from exactly
|
|
299
|
+
// that race — dedupe first (keep the earliest per (issue_id, action_type, request_id),
|
|
300
|
+
// auto-resolve any later duplicate), then create the index, all in one transaction so a
|
|
301
|
+
// failure never leaves the table indexless.
|
|
302
|
+
const humanActionIdx = db.prepare("PRAGMA index_list(human_actions)").all();
|
|
303
|
+
if (!humanActionIdx.some((i) => i.name === "idx_human_actions_open_request")) {
|
|
304
|
+
db.transaction(() => {
|
|
305
|
+
db.exec(`
|
|
306
|
+
UPDATE human_actions SET
|
|
307
|
+
status = 'resolved',
|
|
308
|
+
resolution_json = '{"choice":"resolved_via_dedupe_migration"}',
|
|
309
|
+
resolved_by = 'system',
|
|
310
|
+
resolved_at = datetime('now')
|
|
311
|
+
WHERE status = 'open' AND request_id IS NOT NULL AND rowid NOT IN (
|
|
312
|
+
SELECT MIN(rowid) FROM human_actions
|
|
313
|
+
WHERE status = 'open' AND request_id IS NOT NULL
|
|
314
|
+
GROUP BY issue_id, action_type, request_id
|
|
315
|
+
)
|
|
316
|
+
`);
|
|
317
|
+
db.exec(`
|
|
318
|
+
CREATE UNIQUE INDEX idx_human_actions_open_request ON human_actions(issue_id, action_type, request_id)
|
|
319
|
+
WHERE status = 'open' AND request_id IS NOT NULL
|
|
320
|
+
`);
|
|
321
|
+
})();
|
|
322
|
+
}
|
|
323
|
+
// NOT-111: runtime usage-cap availability (one row per runtime account).
|
|
324
|
+
const runtimeAvail = db
|
|
325
|
+
.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'runtime_availability'")
|
|
326
|
+
.get();
|
|
327
|
+
if (!runtimeAvail) {
|
|
328
|
+
db.exec(`
|
|
329
|
+
CREATE TABLE runtime_availability (
|
|
330
|
+
runtime TEXT PRIMARY KEY,
|
|
331
|
+
unavailable_until TEXT NOT NULL,
|
|
332
|
+
reason TEXT NOT NULL,
|
|
333
|
+
evidence_json TEXT,
|
|
334
|
+
observed_at TEXT NOT NULL
|
|
335
|
+
);
|
|
336
|
+
`);
|
|
337
|
+
}
|
|
338
|
+
// NOT-103: sequential issue admission queue.
|
|
339
|
+
const queueEntries = db
|
|
340
|
+
.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'queue_entries'")
|
|
341
|
+
.get();
|
|
342
|
+
if (!queueEntries) {
|
|
343
|
+
db.exec(`
|
|
344
|
+
CREATE TABLE queue_entries (
|
|
345
|
+
id TEXT PRIMARY KEY,
|
|
346
|
+
issue_id TEXT NOT NULL,
|
|
347
|
+
position INTEGER NOT NULL,
|
|
348
|
+
enqueued_at TEXT NOT NULL,
|
|
349
|
+
state TEXT NOT NULL CHECK (state IN ('queued', 'admitted', 'removed')),
|
|
350
|
+
wait_reason TEXT,
|
|
351
|
+
wait_reason_at TEXT
|
|
352
|
+
);
|
|
353
|
+
CREATE UNIQUE INDEX idx_queue_entries_queued_issue
|
|
354
|
+
ON queue_entries(issue_id) WHERE state = 'queued';
|
|
355
|
+
CREATE INDEX idx_queue_entries_queued_position
|
|
356
|
+
ON queue_entries(position) WHERE state = 'queued';
|
|
357
|
+
`);
|
|
358
|
+
}
|
|
58
359
|
seedBuiltinAgents(db);
|
|
59
360
|
seedIntakeSettings(db);
|
|
60
361
|
migrateLegacyAgentDeckPort(db);
|
|
362
|
+
migrateLinearInboxUnlock(db);
|
|
61
363
|
// Default agents are normal rows — clear legacy built-in flag.
|
|
62
364
|
db.exec("UPDATE agents SET is_builtin = 0 WHERE is_builtin = 1");
|
|
63
365
|
}
|
|
366
|
+
/** One-shot: unlock assigneeMe / stateFilter only when they still hold pre-unlock defaults.
|
|
367
|
+
* Deliberate Settings customizations and env-seeded filters (e.g. Todo+Backlog) are left alone. */
|
|
368
|
+
function migrateLinearInboxUnlock(db) {
|
|
369
|
+
const flag = db
|
|
370
|
+
.prepare("SELECT value_json FROM intake_settings WHERE key = ?")
|
|
371
|
+
.get("linear.inboxUnlockV1");
|
|
372
|
+
if (flag)
|
|
373
|
+
return;
|
|
374
|
+
const openStates = ["Backlog", "Todo", "In Progress", "In Review"];
|
|
375
|
+
const upsert = db.prepare("INSERT INTO intake_settings (key, value_json) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value_json = excluded.value_json");
|
|
376
|
+
const assigneeRow = db
|
|
377
|
+
.prepare("SELECT value_json FROM intake_settings WHERE key = ?")
|
|
378
|
+
.get("linear.assigneeMe");
|
|
379
|
+
let assigneeMe = true; // pre-unlock default when missing
|
|
380
|
+
if (assigneeRow) {
|
|
381
|
+
try {
|
|
382
|
+
assigneeMe = JSON.parse(assigneeRow.value_json);
|
|
383
|
+
}
|
|
384
|
+
catch {
|
|
385
|
+
assigneeMe = true;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (assigneeMe === true) {
|
|
389
|
+
upsert.run("linear.assigneeMe", JSON.stringify(false));
|
|
390
|
+
}
|
|
391
|
+
const stateRow = db
|
|
392
|
+
.prepare("SELECT value_json FROM intake_settings WHERE key = ?")
|
|
393
|
+
.get("linear.stateFilter");
|
|
394
|
+
let stateFilter = ["Todo"]; // pre-unlock code default when missing
|
|
395
|
+
if (stateRow) {
|
|
396
|
+
try {
|
|
397
|
+
stateFilter = JSON.parse(stateRow.value_json);
|
|
398
|
+
}
|
|
399
|
+
catch {
|
|
400
|
+
stateFilter = ["Todo"];
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if (isLegacyLinearStateFilterDefault(stateFilter)) {
|
|
404
|
+
upsert.run("linear.stateFilter", JSON.stringify(openStates));
|
|
405
|
+
}
|
|
406
|
+
upsert.run("linear.inboxUnlockV1", JSON.stringify(true));
|
|
407
|
+
}
|
|
408
|
+
/** Pre-unlock persisted default was exactly `["Todo"]` (see prior getPersistedLinearIntakeConfig). */
|
|
409
|
+
export function isLegacyLinearStateFilterDefault(value) {
|
|
410
|
+
return Array.isArray(value) && value.length === 1 && value[0] === "Todo";
|
|
411
|
+
}
|
|
64
412
|
function migrateLegacyAgentDeckPort(db) {
|
|
65
413
|
const row = db
|
|
66
414
|
.prepare("SELECT value_json FROM intake_settings WHERE key = ?")
|
|
@@ -78,15 +426,16 @@ function migrateLegacyAgentDeckPort(db) {
|
|
|
78
426
|
}
|
|
79
427
|
}
|
|
80
428
|
function seedIntakeSettings(db) {
|
|
81
|
-
const
|
|
429
|
+
const openDefault = ["Backlog", "Todo", "In Progress", "In Review"];
|
|
430
|
+
const stateFilter = (process.env.LINEAR_STATE_FILTER ?? openDefault.join(","))
|
|
82
431
|
.split(",")
|
|
83
432
|
.map((s) => s.trim())
|
|
84
433
|
.filter(Boolean);
|
|
85
434
|
const teamId = process.env.LINEAR_TEAM_ID ?? null;
|
|
86
435
|
const defaults = {
|
|
87
|
-
"linear.stateFilter": stateFilter.length > 0 ? stateFilter :
|
|
436
|
+
"linear.stateFilter": stateFilter.length > 0 ? stateFilter : openDefault,
|
|
88
437
|
"linear.teamId": teamId,
|
|
89
|
-
"linear.assigneeMe":
|
|
438
|
+
"linear.assigneeMe": false,
|
|
90
439
|
"linear.defaultAgentId": null,
|
|
91
440
|
"linear.syncEnabled": true,
|
|
92
441
|
"linear.routingRules": [],
|
|
@@ -117,9 +466,12 @@ function seedBuiltinAgents(db) {
|
|
|
117
466
|
`);
|
|
118
467
|
insert.run(BUILTIN_AGENT_CLAUDE_ID, "Claude", "claude_code", now, now);
|
|
119
468
|
insert.run(BUILTIN_AGENT_CURSOR_ID, "Cursor", "cursor_local", now, now);
|
|
469
|
+
insert.run(BUILTIN_AGENT_CODEX_ID, "Codex", "codex_local", now, now);
|
|
470
|
+
// NOT-71: seeds the role-neutral column the issue workflow actually reads. The legacy
|
|
471
|
+
// default_plan_model / default_execute_model columns are read-only compatibility now
|
|
472
|
+
// (profile-snapshot.ts falls back to them for profiles saved before default_model existed).
|
|
120
473
|
db.prepare(`UPDATE agents SET
|
|
121
|
-
|
|
122
|
-
default_execute_model = COALESCE(default_execute_model, ?),
|
|
474
|
+
default_model = COALESCE(default_model, ?),
|
|
123
475
|
updated_at = ?
|
|
124
|
-
WHERE id = ? AND runtime = 'cursor_local'`).run(CURSOR_DEFAULT_MODEL,
|
|
476
|
+
WHERE id = ? AND runtime = 'cursor_local'`).run(CURSOR_DEFAULT_MODEL, now, BUILTIN_AGENT_CURSOR_ID);
|
|
125
477
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { test, beforeEach, afterEach } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
process.env.AGENT_DEALER_HOME = fs.mkdtempSync(path.join(os.tmpdir(), "dealer-inbox-unlock-"));
|
|
7
|
+
delete process.env.LINEAR_STATE_FILTER;
|
|
8
|
+
delete process.env.LINEAR_TEAM_ID;
|
|
9
|
+
const { migrate, getDb, closeDb } = await import("./index.js");
|
|
10
|
+
const { isLegacyLinearStateFilterDefault } = await import("./index.js");
|
|
11
|
+
function readSetting(key) {
|
|
12
|
+
const row = getDb()
|
|
13
|
+
.prepare("SELECT value_json FROM intake_settings WHERE key = ?")
|
|
14
|
+
.get(key);
|
|
15
|
+
assert.ok(row, `expected setting ${key}`);
|
|
16
|
+
return JSON.parse(row.value_json);
|
|
17
|
+
}
|
|
18
|
+
function writeSetting(key, value) {
|
|
19
|
+
getDb()
|
|
20
|
+
.prepare("INSERT INTO intake_settings (key, value_json) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value_json = excluded.value_json")
|
|
21
|
+
.run(key, JSON.stringify(value));
|
|
22
|
+
}
|
|
23
|
+
function clearUnlockFlag() {
|
|
24
|
+
getDb().prepare("DELETE FROM intake_settings WHERE key = ?").run("linear.inboxUnlockV1");
|
|
25
|
+
}
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
closeDb();
|
|
28
|
+
process.env.AGENT_DEALER_HOME = fs.mkdtempSync(path.join(os.tmpdir(), "dealer-inbox-unlock-"));
|
|
29
|
+
delete process.env.LINEAR_STATE_FILTER;
|
|
30
|
+
migrate();
|
|
31
|
+
});
|
|
32
|
+
afterEach(() => closeDb());
|
|
33
|
+
test("isLegacyLinearStateFilterDefault only matches the pre-unlock [Todo] default", () => {
|
|
34
|
+
assert.equal(isLegacyLinearStateFilterDefault(["Todo"]), true);
|
|
35
|
+
assert.equal(isLegacyLinearStateFilterDefault(["Todo", "Backlog"]), false);
|
|
36
|
+
assert.equal(isLegacyLinearStateFilterDefault(["Backlog", "Todo", "In Progress", "In Review"]), false);
|
|
37
|
+
assert.equal(isLegacyLinearStateFilterDefault("Todo"), false);
|
|
38
|
+
});
|
|
39
|
+
test("inbox unlock flips legacy assigneeMe true + [Todo] filter once", () => {
|
|
40
|
+
clearUnlockFlag();
|
|
41
|
+
writeSetting("linear.assigneeMe", true);
|
|
42
|
+
writeSetting("linear.stateFilter", ["Todo"]);
|
|
43
|
+
closeDb();
|
|
44
|
+
migrate();
|
|
45
|
+
assert.equal(readSetting("linear.assigneeMe"), false);
|
|
46
|
+
assert.deepEqual(readSetting("linear.stateFilter"), ["Backlog", "Todo", "In Progress", "In Review"]);
|
|
47
|
+
assert.equal(readSetting("linear.inboxUnlockV1"), true);
|
|
48
|
+
});
|
|
49
|
+
test("inbox unlock preserves customized assigneeMe false and Todo+Backlog filter", () => {
|
|
50
|
+
clearUnlockFlag();
|
|
51
|
+
writeSetting("linear.assigneeMe", false);
|
|
52
|
+
writeSetting("linear.stateFilter", ["Todo", "Backlog"]);
|
|
53
|
+
closeDb();
|
|
54
|
+
migrate();
|
|
55
|
+
assert.equal(readSetting("linear.assigneeMe"), false);
|
|
56
|
+
assert.deepEqual(readSetting("linear.stateFilter"), ["Todo", "Backlog"]);
|
|
57
|
+
assert.equal(readSetting("linear.inboxUnlockV1"), true);
|
|
58
|
+
});
|
|
59
|
+
test("inbox unlock is a no-op when the flag is already set even if values look legacy", () => {
|
|
60
|
+
writeSetting("linear.inboxUnlockV1", true);
|
|
61
|
+
writeSetting("linear.assigneeMe", true);
|
|
62
|
+
writeSetting("linear.stateFilter", ["Todo"]);
|
|
63
|
+
closeDb();
|
|
64
|
+
migrate();
|
|
65
|
+
assert.equal(readSetting("linear.assigneeMe"), true);
|
|
66
|
+
assert.deepEqual(readSetting("linear.stateFilter"), ["Todo"]);
|
|
67
|
+
});
|