opencode-swarm 7.163.1 → 7.164.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.
Files changed (87) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/{coder-settlement-f1gtvcy8.js → coder-settlement-84je6bza.js} +8 -8
  3. package/dist/cli/{config-doctor-0fnqzqz1.js → config-doctor-vfze38e1.js} +2 -2
  4. package/dist/cli/{core-w81fk5sc.js → core-q5hv8e2w.js} +1 -1
  5. package/dist/cli/{curation-policy-j12j2gvc.js → curation-policy-a2zty4br.js} +5 -5
  6. package/dist/cli/{curator-as495r18.js → curator-4pckmmyf.js} +32 -32
  7. package/dist/cli/{curator-drift-g6w4ga1q.js → curator-drift-wy7ejcvw.js} +105 -37
  8. package/dist/cli/{curator-llm-factory-v8vwz20c.js → curator-llm-factory-msbj3hpn.js} +32 -32
  9. package/dist/cli/{evidence-summary-service-51dxfcn5.js → evidence-summary-service-b15yes21.js} +14 -14
  10. package/dist/cli/{gate-evidence-nb1e8hz0.js → gate-evidence-70h0w4tb.js} +5 -5
  11. package/dist/cli/{guardrail-explain-z2qb6v67.js → guardrail-explain-kksdy032.js} +33 -33
  12. package/dist/cli/{guardrail-log-2kwntcqw.js → guardrail-log-7rxbd20p.js} +6 -6
  13. package/dist/cli/{guardrail-reset-mrxrc56j.js → guardrail-reset-3b4gs357.js} +32 -32
  14. package/dist/cli/{hive-promoter-c62x2ncb.js → hive-promoter-s80g1gwb.js} +32 -32
  15. package/dist/cli/index-0515j0cz.js +403 -0
  16. package/dist/cli/{index-ma52g2pq.js → index-12f1tsn2.js} +3 -3
  17. package/dist/cli/{index-2zgjcmmc.js → index-1b1h770t.js} +3 -3
  18. package/dist/cli/{index-9ss2m4rs.js → index-1hpjwz3s.js} +410 -1
  19. package/dist/cli/{index-kvra3nv1.js → index-1z4jzwja.js} +3 -3
  20. package/dist/cli/{index-0103w5pf.js → index-2s4s9n82.js} +2 -2
  21. package/dist/cli/{index-9crtmnqg.js → index-36nt17m8.js} +1 -1
  22. package/dist/cli/{index-g3znh2yd.js → index-4afh2cz7.js} +5 -5
  23. package/dist/cli/{index-2n1ym7jb.js → index-4jersar8.js} +4 -4
  24. package/dist/cli/{index-n67vx5zw.js → index-70bfj3v1.js} +1 -1
  25. package/dist/cli/{index-zjme8atd.js → index-7r9vpkwa.js} +4 -4
  26. package/dist/cli/{index-8ada7ag7.js → index-93sepnc9.js} +2 -2
  27. package/dist/cli/{index-qynqyfgj.js → index-9sy65e94.js} +3 -3
  28. package/dist/cli/{index-f923njwh.js → index-bm73pych.js} +6 -6
  29. package/dist/cli/{index-xyhmt813.js → index-c4pynht5.js} +1 -1
  30. package/dist/cli/{index-m1fvbah1.js → index-c69jbpex.js} +2 -2
  31. package/dist/cli/{index-t34daffb.js → index-e3hwzcjv.js} +2 -2
  32. package/dist/cli/{index-r56f3f81.js → index-g64154pd.js} +1 -1
  33. package/dist/cli/{index-08y8pccg.js → index-gxd8fwyt.js} +4 -4
  34. package/dist/cli/{index-d3azve4r.js → index-hyq20s2v.js} +118 -296
  35. package/dist/cli/{index-7n9jznv0.js → index-ma8w6pfp.js} +3 -3
  36. package/dist/cli/{index-tnpf9wwv.js → index-nhxaa9j5.js} +1 -1
  37. package/dist/cli/{index-d9gdwh34.js → index-nzrhw31r.js} +1 -1
  38. package/dist/cli/{index-n985274d.js → index-pvgcex0d.js} +3 -3
  39. package/dist/cli/{index-etvq73mp.js → index-qkk2skff.js} +34 -34
  40. package/dist/cli/{index-2v06a1yh.js → index-t5t82jy8.js} +5 -5
  41. package/dist/cli/{index-5056zmpt.js → index-t90gjc0k.js} +1 -1
  42. package/dist/cli/{index-66073kej.js → index-t9rhtvy9.js} +2 -2
  43. package/dist/cli/{index-hm239n1p.js → index-tb1k8jya.js} +1 -1
  44. package/dist/cli/{index-x2f1f011.js → index-td6n935w.js} +3 -2
  45. package/dist/cli/{index-swgy9xvk.js → index-v3a9rmyh.js} +3 -3
  46. package/dist/cli/{index-cqgpvx2d.js → index-wdtckfnf.js} +1 -1
  47. package/dist/cli/{index-0tervb1m.js → index-xk0kc1py.js} +522 -358
  48. package/dist/cli/{index-n2mhyect.js → index-ynqp3jb6.js} +1 -1
  49. package/dist/cli/{index-n249fbw7.js → index-zaab1wz9.js} +5 -0
  50. package/dist/cli/{index-4p0wyz5p.js → index-zbpcpsk0.js} +8 -8
  51. package/dist/cli/index.js +32 -32
  52. package/dist/cli/{knowledge-escalator-fq7dm2g4.js → knowledge-escalator-sttdnwf8.js} +11 -11
  53. package/dist/cli/{knowledge-events-3zanyfkr.js → knowledge-events-4h88stj7.js} +9 -9
  54. package/dist/cli/{knowledge-link-vmdj5e5d.js → knowledge-link-yykmxdyw.js} +4 -4
  55. package/dist/cli/{knowledge-store-vhehnw52.js → knowledge-store-j832fxpx.js} +5 -5
  56. package/dist/cli/{knowledge-validator-0tt0pe6b.js → knowledge-validator-edbg761g.js} +7 -7
  57. package/dist/cli/{pending-delegations-b92pecbj.js → pending-delegations-sphntrs0.js} +4 -4
  58. package/dist/cli/{pr-subscriptions-2mkdt0ak.js → pr-subscriptions-1a4gcnzv.js} +4 -4
  59. package/dist/cli/{runner-3gw2njnx.js → runner-d294h1by.js} +5 -5
  60. package/dist/cli/{scan-cursor-1c8a3b0m.js → scan-cursor-5frvp7nb.js} +6 -6
  61. package/dist/cli/{schema-c554j5c8.js → schema-9xkt5hvd.js} +1 -1
  62. package/dist/cli/{scope-persistence-vtbcszvv.js → scope-persistence-gk55h3sp.js} +8 -8
  63. package/dist/cli/{skill-generator-0es21fck.js → skill-generator-sxxf7jbb.js} +13 -13
  64. package/dist/cli/{sqlite-loader-c92413n7.js → sqlite-loader-8zefc142.js} +1 -1
  65. package/dist/cli/{telemetry-x4zggfd7.js → telemetry-q51s6tyt.js} +1 -1
  66. package/dist/cli/{worktree-collision-ownership-043dg8gg.js → worktree-collision-ownership-7kj2ryt1.js} +4 -4
  67. package/dist/cli/{worktree-isolation-hk42yxw7.js → worktree-isolation-67fs6rz9.js} +32 -32
  68. package/dist/db/canonical-project.d.ts +31 -0
  69. package/dist/db/db-errors.d.ts +24 -0
  70. package/dist/db/driver-parity.d.ts +31 -0
  71. package/dist/db/durability.d.ts +40 -0
  72. package/dist/db/group-commit-writer.d.ts +117 -0
  73. package/dist/db/health.d.ts +35 -0
  74. package/dist/db/insight-candidate-store.d.ts +61 -0
  75. package/dist/db/legacy-import.d.ts +77 -0
  76. package/dist/db/phase-report-store.d.ts +45 -0
  77. package/dist/db/project-db.d.ts +35 -8
  78. package/dist/db/qa-gate-profile.d.ts +2 -0
  79. package/dist/hooks/curator-drift.d.ts +9 -5
  80. package/dist/hooks/curator-types.d.ts +1 -0
  81. package/dist/hooks/knowledge-curator.d.ts +5 -3
  82. package/dist/hooks/micro-reflector.d.ts +12 -7
  83. package/dist/index.js +619 -567
  84. package/dist/memory/schema.d.ts +4 -4
  85. package/dist/prm/index.d.ts +3 -1
  86. package/package.json +3 -2
  87. package/dist/cli/index-c8s9a3zh.js +0 -73
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Canonical project identity for the SQLite durable-state foundation (issue #2480).
3
+ *
4
+ * Policy (docs/sqlite-durable-state.md §Identity):
5
+ * - This helper answers ONE question: "are these two directory spellings the same project
6
+ * root, so they must share ONE `.swarm/swarm.db` connection?" It is project-root
7
+ * IDENTITY, not security-sensitive file equivalence — those are different threat models
8
+ * and deliberately different helpers (issue #2474 / Workstream B1 owns the repo-wide
9
+ * identity rollout; this is the DB-layer-scoped implementation D1 builds on).
10
+ * - Resolution: `path.resolve` (lexical cleanup) → best-effort `fs.realpathSync`
11
+ * (collapses symlinks/junctions and, on Windows, expands 8.3 short names) →
12
+ * case-fold the WHOLE key on win32 only. On POSIX, case is significant: `/a/B` and
13
+ * `/a/b` are different roots and must stay isolated.
14
+ * - Never throws: if `realpathSync` fails (broken symlink, permission, race), the
15
+ * lexically-resolved path is used as-is. A canonicalization failure must not prevent
16
+ * the project DB from opening; it only risks a duplicate handle for exotic spellings,
17
+ * which is the pre-existing behavior.
18
+ */
19
+ import { realpathSync } from 'node:fs';
20
+ /** DI seam for tests (fault-injected realpath failures). */
21
+ export declare const _internals: {
22
+ realpathSync: typeof realpathSync;
23
+ };
24
+ /**
25
+ * Return the canonical cache key for a project directory.
26
+ *
27
+ * - `C:\Proj` and `c:\proj\` map to one key on Windows (one DB handle).
28
+ * - A symlink or junction to the same directory maps to one key on every platform.
29
+ * - Distinct POSIX casings remain distinct roots.
30
+ */
31
+ export declare function canonicalProjectKey(directory: string): string;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Typed errors for the SQLite durable-state foundation (issue #2480).
3
+ *
4
+ * Callers use `category` to make fail-open decisions and to emit ONE coalesced
5
+ * advisory per condition instead of repeating raw SQLite errors. Messages never
6
+ * contain SQL text or user file contents; at most the `.swarm/swarm.db` artifact
7
+ * name appears.
8
+ */
9
+ export type ProjectDbErrorCategory = 'mkdir_failed' | 'driver_unavailable' | 'open_failed' | 'migration_failed';
10
+ export type DbWriteErrorCategory = 'disk_full' | 'read_only' | 'corrupt' | 'busy' | 'unknown';
11
+ export declare class ProjectDbError extends Error {
12
+ readonly category: ProjectDbErrorCategory;
13
+ constructor(category: ProjectDbErrorCategory, message: string);
14
+ }
15
+ export declare class DbWriteError extends Error {
16
+ readonly category: DbWriteErrorCategory;
17
+ constructor(category: DbWriteErrorCategory, message: string);
18
+ }
19
+ /**
20
+ * Classify a write/open failure into a `DbWriteErrorCategory` from errno and
21
+ * SQLite error text. Used by the group-commit writer to decide retry/degrade
22
+ * behavior (issue #2480 disk-full / read-only / corrupt handling).
23
+ */
24
+ export declare function classifyDbWriteError(err: unknown): DbWriteErrorCategory;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Bun ↔ Node driver-parity contract for `.swarm/swarm.db` (issue #2480
3
+ * obligation 2).
4
+ *
5
+ * A single suite, run against WHATEVER driver the shared sqlite loader
6
+ * resolved:
7
+ * - under `bun test`: the real `bun:sqlite` driver (and the node adapter
8
+ * against the strict fake in `sqlite-loader.test.ts`);
9
+ * - under the merge-queue smoke job (`scripts/repro-1873.mjs`, 3-OS, real
10
+ * Node 22): the real `node:sqlite` driver through the adapter.
11
+ *
12
+ * The contract pins the behavioral deltas documented for #1873/#2480:
13
+ * - EXACT parameter counts: every bound-parameter call must pass exactly as
14
+ * many values as the statement has placeholders. `node:sqlite` rejects a
15
+ * mismatched count (`SQLITE_RANGE`); `bun:sqlite` tolerates some lax forms.
16
+ * Portable code never relies on the lax form.
17
+ * - Multi-statement strings only through the no-parameter `run(sql)` path
18
+ * (which routes to `exec` on both drivers).
19
+ * - Transaction + SAVEPOINT nesting round trip.
20
+ * - WAL / busy_timeout / synchronous pragma reads.
21
+ */
22
+ import type { Database } from 'bun:sqlite';
23
+ export interface DriverParityProbe {
24
+ /** True when the resolved driver is the node:sqlite adapter. */
25
+ isNodeAdapter: boolean;
26
+ }
27
+ /**
28
+ * Assert the driver-parity contract on a FRESH database (the caller owns its
29
+ * lifecycle and closes it). Throws on the first violation.
30
+ */
31
+ export declare function runDriverParityContract(db: Database, probe?: DriverParityProbe): void;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Per-table durability classes for `.swarm/swarm.db` (issue #2480 obligation 4).
3
+ *
4
+ * SQLite's `synchronous` pragma is connection-scoped and takes effect at the next
5
+ * commit, so "per-table durability classes" are implemented by escalating the
6
+ * pragma around write transactions whose target table demands it:
7
+ *
8
+ * - `full` — terminal-state streams (authoritative rows whose loss changes an
9
+ * outcome): `synchronous = FULL` for the wrapping transaction. Authoritative
10
+ * state must never inherit the rebuildable-index durability setting.
11
+ * - `normal` — telemetry/operational/diagnostic rows: `synchronous = NORMAL`
12
+ * (WAL + NORMAL is durable across application crashes; only an OS/power
13
+ * failure can lose the tail — acceptable for rebuildable streams).
14
+ *
15
+ * A batch that contains ANY `full`-class write runs the WHOLE transaction at
16
+ * FULL (escalation rule, group-commit writer consults `batchDurabilityClass`).
17
+ */
18
+ import type { Database } from 'bun:sqlite';
19
+ /** The durability class of every table in `.swarm/swarm.db`. */
20
+ export declare const DURABILITY_CLASSES: Readonly<Record<string, 'full' | 'normal'>>;
21
+ /** Escalation rule: any full-class op makes the whole batch full-class. */
22
+ export declare function batchDurabilityClass(classes: Iterable<'full' | 'normal'>): 'full' | 'normal';
23
+ /** Apply the `synchronous` pragma for a durability class. Cheap and idempotent. */
24
+ export declare function applySynchronousForClass(db: Database, cls: 'full' | 'normal'): void;
25
+ /**
26
+ * Run `fn` with the connection's `synchronous` pragma set for `cls`, restoring
27
+ * NORMAL afterwards. `fn` is expected to complete its own transaction (or be
28
+ * composed inside one); the pragma takes effect at commit time.
29
+ *
30
+ * The connection default is NORMAL (telemetry class), so restoring NORMAL — not
31
+ * "the previous value" — is the correct post-condition: nothing outside the
32
+ * foundation ever sets a different value on this connection.
33
+ *
34
+ * NESTING CAVEAT (final-critic note): nesting a normal-class helper inside an
35
+ * open full-class transaction would restore NORMAL before the OUTER commit.
36
+ * No production path nests these helpers today (group-commit ops never call
37
+ * the qa-gate/receipt writers); if nesting is ever introduced, restore the
38
+ * pre-call value instead.
39
+ */
40
+ export declare function withDurabilityClass<T>(db: Database, cls: 'full' | 'normal', fn: () => T): T;
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Group-commit writer for `.swarm/swarm.db` low-risk stores (issue #2480
3
+ * obligation 8: "queue -> one txn per flush").
4
+ *
5
+ * Design:
6
+ * - One writer per canonical project root, created lazily on first use and
7
+ * closed with the DB handle (`closeGroupCommitWriter`). Nothing here runs at
8
+ * plugin init.
9
+ * - `enqueue(op)` queues a write op. A flush applies EVERY queued op inside
10
+ * ONE `BEGIN IMMEDIATE` transaction (the qa-gate-profile lock precedent —
11
+ * plain `db.transaction()` issues a deferred BEGIN that lets two writers
12
+ * deadlock-escalate to SQLITE_BUSY under two-windows contention).
13
+ * - Durability escalation: if any op in the batch is `full`-class, the whole
14
+ * transaction runs with `PRAGMA synchronous = FULL` (authoritative state
15
+ * never inherits the rebuildable-index setting); otherwise NORMAL.
16
+ * - Backpressure: the queue is bounded; overflow forces a synchronous flush
17
+ * rather than dropping writes.
18
+ * - Failure handling: a `BEGIN IMMEDIATE` busy or a disk-full/read-only
19
+ * failure keeps the queue intact, classifies the error (`db-errors.ts`),
20
+ * and coalesces to ONE advisory/telemetry signal per degradation episode
21
+ * (cooldown); the next flush retries. Ops themselves throwing rolls the
22
+ * transaction back and rethrows (the enqueuing store decides fail-open).
23
+ */
24
+ import type { Database } from 'bun:sqlite';
25
+ import { type DbWriteErrorCategory } from './db-errors.js';
26
+ import { getProjectDb } from './project-db.js';
27
+ /** A single durable write, applied inside a flush transaction. */
28
+ export interface GroupCommitOp {
29
+ /** Durability class of the target table/stream (escalation input). */
30
+ durability: 'full' | 'normal';
31
+ /** Apply the write. Must only touch `.swarm/swarm.db` via the given handle. */
32
+ run: (db: Database) => void;
33
+ }
34
+ /**
35
+ * Detect "the underlying handle was closed" failures across BOTH drivers:
36
+ * bun:sqlite says "database has closed"; node:sqlite raises
37
+ * ERR_INVALID_STATE "database is not open".
38
+ */
39
+ declare function isClosedHandleError(err: unknown): boolean;
40
+ /** Queue bound — overflow forces a synchronous flush (never a silent drop). */
41
+ export declare const MAX_QUEUED_OPS = 1024;
42
+ /** Ops at or above this count trigger an immediate synchronous flush. */
43
+ export declare const FLUSH_THRESHOLD_OPS = 64;
44
+ /**
45
+ * Test seam (repo `_internals` convention): the self-heal rebind's handle
46
+ * acquisition, injectable so tests can deterministically exercise the
47
+ * eviction-on-double-closed-handle path (a rebind that returns an ALREADY
48
+ * closed handle).
49
+ */
50
+ export declare const _internals: {
51
+ getProjectDb: typeof getProjectDb;
52
+ isClosedHandleError: typeof isClosedHandleError;
53
+ };
54
+ export declare class GroupCommitWriter {
55
+ private db;
56
+ private queue;
57
+ private flushing;
58
+ private degraded;
59
+ private lastAdvisoryAt;
60
+ private closed;
61
+ /**
62
+ * Canonical cache key this writer was registered under (set by
63
+ * getGroupCommitWriter). A flush against a CLOSED underlying handle —
64
+ * possible when a close site evicted the DB handle without closing the
65
+ * writer — evicts the writer from the registry so the next store call
66
+ * rebinds to the fresh handle instead of failing forever.
67
+ */
68
+ private registryKey;
69
+ constructor(db: Database);
70
+ get queuedOpCount(): number;
71
+ /** #2480: record the registry key for self-healing eviction. */
72
+ bindRegistryKey(key: string): void;
73
+ /**
74
+ * Queue a write op. A synchronous flush runs immediately when the queue
75
+ * reaches the flush threshold (backpressure), and again at the hard
76
+ * `MAX_QUEUED_OPS` bound. Throws `DbWriteError` from an inline flush
77
+ * (threshold or hard bound) when the flush fails — the op is then NOT
78
+ * accepted; the caller's fail-open handling owns the loss.
79
+ */
80
+ enqueue(op: GroupCommitOp): void;
81
+ /**
82
+ * Apply every queued op in ONE immediate transaction. On op failure the
83
+ * transaction rolls back and the error rethrows (queue emptied — the ops
84
+ * are not idempotently retryable by this layer). On busy/disk-full/
85
+ * read-only the queue is RETAINED and a typed `DbWriteError` throws.
86
+ *
87
+ * Self-healing (#2480): if the underlying handle was CLOSED underneath
88
+ * this writer (a close site evicted the DB handle without closing the
89
+ * writer — the pre-fix /swarm close shape), the flush rebinds to a fresh
90
+ * handle ONCE and re-applies the batch, so post-close writes complete
91
+ * transparently instead of failing until restart.
92
+ */
93
+ flushSync(): void;
94
+ /** Async facade over the synchronous flush (callers `await` durability). */
95
+ flush(): Promise<void>;
96
+ /** Close the writer and drop any unflushed queue. */
97
+ close(): void;
98
+ private applyBatch;
99
+ private noteDegraded;
100
+ /** Test/observability access to the degradation state. */
101
+ get degradation(): {
102
+ category: DbWriteErrorCategory;
103
+ until: number;
104
+ } | null;
105
+ }
106
+ /**
107
+ * Return the group-commit writer for a project root, creating it (and the DB
108
+ * handle) lazily on first use. Never runs at plugin init.
109
+ */
110
+ export declare function getGroupCommitWriter(directory: string): GroupCommitWriter;
111
+ /** Flush + close the writer for a root (dispose/exit/close paths). */
112
+ export declare function closeGroupCommitWriter(directory: string): void;
113
+ /** Flush + close every writer, then close every DB handle. Test/close use. */
114
+ export declare function closeAllGroupCommitWriters(): void;
115
+ /** Number of open writers (tests/observability). */
116
+ export declare function getOpenGroupCommitWriterCount(): number;
117
+ export {};
@@ -0,0 +1,35 @@
1
+ /**
2
+ * swarm.db health probe (issue #2480 obligation 5).
3
+ *
4
+ * The one sanctioned read-only surface for diagnose-style health checks: runs
5
+ * the size-capped quick_check + pragma/migration-failure probes and returns a
6
+ * structured snapshot. Keeping the SQL here (inside `src/db/**`) means the
7
+ * raw-handle confinement and writer-registry seams stay honest — callers like
8
+ * `diagnose-service` consume a plain object.
9
+ */
10
+ /** quick_check size cap: an oversized DB reports `too_large` instead of scanning inline. */
11
+ export declare const SWARM_DB_QUICK_CHECK_MAX_BYTES: number;
12
+ export type SwarmDbHealthSnapshot = {
13
+ kind: 'absent';
14
+ } | {
15
+ kind: 'too_large';
16
+ sizeBytes: number;
17
+ } | {
18
+ kind: 'open';
19
+ quickCheck: string;
20
+ journalMode: string;
21
+ pageCount: number;
22
+ migrationFailures: number;
23
+ /** #2480 review F-07: stale marker file present (a recorded
24
+ * failure whose cleanup could not run — surfaced for diagnosis). */
25
+ staleMarker: boolean;
26
+ } | {
27
+ kind: 'error';
28
+ category: string;
29
+ message: string;
30
+ };
31
+ /**
32
+ * Probe `.swarm/swarm.db` health. Never opens-for-create (an absent DB is
33
+ * `absent`, which callers render as healthy) and never throws.
34
+ */
35
+ export declare function getSwarmDbHealthSnapshot(directory: string): SwarmDbHealthSnapshot;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * swarm.db store for insight candidates (issue #2480 D1 migration).
3
+ *
4
+ * Replaces `.swarm/insight-candidates.jsonl` (locked read-modify-write JSONL
5
+ * queue) with the append-only event-stream pattern:
6
+ *
7
+ * - Table `insight_candidate` — PK (stream_id, version) is the
8
+ * UNIQUE(stream_id, version) stream contract; versions are assigned
9
+ * MAX(version)+1 inside the appending transaction.
10
+ * - `appendInsightCandidatesDb` batches appends through the group-commit
11
+ * writer (queue -> one txn per flush).
12
+ * - `consumeInsightCandidatesDb` is the dual-contract transaction: SELECT the
13
+ * pending batch + UPDATE consumed_at for exactly those versions in ONE
14
+ * `BEGIN IMMEDIATE` transaction, so concurrent appends and consumes can
15
+ * never lose or double-take a candidate.
16
+ * - Telemetry-sink retention: consumed rows are DELETE-pruned after 7 days,
17
+ * and the pending queue is FIFO-capped at 500 (both bounds carried over
18
+ * from the legacy store's limits).
19
+ *
20
+ * Payloads are opaque serialized JSON strings; the hook layer owns parsing,
21
+ * validation, and identity (`resolveInsightCandidateId` recomputes identity
22
+ * from content, so no id column exists).
23
+ */
24
+ /** The single stream id used by this store (table is stream-shaped for D2+). */
25
+ export declare const INSIGHT_CANDIDATE_STREAM_ID = "insight-candidates";
26
+ /** Legacy queue filename (imported then cold-archived as `.jsonl.imported`). */
27
+ export declare const INSIGHT_CANDIDATES_LEGACY_FILE = "insight-candidates.jsonl";
28
+ /** FIFO cap on pending (unconsumed) candidates — carried over from the file store. */
29
+ export declare const INSIGHT_PENDING_CAP = 500;
30
+ /** Consumed-row retention window (telemetry-sink DELETE-based retention). */
31
+ export declare const INSIGHT_CONSUMED_RETENTION_DAYS = 7;
32
+ export interface InsightCandidateRow {
33
+ payload: string;
34
+ createdAt: string;
35
+ }
36
+ /**
37
+ * One-time (per process, per canonical root) lazy import of the legacy
38
+ * `.swarm/insight-candidates.jsonl` queue. Never runs at plugin init.
39
+ */
40
+ export declare function ensureInsightLegacyImported(directory: string): void;
41
+ /**
42
+ * Append candidates to the stream via the group-commit writer and await the
43
+ * flush (the legacy store's append was awaited too — durability semantics
44
+ * are preserved; batching coalesces concurrent callers and multi-candidate
45
+ * calls into one transaction).
46
+ */
47
+ export declare function appendInsightCandidatesDb(directory: string, rows: InsightCandidateRow[]): Promise<void>;
48
+ /**
49
+ * Atomically consume up to `limit` pending candidates: the SELECT and the
50
+ * consumed_at UPDATE happen in ONE immediate transaction (dual-contract
51
+ * event+state transition). Consumed rows older than the retention window are
52
+ * DELETE-pruned in the same transaction. Returns the consumed payloads,
53
+ * oldest first.
54
+ */
55
+ export declare function consumeInsightCandidatesDb(directory: string, limit: number): string[];
56
+ /** Count pending (unconsumed) candidates — status/diagnostics surface. */
57
+ export declare function countPendingInsightCandidatesDb(directory: string): number;
58
+ /** List pending payloads, oldest first (postmortem raw-content surface). */
59
+ export declare function listPendingInsightCandidatesDb(directory: string, max: number): string[];
60
+ /** Test hook: reset the per-process import guards. */
61
+ export declare function _resetInsightImportGuards(): void;
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Idempotent legacy file → swarm.db import (issue #2480 obligation 3).
3
+ *
4
+ * Contract (docs/sqlite-durable-state.md §Legacy import):
5
+ * - Legacy artifact PRESENT + target table/stream EMPTY → import every record
6
+ * in ONE `BEGIN IMMEDIATE` transaction (emptiness re-checked inside the
7
+ * transaction so a concurrent importer cannot double-import) → on commit,
8
+ * rename the artifact to `<name>.imported` (bounded Windows rename retry).
9
+ * - Crash before commit → nothing imported; the next run retries (idempotent).
10
+ * - Crash after commit before rename → the next run sees a NON-empty table;
11
+ * the stale file is left in place (never re-imported, never silently
12
+ * deleted) with a once-per-process warning. This also covers a file that
13
+ * reappeared because an older plugin version wrote it again: those lines
14
+ * are preserved on disk rather than destroyed.
15
+ * - Artifact absent, or table already populated → no-op.
16
+ *
17
+ * Import runs lazily on first store use, never at plugin init (the
18
+ * knowledge-receipts migration precedent).
19
+ */
20
+ import { renameSync } from 'node:fs';
21
+ import { getProjectDb } from './project-db.js';
22
+ /**
23
+ * Size cap for a single legacy artifact import (#2480 review F-02 — same
24
+ * guard convention as MAX_TRACEABILITY_BYTES in design-doc-drift). The live
25
+ * stores were FIFO/entry-bounded when they were files, so anything beyond
26
+ * this is pathological; it is skipped inert (never imported, never renamed)
27
+ * rather than loaded into memory.
28
+ */
29
+ export declare const MAX_LEGACY_IMPORT_BYTES: number;
30
+ export interface LegacyImportResult {
31
+ /** Rows imported (0 when the import conditions were not met). */
32
+ imported: number;
33
+ /** Lines/files skipped because they failed validation. */
34
+ skipped: number;
35
+ /** True when the legacy artifact was renamed to `.imported`. */
36
+ archived: boolean;
37
+ }
38
+ declare function warnStaleLegacyOnce(key: string, fileName: string): void;
39
+ /**
40
+ * DI seam for tests: the LOW-LEVEL rename is fault-injectable so the retry
41
+ * loop itself stays under test (replacing the loop would test nothing).
42
+ */
43
+ export declare const _internals: {
44
+ renameSync: typeof renameSync;
45
+ warnStaleLegacyOnce: typeof warnStaleLegacyOnce;
46
+ maxLegacyImportBytes: () => number;
47
+ };
48
+ /**
49
+ * Import a legacy `.jsonl` file into an append-only stream table.
50
+ *
51
+ * `parseLine` returns the row payload string for a valid line or null to skip
52
+ * (corrupt line). Rows are inserted with versions 1..n in file order. The
53
+ * emptiness probe is `streamCount(db)` (e.g. rows for the stream).
54
+ */
55
+ export declare function importLegacyJsonl(directory: string, opts: {
56
+ fileName: string;
57
+ /** Count existing rows for the target stream (inside the txn). */
58
+ streamCount: (db: ReturnType<typeof getProjectDb>) => number;
59
+ /** Insert one row at the given version; payload from the parsed line. */
60
+ insertRow: (db: ReturnType<typeof getProjectDb>, version: number, payload: string) => void;
61
+ parseLine: (line: string) => string | null;
62
+ }): LegacyImportResult;
63
+ /**
64
+ * Import a family of legacy `.json` files into an entity table keyed by
65
+ * `(kind, phase)`. Files are discovered by exact prefix/suffix match in
66
+ * `.swarm/` (readdir — no user-supplied paths), the phase is parsed from the
67
+ * filename, and each file's parsed+validated object becomes the row payload.
68
+ */
69
+ export declare function importLegacyJsonFiles(directory: string, opts: {
70
+ filePrefix: string;
71
+ kind: string;
72
+ /** Count existing rows for the kind (inside the txn). */
73
+ kindCount: (db: ReturnType<typeof getProjectDb>) => number;
74
+ /** Upsert one row; payload is the serialized JSON file content. */
75
+ upsertRow: (db: ReturnType<typeof getProjectDb>, phase: number, payload: string) => void;
76
+ }): LegacyImportResult;
77
+ export {};
@@ -0,0 +1,45 @@
1
+ /**
2
+ * swarm.db store for per-phase reports (issue #2480 D1 migration).
3
+ *
4
+ * Replaces two legacy file families with the entity/KV pattern — one row per
5
+ * (kind, phase), last-write-wins on a same-phase re-run:
6
+ * - `.swarm/drift-report-phase-{N}.json` (curator drift, kind
7
+ * `curator_drift`) — written by `src/hooks/curator-drift.ts`, read back by
8
+ * `readPriorDriftReports` and the curator postmortem.
9
+ * - `.swarm/doc-drift-phase-{N}.json` (design-doc drift, kind
10
+ * `design_doc_drift`) — written by `src/hooks/design-doc-drift.ts` (whose
11
+ * legacy bare `writeFile` was non-atomic; the store fixes that).
12
+ *
13
+ * Payloads are opaque serialized JSON strings; readers own validation
14
+ * (skip-corrupt on read, mirroring the legacy readers).
15
+ */
16
+ export type PhaseReportKind = 'curator_drift' | 'design_doc_drift';
17
+ /** Legacy filename prefixes, one per report kind. */
18
+ export declare const PHASE_REPORT_LEGACY_PREFIXES: Readonly<Record<PhaseReportKind, string>>;
19
+ /**
20
+ * One-time (per process, per canonical root) lazy import of both legacy
21
+ * report families. Never runs at plugin init.
22
+ */
23
+ export declare function ensurePhaseReportsImported(directory: string): void;
24
+ /**
25
+ * Upsert one phase report via the group-commit writer and await the flush.
26
+ * A same-phase re-run overwrites the row (the legacy file rewrite semantic)
27
+ * and `updated_at` moves.
28
+ */
29
+ export declare function upsertPhaseReportDb(directory: string, kind: PhaseReportKind, phase: number, payload: string): Promise<void>;
30
+ /**
31
+ * Read all reports of one kind, ascending by phase. Returns the raw payload
32
+ * strings; the caller validates (the curator drift reader keeps its
33
+ * skip-corrupt schema check).
34
+ */
35
+ export declare function readPhaseReportsDb(directory: string, kind: PhaseReportKind): Array<{
36
+ phase: number;
37
+ payload: string;
38
+ }>;
39
+ /**
40
+ * Stable, human-readable locator for a stored report (event payloads and
41
+ * advisory text reference the DB-backed store instead of a file path).
42
+ */
43
+ export declare function phaseReportLocator(kind: PhaseReportKind, phase: number): string;
44
+ /** Test hook: reset the per-process import guards. */
45
+ export declare function _resetPhaseReportImportGuards(): void;
@@ -1,16 +1,32 @@
1
1
  /**
2
2
  * Per-project SQLite database for opencode-swarm.
3
3
  *
4
- * Owns `.swarm/swarm.db` in each project directory. Stores per-project
5
- * constraints and QA gate profiles. One cached instance per normalized
6
- * directory path.
4
+ * Owns `.swarm/swarm.db` in each project directory: the single durable
5
+ * substrate of Workstream D (issue #2480). One cached instance per CANONICAL
6
+ * project identity (`canonical-project.ts`): case-varied Windows spellings,
7
+ * trailing separators, and symlinked roots of the same project share one
8
+ * connection. Open failures are typed (`db-errors.ts`), failed migrations are
9
+ * recorded for diagnosis and retried on the next open, and close runs a
10
+ * best-effort WAL checkpoint. The durability-class policy for every table
11
+ * lives in `durability.ts`.
7
12
  */
8
13
  import type { Database } from 'bun:sqlite';
14
+ /** Number of currently cached project DB handles (tests / observability). */
15
+ export declare function getOpenProjectDbCount(): number;
16
+ /**
17
+ * Detect "another process applied this migration concurrently" failures:
18
+ * a UNIQUE constraint violation on schema_migrations.version, or an
19
+ * "already exists" DDL error (table/index/trigger created by the winner).
20
+ */
21
+ export declare function isConcurrentMigrationApply(err: unknown): boolean;
9
22
  /**
10
23
  * Run all pending migrations on the provided database.
11
- * Idempotent: existing migrations are not re-applied.
24
+ * Idempotent: existing migrations are not re-applied. Each migration applies
25
+ * in its own transaction; a failure rolls back, is recorded for diagnosis
26
+ * (table row, or the marker fallback), and leaves the version un-bumped so
27
+ * the next open retries it.
12
28
  */
13
- export declare function runProjectMigrations(db: Database): void;
29
+ export declare function runProjectMigrations(db: Database, markerDir?: string): void;
14
30
  /**
15
31
  * Return the absolute path to `.swarm/swarm.db` for the given directory.
16
32
  * Does not create the file or any parent directory.
@@ -26,13 +42,24 @@ export declare function projectDbExists(directory: string): boolean;
26
42
  /**
27
43
  * Return the cached project database for the given directory, opening it
28
44
  * if needed. Creates `.swarm/` if absent and enables WAL + foreign keys.
45
+ * The cache is keyed by canonical project identity, so case-varied Windows
46
+ * spellings, trailing separators, and symlinked roots share ONE handle.
47
+ * Open failures throw a typed `ProjectDbError` and never leave a
48
+ * half-open handle cached.
29
49
  */
30
50
  export declare function getProjectDb(directory: string): Database;
31
51
  /**
32
52
  * Close and remove the cached project database for the given directory.
33
- * Called by the `/swarm close` clean stage before unlinking `swarm.db` (so a
34
- * long-lived WAL-mode connection releases its file lock and Windows `unlink`
35
- * does not fail with EBUSY), and from tests.
53
+ * Runs a best-effort WAL checkpoint (TRUNCATE, PASSIVE fallback) first so the
54
+ * close leaves a self-contained DB file where possible. Called by the
55
+ * `/swarm close` clean stage before unlinking `swarm.db` (so a long-lived
56
+ * WAL-mode connection releases its file lock and Windows `unlink` does not
57
+ * fail with EBUSY), by the plugin dispose/exit close paths, and from tests.
58
+ *
59
+ * Callers that passed a different spelling of the same root (case variant on
60
+ * Windows, symlink) share the canonical handle: closing it invalidates every
61
+ * alias — which is the point (those aliases were previously silent duplicate
62
+ * writers on one file). Reopening via `getProjectDb` always works.
36
63
  */
37
64
  export declare function closeProjectDb(directory: string): void;
38
65
  /**
@@ -31,6 +31,8 @@ export declare const _internals: {
31
31
  profileId: number;
32
32
  storagePlanId: string;
33
33
  }) => void) | undefined;
34
+ /** #2480 test probe: synchronous level observed during the last write txn. */
35
+ lastTxnSynchronous: number;
34
36
  };
35
37
  /**
36
38
  * QA gate flags. All eleven gates are tracked explicitly.
@@ -1,14 +1,18 @@
1
1
  import type { CriticDriftResult, CuratorConfig, CuratorPhaseResult, DriftReport } from './curator-types.js';
2
2
  /**
3
- * Read all prior drift reports from .swarm/drift-report-phase-*.json files.
3
+ * Read all prior drift reports (#2480: from the `phase_report` entity table
4
+ * in `.swarm/swarm.db`, kind `curator_drift`; the legacy
5
+ * `.swarm/drift-report-phase-*.json` files are imported once — idempotent,
6
+ * one-txn — and cold-archived `.json.imported`).
4
7
  * Returns reports sorted ascending by phase number.
5
- * Skips corrupt/unreadable files with a console.warn.
8
+ * Skips corrupt/unreadable payloads with a console.warn.
6
9
  */
7
10
  export declare function readPriorDriftReports(directory: string): Promise<DriftReport[]>;
8
11
  /**
9
- * Write a drift report to .swarm/drift-report-phase-{N}.json.
10
- * Creates .swarm/ if it doesn't exist.
11
- * Returns the absolute path of the written file.
12
+ * Write a drift report to the `phase_report` entity table (#2480: upsert via
13
+ * the group-commit writer one txn per flush, atomic, replacing the legacy
14
+ * non-batched file rewrite). A same-phase re-run overwrites the row.
15
+ * Returns the DB-backed report locator.
12
16
  */
13
17
  export declare function writeDriftReport(directory: string, report: DriftReport): Promise<string>;
14
18
  export declare const _internals: {
@@ -155,6 +155,7 @@ export interface SkillCandidate {
155
155
  export interface CriticDriftResult {
156
156
  phase: number;
157
157
  report: DriftReport;
158
+ /** #2480: DB-backed locator, e.g. `swarm.db:phase_report(curator_drift,3)`. */
158
159
  report_path: string;
159
160
  injection_text: string;
160
161
  }
@@ -69,9 +69,11 @@ export declare function enrichLessonToV3(params: {
69
69
  /** Max insight candidates folded into the store per phase boundary. */
70
70
  export declare const MESO_INSIGHT_BATCH_LIMIT = 20;
71
71
  /**
72
- * Atomically consume up to `batchLimit` insight candidates from
73
- * `.swarm/insight-candidates.jsonl`, writing back the unconsumed tail under the
74
- * same lock so concurrent micro-reflection appends are never lost. Fail-open.
72
+ * Atomically consume up to `batchLimit` insight candidates from the durable
73
+ * `insight_candidate` stream in `.swarm/swarm.db` (issue #2480). The SELECT
74
+ * and the consumed_at UPDATE happen in ONE immediate transaction, so
75
+ * concurrent appends are never lost and a batch is never double-taken.
76
+ * Fail-open: returns [] on any DB error.
75
77
  */
76
78
  export declare function consumeInsightCandidates(directory: string, batchLimit?: number): Promise<InsightCandidate[]>;
77
79
  /** Build a SwarmKnowledgeEntry from an already-v3-actionable insight candidate. */
@@ -123,14 +123,19 @@ export declare function classifyOutcome(transcript: string, trajectory: Trajecto
123
123
  /** Read a task's trajectory slice. Fail-open: [] when absent/corrupt. */
124
124
  export declare function readTaskTrajectory(directory: string, taskId: string): Promise<TrajectoryEntry[]>;
125
125
  export declare const INSIGHT_CANDIDATES_MAX_ENTRIES = 500;
126
- /** Append validated candidates to the insight queue (best-effort, fail-open).
127
- * Uses transactFile for consistency with consumeInsightCandidates and enforces
128
- * a FIFO cap to prevent unbounded growth between phase completions.
126
+ /**
127
+ * Append validated candidates to the insight queue (best-effort, fail-open).
128
+ *
129
+ * #2480: the durable queue is the `insight_candidate` stream table in
130
+ * `.swarm/swarm.db` (group-commit writer, one txn per flush; FIFO cap 500 on
131
+ * the pending queue — same bound as before). The legacy
132
+ * `.swarm/insight-candidates.jsonl` is imported once (idempotent, one-txn)
133
+ * and cold-archived `.jsonl.imported`.
129
134
  *
130
- * EXPORTED (#1821) so the PRM pattern producer can write the SAME durable
131
- * crash backstop the micro-reflector writes. Any producer that only enqueues
132
- * in memory loses its candidate on process death, on queue overflow, and on a
133
- * drain failure — with no phase-boundary fold-in to recover it. */
135
+ * EXPORTED (#1821) so the PRM pattern producer can write the SAME durable
136
+ * crash backstop the micro-reflector writes. Any producer that only enqueues
137
+ * in memory loses its candidate on process death, on queue overflow, and on a
138
+ * drain failure — with no phase-boundary fold-in to recover it. */
134
139
  export declare function appendInsightCandidates(directory: string, candidates: InsightCandidate[]): Promise<void>;
135
140
  /** Build the bounded micro-reflection prompt (≤ MICRO_PROMPT_INPUT_CAP chars). */
136
141
  export declare function buildMicroPrompt(params: {