swarm-mail 0.1.3 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/beads/adapter.d.ts +38 -0
- package/dist/beads/adapter.d.ts.map +1 -0
- package/dist/beads/blocked-cache.d.ts +21 -0
- package/dist/beads/blocked-cache.d.ts.map +1 -0
- package/dist/beads/comments.d.ts +21 -0
- package/dist/beads/comments.d.ts.map +1 -0
- package/dist/beads/dependencies.d.ts +58 -0
- package/dist/beads/dependencies.d.ts.map +1 -0
- package/dist/beads/events.d.ts +163 -0
- package/dist/beads/events.d.ts.map +1 -0
- package/dist/beads/flush-manager.d.ts +71 -0
- package/dist/beads/flush-manager.d.ts.map +1 -0
- package/dist/beads/index.d.ts +25 -0
- package/dist/beads/index.d.ts.map +1 -0
- package/dist/beads/jsonl.d.ts +103 -0
- package/dist/beads/jsonl.d.ts.map +1 -0
- package/dist/beads/labels.d.ts +21 -0
- package/dist/beads/labels.d.ts.map +1 -0
- package/dist/beads/merge.d.ts +99 -0
- package/dist/beads/merge.d.ts.map +1 -0
- package/dist/beads/migrations.d.ts +41 -0
- package/dist/beads/migrations.d.ts.map +1 -0
- package/dist/beads/operations.d.ts +56 -0
- package/dist/beads/operations.d.ts.map +1 -0
- package/dist/beads/projections.d.ts +103 -0
- package/dist/beads/projections.d.ts.map +1 -0
- package/dist/beads/queries.d.ts +77 -0
- package/dist/beads/queries.d.ts.map +1 -0
- package/dist/beads/store.d.ts +98 -0
- package/dist/beads/store.d.ts.map +1 -0
- package/dist/beads/validation.d.ts +75 -0
- package/dist/beads/validation.d.ts.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1957 -39
- package/dist/pglite.d.ts +9 -1
- package/dist/pglite.d.ts.map +1 -1
- package/dist/streams/debug.d.ts.map +1 -1
- package/dist/streams/migrations.d.ts +1 -1
- package/dist/streams/migrations.d.ts.map +1 -1
- package/dist/streams/store.d.ts.map +1 -1
- package/dist/types/beads-adapter.d.ts +397 -0
- package/dist/types/beads-adapter.d.ts.map +1 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -14821,7 +14821,6 @@ async function debugEventsPaginated(options) {
|
|
|
14821
14821
|
}) : batch;
|
|
14822
14822
|
allEvents.push(...filtered);
|
|
14823
14823
|
offset += batchSize;
|
|
14824
|
-
console.log(`[SwarmMail] Fetched ${allEvents.length} events (batch size: ${batchSize})`);
|
|
14825
14824
|
}
|
|
14826
14825
|
allEvents.sort((a, b) => b.sequence - a.sequence);
|
|
14827
14826
|
const limitedEvents = allEvents.slice(0, limit);
|
|
@@ -15063,7 +15062,7 @@ async function ensureVersionTable(db) {
|
|
|
15063
15062
|
async function getCurrentVersion(db) {
|
|
15064
15063
|
await ensureVersionTable(db);
|
|
15065
15064
|
const result = await db.query(`SELECT MAX(version) as version FROM schema_version`);
|
|
15066
|
-
return result.rows[0]?.version ??
|
|
15065
|
+
return result.rows[0]?.version ?? -1;
|
|
15067
15066
|
}
|
|
15068
15067
|
async function getAppliedMigrations(db) {
|
|
15069
15068
|
await ensureVersionTable(db);
|
|
@@ -15141,6 +15140,123 @@ async function getPendingMigrations(db) {
|
|
|
15141
15140
|
var migrations;
|
|
15142
15141
|
var init_migrations = __esm(() => {
|
|
15143
15142
|
migrations = [
|
|
15143
|
+
{
|
|
15144
|
+
version: 0,
|
|
15145
|
+
description: "Create core event store tables",
|
|
15146
|
+
up: `
|
|
15147
|
+
-- Events table: The source of truth (append-only)
|
|
15148
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
15149
|
+
id SERIAL PRIMARY KEY,
|
|
15150
|
+
type TEXT NOT NULL,
|
|
15151
|
+
project_key TEXT NOT NULL,
|
|
15152
|
+
timestamp BIGINT NOT NULL,
|
|
15153
|
+
sequence SERIAL,
|
|
15154
|
+
data JSONB NOT NULL,
|
|
15155
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
15156
|
+
);
|
|
15157
|
+
|
|
15158
|
+
-- Index for efficient queries
|
|
15159
|
+
CREATE INDEX IF NOT EXISTS idx_events_project_key ON events(project_key);
|
|
15160
|
+
CREATE INDEX IF NOT EXISTS idx_events_type ON events(type);
|
|
15161
|
+
CREATE INDEX IF NOT EXISTS idx_events_timestamp ON events(timestamp);
|
|
15162
|
+
CREATE INDEX IF NOT EXISTS idx_events_project_type ON events(project_key, type);
|
|
15163
|
+
|
|
15164
|
+
-- Agents materialized view (rebuilt from events)
|
|
15165
|
+
CREATE TABLE IF NOT EXISTS agents (
|
|
15166
|
+
id SERIAL PRIMARY KEY,
|
|
15167
|
+
project_key TEXT NOT NULL,
|
|
15168
|
+
name TEXT NOT NULL,
|
|
15169
|
+
program TEXT DEFAULT 'opencode',
|
|
15170
|
+
model TEXT DEFAULT 'unknown',
|
|
15171
|
+
task_description TEXT,
|
|
15172
|
+
registered_at BIGINT NOT NULL,
|
|
15173
|
+
last_active_at BIGINT NOT NULL,
|
|
15174
|
+
UNIQUE(project_key, name)
|
|
15175
|
+
);
|
|
15176
|
+
|
|
15177
|
+
CREATE INDEX IF NOT EXISTS idx_agents_project ON agents(project_key);
|
|
15178
|
+
|
|
15179
|
+
-- Messages materialized view
|
|
15180
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
15181
|
+
id SERIAL PRIMARY KEY,
|
|
15182
|
+
project_key TEXT NOT NULL,
|
|
15183
|
+
from_agent TEXT NOT NULL,
|
|
15184
|
+
subject TEXT NOT NULL,
|
|
15185
|
+
body TEXT NOT NULL,
|
|
15186
|
+
thread_id TEXT,
|
|
15187
|
+
importance TEXT DEFAULT 'normal',
|
|
15188
|
+
ack_required BOOLEAN DEFAULT FALSE,
|
|
15189
|
+
created_at BIGINT NOT NULL
|
|
15190
|
+
);
|
|
15191
|
+
|
|
15192
|
+
CREATE INDEX IF NOT EXISTS idx_messages_project ON messages(project_key);
|
|
15193
|
+
CREATE INDEX IF NOT EXISTS idx_messages_thread ON messages(thread_id);
|
|
15194
|
+
CREATE INDEX IF NOT EXISTS idx_messages_created ON messages(created_at DESC);
|
|
15195
|
+
|
|
15196
|
+
-- Message recipients (many-to-many)
|
|
15197
|
+
CREATE TABLE IF NOT EXISTS message_recipients (
|
|
15198
|
+
message_id INTEGER REFERENCES messages(id) ON DELETE CASCADE,
|
|
15199
|
+
agent_name TEXT NOT NULL,
|
|
15200
|
+
read_at BIGINT,
|
|
15201
|
+
acked_at BIGINT,
|
|
15202
|
+
PRIMARY KEY(message_id, agent_name)
|
|
15203
|
+
);
|
|
15204
|
+
|
|
15205
|
+
CREATE INDEX IF NOT EXISTS idx_recipients_agent ON message_recipients(agent_name);
|
|
15206
|
+
|
|
15207
|
+
-- File reservations materialized view
|
|
15208
|
+
CREATE TABLE IF NOT EXISTS reservations (
|
|
15209
|
+
id SERIAL PRIMARY KEY,
|
|
15210
|
+
project_key TEXT NOT NULL,
|
|
15211
|
+
agent_name TEXT NOT NULL,
|
|
15212
|
+
path_pattern TEXT NOT NULL,
|
|
15213
|
+
exclusive BOOLEAN DEFAULT TRUE,
|
|
15214
|
+
reason TEXT,
|
|
15215
|
+
created_at BIGINT NOT NULL,
|
|
15216
|
+
expires_at BIGINT NOT NULL,
|
|
15217
|
+
released_at BIGINT
|
|
15218
|
+
);
|
|
15219
|
+
|
|
15220
|
+
CREATE INDEX IF NOT EXISTS idx_reservations_project ON reservations(project_key);
|
|
15221
|
+
CREATE INDEX IF NOT EXISTS idx_reservations_agent ON reservations(agent_name);
|
|
15222
|
+
CREATE INDEX IF NOT EXISTS idx_reservations_expires ON reservations(expires_at);
|
|
15223
|
+
CREATE INDEX IF NOT EXISTS idx_reservations_active ON reservations(project_key, released_at) WHERE released_at IS NULL;
|
|
15224
|
+
|
|
15225
|
+
-- Locks table for distributed mutual exclusion (DurableLock)
|
|
15226
|
+
CREATE TABLE IF NOT EXISTS locks (
|
|
15227
|
+
resource TEXT PRIMARY KEY,
|
|
15228
|
+
holder TEXT NOT NULL,
|
|
15229
|
+
seq INTEGER NOT NULL DEFAULT 0,
|
|
15230
|
+
acquired_at BIGINT NOT NULL,
|
|
15231
|
+
expires_at BIGINT NOT NULL
|
|
15232
|
+
);
|
|
15233
|
+
|
|
15234
|
+
CREATE INDEX IF NOT EXISTS idx_locks_expires ON locks(expires_at);
|
|
15235
|
+
CREATE INDEX IF NOT EXISTS idx_locks_holder ON locks(holder);
|
|
15236
|
+
`,
|
|
15237
|
+
down: `
|
|
15238
|
+
DROP INDEX IF EXISTS idx_locks_holder;
|
|
15239
|
+
DROP INDEX IF EXISTS idx_locks_expires;
|
|
15240
|
+
DROP TABLE IF EXISTS locks;
|
|
15241
|
+
DROP INDEX IF EXISTS idx_reservations_active;
|
|
15242
|
+
DROP INDEX IF EXISTS idx_reservations_expires;
|
|
15243
|
+
DROP INDEX IF EXISTS idx_reservations_agent;
|
|
15244
|
+
DROP INDEX IF EXISTS idx_reservations_project;
|
|
15245
|
+
DROP TABLE IF EXISTS reservations;
|
|
15246
|
+
DROP INDEX IF EXISTS idx_recipients_agent;
|
|
15247
|
+
DROP TABLE IF EXISTS message_recipients;
|
|
15248
|
+
DROP INDEX IF EXISTS idx_messages_thread;
|
|
15249
|
+
DROP INDEX IF EXISTS idx_messages_project;
|
|
15250
|
+
DROP TABLE IF EXISTS messages;
|
|
15251
|
+
DROP INDEX IF EXISTS idx_agents_project;
|
|
15252
|
+
DROP TABLE IF EXISTS agents;
|
|
15253
|
+
DROP INDEX IF EXISTS idx_events_project_type;
|
|
15254
|
+
DROP INDEX IF EXISTS idx_events_timestamp;
|
|
15255
|
+
DROP INDEX IF EXISTS idx_events_type;
|
|
15256
|
+
DROP INDEX IF EXISTS idx_events_project_key;
|
|
15257
|
+
DROP TABLE IF EXISTS events;
|
|
15258
|
+
`
|
|
15259
|
+
},
|
|
15144
15260
|
{
|
|
15145
15261
|
version: 1,
|
|
15146
15262
|
description: "Add cursors table for DurableCursor",
|
|
@@ -15227,6 +15343,41 @@ var init_migrations = __esm(() => {
|
|
|
15227
15343
|
CREATE INDEX IF NOT EXISTS idx_swarm_contexts_bead ON swarm_contexts(bead_id);
|
|
15228
15344
|
`,
|
|
15229
15345
|
down: `DROP TABLE IF EXISTS swarm_contexts;`
|
|
15346
|
+
},
|
|
15347
|
+
{
|
|
15348
|
+
version: 5,
|
|
15349
|
+
description: "Add project_key and checkpointed_at to swarm_contexts, change primary key",
|
|
15350
|
+
up: `
|
|
15351
|
+
-- Add new columns
|
|
15352
|
+
ALTER TABLE swarm_contexts ADD COLUMN IF NOT EXISTS project_key TEXT;
|
|
15353
|
+
ALTER TABLE swarm_contexts ADD COLUMN IF NOT EXISTS checkpointed_at BIGINT;
|
|
15354
|
+
ALTER TABLE swarm_contexts ADD COLUMN IF NOT EXISTS recovered_at BIGINT;
|
|
15355
|
+
ALTER TABLE swarm_contexts ADD COLUMN IF NOT EXISTS recovered_from_checkpoint BIGINT;
|
|
15356
|
+
|
|
15357
|
+
-- Drop old primary key constraint on id
|
|
15358
|
+
ALTER TABLE swarm_contexts DROP CONSTRAINT IF EXISTS swarm_contexts_pkey;
|
|
15359
|
+
|
|
15360
|
+
-- Make id nullable since we're switching to composite key
|
|
15361
|
+
ALTER TABLE swarm_contexts ALTER COLUMN id DROP NOT NULL;
|
|
15362
|
+
|
|
15363
|
+
-- Create new indexes
|
|
15364
|
+
CREATE INDEX IF NOT EXISTS idx_swarm_contexts_project ON swarm_contexts(project_key);
|
|
15365
|
+
DROP INDEX IF EXISTS idx_swarm_contexts_epic;
|
|
15366
|
+
DROP INDEX IF EXISTS idx_swarm_contexts_bead;
|
|
15367
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_swarm_contexts_unique ON swarm_contexts(project_key, epic_id, bead_id);
|
|
15368
|
+
`,
|
|
15369
|
+
down: `
|
|
15370
|
+
DROP INDEX IF EXISTS idx_swarm_contexts_unique;
|
|
15371
|
+
DROP INDEX IF EXISTS idx_swarm_contexts_project;
|
|
15372
|
+
ALTER TABLE swarm_contexts DROP COLUMN IF EXISTS project_key;
|
|
15373
|
+
ALTER TABLE swarm_contexts DROP COLUMN IF EXISTS checkpointed_at;
|
|
15374
|
+
ALTER TABLE swarm_contexts DROP COLUMN IF EXISTS recovered_at;
|
|
15375
|
+
ALTER TABLE swarm_contexts DROP COLUMN IF EXISTS recovered_from_checkpoint;
|
|
15376
|
+
ALTER TABLE swarm_contexts ALTER COLUMN id SET NOT NULL;
|
|
15377
|
+
ALTER TABLE swarm_contexts ADD CONSTRAINT swarm_contexts_pkey PRIMARY KEY (id);
|
|
15378
|
+
CREATE INDEX IF NOT EXISTS idx_swarm_contexts_epic ON swarm_contexts(epic_id);
|
|
15379
|
+
CREATE INDEX IF NOT EXISTS idx_swarm_contexts_bead ON swarm_contexts(bead_id);
|
|
15380
|
+
`
|
|
15230
15381
|
}
|
|
15231
15382
|
];
|
|
15232
15383
|
});
|
|
@@ -15868,11 +16019,6 @@ function parseTimestamp(timestamp) {
|
|
|
15868
16019
|
async function appendEvent(event, projectPath, dbOverride) {
|
|
15869
16020
|
const db = dbOverride ?? await getDatabase(projectPath);
|
|
15870
16021
|
const { type, project_key, timestamp, ...rest } = event;
|
|
15871
|
-
console.log("[SwarmMail] Appending event", {
|
|
15872
|
-
type,
|
|
15873
|
-
projectKey: project_key,
|
|
15874
|
-
timestamp
|
|
15875
|
-
});
|
|
15876
16022
|
const result = await db.query(`INSERT INTO events (type, project_key, timestamp, data)
|
|
15877
16023
|
VALUES ($1, $2, $3, $4)
|
|
15878
16024
|
RETURNING id, sequence`, [type, project_key, timestamp, JSON.stringify(rest)]);
|
|
@@ -15881,13 +16027,6 @@ async function appendEvent(event, projectPath, dbOverride) {
|
|
|
15881
16027
|
throw new Error("Failed to insert event - no row returned");
|
|
15882
16028
|
}
|
|
15883
16029
|
const { id, sequence } = row;
|
|
15884
|
-
console.log("[SwarmMail] Event appended", {
|
|
15885
|
-
type,
|
|
15886
|
-
id,
|
|
15887
|
-
sequence,
|
|
15888
|
-
projectKey: project_key
|
|
15889
|
-
});
|
|
15890
|
-
console.debug("[SwarmMail] Updating materialized views", { type, id });
|
|
15891
16030
|
await updateMaterializedViews(db, { ...event, id, sequence });
|
|
15892
16031
|
return { ...event, id, sequence };
|
|
15893
16032
|
}
|
|
@@ -16073,7 +16212,6 @@ async function replayEventsBatched2(projectKey, onBatch, options = {}, projectPa
|
|
|
16073
16212
|
processed += events2.length;
|
|
16074
16213
|
const percent = Math.round(processed / total * 100);
|
|
16075
16214
|
await onBatch(events2, { processed, total, percent });
|
|
16076
|
-
console.log(`[SwarmMail] Replaying events: ${processed}/${total} (${percent}%)`);
|
|
16077
16215
|
offset += batchSize;
|
|
16078
16216
|
}
|
|
16079
16217
|
return {
|
|
@@ -16153,12 +16291,6 @@ async function handleAgentRegistered(db, event) {
|
|
|
16153
16291
|
]);
|
|
16154
16292
|
}
|
|
16155
16293
|
async function handleMessageSent(db, event) {
|
|
16156
|
-
console.log("[SwarmMail] Handling message sent event", {
|
|
16157
|
-
from: event.from_agent,
|
|
16158
|
-
to: event.to_agents,
|
|
16159
|
-
subject: event.subject,
|
|
16160
|
-
projectKey: event.project_key
|
|
16161
|
-
});
|
|
16162
16294
|
const result = await db.query(`INSERT INTO messages (project_key, from_agent, subject, body, thread_id, importance, ack_required, created_at)
|
|
16163
16295
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
16164
16296
|
RETURNING id`, [
|
|
@@ -16182,19 +16314,9 @@ async function handleMessageSent(db, event) {
|
|
|
16182
16314
|
await db.query(`INSERT INTO message_recipients (message_id, agent_name)
|
|
16183
16315
|
VALUES ${values}
|
|
16184
16316
|
ON CONFLICT DO NOTHING`, params);
|
|
16185
|
-
console.log("[SwarmMail] Message recipients inserted", {
|
|
16186
|
-
messageId,
|
|
16187
|
-
recipientCount: event.to_agents.length
|
|
16188
|
-
});
|
|
16189
16317
|
}
|
|
16190
16318
|
}
|
|
16191
16319
|
async function handleFileReserved(db, event) {
|
|
16192
|
-
console.log("[SwarmMail] Handling file reservation event", {
|
|
16193
|
-
agent: event.agent_name,
|
|
16194
|
-
paths: event.paths,
|
|
16195
|
-
exclusive: event.exclusive,
|
|
16196
|
-
projectKey: event.project_key
|
|
16197
|
-
});
|
|
16198
16320
|
if (event.paths.length > 0) {
|
|
16199
16321
|
const values = event.paths.map((_, i) => `($1, $2, $${i + 3}, $${event.paths.length + 3}, $${event.paths.length + 4}, $${event.paths.length + 5}, $${event.paths.length + 6})`).join(", ");
|
|
16200
16322
|
const params = [
|
|
@@ -16215,10 +16337,6 @@ async function handleFileReserved(db, event) {
|
|
|
16215
16337
|
}
|
|
16216
16338
|
await db.query(`INSERT INTO reservations (project_key, agent_name, path_pattern, exclusive, reason, created_at, expires_at)
|
|
16217
16339
|
VALUES ${values}`, params);
|
|
16218
|
-
console.log("[SwarmMail] File reservations inserted", {
|
|
16219
|
-
agent: event.agent_name,
|
|
16220
|
-
reservationCount: event.paths.length
|
|
16221
|
-
});
|
|
16222
16340
|
}
|
|
16223
16341
|
}
|
|
16224
16342
|
async function handleFileReleased(db, event) {
|
|
@@ -16322,10 +16440,11 @@ async function handleSwarmCheckpointed(db, event) {
|
|
|
16322
16440
|
if (event.type !== "swarm_checkpointed")
|
|
16323
16441
|
return;
|
|
16324
16442
|
await db.query(`INSERT INTO swarm_contexts (
|
|
16325
|
-
project_key, epic_id, bead_id, strategy, files, dependencies,
|
|
16326
|
-
directives, recovery, checkpointed_at, updated_at
|
|
16327
|
-
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $
|
|
16328
|
-
ON CONFLICT (
|
|
16443
|
+
id, project_key, epic_id, bead_id, strategy, files, dependencies,
|
|
16444
|
+
directives, recovery, created_at, checkpointed_at, updated_at
|
|
16445
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $10, $10)
|
|
16446
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
16447
|
+
project_key = EXCLUDED.project_key,
|
|
16329
16448
|
strategy = EXCLUDED.strategy,
|
|
16330
16449
|
files = EXCLUDED.files,
|
|
16331
16450
|
dependencies = EXCLUDED.dependencies,
|
|
@@ -16333,6 +16452,7 @@ async function handleSwarmCheckpointed(db, event) {
|
|
|
16333
16452
|
recovery = EXCLUDED.recovery,
|
|
16334
16453
|
checkpointed_at = EXCLUDED.checkpointed_at,
|
|
16335
16454
|
updated_at = EXCLUDED.updated_at`, [
|
|
16455
|
+
event.bead_id,
|
|
16336
16456
|
event.project_key,
|
|
16337
16457
|
event.epic_id,
|
|
16338
16458
|
event.bead_id,
|
|
@@ -16428,6 +16548,218 @@ var init_store = __esm(() => {
|
|
|
16428
16548
|
TIMESTAMP_SAFE_UNTIL = new Date("2286-01-01").getTime();
|
|
16429
16549
|
});
|
|
16430
16550
|
|
|
16551
|
+
// src/beads/dependencies.ts
|
|
16552
|
+
var exports_dependencies = {};
|
|
16553
|
+
__export(exports_dependencies, {
|
|
16554
|
+
wouldCreateCycle: () => wouldCreateCycle,
|
|
16555
|
+
rebuildBeadBlockedCache: () => rebuildBeadBlockedCache,
|
|
16556
|
+
rebuildAllBlockedCaches: () => rebuildAllBlockedCaches,
|
|
16557
|
+
invalidateBlockedCache: () => invalidateBlockedCache,
|
|
16558
|
+
getOpenBlockers: () => getOpenBlockers
|
|
16559
|
+
});
|
|
16560
|
+
async function wouldCreateCycle(db, beadId, dependsOnId) {
|
|
16561
|
+
const result = await db.query(`WITH RECURSIVE paths AS (
|
|
16562
|
+
-- Start from the target (what we want to depend on)
|
|
16563
|
+
SELECT
|
|
16564
|
+
bead_id,
|
|
16565
|
+
depends_on_id,
|
|
16566
|
+
1 as depth
|
|
16567
|
+
FROM bead_dependencies
|
|
16568
|
+
WHERE bead_id = $2
|
|
16569
|
+
|
|
16570
|
+
UNION
|
|
16571
|
+
|
|
16572
|
+
-- Follow dependencies transitively
|
|
16573
|
+
SELECT
|
|
16574
|
+
bd.bead_id,
|
|
16575
|
+
bd.depends_on_id,
|
|
16576
|
+
p.depth + 1
|
|
16577
|
+
FROM bead_dependencies bd
|
|
16578
|
+
JOIN paths p ON bd.bead_id = p.depends_on_id
|
|
16579
|
+
WHERE p.depth < $3
|
|
16580
|
+
)
|
|
16581
|
+
SELECT EXISTS(
|
|
16582
|
+
SELECT 1 FROM paths WHERE depends_on_id = $1
|
|
16583
|
+
) as exists`, [beadId, dependsOnId, MAX_DEPENDENCY_DEPTH]);
|
|
16584
|
+
return result.rows[0]?.exists ?? false;
|
|
16585
|
+
}
|
|
16586
|
+
async function getOpenBlockers(db, projectKey, beadId) {
|
|
16587
|
+
const result = await db.query(`WITH RECURSIVE blockers AS (
|
|
16588
|
+
-- Direct blockers
|
|
16589
|
+
SELECT depends_on_id as blocker_id, 1 as depth
|
|
16590
|
+
FROM bead_dependencies
|
|
16591
|
+
WHERE bead_id = $1 AND relationship = 'blocks'
|
|
16592
|
+
|
|
16593
|
+
UNION
|
|
16594
|
+
|
|
16595
|
+
-- Transitive blockers
|
|
16596
|
+
SELECT bd.depends_on_id, b.depth + 1
|
|
16597
|
+
FROM bead_dependencies bd
|
|
16598
|
+
JOIN blockers b ON bd.bead_id = b.blocker_id
|
|
16599
|
+
WHERE bd.relationship = 'blocks' AND b.depth < $3
|
|
16600
|
+
)
|
|
16601
|
+
SELECT DISTINCT b.blocker_id
|
|
16602
|
+
FROM blockers b
|
|
16603
|
+
JOIN beads bead ON b.blocker_id = bead.id
|
|
16604
|
+
WHERE bead.project_key = $2 AND bead.status != 'closed' AND bead.deleted_at IS NULL`, [beadId, projectKey, MAX_DEPENDENCY_DEPTH]);
|
|
16605
|
+
return result.rows.map((r) => r.blocker_id);
|
|
16606
|
+
}
|
|
16607
|
+
async function rebuildBeadBlockedCache(db, projectKey, beadId) {
|
|
16608
|
+
const blockerIds = await getOpenBlockers(db, projectKey, beadId);
|
|
16609
|
+
if (blockerIds.length > 0) {
|
|
16610
|
+
await db.query(`INSERT INTO blocked_beads_cache (bead_id, blocker_ids, updated_at)
|
|
16611
|
+
VALUES ($1, $2, $3)
|
|
16612
|
+
ON CONFLICT (bead_id)
|
|
16613
|
+
DO UPDATE SET blocker_ids = $2, updated_at = $3`, [beadId, blockerIds, Date.now()]);
|
|
16614
|
+
} else {
|
|
16615
|
+
await db.query(`DELETE FROM blocked_beads_cache WHERE bead_id = $1`, [beadId]);
|
|
16616
|
+
}
|
|
16617
|
+
}
|
|
16618
|
+
async function rebuildAllBlockedCaches(db, projectKey) {
|
|
16619
|
+
const result = await db.query(`SELECT DISTINCT b.id FROM beads b
|
|
16620
|
+
JOIN bead_dependencies bd ON b.id = bd.bead_id
|
|
16621
|
+
WHERE b.project_key = $1 AND bd.relationship = 'blocks' AND b.deleted_at IS NULL`, [projectKey]);
|
|
16622
|
+
for (const row of result.rows) {
|
|
16623
|
+
await rebuildBeadBlockedCache(db, projectKey, row.id);
|
|
16624
|
+
}
|
|
16625
|
+
}
|
|
16626
|
+
async function invalidateBlockedCache(db, projectKey, beadId) {
|
|
16627
|
+
await rebuildBeadBlockedCache(db, projectKey, beadId);
|
|
16628
|
+
const dependents = await db.query(`SELECT bead_id FROM bead_dependencies WHERE depends_on_id = $1`, [beadId]);
|
|
16629
|
+
for (const row of dependents.rows) {
|
|
16630
|
+
await rebuildBeadBlockedCache(db, projectKey, row.bead_id);
|
|
16631
|
+
}
|
|
16632
|
+
}
|
|
16633
|
+
var MAX_DEPENDENCY_DEPTH = 100;
|
|
16634
|
+
|
|
16635
|
+
// src/beads/migrations.ts
|
|
16636
|
+
var exports_migrations2 = {};
|
|
16637
|
+
__export(exports_migrations2, {
|
|
16638
|
+
beadsMigrations: () => beadsMigrations,
|
|
16639
|
+
beadsMigration: () => beadsMigration
|
|
16640
|
+
});
|
|
16641
|
+
var beadsMigration, beadsMigrations;
|
|
16642
|
+
var init_migrations2 = __esm(() => {
|
|
16643
|
+
beadsMigration = {
|
|
16644
|
+
version: 6,
|
|
16645
|
+
description: "Add beads tables for issue tracking",
|
|
16646
|
+
up: `
|
|
16647
|
+
-- ========================================================================
|
|
16648
|
+
-- Core Beads Table
|
|
16649
|
+
-- ========================================================================
|
|
16650
|
+
CREATE TABLE IF NOT EXISTS beads (
|
|
16651
|
+
id TEXT PRIMARY KEY,
|
|
16652
|
+
project_key TEXT NOT NULL,
|
|
16653
|
+
type TEXT NOT NULL CHECK (type IN ('bug', 'feature', 'task', 'epic', 'chore', 'message')),
|
|
16654
|
+
status TEXT NOT NULL DEFAULT 'open' CHECK (status IN ('open', 'in_progress', 'blocked', 'closed', 'tombstone')),
|
|
16655
|
+
title TEXT NOT NULL CHECK (length(title) <= 500),
|
|
16656
|
+
description TEXT,
|
|
16657
|
+
priority INTEGER NOT NULL DEFAULT 2 CHECK (priority BETWEEN 0 AND 3),
|
|
16658
|
+
parent_id TEXT REFERENCES beads(id) ON DELETE SET NULL,
|
|
16659
|
+
assignee TEXT,
|
|
16660
|
+
created_at BIGINT NOT NULL,
|
|
16661
|
+
updated_at BIGINT NOT NULL,
|
|
16662
|
+
closed_at BIGINT,
|
|
16663
|
+
closed_reason TEXT,
|
|
16664
|
+
deleted_at BIGINT,
|
|
16665
|
+
deleted_by TEXT,
|
|
16666
|
+
delete_reason TEXT,
|
|
16667
|
+
created_by TEXT,
|
|
16668
|
+
CHECK ((status = 'closed') = (closed_at IS NOT NULL))
|
|
16669
|
+
);
|
|
16670
|
+
|
|
16671
|
+
-- Indexes for common queries
|
|
16672
|
+
CREATE INDEX IF NOT EXISTS idx_beads_project ON beads(project_key);
|
|
16673
|
+
CREATE INDEX IF NOT EXISTS idx_beads_status ON beads(status);
|
|
16674
|
+
CREATE INDEX IF NOT EXISTS idx_beads_type ON beads(type);
|
|
16675
|
+
CREATE INDEX IF NOT EXISTS idx_beads_priority ON beads(priority);
|
|
16676
|
+
CREATE INDEX IF NOT EXISTS idx_beads_assignee ON beads(assignee);
|
|
16677
|
+
CREATE INDEX IF NOT EXISTS idx_beads_parent ON beads(parent_id);
|
|
16678
|
+
CREATE INDEX IF NOT EXISTS idx_beads_created ON beads(created_at);
|
|
16679
|
+
CREATE INDEX IF NOT EXISTS idx_beads_project_status ON beads(project_key, status);
|
|
16680
|
+
|
|
16681
|
+
-- ========================================================================
|
|
16682
|
+
-- Dependencies Table
|
|
16683
|
+
-- ========================================================================
|
|
16684
|
+
CREATE TABLE IF NOT EXISTS bead_dependencies (
|
|
16685
|
+
bead_id TEXT NOT NULL REFERENCES beads(id) ON DELETE CASCADE,
|
|
16686
|
+
depends_on_id TEXT NOT NULL REFERENCES beads(id) ON DELETE CASCADE,
|
|
16687
|
+
relationship TEXT NOT NULL CHECK (relationship IN ('blocks', 'related', 'parent-child', 'discovered-from', 'replies-to', 'relates-to', 'duplicates', 'supersedes')),
|
|
16688
|
+
created_at BIGINT NOT NULL,
|
|
16689
|
+
created_by TEXT,
|
|
16690
|
+
PRIMARY KEY (bead_id, depends_on_id, relationship)
|
|
16691
|
+
);
|
|
16692
|
+
|
|
16693
|
+
CREATE INDEX IF NOT EXISTS idx_bead_deps_bead ON bead_dependencies(bead_id);
|
|
16694
|
+
CREATE INDEX IF NOT EXISTS idx_bead_deps_depends_on ON bead_dependencies(depends_on_id);
|
|
16695
|
+
CREATE INDEX IF NOT EXISTS idx_bead_deps_relationship ON bead_dependencies(relationship);
|
|
16696
|
+
|
|
16697
|
+
-- ========================================================================
|
|
16698
|
+
-- Labels Table
|
|
16699
|
+
-- ========================================================================
|
|
16700
|
+
CREATE TABLE IF NOT EXISTS bead_labels (
|
|
16701
|
+
bead_id TEXT NOT NULL REFERENCES beads(id) ON DELETE CASCADE,
|
|
16702
|
+
label TEXT NOT NULL,
|
|
16703
|
+
created_at BIGINT NOT NULL,
|
|
16704
|
+
PRIMARY KEY (bead_id, label)
|
|
16705
|
+
);
|
|
16706
|
+
|
|
16707
|
+
CREATE INDEX IF NOT EXISTS idx_bead_labels_label ON bead_labels(label);
|
|
16708
|
+
|
|
16709
|
+
-- ========================================================================
|
|
16710
|
+
-- Comments Table
|
|
16711
|
+
-- ========================================================================
|
|
16712
|
+
CREATE TABLE IF NOT EXISTS bead_comments (
|
|
16713
|
+
id SERIAL PRIMARY KEY,
|
|
16714
|
+
bead_id TEXT NOT NULL REFERENCES beads(id) ON DELETE CASCADE,
|
|
16715
|
+
author TEXT NOT NULL,
|
|
16716
|
+
body TEXT NOT NULL,
|
|
16717
|
+
parent_id INTEGER REFERENCES bead_comments(id) ON DELETE CASCADE,
|
|
16718
|
+
created_at BIGINT NOT NULL,
|
|
16719
|
+
updated_at BIGINT
|
|
16720
|
+
);
|
|
16721
|
+
|
|
16722
|
+
CREATE INDEX IF NOT EXISTS idx_bead_comments_bead ON bead_comments(bead_id);
|
|
16723
|
+
CREATE INDEX IF NOT EXISTS idx_bead_comments_author ON bead_comments(author);
|
|
16724
|
+
CREATE INDEX IF NOT EXISTS idx_bead_comments_created ON bead_comments(created_at);
|
|
16725
|
+
|
|
16726
|
+
-- ========================================================================
|
|
16727
|
+
-- Blocked Beads Cache
|
|
16728
|
+
-- ========================================================================
|
|
16729
|
+
-- Materialized view for fast blocked queries
|
|
16730
|
+
-- Updated by projections when dependencies change
|
|
16731
|
+
CREATE TABLE IF NOT EXISTS blocked_beads_cache (
|
|
16732
|
+
bead_id TEXT PRIMARY KEY REFERENCES beads(id) ON DELETE CASCADE,
|
|
16733
|
+
blocker_ids TEXT[] NOT NULL,
|
|
16734
|
+
updated_at BIGINT NOT NULL
|
|
16735
|
+
);
|
|
16736
|
+
|
|
16737
|
+
CREATE INDEX IF NOT EXISTS idx_blocked_beads_updated ON blocked_beads_cache(updated_at);
|
|
16738
|
+
|
|
16739
|
+
-- ========================================================================
|
|
16740
|
+
-- Dirty Beads Table
|
|
16741
|
+
-- ========================================================================
|
|
16742
|
+
-- Tracks beads that need JSONL export (incremental sync)
|
|
16743
|
+
CREATE TABLE IF NOT EXISTS dirty_beads (
|
|
16744
|
+
bead_id TEXT PRIMARY KEY REFERENCES beads(id) ON DELETE CASCADE,
|
|
16745
|
+
marked_at BIGINT NOT NULL
|
|
16746
|
+
);
|
|
16747
|
+
|
|
16748
|
+
CREATE INDEX IF NOT EXISTS idx_dirty_beads_marked ON dirty_beads(marked_at);
|
|
16749
|
+
`,
|
|
16750
|
+
down: `
|
|
16751
|
+
-- Drop in reverse order to handle foreign key constraints
|
|
16752
|
+
DROP TABLE IF EXISTS dirty_beads;
|
|
16753
|
+
DROP TABLE IF EXISTS blocked_beads_cache;
|
|
16754
|
+
DROP TABLE IF EXISTS bead_comments;
|
|
16755
|
+
DROP TABLE IF EXISTS bead_labels;
|
|
16756
|
+
DROP TABLE IF EXISTS bead_dependencies;
|
|
16757
|
+
DROP TABLE IF EXISTS beads;
|
|
16758
|
+
`
|
|
16759
|
+
};
|
|
16760
|
+
beadsMigrations = [beadsMigration];
|
|
16761
|
+
});
|
|
16762
|
+
|
|
16431
16763
|
// src/adapter.ts
|
|
16432
16764
|
init_store();
|
|
16433
16765
|
init_projections();
|
|
@@ -16622,10 +16954,1553 @@ async function closeAllSwarmMail() {
|
|
|
16622
16954
|
|
|
16623
16955
|
// src/index.ts
|
|
16624
16956
|
init_streams();
|
|
16957
|
+
|
|
16958
|
+
// src/beads/store.ts
|
|
16959
|
+
init_streams();
|
|
16960
|
+
|
|
16961
|
+
// src/beads/projections.ts
|
|
16962
|
+
async function updateProjections(db, event) {
|
|
16963
|
+
switch (event.type) {
|
|
16964
|
+
case "bead_created":
|
|
16965
|
+
await handleBeadCreated(db, event);
|
|
16966
|
+
break;
|
|
16967
|
+
case "bead_updated":
|
|
16968
|
+
await handleBeadUpdated(db, event);
|
|
16969
|
+
break;
|
|
16970
|
+
case "bead_status_changed":
|
|
16971
|
+
await handleBeadStatusChanged(db, event);
|
|
16972
|
+
break;
|
|
16973
|
+
case "bead_closed":
|
|
16974
|
+
await handleBeadClosed(db, event);
|
|
16975
|
+
break;
|
|
16976
|
+
case "bead_reopened":
|
|
16977
|
+
await handleBeadReopened(db, event);
|
|
16978
|
+
break;
|
|
16979
|
+
case "bead_deleted":
|
|
16980
|
+
await handleBeadDeleted(db, event);
|
|
16981
|
+
break;
|
|
16982
|
+
case "bead_dependency_added":
|
|
16983
|
+
await handleDependencyAdded(db, event);
|
|
16984
|
+
break;
|
|
16985
|
+
case "bead_dependency_removed":
|
|
16986
|
+
await handleDependencyRemoved(db, event);
|
|
16987
|
+
break;
|
|
16988
|
+
case "bead_label_added":
|
|
16989
|
+
await handleLabelAdded(db, event);
|
|
16990
|
+
break;
|
|
16991
|
+
case "bead_label_removed":
|
|
16992
|
+
await handleLabelRemoved(db, event);
|
|
16993
|
+
break;
|
|
16994
|
+
case "bead_comment_added":
|
|
16995
|
+
await handleCommentAdded(db, event);
|
|
16996
|
+
break;
|
|
16997
|
+
case "bead_comment_updated":
|
|
16998
|
+
await handleCommentUpdated(db, event);
|
|
16999
|
+
break;
|
|
17000
|
+
case "bead_comment_deleted":
|
|
17001
|
+
await handleCommentDeleted(db, event);
|
|
17002
|
+
break;
|
|
17003
|
+
case "bead_epic_child_added":
|
|
17004
|
+
await handleEpicChildAdded(db, event);
|
|
17005
|
+
break;
|
|
17006
|
+
case "bead_epic_child_removed":
|
|
17007
|
+
await handleEpicChildRemoved(db, event);
|
|
17008
|
+
break;
|
|
17009
|
+
case "bead_assigned":
|
|
17010
|
+
await handleBeadAssigned(db, event);
|
|
17011
|
+
break;
|
|
17012
|
+
case "bead_work_started":
|
|
17013
|
+
await handleWorkStarted(db, event);
|
|
17014
|
+
break;
|
|
17015
|
+
default:
|
|
17016
|
+
console.warn(`[beads/projections] Unknown event type: ${event.type}`);
|
|
17017
|
+
}
|
|
17018
|
+
await markBeadDirty(db, event.project_key, event.bead_id);
|
|
17019
|
+
}
|
|
17020
|
+
async function handleBeadCreated(db, event) {
|
|
17021
|
+
await db.query(`INSERT INTO beads (
|
|
17022
|
+
id, project_key, type, status, title, description, priority,
|
|
17023
|
+
parent_id, assignee, created_at, updated_at, created_by
|
|
17024
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)`, [
|
|
17025
|
+
event.bead_id,
|
|
17026
|
+
event.project_key,
|
|
17027
|
+
event.issue_type,
|
|
17028
|
+
"open",
|
|
17029
|
+
event.title,
|
|
17030
|
+
event.description || null,
|
|
17031
|
+
event.priority ?? 2,
|
|
17032
|
+
event.parent_id || null,
|
|
17033
|
+
null,
|
|
17034
|
+
event.timestamp,
|
|
17035
|
+
event.timestamp,
|
|
17036
|
+
event.created_by || null
|
|
17037
|
+
]);
|
|
17038
|
+
}
|
|
17039
|
+
async function handleBeadUpdated(db, event) {
|
|
17040
|
+
const changes = event.changes;
|
|
17041
|
+
const updates = [];
|
|
17042
|
+
const params = [];
|
|
17043
|
+
let paramIndex = 1;
|
|
17044
|
+
if (changes.title) {
|
|
17045
|
+
updates.push(`title = $${paramIndex++}`);
|
|
17046
|
+
params.push(changes.title.new);
|
|
17047
|
+
}
|
|
17048
|
+
if (changes.description) {
|
|
17049
|
+
updates.push(`description = $${paramIndex++}`);
|
|
17050
|
+
params.push(changes.description.new);
|
|
17051
|
+
}
|
|
17052
|
+
if (changes.priority) {
|
|
17053
|
+
updates.push(`priority = $${paramIndex++}`);
|
|
17054
|
+
params.push(changes.priority.new);
|
|
17055
|
+
}
|
|
17056
|
+
if (changes.assignee) {
|
|
17057
|
+
updates.push(`assignee = $${paramIndex++}`);
|
|
17058
|
+
params.push(changes.assignee.new);
|
|
17059
|
+
}
|
|
17060
|
+
if (updates.length > 0) {
|
|
17061
|
+
updates.push(`updated_at = $${paramIndex++}`);
|
|
17062
|
+
params.push(event.timestamp);
|
|
17063
|
+
params.push(event.bead_id);
|
|
17064
|
+
await db.query(`UPDATE beads SET ${updates.join(", ")} WHERE id = $${paramIndex}`, params);
|
|
17065
|
+
}
|
|
17066
|
+
}
|
|
17067
|
+
async function handleBeadStatusChanged(db, event) {
|
|
17068
|
+
await db.query(`UPDATE beads SET status = $1, updated_at = $2 WHERE id = $3`, [event.to_status, event.timestamp, event.bead_id]);
|
|
17069
|
+
}
|
|
17070
|
+
async function handleBeadClosed(db, event) {
|
|
17071
|
+
await db.query(`UPDATE beads SET
|
|
17072
|
+
status = 'closed',
|
|
17073
|
+
closed_at = $1,
|
|
17074
|
+
closed_reason = $2,
|
|
17075
|
+
updated_at = $3
|
|
17076
|
+
WHERE id = $4`, [event.timestamp, event.reason, event.timestamp, event.bead_id]);
|
|
17077
|
+
const { invalidateBlockedCache: invalidateBlockedCache2 } = await Promise.resolve().then(() => exports_dependencies);
|
|
17078
|
+
await invalidateBlockedCache2(db, event.project_key, event.bead_id);
|
|
17079
|
+
}
|
|
17080
|
+
async function handleBeadReopened(db, event) {
|
|
17081
|
+
await db.query(`UPDATE beads SET
|
|
17082
|
+
status = 'open',
|
|
17083
|
+
closed_at = NULL,
|
|
17084
|
+
closed_reason = NULL,
|
|
17085
|
+
updated_at = $1
|
|
17086
|
+
WHERE id = $2`, [event.timestamp, event.bead_id]);
|
|
17087
|
+
}
|
|
17088
|
+
async function handleBeadDeleted(db, event) {
|
|
17089
|
+
await db.query(`UPDATE beads SET
|
|
17090
|
+
deleted_at = $1,
|
|
17091
|
+
deleted_by = $2,
|
|
17092
|
+
delete_reason = $3,
|
|
17093
|
+
updated_at = $4
|
|
17094
|
+
WHERE id = $5`, [event.timestamp, event.deleted_by || null, event.reason || null, event.timestamp, event.bead_id]);
|
|
17095
|
+
}
|
|
17096
|
+
async function handleDependencyAdded(db, event) {
|
|
17097
|
+
const dep = event.dependency;
|
|
17098
|
+
await db.query(`INSERT INTO bead_dependencies (bead_id, depends_on_id, relationship, created_at, created_by)
|
|
17099
|
+
VALUES ($1, $2, $3, $4, $5)
|
|
17100
|
+
ON CONFLICT (bead_id, depends_on_id, relationship) DO NOTHING`, [event.bead_id, dep.target, dep.type, event.timestamp, event.added_by || null]);
|
|
17101
|
+
const { invalidateBlockedCache: invalidate } = await Promise.resolve().then(() => exports_dependencies);
|
|
17102
|
+
await invalidate(db, event.project_key, event.bead_id);
|
|
17103
|
+
}
|
|
17104
|
+
async function handleDependencyRemoved(db, event) {
|
|
17105
|
+
const dep = event.dependency;
|
|
17106
|
+
await db.query(`DELETE FROM bead_dependencies
|
|
17107
|
+
WHERE bead_id = $1 AND depends_on_id = $2 AND relationship = $3`, [event.bead_id, dep.target, dep.type]);
|
|
17108
|
+
const { invalidateBlockedCache: invalidate } = await Promise.resolve().then(() => exports_dependencies);
|
|
17109
|
+
await invalidate(db, event.project_key, event.bead_id);
|
|
17110
|
+
}
|
|
17111
|
+
async function handleLabelAdded(db, event) {
|
|
17112
|
+
await db.query(`INSERT INTO bead_labels (bead_id, label, created_at)
|
|
17113
|
+
VALUES ($1, $2, $3)
|
|
17114
|
+
ON CONFLICT (bead_id, label) DO NOTHING`, [event.bead_id, event.label, event.timestamp]);
|
|
17115
|
+
}
|
|
17116
|
+
async function handleLabelRemoved(db, event) {
|
|
17117
|
+
await db.query(`DELETE FROM bead_labels WHERE bead_id = $1 AND label = $2`, [event.bead_id, event.label]);
|
|
17118
|
+
}
|
|
17119
|
+
async function handleCommentAdded(db, event) {
|
|
17120
|
+
await db.query(`INSERT INTO bead_comments (bead_id, author, body, parent_id, created_at)
|
|
17121
|
+
VALUES ($1, $2, $3, $4, $5)`, [event.bead_id, event.author, event.body, event.parent_comment_id || null, event.timestamp]);
|
|
17122
|
+
}
|
|
17123
|
+
async function handleCommentUpdated(db, event) {
|
|
17124
|
+
await db.query(`UPDATE bead_comments SET body = $1, updated_at = $2 WHERE id = $3`, [event.new_body, event.timestamp, event.comment_id]);
|
|
17125
|
+
}
|
|
17126
|
+
async function handleCommentDeleted(db, event) {
|
|
17127
|
+
await db.query(`DELETE FROM bead_comments WHERE id = $1`, [event.comment_id]);
|
|
17128
|
+
}
|
|
17129
|
+
async function handleEpicChildAdded(db, event) {
|
|
17130
|
+
await db.query(`UPDATE beads SET parent_id = $1, updated_at = $2 WHERE id = $3`, [event.bead_id, event.timestamp, event.child_id]);
|
|
17131
|
+
}
|
|
17132
|
+
async function handleEpicChildRemoved(db, event) {
|
|
17133
|
+
await db.query(`UPDATE beads SET parent_id = NULL, updated_at = $1 WHERE id = $2`, [event.timestamp, event.child_id]);
|
|
17134
|
+
}
|
|
17135
|
+
async function handleBeadAssigned(db, event) {
|
|
17136
|
+
await db.query(`UPDATE beads SET assignee = $1, updated_at = $2 WHERE id = $3`, [event.assignee, event.timestamp, event.bead_id]);
|
|
17137
|
+
}
|
|
17138
|
+
async function handleWorkStarted(db, event) {
|
|
17139
|
+
await db.query(`UPDATE beads SET status = 'in_progress', updated_at = $1 WHERE id = $2`, [event.timestamp, event.bead_id]);
|
|
17140
|
+
}
|
|
17141
|
+
async function getBead(db, projectKey, beadId) {
|
|
17142
|
+
const result = await db.query(`SELECT * FROM beads WHERE project_key = $1 AND id = $2 AND deleted_at IS NULL`, [projectKey, beadId]);
|
|
17143
|
+
return result.rows[0] ?? null;
|
|
17144
|
+
}
|
|
17145
|
+
async function queryBeads(db, projectKey, options = {}) {
|
|
17146
|
+
const conditions = ["project_key = $1"];
|
|
17147
|
+
const params = [projectKey];
|
|
17148
|
+
let paramIndex = 2;
|
|
17149
|
+
if (!options.include_deleted) {
|
|
17150
|
+
conditions.push("deleted_at IS NULL");
|
|
17151
|
+
}
|
|
17152
|
+
if (options.status) {
|
|
17153
|
+
const statuses = Array.isArray(options.status) ? options.status : [options.status];
|
|
17154
|
+
conditions.push(`status = ANY($${paramIndex++})`);
|
|
17155
|
+
params.push(statuses);
|
|
17156
|
+
}
|
|
17157
|
+
if (options.type) {
|
|
17158
|
+
const types2 = Array.isArray(options.type) ? options.type : [options.type];
|
|
17159
|
+
conditions.push(`type = ANY($${paramIndex++})`);
|
|
17160
|
+
params.push(types2);
|
|
17161
|
+
}
|
|
17162
|
+
if (options.parent_id) {
|
|
17163
|
+
conditions.push(`parent_id = $${paramIndex++}`);
|
|
17164
|
+
params.push(options.parent_id);
|
|
17165
|
+
}
|
|
17166
|
+
if (options.assignee) {
|
|
17167
|
+
conditions.push(`assignee = $${paramIndex++}`);
|
|
17168
|
+
params.push(options.assignee);
|
|
17169
|
+
}
|
|
17170
|
+
let query = `SELECT * FROM beads WHERE ${conditions.join(" AND ")} ORDER BY priority DESC, created_at ASC`;
|
|
17171
|
+
if (options.limit) {
|
|
17172
|
+
query += ` LIMIT $${paramIndex++}`;
|
|
17173
|
+
params.push(options.limit);
|
|
17174
|
+
}
|
|
17175
|
+
if (options.offset) {
|
|
17176
|
+
query += ` OFFSET $${paramIndex++}`;
|
|
17177
|
+
params.push(options.offset);
|
|
17178
|
+
}
|
|
17179
|
+
const result = await db.query(query, params);
|
|
17180
|
+
return result.rows;
|
|
17181
|
+
}
|
|
17182
|
+
async function getDependencies(db, projectKey, beadId) {
|
|
17183
|
+
const result = await db.query(`SELECT * FROM bead_dependencies WHERE bead_id = $1`, [beadId]);
|
|
17184
|
+
return result.rows;
|
|
17185
|
+
}
|
|
17186
|
+
async function getDependents(db, projectKey, beadId) {
|
|
17187
|
+
const result = await db.query(`SELECT * FROM bead_dependencies WHERE depends_on_id = $1`, [beadId]);
|
|
17188
|
+
return result.rows;
|
|
17189
|
+
}
|
|
17190
|
+
async function isBlocked(db, projectKey, beadId) {
|
|
17191
|
+
const result = await db.query(`SELECT EXISTS(SELECT 1 FROM blocked_beads_cache WHERE bead_id = $1) as exists`, [beadId]);
|
|
17192
|
+
return result.rows[0]?.exists ?? false;
|
|
17193
|
+
}
|
|
17194
|
+
async function getBlockers(db, projectKey, beadId) {
|
|
17195
|
+
const result = await db.query(`SELECT blocker_ids FROM blocked_beads_cache WHERE bead_id = $1`, [beadId]);
|
|
17196
|
+
return result.rows[0]?.blocker_ids ?? [];
|
|
17197
|
+
}
|
|
17198
|
+
async function getLabels(db, projectKey, beadId) {
|
|
17199
|
+
const result = await db.query(`SELECT label FROM bead_labels WHERE bead_id = $1 ORDER BY label`, [beadId]);
|
|
17200
|
+
return result.rows.map((r) => r.label);
|
|
17201
|
+
}
|
|
17202
|
+
async function getComments(db, projectKey, beadId) {
|
|
17203
|
+
const result = await db.query(`SELECT * FROM bead_comments WHERE bead_id = $1 ORDER BY created_at ASC`, [beadId]);
|
|
17204
|
+
return result.rows;
|
|
17205
|
+
}
|
|
17206
|
+
async function getNextReadyBead(db, projectKey) {
|
|
17207
|
+
const result = await db.query(`SELECT b.* FROM beads b
|
|
17208
|
+
WHERE b.project_key = $1
|
|
17209
|
+
AND b.status = 'open'
|
|
17210
|
+
AND b.deleted_at IS NULL
|
|
17211
|
+
AND NOT EXISTS (
|
|
17212
|
+
SELECT 1 FROM blocked_beads_cache bbc WHERE bbc.bead_id = b.id
|
|
17213
|
+
)
|
|
17214
|
+
ORDER BY b.priority DESC, b.created_at ASC
|
|
17215
|
+
LIMIT 1`, [projectKey]);
|
|
17216
|
+
return result.rows[0] ?? null;
|
|
17217
|
+
}
|
|
17218
|
+
async function getInProgressBeads(db, projectKey) {
|
|
17219
|
+
const result = await db.query(`SELECT * FROM beads
|
|
17220
|
+
WHERE project_key = $1 AND status = 'in_progress' AND deleted_at IS NULL
|
|
17221
|
+
ORDER BY priority DESC, created_at ASC`, [projectKey]);
|
|
17222
|
+
return result.rows;
|
|
17223
|
+
}
|
|
17224
|
+
async function getBlockedBeads(db, projectKey) {
|
|
17225
|
+
const result = await db.query(`SELECT b.*, bbc.blocker_ids
|
|
17226
|
+
FROM beads b
|
|
17227
|
+
JOIN blocked_beads_cache bbc ON b.id = bbc.bead_id
|
|
17228
|
+
WHERE b.project_key = $1 AND b.deleted_at IS NULL
|
|
17229
|
+
ORDER BY b.priority DESC, b.created_at ASC`, [projectKey]);
|
|
17230
|
+
return result.rows.map((r) => {
|
|
17231
|
+
const { blocker_ids, ...bead } = r;
|
|
17232
|
+
return { bead, blockers: blocker_ids };
|
|
17233
|
+
});
|
|
17234
|
+
}
|
|
17235
|
+
async function markBeadDirty(db, projectKey, beadId) {
|
|
17236
|
+
await db.query(`INSERT INTO dirty_beads (bead_id, marked_at)
|
|
17237
|
+
VALUES ($1, $2)
|
|
17238
|
+
ON CONFLICT (bead_id) DO UPDATE SET marked_at = $2`, [beadId, Date.now()]);
|
|
17239
|
+
}
|
|
17240
|
+
async function getDirtyBeads(db, projectKey) {
|
|
17241
|
+
const result = await db.query(`SELECT db.bead_id FROM dirty_beads db
|
|
17242
|
+
JOIN beads b ON db.bead_id = b.id
|
|
17243
|
+
WHERE b.project_key = $1
|
|
17244
|
+
ORDER BY db.marked_at ASC`, [projectKey]);
|
|
17245
|
+
return result.rows.map((r) => r.bead_id);
|
|
17246
|
+
}
|
|
17247
|
+
async function clearDirtyBead(db, projectKey, beadId) {
|
|
17248
|
+
await db.query(`DELETE FROM dirty_beads WHERE bead_id = $1`, [beadId]);
|
|
17249
|
+
}
|
|
17250
|
+
async function clearAllDirtyBeads(db, projectKey) {
|
|
17251
|
+
await db.query(`DELETE FROM dirty_beads WHERE bead_id IN (
|
|
17252
|
+
SELECT id FROM beads WHERE project_key = $1
|
|
17253
|
+
)`, [projectKey]);
|
|
17254
|
+
}
|
|
17255
|
+
|
|
17256
|
+
// src/beads/store.ts
|
|
17257
|
+
function parseTimestamp2(timestamp) {
|
|
17258
|
+
const ts = parseInt(timestamp, 10);
|
|
17259
|
+
if (Number.isNaN(ts)) {
|
|
17260
|
+
throw new Error(`[BeadsStore] Invalid timestamp: ${timestamp}`);
|
|
17261
|
+
}
|
|
17262
|
+
if (ts > Number.MAX_SAFE_INTEGER) {
|
|
17263
|
+
console.warn(`[BeadsStore] Timestamp ${timestamp} exceeds MAX_SAFE_INTEGER (year 2286+)`);
|
|
17264
|
+
}
|
|
17265
|
+
return ts;
|
|
17266
|
+
}
|
|
17267
|
+
async function appendBeadEvent(event, projectPath, dbOverride) {
|
|
17268
|
+
const db = dbOverride ?? await getDatabase(projectPath);
|
|
17269
|
+
const { type, project_key, timestamp, ...rest } = event;
|
|
17270
|
+
const result = await db.query(`INSERT INTO events (type, project_key, timestamp, data)
|
|
17271
|
+
VALUES ($1, $2, $3, $4)
|
|
17272
|
+
RETURNING id, sequence`, [type, project_key, timestamp, JSON.stringify(rest)]);
|
|
17273
|
+
const row = result.rows[0];
|
|
17274
|
+
if (!row) {
|
|
17275
|
+
throw new Error("[BeadsStore] Failed to insert event - no row returned");
|
|
17276
|
+
}
|
|
17277
|
+
const { id, sequence } = row;
|
|
17278
|
+
await updateProjections(db, { ...event, id, sequence });
|
|
17279
|
+
return { ...event, id, sequence };
|
|
17280
|
+
}
|
|
17281
|
+
async function readBeadEvents(options = {}, projectPath, dbOverride) {
|
|
17282
|
+
return withTiming("readBeadEvents", async () => {
|
|
17283
|
+
const db = dbOverride ?? await getDatabase(projectPath);
|
|
17284
|
+
const conditions = [];
|
|
17285
|
+
const params = [];
|
|
17286
|
+
let paramIndex = 1;
|
|
17287
|
+
conditions.push(`type LIKE 'bead_%'`);
|
|
17288
|
+
if (options.projectKey) {
|
|
17289
|
+
conditions.push(`project_key = $${paramIndex++}`);
|
|
17290
|
+
params.push(options.projectKey);
|
|
17291
|
+
}
|
|
17292
|
+
if (options.beadId) {
|
|
17293
|
+
conditions.push(`data->>'bead_id' = $${paramIndex++}`);
|
|
17294
|
+
params.push(options.beadId);
|
|
17295
|
+
}
|
|
17296
|
+
if (options.types && options.types.length > 0) {
|
|
17297
|
+
conditions.push(`type = ANY($${paramIndex++})`);
|
|
17298
|
+
params.push(options.types);
|
|
17299
|
+
}
|
|
17300
|
+
if (options.since !== undefined) {
|
|
17301
|
+
conditions.push(`timestamp >= $${paramIndex++}`);
|
|
17302
|
+
params.push(options.since);
|
|
17303
|
+
}
|
|
17304
|
+
if (options.until !== undefined) {
|
|
17305
|
+
conditions.push(`timestamp <= $${paramIndex++}`);
|
|
17306
|
+
params.push(options.until);
|
|
17307
|
+
}
|
|
17308
|
+
if (options.afterSequence !== undefined) {
|
|
17309
|
+
conditions.push(`sequence > $${paramIndex++}`);
|
|
17310
|
+
params.push(options.afterSequence);
|
|
17311
|
+
}
|
|
17312
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
17313
|
+
let query = `
|
|
17314
|
+
SELECT id, type, project_key, timestamp, sequence, data
|
|
17315
|
+
FROM events
|
|
17316
|
+
${whereClause}
|
|
17317
|
+
ORDER BY sequence ASC
|
|
17318
|
+
`;
|
|
17319
|
+
if (options.limit) {
|
|
17320
|
+
query += ` LIMIT $${paramIndex++}`;
|
|
17321
|
+
params.push(options.limit);
|
|
17322
|
+
}
|
|
17323
|
+
if (options.offset) {
|
|
17324
|
+
query += ` OFFSET $${paramIndex++}`;
|
|
17325
|
+
params.push(options.offset);
|
|
17326
|
+
}
|
|
17327
|
+
const result = await db.query(query, params);
|
|
17328
|
+
return result.rows.map((row) => {
|
|
17329
|
+
const data = typeof row.data === "string" ? JSON.parse(row.data) : row.data;
|
|
17330
|
+
return {
|
|
17331
|
+
id: row.id,
|
|
17332
|
+
type: row.type,
|
|
17333
|
+
project_key: row.project_key,
|
|
17334
|
+
timestamp: parseTimestamp2(row.timestamp),
|
|
17335
|
+
sequence: row.sequence,
|
|
17336
|
+
...data
|
|
17337
|
+
};
|
|
17338
|
+
});
|
|
17339
|
+
});
|
|
17340
|
+
}
|
|
17341
|
+
async function replayBeadEvents(options = {}, projectPath, dbOverride) {
|
|
17342
|
+
return withTiming("replayBeadEvents", async () => {
|
|
17343
|
+
const startTime = Date.now();
|
|
17344
|
+
const db = dbOverride ?? await getDatabase(projectPath);
|
|
17345
|
+
if (options.clearViews) {
|
|
17346
|
+
if (options.projectKey) {
|
|
17347
|
+
await db.query(`DELETE FROM bead_comments WHERE bead_id IN (
|
|
17348
|
+
SELECT id FROM beads WHERE project_key = $1
|
|
17349
|
+
)`, [options.projectKey]);
|
|
17350
|
+
await db.query(`DELETE FROM bead_labels WHERE bead_id IN (
|
|
17351
|
+
SELECT id FROM beads WHERE project_key = $1
|
|
17352
|
+
)`, [options.projectKey]);
|
|
17353
|
+
await db.query(`DELETE FROM bead_dependencies WHERE bead_id IN (
|
|
17354
|
+
SELECT id FROM beads WHERE project_key = $1
|
|
17355
|
+
)`, [options.projectKey]);
|
|
17356
|
+
await db.query(`DELETE FROM blocked_beads_cache WHERE bead_id IN (
|
|
17357
|
+
SELECT id FROM beads WHERE project_key = $1
|
|
17358
|
+
)`, [options.projectKey]);
|
|
17359
|
+
await db.query(`DELETE FROM dirty_beads WHERE bead_id IN (
|
|
17360
|
+
SELECT id FROM beads WHERE project_key = $1
|
|
17361
|
+
)`, [options.projectKey]);
|
|
17362
|
+
await db.query(`DELETE FROM beads WHERE project_key = $1`, [
|
|
17363
|
+
options.projectKey
|
|
17364
|
+
]);
|
|
17365
|
+
} else {
|
|
17366
|
+
await db.exec(`
|
|
17367
|
+
DELETE FROM bead_comments;
|
|
17368
|
+
DELETE FROM bead_labels;
|
|
17369
|
+
DELETE FROM bead_dependencies;
|
|
17370
|
+
DELETE FROM blocked_beads_cache;
|
|
17371
|
+
DELETE FROM dirty_beads;
|
|
17372
|
+
DELETE FROM beads;
|
|
17373
|
+
`);
|
|
17374
|
+
}
|
|
17375
|
+
}
|
|
17376
|
+
const events2 = await readBeadEvents({
|
|
17377
|
+
projectKey: options.projectKey,
|
|
17378
|
+
afterSequence: options.fromSequence
|
|
17379
|
+
}, projectPath, dbOverride);
|
|
17380
|
+
for (const event of events2) {
|
|
17381
|
+
await updateProjections(db, event);
|
|
17382
|
+
}
|
|
17383
|
+
return {
|
|
17384
|
+
eventsReplayed: events2.length,
|
|
17385
|
+
duration: Date.now() - startTime
|
|
17386
|
+
};
|
|
17387
|
+
});
|
|
17388
|
+
}
|
|
17389
|
+
|
|
17390
|
+
// src/beads/adapter.ts
|
|
17391
|
+
function createBeadsAdapter(db, projectKey) {
|
|
17392
|
+
return {
|
|
17393
|
+
async createBead(projectKeyParam, options, projectPath) {
|
|
17394
|
+
const event = {
|
|
17395
|
+
type: "bead_created",
|
|
17396
|
+
project_key: projectKeyParam,
|
|
17397
|
+
bead_id: generateBeadId(projectKeyParam),
|
|
17398
|
+
timestamp: Date.now(),
|
|
17399
|
+
title: options.title,
|
|
17400
|
+
description: options.description || null,
|
|
17401
|
+
issue_type: options.type,
|
|
17402
|
+
priority: options.priority ?? 2,
|
|
17403
|
+
parent_id: options.parent_id || null,
|
|
17404
|
+
created_by: options.created_by || null,
|
|
17405
|
+
metadata: options.metadata || null
|
|
17406
|
+
};
|
|
17407
|
+
await appendBeadEvent(event, projectPath, db);
|
|
17408
|
+
if (options.assignee) {
|
|
17409
|
+
const assignEvent = {
|
|
17410
|
+
type: "bead_assigned",
|
|
17411
|
+
project_key: projectKeyParam,
|
|
17412
|
+
bead_id: event.bead_id,
|
|
17413
|
+
timestamp: Date.now(),
|
|
17414
|
+
assignee: options.assignee,
|
|
17415
|
+
assigned_by: options.created_by || null
|
|
17416
|
+
};
|
|
17417
|
+
await appendBeadEvent(assignEvent, projectPath, db);
|
|
17418
|
+
}
|
|
17419
|
+
const bead = await getBead(db, projectKeyParam, event.bead_id);
|
|
17420
|
+
if (!bead) {
|
|
17421
|
+
throw new Error(`[BeadsAdapter] Failed to create bead - not found after insert`);
|
|
17422
|
+
}
|
|
17423
|
+
return bead;
|
|
17424
|
+
},
|
|
17425
|
+
async getBead(projectKeyParam, beadId, projectPath) {
|
|
17426
|
+
return getBead(db, projectKeyParam, beadId);
|
|
17427
|
+
},
|
|
17428
|
+
async queryBeads(projectKeyParam, options, projectPath) {
|
|
17429
|
+
return queryBeads(db, projectKeyParam, options);
|
|
17430
|
+
},
|
|
17431
|
+
async updateBead(projectKeyParam, beadId, options, projectPath) {
|
|
17432
|
+
const existingBead = await getBead(db, projectKeyParam, beadId);
|
|
17433
|
+
if (!existingBead) {
|
|
17434
|
+
throw new Error(`[BeadsAdapter] Bead not found: ${beadId}`);
|
|
17435
|
+
}
|
|
17436
|
+
const changes = {};
|
|
17437
|
+
if (options.title && options.title !== existingBead.title) {
|
|
17438
|
+
changes.title = { old: existingBead.title, new: options.title };
|
|
17439
|
+
}
|
|
17440
|
+
if (options.description !== undefined && options.description !== existingBead.description) {
|
|
17441
|
+
changes.description = { old: existingBead.description, new: options.description };
|
|
17442
|
+
}
|
|
17443
|
+
if (options.priority !== undefined && options.priority !== existingBead.priority) {
|
|
17444
|
+
changes.priority = { old: existingBead.priority, new: options.priority };
|
|
17445
|
+
}
|
|
17446
|
+
if (options.assignee !== undefined && options.assignee !== existingBead.assignee) {
|
|
17447
|
+
changes.assignee = { old: existingBead.assignee, new: options.assignee };
|
|
17448
|
+
}
|
|
17449
|
+
if (Object.keys(changes).length === 0) {
|
|
17450
|
+
return existingBead;
|
|
17451
|
+
}
|
|
17452
|
+
const event = {
|
|
17453
|
+
type: "bead_updated",
|
|
17454
|
+
project_key: projectKeyParam,
|
|
17455
|
+
bead_id: beadId,
|
|
17456
|
+
timestamp: Date.now(),
|
|
17457
|
+
changes,
|
|
17458
|
+
updated_by: options.updated_by || null
|
|
17459
|
+
};
|
|
17460
|
+
await appendBeadEvent(event, projectPath, db);
|
|
17461
|
+
const updated = await getBead(db, projectKeyParam, beadId);
|
|
17462
|
+
if (!updated) {
|
|
17463
|
+
throw new Error(`[BeadsAdapter] Bead disappeared after update: ${beadId}`);
|
|
17464
|
+
}
|
|
17465
|
+
return updated;
|
|
17466
|
+
},
|
|
17467
|
+
async changeBeadStatus(projectKeyParam, beadId, toStatus, options, projectPath) {
|
|
17468
|
+
const existingBead = await getBead(db, projectKeyParam, beadId);
|
|
17469
|
+
if (!existingBead) {
|
|
17470
|
+
throw new Error(`[BeadsAdapter] Bead not found: ${beadId}`);
|
|
17471
|
+
}
|
|
17472
|
+
const event = {
|
|
17473
|
+
type: "bead_status_changed",
|
|
17474
|
+
project_key: projectKeyParam,
|
|
17475
|
+
bead_id: beadId,
|
|
17476
|
+
timestamp: Date.now(),
|
|
17477
|
+
from_status: existingBead.status,
|
|
17478
|
+
to_status: toStatus,
|
|
17479
|
+
reason: options?.reason || null,
|
|
17480
|
+
changed_by: options?.changed_by || null
|
|
17481
|
+
};
|
|
17482
|
+
await appendBeadEvent(event, projectPath, db);
|
|
17483
|
+
const updated = await getBead(db, projectKeyParam, beadId);
|
|
17484
|
+
if (!updated) {
|
|
17485
|
+
throw new Error(`[BeadsAdapter] Bead disappeared after status change: ${beadId}`);
|
|
17486
|
+
}
|
|
17487
|
+
return updated;
|
|
17488
|
+
},
|
|
17489
|
+
async closeBead(projectKeyParam, beadId, reason, options, projectPath) {
|
|
17490
|
+
const existingBead = await getBead(db, projectKeyParam, beadId);
|
|
17491
|
+
if (!existingBead) {
|
|
17492
|
+
throw new Error(`[BeadsAdapter] Bead not found: ${beadId}`);
|
|
17493
|
+
}
|
|
17494
|
+
const event = {
|
|
17495
|
+
type: "bead_closed",
|
|
17496
|
+
project_key: projectKeyParam,
|
|
17497
|
+
bead_id: beadId,
|
|
17498
|
+
timestamp: Date.now(),
|
|
17499
|
+
reason,
|
|
17500
|
+
closed_by: options?.closed_by || null,
|
|
17501
|
+
files_touched: options?.files_touched || null,
|
|
17502
|
+
duration_ms: options?.duration_ms || null
|
|
17503
|
+
};
|
|
17504
|
+
await appendBeadEvent(event, projectPath, db);
|
|
17505
|
+
const updated = await getBead(db, projectKeyParam, beadId);
|
|
17506
|
+
if (!updated) {
|
|
17507
|
+
throw new Error(`[BeadsAdapter] Bead disappeared after close: ${beadId}`);
|
|
17508
|
+
}
|
|
17509
|
+
return updated;
|
|
17510
|
+
},
|
|
17511
|
+
async reopenBead(projectKeyParam, beadId, options, projectPath) {
|
|
17512
|
+
const existingBead = await getBead(db, projectKeyParam, beadId);
|
|
17513
|
+
if (!existingBead) {
|
|
17514
|
+
throw new Error(`[BeadsAdapter] Bead not found: ${beadId}`);
|
|
17515
|
+
}
|
|
17516
|
+
const event = {
|
|
17517
|
+
type: "bead_reopened",
|
|
17518
|
+
project_key: projectKeyParam,
|
|
17519
|
+
bead_id: beadId,
|
|
17520
|
+
timestamp: Date.now(),
|
|
17521
|
+
reason: options?.reason || null,
|
|
17522
|
+
reopened_by: options?.reopened_by || null
|
|
17523
|
+
};
|
|
17524
|
+
await appendBeadEvent(event, projectPath, db);
|
|
17525
|
+
const updated = await getBead(db, projectKeyParam, beadId);
|
|
17526
|
+
if (!updated) {
|
|
17527
|
+
throw new Error(`[BeadsAdapter] Bead disappeared after reopen: ${beadId}`);
|
|
17528
|
+
}
|
|
17529
|
+
return updated;
|
|
17530
|
+
},
|
|
17531
|
+
async deleteBead(projectKeyParam, beadId, options, projectPath) {
|
|
17532
|
+
const existingBead = await getBead(db, projectKeyParam, beadId);
|
|
17533
|
+
if (!existingBead) {
|
|
17534
|
+
throw new Error(`[BeadsAdapter] Bead not found: ${beadId}`);
|
|
17535
|
+
}
|
|
17536
|
+
const event = {
|
|
17537
|
+
type: "bead_deleted",
|
|
17538
|
+
project_key: projectKeyParam,
|
|
17539
|
+
bead_id: beadId,
|
|
17540
|
+
timestamp: Date.now(),
|
|
17541
|
+
reason: options?.reason || null,
|
|
17542
|
+
deleted_by: options?.deleted_by || null
|
|
17543
|
+
};
|
|
17544
|
+
await appendBeadEvent(event, projectPath, db);
|
|
17545
|
+
},
|
|
17546
|
+
async addDependency(projectKeyParam, beadId, dependsOnId, relationship, options, projectPath) {
|
|
17547
|
+
const sourceBead = await getBead(db, projectKeyParam, beadId);
|
|
17548
|
+
if (!sourceBead) {
|
|
17549
|
+
throw new Error(`[BeadsAdapter] Bead not found: ${beadId}`);
|
|
17550
|
+
}
|
|
17551
|
+
const targetBead = await getBead(db, projectKeyParam, dependsOnId);
|
|
17552
|
+
if (!targetBead) {
|
|
17553
|
+
throw new Error(`[BeadsAdapter] Target bead not found: ${dependsOnId}`);
|
|
17554
|
+
}
|
|
17555
|
+
if (beadId === dependsOnId) {
|
|
17556
|
+
throw new Error(`[BeadsAdapter] Bead cannot depend on itself`);
|
|
17557
|
+
}
|
|
17558
|
+
const { wouldCreateCycle: wouldCreateCycle2 } = await Promise.resolve().then(() => exports_dependencies);
|
|
17559
|
+
const hasCycle = await wouldCreateCycle2(db, beadId, dependsOnId);
|
|
17560
|
+
if (hasCycle) {
|
|
17561
|
+
throw new Error(`[BeadsAdapter] Adding dependency would create a cycle`);
|
|
17562
|
+
}
|
|
17563
|
+
const event = {
|
|
17564
|
+
type: "bead_dependency_added",
|
|
17565
|
+
project_key: projectKeyParam,
|
|
17566
|
+
bead_id: beadId,
|
|
17567
|
+
timestamp: Date.now(),
|
|
17568
|
+
dependency: {
|
|
17569
|
+
target: dependsOnId,
|
|
17570
|
+
type: relationship
|
|
17571
|
+
},
|
|
17572
|
+
reason: options?.reason || null,
|
|
17573
|
+
added_by: options?.added_by || null
|
|
17574
|
+
};
|
|
17575
|
+
await appendBeadEvent(event, projectPath, db);
|
|
17576
|
+
const deps = await getDependencies(db, projectKeyParam, beadId);
|
|
17577
|
+
const dep = deps.find((d) => d.depends_on_id === dependsOnId && d.relationship === relationship);
|
|
17578
|
+
if (!dep) {
|
|
17579
|
+
throw new Error(`[BeadsAdapter] Dependency not found after insert`);
|
|
17580
|
+
}
|
|
17581
|
+
return dep;
|
|
17582
|
+
},
|
|
17583
|
+
async removeDependency(projectKeyParam, beadId, dependsOnId, relationship, options, projectPath) {
|
|
17584
|
+
const event = {
|
|
17585
|
+
type: "bead_dependency_removed",
|
|
17586
|
+
project_key: projectKeyParam,
|
|
17587
|
+
bead_id: beadId,
|
|
17588
|
+
timestamp: Date.now(),
|
|
17589
|
+
dependency: {
|
|
17590
|
+
target: dependsOnId,
|
|
17591
|
+
type: relationship
|
|
17592
|
+
},
|
|
17593
|
+
reason: options?.reason || null,
|
|
17594
|
+
removed_by: options?.removed_by || null
|
|
17595
|
+
};
|
|
17596
|
+
await appendBeadEvent(event, projectPath, db);
|
|
17597
|
+
},
|
|
17598
|
+
async getDependencies(projectKeyParam, beadId, projectPath) {
|
|
17599
|
+
return getDependencies(db, projectKeyParam, beadId);
|
|
17600
|
+
},
|
|
17601
|
+
async getDependents(projectKeyParam, beadId, projectPath) {
|
|
17602
|
+
return getDependents(db, projectKeyParam, beadId);
|
|
17603
|
+
},
|
|
17604
|
+
async isBlocked(projectKeyParam, beadId, projectPath) {
|
|
17605
|
+
return isBlocked(db, projectKeyParam, beadId);
|
|
17606
|
+
},
|
|
17607
|
+
async getBlockers(projectKeyParam, beadId, projectPath) {
|
|
17608
|
+
return getBlockers(db, projectKeyParam, beadId);
|
|
17609
|
+
},
|
|
17610
|
+
async addLabel(projectKeyParam, beadId, label, options, projectPath) {
|
|
17611
|
+
const event = {
|
|
17612
|
+
type: "bead_label_added",
|
|
17613
|
+
project_key: projectKeyParam,
|
|
17614
|
+
bead_id: beadId,
|
|
17615
|
+
timestamp: Date.now(),
|
|
17616
|
+
label,
|
|
17617
|
+
added_by: options?.added_by || null
|
|
17618
|
+
};
|
|
17619
|
+
await appendBeadEvent(event, projectPath, db);
|
|
17620
|
+
return {
|
|
17621
|
+
bead_id: beadId,
|
|
17622
|
+
label,
|
|
17623
|
+
created_at: event.timestamp
|
|
17624
|
+
};
|
|
17625
|
+
},
|
|
17626
|
+
async removeLabel(projectKeyParam, beadId, label, options, projectPath) {
|
|
17627
|
+
const event = {
|
|
17628
|
+
type: "bead_label_removed",
|
|
17629
|
+
project_key: projectKeyParam,
|
|
17630
|
+
bead_id: beadId,
|
|
17631
|
+
timestamp: Date.now(),
|
|
17632
|
+
label,
|
|
17633
|
+
removed_by: options?.removed_by || null
|
|
17634
|
+
};
|
|
17635
|
+
await appendBeadEvent(event, projectPath, db);
|
|
17636
|
+
},
|
|
17637
|
+
async getLabels(projectKeyParam, beadId, projectPath) {
|
|
17638
|
+
return getLabels(db, projectKeyParam, beadId);
|
|
17639
|
+
},
|
|
17640
|
+
async getBeadsWithLabel(projectKeyParam, label, projectPath) {
|
|
17641
|
+
return queryBeads(db, projectKeyParam, { labels: [label] });
|
|
17642
|
+
},
|
|
17643
|
+
async addComment(projectKeyParam, beadId, author, body, options, projectPath) {
|
|
17644
|
+
const event = {
|
|
17645
|
+
type: "bead_comment_added",
|
|
17646
|
+
project_key: projectKeyParam,
|
|
17647
|
+
bead_id: beadId,
|
|
17648
|
+
timestamp: Date.now(),
|
|
17649
|
+
author,
|
|
17650
|
+
body,
|
|
17651
|
+
parent_comment_id: options?.parent_id || null,
|
|
17652
|
+
metadata: options?.metadata || null
|
|
17653
|
+
};
|
|
17654
|
+
await appendBeadEvent(event, projectPath, db);
|
|
17655
|
+
const comments = await getComments(db, projectKeyParam, beadId);
|
|
17656
|
+
const comment = comments[comments.length - 1];
|
|
17657
|
+
if (!comment) {
|
|
17658
|
+
throw new Error(`[BeadsAdapter] Comment not found after insert`);
|
|
17659
|
+
}
|
|
17660
|
+
return comment;
|
|
17661
|
+
},
|
|
17662
|
+
async updateComment(projectKeyParam, commentId, newBody, updated_by, projectPath) {
|
|
17663
|
+
const event = {
|
|
17664
|
+
type: "bead_comment_updated",
|
|
17665
|
+
project_key: projectKeyParam,
|
|
17666
|
+
bead_id: "",
|
|
17667
|
+
timestamp: Date.now(),
|
|
17668
|
+
comment_id: commentId,
|
|
17669
|
+
new_body: newBody,
|
|
17670
|
+
updated_by
|
|
17671
|
+
};
|
|
17672
|
+
await appendBeadEvent(event, projectPath, db);
|
|
17673
|
+
return {
|
|
17674
|
+
id: commentId,
|
|
17675
|
+
bead_id: "",
|
|
17676
|
+
author: updated_by,
|
|
17677
|
+
body: newBody,
|
|
17678
|
+
parent_id: null,
|
|
17679
|
+
created_at: Date.now(),
|
|
17680
|
+
updated_at: event.timestamp
|
|
17681
|
+
};
|
|
17682
|
+
},
|
|
17683
|
+
async deleteComment(projectKeyParam, commentId, deleted_by, options, projectPath) {
|
|
17684
|
+
const event = {
|
|
17685
|
+
type: "bead_comment_deleted",
|
|
17686
|
+
project_key: projectKeyParam,
|
|
17687
|
+
bead_id: "",
|
|
17688
|
+
timestamp: Date.now(),
|
|
17689
|
+
comment_id: commentId,
|
|
17690
|
+
deleted_by,
|
|
17691
|
+
reason: options?.reason || null
|
|
17692
|
+
};
|
|
17693
|
+
await appendBeadEvent(event, projectPath, db);
|
|
17694
|
+
},
|
|
17695
|
+
async getComments(projectKeyParam, beadId, projectPath) {
|
|
17696
|
+
return getComments(db, projectKeyParam, beadId);
|
|
17697
|
+
},
|
|
17698
|
+
async addChildToEpic(projectKeyParam, epicId, childId, options, projectPath) {
|
|
17699
|
+
const event = {
|
|
17700
|
+
type: "bead_epic_child_added",
|
|
17701
|
+
project_key: projectKeyParam,
|
|
17702
|
+
bead_id: epicId,
|
|
17703
|
+
timestamp: Date.now(),
|
|
17704
|
+
child_id: childId,
|
|
17705
|
+
child_index: options?.child_index || null,
|
|
17706
|
+
added_by: options?.added_by || null
|
|
17707
|
+
};
|
|
17708
|
+
await appendBeadEvent(event, projectPath, db);
|
|
17709
|
+
},
|
|
17710
|
+
async removeChildFromEpic(projectKeyParam, epicId, childId, options, projectPath) {
|
|
17711
|
+
const event = {
|
|
17712
|
+
type: "bead_epic_child_removed",
|
|
17713
|
+
project_key: projectKeyParam,
|
|
17714
|
+
bead_id: epicId,
|
|
17715
|
+
timestamp: Date.now(),
|
|
17716
|
+
child_id: childId,
|
|
17717
|
+
reason: options?.reason || null,
|
|
17718
|
+
removed_by: options?.removed_by || null
|
|
17719
|
+
};
|
|
17720
|
+
await appendBeadEvent(event, projectPath, db);
|
|
17721
|
+
},
|
|
17722
|
+
async getEpicChildren(projectKeyParam, epicId, projectPath) {
|
|
17723
|
+
return queryBeads(db, projectKeyParam, { parent_id: epicId });
|
|
17724
|
+
},
|
|
17725
|
+
async isEpicClosureEligible(projectKeyParam, epicId, projectPath) {
|
|
17726
|
+
const children = await queryBeads(db, projectKeyParam, { parent_id: epicId });
|
|
17727
|
+
return children.every((child) => child.status === "closed");
|
|
17728
|
+
},
|
|
17729
|
+
async getNextReadyBead(projectKeyParam, projectPath) {
|
|
17730
|
+
return getNextReadyBead(db, projectKeyParam);
|
|
17731
|
+
},
|
|
17732
|
+
async getInProgressBeads(projectKeyParam, projectPath) {
|
|
17733
|
+
return getInProgressBeads(db, projectKeyParam);
|
|
17734
|
+
},
|
|
17735
|
+
async getBlockedBeads(projectKeyParam, projectPath) {
|
|
17736
|
+
return getBlockedBeads(db, projectKeyParam);
|
|
17737
|
+
},
|
|
17738
|
+
async markDirty(projectKeyParam, beadId, projectPath) {
|
|
17739
|
+
await markBeadDirty(db, projectKeyParam, beadId);
|
|
17740
|
+
},
|
|
17741
|
+
async getDirtyBeads(projectKeyParam, projectPath) {
|
|
17742
|
+
return getDirtyBeads(db, projectKeyParam);
|
|
17743
|
+
},
|
|
17744
|
+
async clearDirty(projectKeyParam, beadId, projectPath) {
|
|
17745
|
+
await clearDirtyBead(db, projectKeyParam, beadId);
|
|
17746
|
+
},
|
|
17747
|
+
async runMigrations(projectPath) {
|
|
17748
|
+
const { beadsMigration: beadsMigration2 } = await Promise.resolve().then(() => (init_migrations2(), exports_migrations2));
|
|
17749
|
+
await db.exec(`
|
|
17750
|
+
CREATE TABLE IF NOT EXISTS schema_version (
|
|
17751
|
+
version INTEGER PRIMARY KEY,
|
|
17752
|
+
applied_at BIGINT NOT NULL,
|
|
17753
|
+
description TEXT
|
|
17754
|
+
);
|
|
17755
|
+
`);
|
|
17756
|
+
await db.exec("BEGIN");
|
|
17757
|
+
try {
|
|
17758
|
+
await db.exec(beadsMigration2.up);
|
|
17759
|
+
await db.query(`INSERT INTO schema_version (version, applied_at, description) VALUES ($1, $2, $3)
|
|
17760
|
+
ON CONFLICT (version) DO NOTHING`, [beadsMigration2.version, Date.now(), beadsMigration2.description]);
|
|
17761
|
+
await db.exec("COMMIT");
|
|
17762
|
+
} catch (error45) {
|
|
17763
|
+
await db.exec("ROLLBACK");
|
|
17764
|
+
throw error45;
|
|
17765
|
+
}
|
|
17766
|
+
},
|
|
17767
|
+
async getBeadsStats(projectPath) {
|
|
17768
|
+
const [totalResult, openResult, inProgressResult, blockedResult, closedResult] = await Promise.all([
|
|
17769
|
+
db.query("SELECT COUNT(*) as count FROM beads WHERE project_key = $1", [projectKey]),
|
|
17770
|
+
db.query("SELECT COUNT(*) as count FROM beads WHERE project_key = $1 AND status = 'open'", [projectKey]),
|
|
17771
|
+
db.query("SELECT COUNT(*) as count FROM beads WHERE project_key = $1 AND status = 'in_progress'", [projectKey]),
|
|
17772
|
+
db.query("SELECT COUNT(*) as count FROM beads WHERE project_key = $1 AND status = 'blocked'", [projectKey]),
|
|
17773
|
+
db.query("SELECT COUNT(*) as count FROM beads WHERE project_key = $1 AND status = 'closed'", [projectKey])
|
|
17774
|
+
]);
|
|
17775
|
+
const byTypeResult = await db.query("SELECT type, COUNT(*) as count FROM beads WHERE project_key = $1 GROUP BY type", [projectKey]);
|
|
17776
|
+
const by_type = {};
|
|
17777
|
+
for (const row of byTypeResult.rows) {
|
|
17778
|
+
by_type[row.type] = parseInt(row.count);
|
|
17779
|
+
}
|
|
17780
|
+
return {
|
|
17781
|
+
total_beads: parseInt(totalResult.rows[0]?.count || "0"),
|
|
17782
|
+
open: parseInt(openResult.rows[0]?.count || "0"),
|
|
17783
|
+
in_progress: parseInt(inProgressResult.rows[0]?.count || "0"),
|
|
17784
|
+
blocked: parseInt(blockedResult.rows[0]?.count || "0"),
|
|
17785
|
+
closed: parseInt(closedResult.rows[0]?.count || "0"),
|
|
17786
|
+
by_type
|
|
17787
|
+
};
|
|
17788
|
+
},
|
|
17789
|
+
async rebuildBlockedCache(projectKeyParam, projectPath) {
|
|
17790
|
+
const { rebuildAllBlockedCaches: rebuildAllBlockedCaches2 } = await Promise.resolve().then(() => exports_dependencies);
|
|
17791
|
+
await rebuildAllBlockedCaches2(db, projectKeyParam);
|
|
17792
|
+
},
|
|
17793
|
+
async getDatabase(projectPath) {
|
|
17794
|
+
return db;
|
|
17795
|
+
},
|
|
17796
|
+
async close(projectPath) {
|
|
17797
|
+
if (db.close) {
|
|
17798
|
+
await db.close();
|
|
17799
|
+
}
|
|
17800
|
+
},
|
|
17801
|
+
async closeAll() {
|
|
17802
|
+
if (db.close) {
|
|
17803
|
+
await db.close();
|
|
17804
|
+
}
|
|
17805
|
+
}
|
|
17806
|
+
};
|
|
17807
|
+
}
|
|
17808
|
+
function generateBeadId(projectKey) {
|
|
17809
|
+
const hash2 = projectKey.split("").reduce((acc, char) => (acc << 5) - acc + char.charCodeAt(0) | 0, 0).toString(36).slice(0, 6);
|
|
17810
|
+
const timestamp = Date.now().toString(36);
|
|
17811
|
+
const random = Math.random().toString(36).slice(2, 5);
|
|
17812
|
+
return `bd-${hash2}-${timestamp}${random}`;
|
|
17813
|
+
}
|
|
17814
|
+
|
|
17815
|
+
// src/beads/index.ts
|
|
17816
|
+
init_migrations2();
|
|
17817
|
+
|
|
17818
|
+
// src/beads/labels.ts
|
|
17819
|
+
async function getBeadsByLabel(db, projectKey, label) {
|
|
17820
|
+
const result = await db.query(`SELECT b.* FROM beads b
|
|
17821
|
+
JOIN bead_labels bl ON b.id = bl.bead_id
|
|
17822
|
+
WHERE b.project_key = $1 AND bl.label = $2 AND b.deleted_at IS NULL
|
|
17823
|
+
ORDER BY b.priority DESC, b.created_at ASC`, [projectKey, label]);
|
|
17824
|
+
return result.rows;
|
|
17825
|
+
}
|
|
17826
|
+
async function getAllLabels(db, projectKey) {
|
|
17827
|
+
const result = await db.query(`SELECT DISTINCT bl.label FROM bead_labels bl
|
|
17828
|
+
JOIN beads b ON bl.bead_id = b.id
|
|
17829
|
+
WHERE b.project_key = $1 AND b.deleted_at IS NULL
|
|
17830
|
+
ORDER BY bl.label`, [projectKey]);
|
|
17831
|
+
return result.rows.map((r) => r.label);
|
|
17832
|
+
}
|
|
17833
|
+
// src/beads/comments.ts
|
|
17834
|
+
async function getCommentById(db, commentId) {
|
|
17835
|
+
const result = await db.query(`SELECT * FROM bead_comments WHERE id = $1`, [commentId]);
|
|
17836
|
+
return result.rows[0] ?? null;
|
|
17837
|
+
}
|
|
17838
|
+
async function getCommentThread(db, rootCommentId) {
|
|
17839
|
+
const result = await db.query(`WITH RECURSIVE thread AS (
|
|
17840
|
+
-- Root comment
|
|
17841
|
+
SELECT * FROM bead_comments WHERE id = $1
|
|
17842
|
+
|
|
17843
|
+
UNION
|
|
17844
|
+
|
|
17845
|
+
-- Replies
|
|
17846
|
+
SELECT c.* FROM bead_comments c
|
|
17847
|
+
JOIN thread t ON c.parent_id = t.id
|
|
17848
|
+
)
|
|
17849
|
+
SELECT * FROM thread ORDER BY created_at ASC`, [rootCommentId]);
|
|
17850
|
+
return result.rows;
|
|
17851
|
+
}
|
|
17852
|
+
// src/beads/jsonl.ts
|
|
17853
|
+
import { createHash } from "node:crypto";
|
|
17854
|
+
function serializeToJSONL(bead) {
|
|
17855
|
+
return JSON.stringify(bead);
|
|
17856
|
+
}
|
|
17857
|
+
function parseJSONL(jsonl) {
|
|
17858
|
+
if (!jsonl || jsonl.trim() === "") {
|
|
17859
|
+
return [];
|
|
17860
|
+
}
|
|
17861
|
+
const lines = jsonl.split(`
|
|
17862
|
+
`);
|
|
17863
|
+
const beads = [];
|
|
17864
|
+
for (const line of lines) {
|
|
17865
|
+
const trimmed = line.trim();
|
|
17866
|
+
if (trimmed === "") {
|
|
17867
|
+
continue;
|
|
17868
|
+
}
|
|
17869
|
+
try {
|
|
17870
|
+
const bead = JSON.parse(trimmed);
|
|
17871
|
+
beads.push(bead);
|
|
17872
|
+
} catch (err) {
|
|
17873
|
+
throw new Error(`Invalid JSON in JSONL: ${err instanceof Error ? err.message : String(err)}`);
|
|
17874
|
+
}
|
|
17875
|
+
}
|
|
17876
|
+
return beads;
|
|
17877
|
+
}
|
|
17878
|
+
function computeContentHash(bead) {
|
|
17879
|
+
const canonical = JSON.stringify(bead, Object.keys(bead).sort());
|
|
17880
|
+
return createHash("sha256").update(canonical).digest("hex");
|
|
17881
|
+
}
|
|
17882
|
+
async function exportToJSONL(adapter, projectKey, options = {}) {
|
|
17883
|
+
const db = await adapter.getDatabase();
|
|
17884
|
+
const conditions = ["project_key = $1"];
|
|
17885
|
+
const params = [projectKey];
|
|
17886
|
+
let paramIndex = 2;
|
|
17887
|
+
if (!options.includeDeleted) {
|
|
17888
|
+
conditions.push("deleted_at IS NULL");
|
|
17889
|
+
}
|
|
17890
|
+
if (options.beadIds && options.beadIds.length > 0) {
|
|
17891
|
+
conditions.push(`id = ANY($${paramIndex++})`);
|
|
17892
|
+
params.push(options.beadIds);
|
|
17893
|
+
}
|
|
17894
|
+
const query = `
|
|
17895
|
+
SELECT * FROM beads
|
|
17896
|
+
WHERE ${conditions.join(" AND ")}
|
|
17897
|
+
ORDER BY id ASC
|
|
17898
|
+
`;
|
|
17899
|
+
const result = await db.query(query, params);
|
|
17900
|
+
const beads = result.rows;
|
|
17901
|
+
if (beads.length === 0) {
|
|
17902
|
+
return "";
|
|
17903
|
+
}
|
|
17904
|
+
const lines = [];
|
|
17905
|
+
for (const bead of beads) {
|
|
17906
|
+
const deps = await getDependencies(db, projectKey, bead.id);
|
|
17907
|
+
const dependencies = deps.map((d) => ({
|
|
17908
|
+
depends_on_id: d.depends_on_id,
|
|
17909
|
+
type: d.relationship
|
|
17910
|
+
}));
|
|
17911
|
+
const labels = await getLabels(db, projectKey, bead.id);
|
|
17912
|
+
const comments = await getComments(db, projectKey, bead.id);
|
|
17913
|
+
const commentExports = comments.map((c) => ({
|
|
17914
|
+
author: c.author,
|
|
17915
|
+
text: c.body
|
|
17916
|
+
}));
|
|
17917
|
+
const beadExport = {
|
|
17918
|
+
id: bead.id,
|
|
17919
|
+
title: bead.title,
|
|
17920
|
+
description: bead.description || undefined,
|
|
17921
|
+
status: bead.deleted_at ? "tombstone" : bead.status,
|
|
17922
|
+
priority: bead.priority,
|
|
17923
|
+
issue_type: bead.type,
|
|
17924
|
+
created_at: new Date(bead.created_at).toISOString(),
|
|
17925
|
+
updated_at: new Date(bead.updated_at).toISOString(),
|
|
17926
|
+
closed_at: bead.closed_at ? new Date(bead.closed_at).toISOString() : undefined,
|
|
17927
|
+
assignee: bead.assignee || undefined,
|
|
17928
|
+
parent_id: bead.parent_id || undefined,
|
|
17929
|
+
dependencies,
|
|
17930
|
+
labels,
|
|
17931
|
+
comments: commentExports
|
|
17932
|
+
};
|
|
17933
|
+
lines.push(serializeToJSONL(beadExport));
|
|
17934
|
+
}
|
|
17935
|
+
return lines.join(`
|
|
17936
|
+
`);
|
|
17937
|
+
}
|
|
17938
|
+
async function exportDirtyBeads(adapter, projectKey) {
|
|
17939
|
+
const db = await adapter.getDatabase();
|
|
17940
|
+
const dirtyIds = await getDirtyBeads(db, projectKey);
|
|
17941
|
+
if (dirtyIds.length === 0) {
|
|
17942
|
+
return { jsonl: "", beadIds: [] };
|
|
17943
|
+
}
|
|
17944
|
+
const jsonl = await exportToJSONL(adapter, projectKey, {
|
|
17945
|
+
beadIds: dirtyIds
|
|
17946
|
+
});
|
|
17947
|
+
return { jsonl, beadIds: dirtyIds };
|
|
17948
|
+
}
|
|
17949
|
+
async function importFromJSONL(adapter, projectKey, jsonl, options = {}) {
|
|
17950
|
+
const beads = parseJSONL(jsonl);
|
|
17951
|
+
const result = {
|
|
17952
|
+
created: 0,
|
|
17953
|
+
updated: 0,
|
|
17954
|
+
skipped: 0,
|
|
17955
|
+
errors: []
|
|
17956
|
+
};
|
|
17957
|
+
for (const beadExport of beads) {
|
|
17958
|
+
try {
|
|
17959
|
+
await importSingleBead(adapter, projectKey, beadExport, options, result);
|
|
17960
|
+
} catch (err) {
|
|
17961
|
+
result.errors.push({
|
|
17962
|
+
beadId: beadExport.id,
|
|
17963
|
+
error: err instanceof Error ? err.message : String(err)
|
|
17964
|
+
});
|
|
17965
|
+
}
|
|
17966
|
+
}
|
|
17967
|
+
return result;
|
|
17968
|
+
}
|
|
17969
|
+
async function importSingleBead(adapter, projectKey, beadExport, options, result) {
|
|
17970
|
+
const existing = await adapter.getBead(projectKey, beadExport.id);
|
|
17971
|
+
if (existing && options.skipExisting) {
|
|
17972
|
+
result.skipped++;
|
|
17973
|
+
return;
|
|
17974
|
+
}
|
|
17975
|
+
if (existing) {
|
|
17976
|
+
const existingHash = await computeBeadHash(adapter, projectKey, existing.id);
|
|
17977
|
+
const importHash = computeContentHash(beadExport);
|
|
17978
|
+
if (existingHash === importHash) {
|
|
17979
|
+
result.skipped++;
|
|
17980
|
+
return;
|
|
17981
|
+
}
|
|
17982
|
+
}
|
|
17983
|
+
if (options.dryRun) {
|
|
17984
|
+
if (existing) {
|
|
17985
|
+
result.updated++;
|
|
17986
|
+
} else {
|
|
17987
|
+
result.created++;
|
|
17988
|
+
}
|
|
17989
|
+
return;
|
|
17990
|
+
}
|
|
17991
|
+
if (!existing) {
|
|
17992
|
+
const db = await adapter.getDatabase();
|
|
17993
|
+
await db.query(`INSERT INTO beads (
|
|
17994
|
+
id, project_key, type, status, title, description, priority,
|
|
17995
|
+
parent_id, assignee, created_at, updated_at
|
|
17996
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`, [
|
|
17997
|
+
beadExport.id,
|
|
17998
|
+
projectKey,
|
|
17999
|
+
beadExport.issue_type,
|
|
18000
|
+
beadExport.status === "tombstone" ? "closed" : beadExport.status,
|
|
18001
|
+
beadExport.title,
|
|
18002
|
+
beadExport.description || null,
|
|
18003
|
+
beadExport.priority,
|
|
18004
|
+
beadExport.parent_id || null,
|
|
18005
|
+
beadExport.assignee || null,
|
|
18006
|
+
new Date(beadExport.created_at).getTime(),
|
|
18007
|
+
new Date(beadExport.updated_at).getTime()
|
|
18008
|
+
]);
|
|
18009
|
+
if (beadExport.status === "closed" && beadExport.closed_at) {
|
|
18010
|
+
await db.query("UPDATE beads SET closed_at = $1 WHERE id = $2", [new Date(beadExport.closed_at).getTime(), beadExport.id]);
|
|
18011
|
+
}
|
|
18012
|
+
if (beadExport.status === "tombstone") {
|
|
18013
|
+
await db.query("UPDATE beads SET deleted_at = $1 WHERE id = $2", [Date.now(), beadExport.id]);
|
|
18014
|
+
}
|
|
18015
|
+
result.created++;
|
|
18016
|
+
} else {
|
|
18017
|
+
await adapter.updateBead(projectKey, beadExport.id, {
|
|
18018
|
+
title: beadExport.title,
|
|
18019
|
+
description: beadExport.description,
|
|
18020
|
+
priority: beadExport.priority,
|
|
18021
|
+
assignee: beadExport.assignee
|
|
18022
|
+
});
|
|
18023
|
+
if (existing.status !== beadExport.status) {
|
|
18024
|
+
if (beadExport.status === "closed") {
|
|
18025
|
+
await adapter.closeBead(projectKey, beadExport.id, "imported");
|
|
18026
|
+
} else if (beadExport.status === "in_progress") {
|
|
18027
|
+
const db = await adapter.getDatabase();
|
|
18028
|
+
await db.query("UPDATE beads SET status = $1, updated_at = $2 WHERE id = $3", ["in_progress", Date.now(), beadExport.id]);
|
|
18029
|
+
}
|
|
18030
|
+
}
|
|
18031
|
+
result.updated++;
|
|
18032
|
+
}
|
|
18033
|
+
await importDependencies(adapter, projectKey, beadExport);
|
|
18034
|
+
await importLabels(adapter, projectKey, beadExport);
|
|
18035
|
+
await importComments(adapter, projectKey, beadExport);
|
|
18036
|
+
}
|
|
18037
|
+
async function computeBeadHash(adapter, projectKey, beadId) {
|
|
18038
|
+
const db = await adapter.getDatabase();
|
|
18039
|
+
const beadResult = await db.query("SELECT * FROM beads WHERE project_key = $1 AND id = $2", [projectKey, beadId]);
|
|
18040
|
+
const bead = beadResult.rows[0];
|
|
18041
|
+
if (!bead) {
|
|
18042
|
+
throw new Error(`Bead not found: ${beadId}`);
|
|
18043
|
+
}
|
|
18044
|
+
const deps = await getDependencies(db, projectKey, beadId);
|
|
18045
|
+
const dependencies = deps.map((d) => ({
|
|
18046
|
+
depends_on_id: d.depends_on_id,
|
|
18047
|
+
type: d.relationship
|
|
18048
|
+
}));
|
|
18049
|
+
const labels = await getLabels(db, projectKey, beadId);
|
|
18050
|
+
const comments = await getComments(db, projectKey, beadId);
|
|
18051
|
+
const commentExports = comments.map((c) => ({
|
|
18052
|
+
author: c.author,
|
|
18053
|
+
text: c.body
|
|
18054
|
+
}));
|
|
18055
|
+
const beadExport = {
|
|
18056
|
+
id: bead.id,
|
|
18057
|
+
title: bead.title,
|
|
18058
|
+
description: bead.description || undefined,
|
|
18059
|
+
status: bead.deleted_at ? "tombstone" : bead.status,
|
|
18060
|
+
priority: bead.priority,
|
|
18061
|
+
issue_type: bead.type,
|
|
18062
|
+
created_at: new Date(bead.created_at).toISOString(),
|
|
18063
|
+
updated_at: new Date(bead.updated_at).toISOString(),
|
|
18064
|
+
closed_at: bead.closed_at ? new Date(bead.closed_at).toISOString() : undefined,
|
|
18065
|
+
assignee: bead.assignee || undefined,
|
|
18066
|
+
parent_id: bead.parent_id || undefined,
|
|
18067
|
+
dependencies,
|
|
18068
|
+
labels,
|
|
18069
|
+
comments: commentExports
|
|
18070
|
+
};
|
|
18071
|
+
return computeContentHash(beadExport);
|
|
18072
|
+
}
|
|
18073
|
+
async function importDependencies(adapter, projectKey, beadExport) {
|
|
18074
|
+
const db = await adapter.getDatabase();
|
|
18075
|
+
await db.query("DELETE FROM bead_dependencies WHERE bead_id = $1", [
|
|
18076
|
+
beadExport.id
|
|
18077
|
+
]);
|
|
18078
|
+
for (const dep of beadExport.dependencies) {
|
|
18079
|
+
await adapter.addDependency(projectKey, beadExport.id, dep.depends_on_id, dep.type);
|
|
18080
|
+
}
|
|
18081
|
+
}
|
|
18082
|
+
async function importLabels(adapter, projectKey, beadExport) {
|
|
18083
|
+
const db = await adapter.getDatabase();
|
|
18084
|
+
await db.query("DELETE FROM bead_labels WHERE bead_id = $1", [
|
|
18085
|
+
beadExport.id
|
|
18086
|
+
]);
|
|
18087
|
+
for (const label of beadExport.labels) {
|
|
18088
|
+
await adapter.addLabel(projectKey, beadExport.id, label);
|
|
18089
|
+
}
|
|
18090
|
+
}
|
|
18091
|
+
async function importComments(adapter, projectKey, beadExport) {
|
|
18092
|
+
const db = await adapter.getDatabase();
|
|
18093
|
+
await db.query("DELETE FROM bead_comments WHERE bead_id = $1", [
|
|
18094
|
+
beadExport.id
|
|
18095
|
+
]);
|
|
18096
|
+
for (const comment of beadExport.comments) {
|
|
18097
|
+
await adapter.addComment(projectKey, beadExport.id, comment.author, comment.text);
|
|
18098
|
+
}
|
|
18099
|
+
}
|
|
18100
|
+
// src/beads/flush-manager.ts
|
|
18101
|
+
import { writeFile } from "node:fs/promises";
|
|
18102
|
+
class FlushManager {
|
|
18103
|
+
adapter;
|
|
18104
|
+
projectKey;
|
|
18105
|
+
outputPath;
|
|
18106
|
+
debounceMs;
|
|
18107
|
+
onFlush;
|
|
18108
|
+
timer = null;
|
|
18109
|
+
flushing = false;
|
|
18110
|
+
constructor(options) {
|
|
18111
|
+
this.adapter = options.adapter;
|
|
18112
|
+
this.projectKey = options.projectKey;
|
|
18113
|
+
this.outputPath = options.outputPath;
|
|
18114
|
+
this.debounceMs = options.debounceMs ?? 30000;
|
|
18115
|
+
this.onFlush = options.onFlush;
|
|
18116
|
+
}
|
|
18117
|
+
scheduleFlush() {
|
|
18118
|
+
if (this.timer) {
|
|
18119
|
+
clearTimeout(this.timer);
|
|
18120
|
+
}
|
|
18121
|
+
this.timer = setTimeout(() => {
|
|
18122
|
+
this.flush().catch((err) => {
|
|
18123
|
+
console.error("[FlushManager] Flush error:", err);
|
|
18124
|
+
});
|
|
18125
|
+
}, this.debounceMs);
|
|
18126
|
+
}
|
|
18127
|
+
async flush() {
|
|
18128
|
+
const startTime = Date.now();
|
|
18129
|
+
if (this.flushing) {
|
|
18130
|
+
return {
|
|
18131
|
+
beadsExported: 0,
|
|
18132
|
+
bytesWritten: 0,
|
|
18133
|
+
duration: 0
|
|
18134
|
+
};
|
|
18135
|
+
}
|
|
18136
|
+
this.flushing = true;
|
|
18137
|
+
try {
|
|
18138
|
+
if (this.timer) {
|
|
18139
|
+
clearTimeout(this.timer);
|
|
18140
|
+
this.timer = null;
|
|
18141
|
+
}
|
|
18142
|
+
const { jsonl, beadIds } = await exportDirtyBeads(this.adapter, this.projectKey);
|
|
18143
|
+
if (beadIds.length === 0) {
|
|
18144
|
+
return {
|
|
18145
|
+
beadsExported: 0,
|
|
18146
|
+
bytesWritten: 0,
|
|
18147
|
+
duration: Date.now() - startTime
|
|
18148
|
+
};
|
|
18149
|
+
}
|
|
18150
|
+
await writeFile(this.outputPath, jsonl, "utf-8");
|
|
18151
|
+
const bytesWritten = Buffer.byteLength(jsonl, "utf-8");
|
|
18152
|
+
const db = await this.adapter.getDatabase();
|
|
18153
|
+
for (const beadId of beadIds) {
|
|
18154
|
+
await clearDirtyBead(db, this.projectKey, beadId);
|
|
18155
|
+
}
|
|
18156
|
+
const result = {
|
|
18157
|
+
beadsExported: beadIds.length,
|
|
18158
|
+
bytesWritten,
|
|
18159
|
+
duration: Date.now() - startTime
|
|
18160
|
+
};
|
|
18161
|
+
if (this.onFlush) {
|
|
18162
|
+
this.onFlush(result);
|
|
18163
|
+
}
|
|
18164
|
+
return result;
|
|
18165
|
+
} finally {
|
|
18166
|
+
this.flushing = false;
|
|
18167
|
+
}
|
|
18168
|
+
}
|
|
18169
|
+
stop() {
|
|
18170
|
+
if (this.timer) {
|
|
18171
|
+
clearTimeout(this.timer);
|
|
18172
|
+
this.timer = null;
|
|
18173
|
+
}
|
|
18174
|
+
}
|
|
18175
|
+
}
|
|
18176
|
+
// src/beads/merge.ts
|
|
18177
|
+
var DEFAULT_TOMBSTONE_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
18178
|
+
var MIN_TOMBSTONE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
18179
|
+
var CLOCK_SKEW_GRACE_MS = 60 * 60 * 1000;
|
|
18180
|
+
var STATUS_TOMBSTONE = "tombstone";
|
|
18181
|
+
function makeKey(bead) {
|
|
18182
|
+
const key = {
|
|
18183
|
+
id: bead.id,
|
|
18184
|
+
createdAt: bead.created_at,
|
|
18185
|
+
createdBy: undefined
|
|
18186
|
+
};
|
|
18187
|
+
return JSON.stringify(key);
|
|
18188
|
+
}
|
|
18189
|
+
function isTombstone(bead) {
|
|
18190
|
+
return bead.status === STATUS_TOMBSTONE;
|
|
18191
|
+
}
|
|
18192
|
+
function isExpiredTombstone(bead, ttlMs = DEFAULT_TOMBSTONE_TTL_MS) {
|
|
18193
|
+
if (!isTombstone(bead)) {
|
|
18194
|
+
return false;
|
|
18195
|
+
}
|
|
18196
|
+
if (!bead.closed_at) {
|
|
18197
|
+
return false;
|
|
18198
|
+
}
|
|
18199
|
+
const deletedAt = new Date(bead.closed_at).getTime();
|
|
18200
|
+
if (Number.isNaN(deletedAt)) {
|
|
18201
|
+
return false;
|
|
18202
|
+
}
|
|
18203
|
+
const effectiveTtl = ttlMs + CLOCK_SKEW_GRACE_MS;
|
|
18204
|
+
const expirationTime = deletedAt + effectiveTtl;
|
|
18205
|
+
return Date.now() > expirationTime;
|
|
18206
|
+
}
|
|
18207
|
+
function isTimeAfter(t1, t2) {
|
|
18208
|
+
if (!t1)
|
|
18209
|
+
return false;
|
|
18210
|
+
if (!t2)
|
|
18211
|
+
return true;
|
|
18212
|
+
const time1 = new Date(t1).getTime();
|
|
18213
|
+
const time22 = new Date(t2).getTime();
|
|
18214
|
+
const err1 = Number.isNaN(time1);
|
|
18215
|
+
const err2 = Number.isNaN(time22);
|
|
18216
|
+
if (err1 && err2)
|
|
18217
|
+
return true;
|
|
18218
|
+
if (err1)
|
|
18219
|
+
return false;
|
|
18220
|
+
if (err2)
|
|
18221
|
+
return true;
|
|
18222
|
+
return time1 >= time22;
|
|
18223
|
+
}
|
|
18224
|
+
function maxTime(t1, t2) {
|
|
18225
|
+
if (!t1 && !t2)
|
|
18226
|
+
return;
|
|
18227
|
+
if (!t1)
|
|
18228
|
+
return t2;
|
|
18229
|
+
if (!t2)
|
|
18230
|
+
return t1;
|
|
18231
|
+
const time1 = new Date(t1).getTime();
|
|
18232
|
+
const time22 = new Date(t2).getTime();
|
|
18233
|
+
if (Number.isNaN(time1) && Number.isNaN(time22))
|
|
18234
|
+
return t2;
|
|
18235
|
+
if (Number.isNaN(time1))
|
|
18236
|
+
return t2;
|
|
18237
|
+
if (Number.isNaN(time22))
|
|
18238
|
+
return t1;
|
|
18239
|
+
return time1 > time22 ? t1 : t2;
|
|
18240
|
+
}
|
|
18241
|
+
function mergeField(base, left, right) {
|
|
18242
|
+
if (base === left && base !== right)
|
|
18243
|
+
return right;
|
|
18244
|
+
if (base === right && base !== left)
|
|
18245
|
+
return left;
|
|
18246
|
+
return left;
|
|
18247
|
+
}
|
|
18248
|
+
function mergeFieldByUpdatedAt(base, left, right, leftUpdatedAt, rightUpdatedAt) {
|
|
18249
|
+
if (base === left && base !== right)
|
|
18250
|
+
return right;
|
|
18251
|
+
if (base === right && base !== left)
|
|
18252
|
+
return left;
|
|
18253
|
+
if (left === right)
|
|
18254
|
+
return left;
|
|
18255
|
+
return isTimeAfter(leftUpdatedAt, rightUpdatedAt) ? left : right;
|
|
18256
|
+
}
|
|
18257
|
+
function mergeStatus(base, left, right) {
|
|
18258
|
+
if (left === STATUS_TOMBSTONE || right === STATUS_TOMBSTONE) {
|
|
18259
|
+
return STATUS_TOMBSTONE;
|
|
18260
|
+
}
|
|
18261
|
+
if (left === "closed" || right === "closed") {
|
|
18262
|
+
return "closed";
|
|
18263
|
+
}
|
|
18264
|
+
return mergeField(base ?? "open", left ?? "open", right ?? "open");
|
|
18265
|
+
}
|
|
18266
|
+
function mergePriority(base, left, right) {
|
|
18267
|
+
if (base === left && base !== right)
|
|
18268
|
+
return right;
|
|
18269
|
+
if (base === right && base !== left)
|
|
18270
|
+
return left;
|
|
18271
|
+
if (left === right)
|
|
18272
|
+
return left;
|
|
18273
|
+
if (left === 0 && right !== 0)
|
|
18274
|
+
return right;
|
|
18275
|
+
if (right === 0 && left !== 0)
|
|
18276
|
+
return left;
|
|
18277
|
+
return left < right ? left : right;
|
|
18278
|
+
}
|
|
18279
|
+
function mergeDependencies(left, right) {
|
|
18280
|
+
const seen = new Set;
|
|
18281
|
+
const result = [];
|
|
18282
|
+
for (const dep of left) {
|
|
18283
|
+
const key = `${dep.depends_on_id}:${dep.type}`;
|
|
18284
|
+
if (!seen.has(key)) {
|
|
18285
|
+
seen.add(key);
|
|
18286
|
+
result.push(dep);
|
|
18287
|
+
}
|
|
18288
|
+
}
|
|
18289
|
+
for (const dep of right) {
|
|
18290
|
+
const key = `${dep.depends_on_id}:${dep.type}`;
|
|
18291
|
+
if (!seen.has(key)) {
|
|
18292
|
+
seen.add(key);
|
|
18293
|
+
result.push(dep);
|
|
18294
|
+
}
|
|
18295
|
+
}
|
|
18296
|
+
return result;
|
|
18297
|
+
}
|
|
18298
|
+
function mergeLabels(left, right) {
|
|
18299
|
+
const seen = new Set;
|
|
18300
|
+
const result = [];
|
|
18301
|
+
for (const label of left) {
|
|
18302
|
+
if (!seen.has(label)) {
|
|
18303
|
+
seen.add(label);
|
|
18304
|
+
result.push(label);
|
|
18305
|
+
}
|
|
18306
|
+
}
|
|
18307
|
+
for (const label of right) {
|
|
18308
|
+
if (!seen.has(label)) {
|
|
18309
|
+
seen.add(label);
|
|
18310
|
+
result.push(label);
|
|
18311
|
+
}
|
|
18312
|
+
}
|
|
18313
|
+
return result;
|
|
18314
|
+
}
|
|
18315
|
+
function mergeComments(left, right) {
|
|
18316
|
+
const seen = new Set;
|
|
18317
|
+
const result = [];
|
|
18318
|
+
for (const comment of left) {
|
|
18319
|
+
const key = `${comment.author}:${comment.text}`;
|
|
18320
|
+
if (!seen.has(key)) {
|
|
18321
|
+
seen.add(key);
|
|
18322
|
+
result.push(comment);
|
|
18323
|
+
}
|
|
18324
|
+
}
|
|
18325
|
+
for (const comment of right) {
|
|
18326
|
+
const key = `${comment.author}:${comment.text}`;
|
|
18327
|
+
if (!seen.has(key)) {
|
|
18328
|
+
seen.add(key);
|
|
18329
|
+
result.push(comment);
|
|
18330
|
+
}
|
|
18331
|
+
}
|
|
18332
|
+
return result;
|
|
18333
|
+
}
|
|
18334
|
+
function mergeTombstones(left, right) {
|
|
18335
|
+
if (!left.closed_at && !right.closed_at)
|
|
18336
|
+
return left;
|
|
18337
|
+
if (!left.closed_at)
|
|
18338
|
+
return right;
|
|
18339
|
+
if (!right.closed_at)
|
|
18340
|
+
return left;
|
|
18341
|
+
return isTimeAfter(left.closed_at, right.closed_at) ? left : right;
|
|
18342
|
+
}
|
|
18343
|
+
function mergeIssue(base, left, right) {
|
|
18344
|
+
const merged = {
|
|
18345
|
+
id: base.id,
|
|
18346
|
+
title: "",
|
|
18347
|
+
status: "open",
|
|
18348
|
+
priority: 0,
|
|
18349
|
+
issue_type: base.issue_type,
|
|
18350
|
+
created_at: base.created_at,
|
|
18351
|
+
updated_at: "",
|
|
18352
|
+
dependencies: [],
|
|
18353
|
+
labels: [],
|
|
18354
|
+
comments: []
|
|
18355
|
+
};
|
|
18356
|
+
merged.title = mergeFieldByUpdatedAt(base.title, left.title, right.title, left.updated_at, right.updated_at) ?? left.title;
|
|
18357
|
+
merged.description = mergeFieldByUpdatedAt(base.description, left.description, right.description, left.updated_at, right.updated_at);
|
|
18358
|
+
merged.status = mergeStatus(base.status, left.status, right.status);
|
|
18359
|
+
merged.priority = mergePriority(base.priority, left.priority, right.priority);
|
|
18360
|
+
merged.issue_type = mergeField(base.issue_type, left.issue_type, right.issue_type);
|
|
18361
|
+
merged.updated_at = maxTime(left.updated_at, right.updated_at) ?? left.updated_at;
|
|
18362
|
+
if (merged.status === "closed") {
|
|
18363
|
+
merged.closed_at = maxTime(left.closed_at, right.closed_at);
|
|
18364
|
+
}
|
|
18365
|
+
merged.assignee = mergeField(base.assignee, left.assignee, right.assignee);
|
|
18366
|
+
merged.parent_id = mergeField(base.parent_id, left.parent_id, right.parent_id);
|
|
18367
|
+
merged.dependencies = mergeDependencies(left.dependencies, right.dependencies);
|
|
18368
|
+
merged.labels = mergeLabels(left.labels, right.labels);
|
|
18369
|
+
merged.comments = mergeComments(left.comments, right.comments);
|
|
18370
|
+
return { merged, conflict: "" };
|
|
18371
|
+
}
|
|
18372
|
+
function merge3Way(base, left, right, options = {}) {
|
|
18373
|
+
const ttl = options.tombstoneTtlMs ?? DEFAULT_TOMBSTONE_TTL_MS;
|
|
18374
|
+
const debug2 = options.debug ?? false;
|
|
18375
|
+
const baseMap = new Map;
|
|
18376
|
+
for (const bead of base) {
|
|
18377
|
+
baseMap.set(makeKey(bead), bead);
|
|
18378
|
+
}
|
|
18379
|
+
const leftMap = new Map;
|
|
18380
|
+
for (const bead of left) {
|
|
18381
|
+
leftMap.set(makeKey(bead), bead);
|
|
18382
|
+
}
|
|
18383
|
+
const rightMap = new Map;
|
|
18384
|
+
for (const bead of right) {
|
|
18385
|
+
rightMap.set(makeKey(bead), bead);
|
|
18386
|
+
}
|
|
18387
|
+
const allKeys = new Set;
|
|
18388
|
+
for (const key of baseMap.keys())
|
|
18389
|
+
allKeys.add(key);
|
|
18390
|
+
for (const key of leftMap.keys())
|
|
18391
|
+
allKeys.add(key);
|
|
18392
|
+
for (const key of rightMap.keys())
|
|
18393
|
+
allKeys.add(key);
|
|
18394
|
+
const result = [];
|
|
18395
|
+
const conflicts = [];
|
|
18396
|
+
for (const key of allKeys) {
|
|
18397
|
+
const baseBead = baseMap.get(key);
|
|
18398
|
+
const leftBead = leftMap.get(key);
|
|
18399
|
+
const rightBead = rightMap.get(key);
|
|
18400
|
+
const leftTombstone = leftBead !== undefined && isTombstone(leftBead);
|
|
18401
|
+
const rightTombstone = rightBead !== undefined && isTombstone(rightBead);
|
|
18402
|
+
if (baseBead && leftBead && rightBead) {
|
|
18403
|
+
if (leftTombstone && rightTombstone) {
|
|
18404
|
+
result.push(mergeTombstones(leftBead, rightBead));
|
|
18405
|
+
continue;
|
|
18406
|
+
}
|
|
18407
|
+
if (leftTombstone && !rightTombstone) {
|
|
18408
|
+
if (isExpiredTombstone(leftBead, ttl)) {
|
|
18409
|
+
result.push(rightBead);
|
|
18410
|
+
} else {
|
|
18411
|
+
result.push(leftBead);
|
|
18412
|
+
}
|
|
18413
|
+
continue;
|
|
18414
|
+
}
|
|
18415
|
+
if (rightTombstone && !leftTombstone) {
|
|
18416
|
+
if (isExpiredTombstone(rightBead, ttl)) {
|
|
18417
|
+
result.push(leftBead);
|
|
18418
|
+
} else {
|
|
18419
|
+
result.push(rightBead);
|
|
18420
|
+
}
|
|
18421
|
+
continue;
|
|
18422
|
+
}
|
|
18423
|
+
const { merged, conflict } = mergeIssue(baseBead, leftBead, rightBead);
|
|
18424
|
+
if (conflict) {
|
|
18425
|
+
conflicts.push(conflict);
|
|
18426
|
+
} else {
|
|
18427
|
+
result.push(merged);
|
|
18428
|
+
}
|
|
18429
|
+
} else if (!baseBead && leftBead && rightBead) {
|
|
18430
|
+
if (leftTombstone && rightTombstone) {
|
|
18431
|
+
result.push(mergeTombstones(leftBead, rightBead));
|
|
18432
|
+
continue;
|
|
18433
|
+
}
|
|
18434
|
+
if (leftTombstone && !rightTombstone) {
|
|
18435
|
+
if (isExpiredTombstone(leftBead, ttl)) {
|
|
18436
|
+
result.push(rightBead);
|
|
18437
|
+
} else {
|
|
18438
|
+
result.push(leftBead);
|
|
18439
|
+
}
|
|
18440
|
+
continue;
|
|
18441
|
+
}
|
|
18442
|
+
if (rightTombstone && !leftTombstone) {
|
|
18443
|
+
if (isExpiredTombstone(rightBead, ttl)) {
|
|
18444
|
+
result.push(leftBead);
|
|
18445
|
+
} else {
|
|
18446
|
+
result.push(rightBead);
|
|
18447
|
+
}
|
|
18448
|
+
continue;
|
|
18449
|
+
}
|
|
18450
|
+
const emptyBase = {
|
|
18451
|
+
id: leftBead.id,
|
|
18452
|
+
title: "",
|
|
18453
|
+
status: "open",
|
|
18454
|
+
priority: 0,
|
|
18455
|
+
issue_type: leftBead.issue_type,
|
|
18456
|
+
created_at: leftBead.created_at,
|
|
18457
|
+
updated_at: leftBead.created_at,
|
|
18458
|
+
dependencies: [],
|
|
18459
|
+
labels: [],
|
|
18460
|
+
comments: []
|
|
18461
|
+
};
|
|
18462
|
+
const { merged } = mergeIssue(emptyBase, leftBead, rightBead);
|
|
18463
|
+
result.push(merged);
|
|
18464
|
+
} else if (baseBead && leftBead && !rightBead) {
|
|
18465
|
+
if (leftTombstone) {
|
|
18466
|
+
result.push(leftBead);
|
|
18467
|
+
}
|
|
18468
|
+
} else if (baseBead && !leftBead && rightBead) {
|
|
18469
|
+
if (rightTombstone) {
|
|
18470
|
+
result.push(rightBead);
|
|
18471
|
+
}
|
|
18472
|
+
} else if (!baseBead && leftBead && !rightBead) {
|
|
18473
|
+
result.push(leftBead);
|
|
18474
|
+
} else if (!baseBead && !leftBead && rightBead) {
|
|
18475
|
+
result.push(rightBead);
|
|
18476
|
+
}
|
|
18477
|
+
}
|
|
18478
|
+
if (debug2) {}
|
|
18479
|
+
return { merged: result, conflicts };
|
|
18480
|
+
}
|
|
18481
|
+
function mergeJsonl(baseJsonl, leftJsonl, rightJsonl, options = {}) {
|
|
18482
|
+
const parseJSONL2 = (jsonl2) => {
|
|
18483
|
+
if (!jsonl2 || jsonl2.trim() === "")
|
|
18484
|
+
return [];
|
|
18485
|
+
return jsonl2.split(`
|
|
18486
|
+
`).filter((line) => line.trim() !== "").map((line) => JSON.parse(line));
|
|
18487
|
+
};
|
|
18488
|
+
const base = parseJSONL2(baseJsonl);
|
|
18489
|
+
const left = parseJSONL2(leftJsonl);
|
|
18490
|
+
const right = parseJSONL2(rightJsonl);
|
|
18491
|
+
const { merged, conflicts } = merge3Way(base, left, right, options);
|
|
18492
|
+
const jsonl = merged.map((bead) => JSON.stringify(bead)).join(`
|
|
18493
|
+
`);
|
|
18494
|
+
return { jsonl, conflicts };
|
|
18495
|
+
}
|
|
18496
|
+
// src/index.ts
|
|
16625
18497
|
var SWARM_MAIL_VERSION = "0.1.0";
|
|
16626
18498
|
export {
|
|
18499
|
+
wouldCreateCycle,
|
|
16627
18500
|
withTiming,
|
|
16628
18501
|
withTimeout,
|
|
18502
|
+
updateProjections,
|
|
18503
|
+
serializeToJSONL,
|
|
16629
18504
|
sendSwarmMessage,
|
|
16630
18505
|
sendMessage,
|
|
16631
18506
|
sendAgentMessage,
|
|
@@ -16637,38 +18512,69 @@ export {
|
|
|
16637
18512
|
reserveAgentFiles,
|
|
16638
18513
|
replayEventsBatched2 as replayEventsBatched,
|
|
16639
18514
|
replayEvents,
|
|
18515
|
+
replayBeadEvents,
|
|
16640
18516
|
releaseSwarmFiles,
|
|
16641
18517
|
releaseAgentFiles,
|
|
16642
18518
|
registerAgent,
|
|
18519
|
+
rebuildBeadBlockedCache,
|
|
18520
|
+
rebuildAllBlockedCaches,
|
|
16643
18521
|
readSwarmMessage,
|
|
16644
18522
|
readEvents,
|
|
18523
|
+
readBeadEvents,
|
|
16645
18524
|
readAgentMessage,
|
|
18525
|
+
queryBeads,
|
|
18526
|
+
parseJSONL,
|
|
16646
18527
|
migrations,
|
|
18528
|
+
mergeJsonl,
|
|
18529
|
+
merge3Way,
|
|
18530
|
+
markBeadDirty,
|
|
18531
|
+
isTombstone,
|
|
16647
18532
|
isMigrationApplied,
|
|
18533
|
+
isExpiredTombstone,
|
|
16648
18534
|
isEventType,
|
|
16649
18535
|
isDatabaseHealthy,
|
|
18536
|
+
isBlocked,
|
|
18537
|
+
invalidateBlockedCache,
|
|
16650
18538
|
inspectState,
|
|
16651
18539
|
initSwarmAgent,
|
|
16652
18540
|
initAgent,
|
|
18541
|
+
importFromJSONL,
|
|
16653
18542
|
getThreadMessages,
|
|
16654
18543
|
getSwarmMail,
|
|
16655
18544
|
getSwarmInbox,
|
|
16656
18545
|
getPendingMigrations,
|
|
18546
|
+
getOpenBlockers,
|
|
18547
|
+
getNextReadyBead,
|
|
16657
18548
|
getMessage,
|
|
16658
18549
|
getLatestSequence,
|
|
18550
|
+
getLabels,
|
|
16659
18551
|
getInbox,
|
|
18552
|
+
getInProgressBeads,
|
|
16660
18553
|
getEventTimeline,
|
|
16661
18554
|
getEvalStats,
|
|
16662
18555
|
getEvalRecords,
|
|
18556
|
+
getDirtyBeads,
|
|
18557
|
+
getDependents,
|
|
18558
|
+
getDependencies,
|
|
16663
18559
|
getDatabaseStats,
|
|
16664
18560
|
getDatabasePath2 as getDatabasePath,
|
|
16665
18561
|
getDatabase,
|
|
16666
18562
|
getCurrentVersion,
|
|
18563
|
+
getComments,
|
|
18564
|
+
getCommentThread,
|
|
18565
|
+
getCommentById,
|
|
18566
|
+
getBlockers,
|
|
18567
|
+
getBlockedBeads,
|
|
18568
|
+
getBeadsByLabel,
|
|
18569
|
+
getBead,
|
|
16667
18570
|
getAppliedMigrations,
|
|
18571
|
+
getAllLabels,
|
|
16668
18572
|
getAgents,
|
|
16669
18573
|
getAgentInbox,
|
|
16670
18574
|
getAgent,
|
|
16671
18575
|
getActiveReservations,
|
|
18576
|
+
exportToJSONL,
|
|
18577
|
+
exportDirtyBeads,
|
|
16672
18578
|
debugReservations,
|
|
16673
18579
|
debugMessage,
|
|
16674
18580
|
debugEvents,
|
|
@@ -16676,15 +18582,22 @@ export {
|
|
|
16676
18582
|
createSwarmMailAdapter,
|
|
16677
18583
|
createInMemorySwarmMail,
|
|
16678
18584
|
createEvent,
|
|
18585
|
+
createBeadsAdapter,
|
|
18586
|
+
computeContentHash,
|
|
16679
18587
|
closeSwarmMail,
|
|
16680
18588
|
closeDatabase,
|
|
16681
18589
|
closeAllSwarmMail,
|
|
16682
18590
|
closeAllDatabases,
|
|
18591
|
+
clearDirtyBead,
|
|
18592
|
+
clearAllDirtyBeads,
|
|
16683
18593
|
checkSwarmHealth,
|
|
16684
18594
|
checkHealth,
|
|
16685
18595
|
checkConflicts,
|
|
18596
|
+
beadsMigrations,
|
|
18597
|
+
beadsMigration,
|
|
16686
18598
|
appendEvents,
|
|
16687
18599
|
appendEvent,
|
|
18600
|
+
appendBeadEvent,
|
|
16688
18601
|
acknowledgeSwarmMessage,
|
|
16689
18602
|
acknowledgeMessage,
|
|
16690
18603
|
TaskStartedEventSchema,
|
|
@@ -16695,14 +18608,19 @@ export {
|
|
|
16695
18608
|
SwarmCheckpointedEventSchema,
|
|
16696
18609
|
SubtaskOutcomeEventSchema,
|
|
16697
18610
|
SWARM_MAIL_VERSION,
|
|
18611
|
+
STATUS_TOMBSTONE,
|
|
16698
18612
|
PGlite2 as PGlite,
|
|
16699
18613
|
MessageSentEventSchema,
|
|
16700
18614
|
MessageReadEventSchema,
|
|
16701
18615
|
MessageAckedEventSchema,
|
|
18616
|
+
MIN_TOMBSTONE_TTL_MS,
|
|
16702
18617
|
HumanFeedbackEventSchema,
|
|
18618
|
+
FlushManager,
|
|
16703
18619
|
FileReservedEventSchema,
|
|
16704
18620
|
FileReleasedEventSchema,
|
|
16705
18621
|
DecompositionGeneratedEventSchema,
|
|
18622
|
+
DEFAULT_TOMBSTONE_TTL_MS,
|
|
18623
|
+
CLOCK_SKEW_GRACE_MS,
|
|
16706
18624
|
BaseEventSchema,
|
|
16707
18625
|
AgentRegisteredEventSchema,
|
|
16708
18626
|
AgentEventSchema,
|