@mean-weasel/lineage 0.1.6 → 0.1.8

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/dist/server.js CHANGED
@@ -328,6 +328,10 @@ function lineageDb() {
328
328
  create index if not exists assets_project_source_seen on assets(project_id, source, last_seen_at);
329
329
  create index if not exists assets_project_checksum on assets(project_id, checksum_sha256);
330
330
  create index if not exists assets_project_channel_campaign on assets(project_id, channel, campaign);
331
+ create table if not exists lineage_schema_migrations (
332
+ key text primary key,
333
+ applied_at text not null
334
+ );
331
335
  create table if not exists asset_edges (
332
336
  id text primary key,
333
337
  project_id text not null references projects(id),
@@ -633,12 +637,49 @@ function lineageDb() {
633
637
  );
634
638
  create index if not exists generation_job_receipts_job on generation_job_receipts(job_id, created_at);
635
639
  create table if not exists adapter_settings (project_id text not null references projects(id), adapter_type text not null check (adapter_type in ('cloud', 'scheduler', 'image_generator')), provider text not null, enabled integer not null check (enabled in (0, 1)), secret_ref text, safe_config_json text not null, created_at text not null, updated_at text not null, primary key(project_id, adapter_type, provider)); create index if not exists adapter_settings_project_type on adapter_settings(project_id, adapter_type);
640
+ create table if not exists lineage_tasks (
641
+ id text primary key,
642
+ project_id text not null references projects(id),
643
+ root_asset_id text not null references assets(id),
644
+ target_asset_id text not null references assets(id),
645
+ task_type text not null check (task_type in ('iterate', 'reroll')),
646
+ status text not null check (status in ('pending', 'claimed', 'in_progress', 'resolved', 'cancelled')),
647
+ instructions text,
648
+ created_by text not null check (created_by in ('human', 'agent', 'system')),
649
+ created_at text not null,
650
+ updated_at text not null,
651
+ claimed_at text,
652
+ started_at text,
653
+ resolved_at text,
654
+ cancelled_at text,
655
+ resolved_generation_job_id text,
656
+ resolved_asset_id text references assets(id),
657
+ metadata_json text
658
+ );
659
+ create unique index if not exists lineage_tasks_one_open
660
+ on lineage_tasks(project_id, root_asset_id, target_asset_id, task_type)
661
+ where status in ('pending', 'claimed', 'in_progress');
662
+ create index if not exists lineage_tasks_root_status
663
+ on lineage_tasks(project_id, root_asset_id, status, updated_at);
664
+ create index if not exists lineage_tasks_target
665
+ on lineage_tasks(project_id, root_asset_id, target_asset_id, task_type, status);
666
+ create table if not exists lineage_task_events (
667
+ id text primary key,
668
+ task_id text not null references lineage_tasks(id) on delete cascade,
669
+ event_type text not null,
670
+ actor text,
671
+ message text,
672
+ created_at text not null,
673
+ metadata_json text
674
+ );
675
+ create index if not exists lineage_task_events_task_created
676
+ on lineage_task_events(task_id, created_at);
636
677
  create table if not exists agent_claims (
637
678
  id text primary key,
638
679
  token_hash text not null,
639
680
  project_id text not null references projects(id),
640
681
  channel text,
641
- scope_type text not null check (scope_type in ('lineage_workspace', 'content_post', 'content_queue_lane', 'selection_set', 'project_channel')),
682
+ scope_type text not null check (scope_type in ('lineage_workspace', 'lineage_task', 'content_post', 'content_queue_lane', 'selection_set', 'project_channel')),
642
683
  target_id text not null,
643
684
  target_title text,
644
685
  agent_id text,
@@ -672,13 +713,76 @@ function lineageDb() {
672
713
  migrateAssetSelections(database);
673
714
  dropLegacyAssetSelectionRootUnique(database);
674
715
  ensureColumn(database, "asset_selections", "notes", "text");
716
+ backfillLineageTasks(database);
675
717
  ensureColumn(database, "asset_ledger_records", "first_seen_at", "text");
676
718
  ensureColumn(database, "asset_ledger_records", "indexed_by_run_id", "text");
677
719
  ensureColumn(database, "asset_ledger_sources", "first_seen_at", "text");
678
720
  ensureColumn(database, "asset_ledger_sources", "indexed_by_run_id", "text");
679
721
  ensureReviewStateValues(database);
722
+ ensureAgentClaimScopeValues(database);
723
+ ensureGenerationReceiptCheckValues(database);
680
724
  return database;
681
725
  }
726
+ function backfillLineageTasks(database) {
727
+ database.exec(`
728
+ create table if not exists lineage_schema_migrations (
729
+ key text primary key,
730
+ applied_at text not null
731
+ )
732
+ `);
733
+ const marker = database.prepare("select key from lineage_schema_migrations where key = 'lineage_tasks_backfilled_v1'").get();
734
+ if (marker) return;
735
+ database.exec("BEGIN IMMEDIATE");
736
+ try {
737
+ database.exec(`
738
+ insert or ignore into lineage_tasks (
739
+ id, project_id, root_asset_id, target_asset_id, task_type, status, instructions,
740
+ created_by, created_at, updated_at, metadata_json
741
+ )
742
+ select
743
+ s.project_id || ':' || s.root_asset_id || ':lineage-task:iterate:' || s.asset_id,
744
+ s.project_id,
745
+ s.root_asset_id,
746
+ s.asset_id,
747
+ 'iterate',
748
+ 'pending',
749
+ s.notes,
750
+ 'human',
751
+ s.selected_at,
752
+ s.selected_at,
753
+ null
754
+ from asset_selections s;
755
+
756
+ insert or ignore into lineage_tasks (
757
+ id, project_id, root_asset_id, target_asset_id, task_type, status, instructions,
758
+ created_by, created_at, updated_at, metadata_json
759
+ )
760
+ select
761
+ r.project_id || ':' || r.root_asset_id || ':lineage-task:reroll:' || r.node_asset_id,
762
+ r.project_id,
763
+ r.root_asset_id,
764
+ r.node_asset_id,
765
+ 'reroll',
766
+ 'pending',
767
+ r.notes,
768
+ r.requested_by,
769
+ r.created_at,
770
+ r.created_at,
771
+ null
772
+ from asset_reroll_requests r
773
+ where r.status = 'pending';
774
+ `);
775
+ database.prepare(`
776
+ insert into lineage_schema_migrations (key, applied_at)
777
+ values ('lineage_tasks_backfilled_v1', ?)
778
+ on conflict(key) do nothing
779
+ `).run(nowIso());
780
+ database.exec("COMMIT");
781
+ } catch (error) {
782
+ database.exec("ROLLBACK");
783
+ throw error;
784
+ }
785
+ }
682
786
  function ensureColumn(database, table, column, definition) {
683
787
  const rows = database.prepare(`pragma table_info(${table})`).all();
684
788
  if (!rows.some((row) => row.name === column)) database.exec(`alter table ${table} add column ${column} ${definition}`);
@@ -742,6 +846,132 @@ function ensureReviewStateValues(database) {
742
846
  drop table asset_reviews_old;
743
847
  `);
744
848
  }
849
+ function ensureAgentClaimScopeValues(database) {
850
+ const createSql = database.prepare("select sql from sqlite_master where type = 'table' and name = 'agent_claims'").get();
851
+ if (createSql?.sql?.includes("'lineage_task'")) return;
852
+ database.exec("PRAGMA foreign_keys = OFF; PRAGMA legacy_alter_table = ON; BEGIN IMMEDIATE");
853
+ try {
854
+ database.exec(`
855
+ alter table agent_claims rename to agent_claims_old;
856
+ create table agent_claims (
857
+ id text primary key,
858
+ token_hash text not null,
859
+ project_id text not null references projects(id),
860
+ channel text,
861
+ scope_type text not null check (scope_type in ('lineage_workspace', 'lineage_task', 'content_post', 'content_queue_lane', 'selection_set', 'project_channel')),
862
+ target_id text not null,
863
+ target_title text,
864
+ agent_id text,
865
+ agent_name text not null,
866
+ agent_kind text not null,
867
+ thread_id text,
868
+ status text not null check (status in ('active', 'expired', 'released', 'revoked', 'transferred')),
869
+ created_at text not null,
870
+ heartbeat_at text not null,
871
+ expires_at text not null,
872
+ released_at text,
873
+ revoked_at text,
874
+ revoked_by text,
875
+ override_reason text,
876
+ metadata_json text
877
+ );
878
+ insert into agent_claims
879
+ select * from agent_claims_old;
880
+ drop table agent_claims_old;
881
+ create unique index if not exists agent_claims_token_hash on agent_claims(token_hash);
882
+ create index if not exists agent_claims_project_status on agent_claims(project_id, status, heartbeat_at);
883
+ create index if not exists agent_claims_target on agent_claims(project_id, channel, scope_type, target_id, status);
884
+ `);
885
+ const violations = database.prepare("pragma foreign_key_check").all();
886
+ if (violations.length > 0) throw new Error(`Agent claim scope migration failed foreign key check: ${JSON.stringify(violations)}`);
887
+ database.exec("COMMIT");
888
+ } catch (error) {
889
+ database.exec("ROLLBACK");
890
+ throw error;
891
+ } finally {
892
+ database.exec("PRAGMA legacy_alter_table = OFF; PRAGMA foreign_keys = ON");
893
+ }
894
+ }
895
+ function tableCreateSql(database, table) {
896
+ const row = database.prepare("select sql from sqlite_master where type = 'table' and name = ?").get(table);
897
+ return row?.sql || "";
898
+ }
899
+ function ensureGenerationReceiptCheckValues(database) {
900
+ const jobsSql = tableCreateSql(database, "generation_jobs");
901
+ const inputsSql = tableCreateSql(database, "generation_job_inputs");
902
+ const needsJobsMigration = Boolean(jobsSql && !jobsSql.includes("'lineage_reroll'"));
903
+ const needsInputsMigration = Boolean(inputsSql && !inputsSql.includes("'reroll_target'"));
904
+ if (!needsJobsMigration && !needsInputsMigration) return;
905
+ database.exec("PRAGMA foreign_keys = OFF; PRAGMA legacy_alter_table = ON; BEGIN IMMEDIATE");
906
+ try {
907
+ if (needsJobsMigration) migrateGenerationJobsCheck(database);
908
+ if (needsInputsMigration) migrateGenerationJobInputsCheck(database);
909
+ const violations = database.prepare("pragma foreign_key_check").all();
910
+ if (violations.length > 0) throw new Error(`Generation receipt migration failed foreign key check: ${JSON.stringify(violations)}`);
911
+ database.exec("COMMIT");
912
+ } catch (error) {
913
+ database.exec("ROLLBACK");
914
+ throw error;
915
+ } finally {
916
+ database.exec("PRAGMA legacy_alter_table = OFF; PRAGMA foreign_keys = ON");
917
+ }
918
+ }
919
+ function migrateGenerationJobsCheck(database) {
920
+ database.exec(`
921
+ alter table generation_jobs rename to generation_jobs_legacy_check;
922
+ create table generation_jobs (
923
+ id text primary key,
924
+ project_id text not null references projects(id),
925
+ provider text not null default 'codex-handoff',
926
+ adapter_version text not null,
927
+ source_mode text not null check (source_mode in ('lineage_selection', 'lineage_reroll')),
928
+ root_asset_id text not null references assets(id),
929
+ prompt text not null,
930
+ expected_output_count integer not null check (expected_output_count > 0),
931
+ status text not null check (status in ('planned', 'imported', 'failed', 'cancelled')),
932
+ output_dir text,
933
+ handoff_json text,
934
+ created_at text not null,
935
+ updated_at text not null,
936
+ imported_at text
937
+ );
938
+ insert into generation_jobs (
939
+ id, project_id, provider, adapter_version, source_mode, root_asset_id, prompt,
940
+ expected_output_count, status, output_dir, handoff_json, created_at, updated_at, imported_at
941
+ )
942
+ select
943
+ id, project_id, provider, adapter_version, source_mode, root_asset_id, prompt,
944
+ expected_output_count, status, output_dir, handoff_json, created_at, updated_at, imported_at
945
+ from generation_jobs_legacy_check;
946
+ drop table generation_jobs_legacy_check;
947
+ create index if not exists generation_jobs_project_created on generation_jobs(project_id, created_at);
948
+ `);
949
+ }
950
+ function migrateGenerationJobInputsCheck(database) {
951
+ database.exec(`
952
+ alter table generation_job_inputs rename to generation_job_inputs_legacy_check;
953
+ create table generation_job_inputs (
954
+ id text primary key,
955
+ job_id text not null references generation_jobs(id) on delete cascade,
956
+ project_id text not null references projects(id),
957
+ asset_id text not null references assets(id),
958
+ root_asset_id text not null references assets(id),
959
+ role text not null check (role in ('lineage_next_base', 'reference', 'reroll_target')),
960
+ position integer not null,
961
+ selection_strategy text not null,
962
+ selection_snapshot_json text not null,
963
+ unique(job_id, asset_id, role)
964
+ );
965
+ insert into generation_job_inputs (
966
+ id, job_id, project_id, asset_id, root_asset_id, role, position, selection_strategy, selection_snapshot_json
967
+ )
968
+ select
969
+ id, job_id, project_id, asset_id, root_asset_id, role, position, selection_strategy, selection_snapshot_json
970
+ from generation_job_inputs_legacy_check;
971
+ drop table generation_job_inputs_legacy_check;
972
+ create index if not exists generation_job_inputs_job on generation_job_inputs(job_id, position);
973
+ `);
974
+ }
745
975
 
746
976
  // src/server/assetLedgerWorkflow.ts
747
977
  function ensureProject(database, project) {
@@ -1355,327 +1585,440 @@ function normalizeSelectionInput(fields) {
1355
1585
  return [...new Set([...fields.assetIds || [], fields.assetId || ""].map((assetId) => assetId.trim()).filter(Boolean))];
1356
1586
  }
1357
1587
 
1358
- // src/server/assetLineageWorkspaces.ts
1359
- var actors = /* @__PURE__ */ new Set(["human", "agent", "system"]);
1360
- var statuses = /* @__PURE__ */ new Set(["active", "paused", "archived"]);
1361
- var LineageWorkspaceError = class extends Error {
1362
- constructor(message, status = 400) {
1588
+ // src/server/assetLineageTasks.ts
1589
+ import { randomBytes as randomBytes2 } from "node:crypto";
1590
+
1591
+ // src/server/agentClaims.ts
1592
+ import { createHash as createHash2, randomBytes, timingSafeEqual } from "node:crypto";
1593
+ var defaultTtlSeconds = 20 * 60;
1594
+ var idleAfterSeconds = 5 * 60;
1595
+ var staleAfterSeconds = 15 * 60;
1596
+ var scopes = /* @__PURE__ */ new Set(["lineage_workspace", "lineage_task", "content_post", "content_queue_lane", "selection_set", "project_channel"]);
1597
+ var AgentClaimError = class extends Error {
1598
+ constructor(message, status = 400, code = "agent_claim_error", conflicts = []) {
1363
1599
  super(message);
1364
1600
  this.status = status;
1601
+ this.code = code;
1602
+ this.conflicts = conflicts;
1365
1603
  }
1366
1604
  status;
1605
+ code;
1606
+ conflicts;
1367
1607
  };
1368
- function isLineageWorkspaceError(error) {
1369
- return error instanceof LineageWorkspaceError;
1608
+ function isAgentClaimError(error) {
1609
+ return error instanceof AgentClaimError;
1370
1610
  }
1371
- function lineageWorkspaceId(project, rootAssetId) {
1372
- return `${project}:lineage-workspace:${rootAssetId}`;
1611
+ function parseClaimTtl(value) {
1612
+ if (!value) return defaultTtlSeconds;
1613
+ const match = value.trim().match(/^(\d+)(s|m|h)?$/);
1614
+ if (!match) throw new AgentClaimError(`Invalid claim ttl: ${value}`);
1615
+ const amount = Number(match[1]);
1616
+ const unit = match[2] || "s";
1617
+ const multiplier = unit === "h" ? 3600 : unit === "m" ? 60 : 1;
1618
+ const seconds = amount * multiplier;
1619
+ if (!Number.isInteger(seconds) || seconds < 30 || seconds > 24 * 60 * 60) {
1620
+ throw new AgentClaimError("Claim ttl must be between 30 seconds and 24 hours");
1621
+ }
1622
+ return seconds;
1373
1623
  }
1374
- function ensureProject2(database, project) {
1375
- const timestamp = nowIso();
1376
- database.prepare(`
1377
- insert into projects (id, product, created_at, updated_at)
1378
- values (?, ?, ?, ?)
1379
- on conflict(id) do update set product = excluded.product, updated_at = excluded.updated_at
1380
- `).run(project, project, timestamp, timestamp);
1624
+ function randomId(prefix) {
1625
+ return `${prefix}_${Date.now().toString(36)}_${randomBytes(6).toString("base64url").toLowerCase()}`;
1381
1626
  }
1382
- function normalizeStatus(value, fallback) {
1383
- const status = value || fallback;
1384
- if (!statuses.has(status)) throw new LineageWorkspaceError(`Unsupported lineage workspace status: ${status}`);
1385
- return status;
1627
+ function tokenHash(token) {
1628
+ return createHash2("sha256").update(token).digest("hex");
1386
1629
  }
1387
- function normalizeActor(value) {
1388
- const actor = value || "human";
1389
- if (!actors.has(actor)) throw new LineageWorkspaceError(`Unsupported lineage workspace actor: ${actor}`);
1390
- return actor;
1630
+ function safeEqual(a, b) {
1631
+ const left = Buffer.from(a);
1632
+ const right = Buffer.from(b);
1633
+ return left.length === right.length && timingSafeEqual(left, right);
1391
1634
  }
1392
- function requireAsset(database, project, assetId) {
1393
- const row = database.prepare("select id, title from assets where project_id = ? and id = ?").get(project, assetId);
1394
- if (!row) throw new LineageWorkspaceError(`Unknown indexed asset: ${assetId}`, 404);
1395
- return row;
1635
+ function expiresAtFrom(timestamp, ttlSeconds) {
1636
+ return new Date(new Date(timestamp).getTime() + ttlSeconds * 1e3).toISOString();
1396
1637
  }
1397
- function rowToWorkspace(row) {
1638
+ function metadataJson(metadata2) {
1639
+ return metadata2 ? JSON.stringify(metadata2) : null;
1640
+ }
1641
+ function parseMetadata(value) {
1642
+ if (typeof value !== "string" || !value) return void 0;
1643
+ try {
1644
+ const parsed = JSON.parse(value);
1645
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
1646
+ } catch {
1647
+ return void 0;
1648
+ }
1649
+ }
1650
+ function derivedState(row, now = /* @__PURE__ */ new Date()) {
1651
+ if (row.status !== "active") return row.status === "expired" ? "expired" : "stale";
1652
+ if (new Date(row.expires_at).getTime() <= now.getTime()) return "expired";
1653
+ const ageSeconds = Math.max(0, Math.floor((now.getTime() - new Date(row.heartbeat_at).getTime()) / 1e3));
1654
+ if (ageSeconds >= staleAfterSeconds) return "stale";
1655
+ if (ageSeconds >= idleAfterSeconds) return "idle";
1656
+ return "active";
1657
+ }
1658
+ function rowToClaim(row, now = /* @__PURE__ */ new Date()) {
1659
+ const heartbeatAt = String(row.heartbeat_at);
1398
1660
  return {
1399
1661
  id: String(row.id),
1400
1662
  project: String(row.project_id),
1401
- root_asset_id: String(row.root_asset_id),
1402
- title: String(row.title),
1663
+ channel: typeof row.channel === "string" ? row.channel : void 0,
1664
+ scope_type: String(row.scope_type),
1665
+ target_id: String(row.target_id),
1666
+ target_title: typeof row.target_title === "string" ? row.target_title : void 0,
1667
+ agent_id: typeof row.agent_id === "string" ? row.agent_id : void 0,
1668
+ agent_name: String(row.agent_name),
1669
+ agent_kind: String(row.agent_kind),
1670
+ thread_id: typeof row.thread_id === "string" ? row.thread_id : void 0,
1403
1671
  status: String(row.status),
1404
- notes: typeof row.notes === "string" ? row.notes : void 0,
1405
- created_by: String(row.created_by),
1406
- active_at: typeof row.active_at === "string" ? row.active_at : void 0,
1407
1672
  created_at: String(row.created_at),
1408
- updated_at: String(row.updated_at)
1673
+ heartbeat_at: heartbeatAt,
1674
+ expires_at: String(row.expires_at),
1675
+ released_at: typeof row.released_at === "string" ? row.released_at : void 0,
1676
+ revoked_at: typeof row.revoked_at === "string" ? row.revoked_at : void 0,
1677
+ revoked_by: typeof row.revoked_by === "string" ? row.revoked_by : void 0,
1678
+ override_reason: typeof row.override_reason === "string" ? row.override_reason : void 0,
1679
+ metadata: parseMetadata(row.metadata_json),
1680
+ heartbeat_age_seconds: Math.max(0, Math.floor((now.getTime() - new Date(heartbeatAt).getTime()) / 1e3)),
1681
+ derived_state: derivedState({
1682
+ expires_at: String(row.expires_at),
1683
+ heartbeat_at: heartbeatAt,
1684
+ status: String(row.status)
1685
+ }, now)
1409
1686
  };
1410
1687
  }
1411
- function workspaceById(database, project, id) {
1412
- const row = database.prepare("select * from lineage_workspaces where project_id = ? and id = ?").get(project, id);
1413
- return row ? rowToWorkspace(row) : null;
1688
+ function eventToRow(row) {
1689
+ return {
1690
+ claim_id: String(row.claim_id),
1691
+ event_type: String(row.event_type),
1692
+ actor: typeof row.actor === "string" ? row.actor : void 0,
1693
+ message: typeof row.message === "string" ? row.message : void 0,
1694
+ created_at: String(row.created_at),
1695
+ metadata: parseMetadata(row.metadata_json)
1696
+ };
1414
1697
  }
1415
- function workspaceByRoot(database, project, rootAssetId) {
1416
- const row = database.prepare("select * from lineage_workspaces where project_id = ? and root_asset_id = ?").get(project, rootAssetId);
1417
- return row ? rowToWorkspace(row) : null;
1698
+ function ensureProject2(database, project) {
1699
+ const timestamp = nowIso();
1700
+ database.prepare(`
1701
+ insert into projects (id, product, created_at, updated_at)
1702
+ values (?, ?, ?, ?)
1703
+ on conflict(id) do update set product = excluded.product, updated_at = excluded.updated_at
1704
+ `).run(project, project, timestamp, timestamp);
1418
1705
  }
1419
- function knownRoots(database, project) {
1420
- const rows = database.prepare(`
1421
- select root_asset_id, max(selected_at) selected_at from asset_selections where project_id = ? group by root_asset_id
1422
- union
1423
- select root_asset_id, null selected_at from asset_layouts where project_id = ? group by root_asset_id
1424
- union
1425
- select parent_asset_id root_asset_id, null selected_at
1426
- from asset_edges
1427
- where project_id = ?
1428
- and parent_asset_id not in (select child_asset_id from asset_edges where project_id = ?)
1429
- group by parent_asset_id
1430
- `).all(project, project, project, project);
1431
- return rows.map((row) => ({ root_asset_id: row.root_asset_id, selected_at: row.selected_at || void 0 }));
1706
+ function recordEvent(database, claimId, eventType, actor, message, metadata2) {
1707
+ database.prepare(`
1708
+ insert into agent_claim_events (id, claim_id, event_type, actor, message, created_at, metadata_json)
1709
+ values (?, ?, ?, ?, ?, ?, ?)
1710
+ `).run(randomId("claim_event"), claimId, eventType, actor || null, message || null, nowIso(), metadataJson(metadata2));
1432
1711
  }
1433
- function seedLegacyWorkspaces(database, project) {
1434
- ensureProject2(database, project);
1712
+ function expireActiveClaims(database) {
1435
1713
  const timestamp = nowIso();
1436
- const statement = database.prepare(`
1437
- insert into lineage_workspaces (
1438
- id, project_id, root_asset_id, title, status, notes, created_by, active_at, created_at, updated_at
1439
- )
1440
- select ?, ?, a.id, a.title || ' lineage', 'active', null, 'system', ?, ?, ?
1441
- from assets a
1442
- where a.project_id = ? and a.id = ?
1443
- on conflict(project_id, root_asset_id) do nothing
1444
- `);
1445
- for (const root of knownRoots(database, project)) {
1446
- statement.run(
1447
- lineageWorkspaceId(project, root.root_asset_id),
1448
- project,
1449
- root.selected_at || null,
1450
- timestamp,
1451
- timestamp,
1452
- project,
1453
- root.root_asset_id
1454
- );
1455
- }
1456
- }
1457
- function listRows(database, project) {
1458
- return database.prepare(`
1459
- select * from lineage_workspaces
1460
- where project_id = ?
1461
- order by
1462
- case status when 'active' then 0 when 'paused' then 1 else 2 end,
1463
- active_at desc nulls last,
1464
- updated_at desc,
1465
- title
1466
- `).all(project).map(rowToWorkspace);
1714
+ const expired = database.prepare(`
1715
+ select id from agent_claims where status = 'active' and expires_at <= ?
1716
+ `).all(timestamp);
1717
+ if (expired.length === 0) return;
1718
+ database.prepare(`
1719
+ update agent_claims set status = 'expired' where status = 'active' and expires_at <= ?
1720
+ `).run(timestamp);
1721
+ for (const claim of expired) recordEvent(database, claim.id, "expired", "system", "Claim expired after missed heartbeat.");
1467
1722
  }
1468
- function activeWorkspace(database, project) {
1469
- const row = database.prepare(`
1470
- select * from lineage_workspaces
1471
- where project_id = ? and status != 'archived'
1472
- order by active_at desc nulls last, updated_at desc
1473
- limit 1
1474
- `).get(project);
1475
- return row ? rowToWorkspace(row) : null;
1723
+ function normalizeScope(scopeType) {
1724
+ if (!scopes.has(scopeType)) throw new AgentClaimError(`Unsupported claim scope: ${scopeType}`);
1725
+ return scopeType;
1476
1726
  }
1477
- function listLineageWorkspaces(project) {
1727
+ function channelOverlaps(left, right) {
1728
+ return !left || !right || left === right;
1729
+ }
1730
+ function claimOverlaps(candidate, existing) {
1731
+ if (candidate.project !== existing.project) return false;
1732
+ if (!channelOverlaps(candidate.channel, existing.channel)) return false;
1733
+ if (candidate.scope_type === existing.scope_type && candidate.target_id === existing.target_id) return true;
1734
+ return candidate.scope_type === "project_channel" || existing.scope_type === "project_channel";
1735
+ }
1736
+ function activeClaims(database, project) {
1737
+ const rows = project ? database.prepare("select * from agent_claims where project_id = ? and status = 'active' order by heartbeat_at desc").all(project) : database.prepare("select * from agent_claims where status = 'active' order by project_id, heartbeat_at desc").all();
1738
+ return rows.map((row) => rowToClaim(row));
1739
+ }
1740
+ function findClaimById(database, claimId, project) {
1741
+ const row = project ? database.prepare("select * from agent_claims where project_id = ? and id = ?").get(project, claimId) : database.prepare("select * from agent_claims where id = ?").get(claimId);
1742
+ return row ? rowToClaim(row) : null;
1743
+ }
1744
+ function findClaimRowByToken(database, claimToken) {
1745
+ const claimId = claimToken.split(".")[0];
1746
+ const row = database.prepare("select * from agent_claims where id = ?").get(claimId);
1747
+ if (!row) return null;
1748
+ return safeEqual(String(row.token_hash), tokenHash(claimToken)) ? row : null;
1749
+ }
1750
+ function denied(code, message, conflicts = []) {
1751
+ return { ok: false, code, message, conflicts };
1752
+ }
1753
+ function scopeAllowsWrite(claim, scopeType, targetId, writeKind) {
1754
+ if (claim.scope_type === scopeType && claim.target_id === targetId) return true;
1755
+ if (claim.scope_type === "project_channel") return true;
1756
+ if (claim.scope_type === "lineage_workspace" && scopeType === "lineage_workspace") return claim.target_id === targetId;
1757
+ if (claim.scope_type === "content_queue_lane" && writeKind === "content_queue_next") return true;
1758
+ return false;
1759
+ }
1760
+ function createAgentClaim(fields) {
1761
+ const project = fields.project.trim();
1762
+ const targetId = fields.targetId.trim();
1763
+ const agentName = fields.agentName.trim();
1764
+ if (!project) throw new AgentClaimError("Agent claim requires project");
1765
+ if (!targetId) throw new AgentClaimError("Agent claim requires target");
1766
+ if (!agentName) throw new AgentClaimError("Agent claim requires agent name");
1767
+ const scopeType = normalizeScope(fields.scopeType);
1768
+ const ttlSeconds = fields.ttlSeconds || defaultTtlSeconds;
1478
1769
  const database = lineageDb();
1479
1770
  try {
1480
- seedLegacyWorkspaces(database, project);
1481
- return {
1771
+ ensureProject2(database, project);
1772
+ expireActiveClaims(database);
1773
+ const candidate = { project, channel: fields.channel?.trim() || void 0, scope_type: scopeType, target_id: targetId };
1774
+ const conflicts = activeClaims(database, project).filter((claim2) => claimOverlaps(candidate, claim2));
1775
+ if (conflicts.length > 0 && !fields.force) {
1776
+ throw new AgentClaimError("Target already has an active overlapping agent claim.", 409, "target_already_claimed", conflicts);
1777
+ }
1778
+ if (conflicts.length > 0 && !fields.reason?.trim()) {
1779
+ throw new AgentClaimError("Overriding an active claim requires --reason.", 400, "override_reason_required", conflicts);
1780
+ }
1781
+ const timestamp = nowIso();
1782
+ for (const conflict of conflicts) {
1783
+ database.prepare(`
1784
+ update agent_claims
1785
+ set status = 'revoked', revoked_at = ?, revoked_by = ?, override_reason = ?
1786
+ where id = ? and status = 'active'
1787
+ `).run(timestamp, agentName, fields.reason || null, conflict.id);
1788
+ recordEvent(database, conflict.id, "revoked", agentName, fields.reason || "Revoked by forced claim takeover.");
1789
+ recordEvent(database, conflict.id, "conflict", agentName, `Overridden by ${agentName}.`, { new_claim_target: targetId });
1790
+ }
1791
+ const id = randomId("claim");
1792
+ const secret = randomBytes(24).toString("base64url");
1793
+ const claimToken = `${id}.${secret}`;
1794
+ const expiresAt = expiresAtFrom(timestamp, ttlSeconds);
1795
+ database.prepare(`
1796
+ insert into agent_claims (
1797
+ id, token_hash, project_id, channel, scope_type, target_id, target_title, agent_id, agent_name,
1798
+ agent_kind, thread_id, status, created_at, heartbeat_at, expires_at, metadata_json
1799
+ ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', ?, ?, ?, ?)
1800
+ `).run(
1801
+ id,
1802
+ tokenHash(claimToken),
1482
1803
  project,
1483
- active_workspace: activeWorkspace(database, project),
1484
- workspaces: listRows(database, project),
1485
- fetchedAt: nowIso()
1486
- };
1804
+ candidate.channel || null,
1805
+ scopeType,
1806
+ targetId,
1807
+ fields.targetTitle?.trim() || null,
1808
+ fields.agentId?.trim() || null,
1809
+ agentName,
1810
+ fields.agentKind?.trim() || "codex",
1811
+ fields.threadId?.trim() || null,
1812
+ timestamp,
1813
+ timestamp,
1814
+ expiresAt,
1815
+ metadataJson(fields.metadata)
1816
+ );
1817
+ recordEvent(database, id, "created", agentName, `Claimed ${scopeType} ${targetId}.`, { ttl_seconds: ttlSeconds });
1818
+ const claim = findClaimById(database, id);
1819
+ return { ok: true, claim, claim_token: claimToken, conflicts_revoked: conflicts.map((conflict) => conflict.id) };
1487
1820
  } finally {
1488
1821
  database.close();
1489
1822
  }
1490
1823
  }
1491
- function inspectLineageWorkspace(project, workspaceId) {
1824
+ function listAgentClaims(project) {
1492
1825
  const database = lineageDb();
1493
1826
  try {
1494
- seedLegacyWorkspaces(database, project);
1495
- const workspace = workspaceById(database, project, workspaceId) || workspaceByRoot(database, project, workspaceId);
1496
- if (!workspace) throw new LineageWorkspaceError(`Unknown lineage workspace: ${workspaceId}`, 404);
1497
- return workspace;
1827
+ expireActiveClaims(database);
1828
+ const rows = project ? database.prepare("select * from agent_claims where project_id = ? order by status, heartbeat_at desc").all(project) : database.prepare("select * from agent_claims order by project_id, status, heartbeat_at desc").all();
1829
+ return { ok: true, claims: rows.map((row) => rowToClaim(row)), fetchedAt: nowIso() };
1498
1830
  } finally {
1499
1831
  database.close();
1500
1832
  }
1501
1833
  }
1502
- function createLineageWorkspace(project, fields) {
1503
- const rootAssetId = fields.rootAssetId.trim();
1504
- if (!rootAssetId) throw new LineageWorkspaceError("Lineage workspace requires rootAssetId");
1505
- const status = normalizeStatus(fields.status, "active");
1506
- const actor = normalizeActor(fields.createdBy);
1834
+ function inspectAgentClaim(claimId, project) {
1507
1835
  const database = lineageDb();
1508
1836
  try {
1509
- const root = requireAsset(database, project, rootAssetId);
1510
- const timestamp = nowIso();
1511
- const workspace = {
1512
- id: lineageWorkspaceId(project, rootAssetId),
1513
- project,
1514
- root_asset_id: rootAssetId,
1515
- title: fields.title?.trim() || `${root.title} lineage`,
1516
- status,
1517
- notes: fields.notes?.trim() || void 0,
1518
- created_by: actor,
1519
- active_at: fields.activate !== false && status !== "archived" ? timestamp : void 0,
1520
- created_at: timestamp,
1521
- updated_at: timestamp
1522
- };
1523
- if (!fields.confirmWrite) return { ok: true, dryRun: true, workspace };
1524
- ensureProject2(database, project);
1525
- database.prepare(`
1526
- insert into lineage_workspaces (
1527
- id, project_id, root_asset_id, title, status, notes, created_by, active_at, created_at, updated_at
1528
- ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1529
- on conflict(project_id, root_asset_id) do update set
1530
- title = excluded.title,
1531
- status = excluded.status,
1532
- notes = excluded.notes,
1533
- active_at = coalesce(excluded.active_at, lineage_workspaces.active_at),
1534
- updated_at = excluded.updated_at
1535
- `).run(
1536
- workspace.id,
1537
- project,
1538
- workspace.root_asset_id,
1539
- workspace.title,
1540
- workspace.status,
1541
- workspace.notes || null,
1542
- workspace.created_by,
1543
- workspace.active_at || null,
1544
- workspace.created_at,
1545
- workspace.updated_at
1546
- );
1547
- return {
1548
- ok: true,
1549
- message: `Saved lineage workspace ${workspace.title}`,
1550
- workspace: workspaceById(database, project, workspace.id)
1551
- };
1837
+ expireActiveClaims(database);
1838
+ const claim = findClaimById(database, claimId, project);
1839
+ if (!claim) throw new AgentClaimError(`Unknown agent claim: ${claimId}`, 404, "claim_not_found");
1840
+ const events = database.prepare("select * from agent_claim_events where claim_id = ? order by created_at").all(claim.id);
1841
+ return { ok: true, claim, events: events.map(eventToRow) };
1552
1842
  } finally {
1553
1843
  database.close();
1554
1844
  }
1555
1845
  }
1556
- function updateLineageWorkspace(project, workspaceId, fields) {
1846
+ function heartbeatAgentClaim(claimToken, ttlSeconds = defaultTtlSeconds) {
1557
1847
  const database = lineageDb();
1558
1848
  try {
1559
- seedLegacyWorkspaces(database, project);
1560
- const current = workspaceById(database, project, workspaceId) || workspaceByRoot(database, project, workspaceId);
1561
- if (!current) throw new LineageWorkspaceError(`Unknown lineage workspace: ${workspaceId}`, 404);
1849
+ expireActiveClaims(database);
1850
+ const row = findClaimRowByToken(database, claimToken);
1851
+ if (!row) throw new AgentClaimError("Unknown or invalid agent claim token.", 401, "claim_token_invalid");
1852
+ const claim = rowToClaim(row);
1853
+ if (claim.status !== "active") throw new AgentClaimError(`Agent claim is ${claim.status}.`, 409, "claim_not_active");
1562
1854
  const timestamp = nowIso();
1563
- const next = {
1564
- ...current,
1565
- title: fields.title?.trim() || current.title,
1566
- status: normalizeStatus(fields.status, current.status),
1567
- notes: fields.notes === void 0 ? current.notes : fields.notes.trim() || void 0,
1568
- active_at: fields.activate ? timestamp : current.active_at,
1569
- updated_at: timestamp
1570
- };
1571
- if (!fields.confirmWrite) return { ok: true, dryRun: true, workspace: next };
1572
- database.prepare(`
1573
- update lineage_workspaces
1574
- set title = ?, status = ?, notes = ?, active_at = ?, updated_at = ?
1575
- where project_id = ? and id = ?
1576
- `).run(next.title, next.status, next.notes || null, next.active_at || null, timestamp, project, current.id);
1577
- return {
1578
- ok: true,
1579
- message: `Updated lineage workspace ${next.title}`,
1580
- workspace: workspaceById(database, project, current.id)
1581
- };
1855
+ database.prepare("update agent_claims set heartbeat_at = ?, expires_at = ? where id = ?").run(timestamp, expiresAtFrom(timestamp, ttlSeconds), claim.id);
1856
+ recordEvent(database, claim.id, "heartbeat", claim.agent_name, "Claim heartbeat received.");
1857
+ return { ok: true, claim: findClaimById(database, claim.id) };
1582
1858
  } finally {
1583
1859
  database.close();
1584
1860
  }
1585
1861
  }
1586
- function activateLineageWorkspace(project, workspaceId, confirmWrite) {
1587
- return updateLineageWorkspace(project, workspaceId, { activate: true, status: "active", confirmWrite });
1588
- }
1589
- function archiveLineageWorkspace(project, workspaceId, confirmWrite) {
1862
+ function releaseAgentClaim(claimToken) {
1590
1863
  const database = lineageDb();
1591
1864
  try {
1592
- seedLegacyWorkspaces(database, project);
1593
- const current = workspaceById(database, project, workspaceId) || workspaceByRoot(database, project, workspaceId);
1594
- if (!current) throw new LineageWorkspaceError(`Unknown lineage workspace: ${workspaceId}`, 404);
1865
+ expireActiveClaims(database);
1866
+ const row = findClaimRowByToken(database, claimToken);
1867
+ if (!row) throw new AgentClaimError("Unknown or invalid agent claim token.", 401, "claim_token_invalid");
1868
+ const claim = rowToClaim(row);
1869
+ if (claim.status !== "active") throw new AgentClaimError(`Agent claim is ${claim.status}.`, 409, "claim_not_active");
1595
1870
  const timestamp = nowIso();
1596
- const next = {
1597
- ...current,
1598
- status: "archived",
1599
- active_at: void 0,
1600
- updated_at: timestamp
1601
- };
1602
- if (!confirmWrite) return { ok: true, dryRun: true, workspace: next };
1603
- database.prepare(`
1604
- update lineage_workspaces
1605
- set status = 'archived', active_at = null, updated_at = ?
1606
- where project_id = ? and id = ?
1607
- `).run(timestamp, project, current.id);
1608
- database.prepare("delete from asset_selections where project_id = ? and root_asset_id = ?").run(project, current.root_asset_id);
1609
- return {
1610
- ok: true,
1611
- message: `Archived lineage workspace ${current.title}`,
1612
- workspace: workspaceById(database, project, current.id)
1613
- };
1871
+ database.prepare("update agent_claims set status = 'released', released_at = ? where id = ?").run(timestamp, claim.id);
1872
+ recordEvent(database, claim.id, "released", claim.agent_name, "Claim released by token holder.");
1873
+ return { ok: true, claim: findClaimById(database, claim.id) };
1614
1874
  } finally {
1615
1875
  database.close();
1616
1876
  }
1617
1877
  }
1618
- function activeLineageWorkspaceRoot(project) {
1878
+ function releaseStaleAgentClaim(project, claimId, fields) {
1879
+ if (!fields.confirmWrite) throw new AgentClaimError("Releasing a stale agent claim requires confirmWrite=true.", 400, "confirm_write_required");
1880
+ if (!fields.reason?.trim()) throw new AgentClaimError("Releasing a stale agent claim requires a reason.", 400, "reason_required");
1619
1881
  const database = lineageDb();
1620
1882
  try {
1621
- seedLegacyWorkspaces(database, project);
1622
- return activeWorkspace(database, project)?.root_asset_id;
1883
+ expireActiveClaims(database);
1884
+ const claim = findClaimById(database, claimId, project);
1885
+ if (!claim) throw new AgentClaimError(`Unknown agent claim: ${claimId}`, 404, "claim_not_found");
1886
+ if (claim.status !== "active" || claim.derived_state !== "stale") {
1887
+ throw new AgentClaimError("Only stale active claims can be released without the claim token.", 409, "claim_not_stale", [claim]);
1888
+ }
1889
+ const timestamp = nowIso();
1890
+ database.prepare("update agent_claims set status = 'released', released_at = ?, revoked_by = ?, override_reason = ? where id = ?").run(timestamp, fields.actor || "human", fields.reason, claim.id);
1891
+ recordEvent(database, claim.id, "released", fields.actor || "human", fields.reason);
1892
+ return { ok: true, claim: findClaimById(database, claim.id) };
1893
+ } finally {
1894
+ database.close();
1895
+ }
1896
+ }
1897
+ function revokeAgentClaim(project, claimId, fields) {
1898
+ if (!fields.confirmWrite) throw new AgentClaimError("Revoking an agent claim requires confirmWrite=true.", 400, "confirm_write_required");
1899
+ if (!fields.reason?.trim()) throw new AgentClaimError("Revoking an agent claim requires a reason.", 400, "reason_required");
1900
+ const database = lineageDb();
1901
+ try {
1902
+ expireActiveClaims(database);
1903
+ const claim = findClaimById(database, claimId, project);
1904
+ if (!claim) throw new AgentClaimError(`Unknown agent claim: ${claimId}`, 404, "claim_not_found");
1905
+ const timestamp = nowIso();
1906
+ database.prepare(`
1907
+ update agent_claims set status = 'revoked', revoked_at = ?, revoked_by = ?, override_reason = ?
1908
+ where id = ?
1909
+ `).run(timestamp, fields.actor || "human", fields.reason, claim.id);
1910
+ recordEvent(database, claim.id, "revoked", fields.actor || "human", fields.reason);
1911
+ return { ok: true, claim: findClaimById(database, claim.id) };
1912
+ } finally {
1913
+ database.close();
1914
+ }
1915
+ }
1916
+ function revokeAgentClaimInDatabase(database, project, claimId, fields) {
1917
+ expireActiveClaims(database);
1918
+ const claim = findClaimById(database, claimId, project);
1919
+ if (!claim) throw new AgentClaimError(`Unknown agent claim: ${claimId}`, 404, "claim_not_found");
1920
+ if (claim.status !== "active") return claim;
1921
+ const timestamp = nowIso();
1922
+ database.prepare(`
1923
+ update agent_claims set status = 'revoked', revoked_at = ?, revoked_by = ?, override_reason = ?
1924
+ where id = ? and status = 'active'
1925
+ `).run(timestamp, fields.actor || "human", fields.reason || null, claim.id);
1926
+ recordEvent(database, claim.id, "revoked", fields.actor || "human", fields.reason);
1927
+ return findClaimById(database, claim.id, project);
1928
+ }
1929
+ function transferAgentClaim(project, claimId, fields) {
1930
+ if (!fields.confirmWrite) throw new AgentClaimError("Transferring an agent claim requires confirmWrite=true.", 400, "confirm_write_required");
1931
+ const toAgentName = fields.toAgentName.trim();
1932
+ if (!toAgentName) throw new AgentClaimError("Transfer requires toAgentName.", 400, "agent_name_required");
1933
+ const database = lineageDb();
1934
+ try {
1935
+ expireActiveClaims(database);
1936
+ const claim = findClaimById(database, claimId, project);
1937
+ if (!claim) throw new AgentClaimError(`Unknown agent claim: ${claimId}`, 404, "claim_not_found");
1938
+ if (claim.status !== "active") throw new AgentClaimError(`Agent claim is ${claim.status}.`, 409, "claim_not_active");
1939
+ database.prepare("update agent_claims set agent_name = ? where id = ?").run(toAgentName, claim.id);
1940
+ recordEvent(database, claim.id, "transferred", fields.actor || "human", fields.reason || `Transferred claim to ${toAgentName}.`, { to_agent_name: toAgentName });
1941
+ return { ok: true, claim: findClaimById(database, claim.id) };
1942
+ } finally {
1943
+ database.close();
1944
+ }
1945
+ }
1946
+ function recordAgentClaimWriteAllowed(claim, fields) {
1947
+ const database = lineageDb();
1948
+ try {
1949
+ recordEvent(database, claim.id, "write_allowed", claim.agent_name, `${fields.writeKind} allowed.`, {
1950
+ danger_level: fields.dangerLevel,
1951
+ target_id: fields.targetId,
1952
+ write_kind: fields.writeKind
1953
+ });
1954
+ return { ok: true };
1955
+ } finally {
1956
+ database.close();
1957
+ }
1958
+ }
1959
+ function validateAgentClaimForWrite(fields) {
1960
+ if (fields.dangerLevel === "danger" && !fields.confirmWrite) {
1961
+ return denied("human_confirmation_required", "Dangerous write requires explicit human confirmation.");
1962
+ }
1963
+ if (!fields.claimToken) return denied("claim_required", "Mutating agent write requires a matching claim token.");
1964
+ const database = lineageDb();
1965
+ try {
1966
+ expireActiveClaims(database);
1967
+ const row = findClaimRowByToken(database, fields.claimToken);
1968
+ if (!row) return denied("claim_token_invalid", "Unknown or invalid claim token.");
1969
+ const claim = rowToClaim(row);
1970
+ if (claim.status !== "active") return denied("claim_not_active", `Agent claim is ${claim.status}.`);
1971
+ if (new Date(claim.expires_at).getTime() <= Date.now()) return denied("claim_expired", "Agent claim has expired.");
1972
+ if (claim.project !== fields.project) return denied("claim_project_mismatch", `Claim project ${claim.project} does not match ${fields.project}.`, [claim]);
1973
+ if (fields.channel && claim.channel && claim.channel !== fields.channel) {
1974
+ return denied("claim_channel_mismatch", `Claim channel ${claim.channel} does not match ${fields.channel}.`, [claim]);
1975
+ }
1976
+ if (!scopeAllowsWrite(claim, fields.scopeType, fields.targetId, fields.writeKind)) {
1977
+ return denied("claim_scope_mismatch", `Claim does not cover ${fields.scopeType} ${fields.targetId}.`, [claim]);
1978
+ }
1979
+ if (fields.recordEvent !== false) {
1980
+ recordEvent(database, claim.id, "write_allowed", claim.agent_name, `${fields.writeKind} allowed.`, {
1981
+ danger_level: fields.dangerLevel,
1982
+ target_id: fields.targetId,
1983
+ write_kind: fields.writeKind
1984
+ });
1985
+ }
1986
+ return { ok: true, claim, warnings: [] };
1623
1987
  } finally {
1624
1988
  database.close();
1625
1989
  }
1626
1990
  }
1627
1991
 
1628
- // src/server/agentClaims.ts
1629
- import { createHash as createHash2, randomBytes, timingSafeEqual } from "node:crypto";
1630
- var defaultTtlSeconds = 20 * 60;
1631
- var idleAfterSeconds = 5 * 60;
1632
- var staleAfterSeconds = 15 * 60;
1633
- var scopes = /* @__PURE__ */ new Set(["lineage_workspace", "content_post", "content_queue_lane", "selection_set", "project_channel"]);
1634
- var AgentClaimError = class extends Error {
1635
- constructor(message, status = 400, code = "agent_claim_error", conflicts = []) {
1992
+ // src/server/assetLineageTasks.ts
1993
+ var LineageTaskError = class extends Error {
1994
+ constructor(message, status = 400) {
1636
1995
  super(message);
1637
1996
  this.status = status;
1638
- this.code = code;
1639
- this.conflicts = conflicts;
1640
1997
  }
1641
1998
  status;
1642
- code;
1643
- conflicts;
1644
1999
  };
1645
- function isAgentClaimError(error) {
1646
- return error instanceof AgentClaimError;
1647
- }
1648
- function parseClaimTtl(value) {
1649
- if (!value) return defaultTtlSeconds;
1650
- const match = value.trim().match(/^(\d+)(s|m|h)?$/);
1651
- if (!match) throw new AgentClaimError(`Invalid claim ttl: ${value}`);
1652
- const amount = Number(match[1]);
1653
- const unit = match[2] || "s";
1654
- const multiplier = unit === "h" ? 3600 : unit === "m" ? 60 : 1;
1655
- const seconds = amount * multiplier;
1656
- if (!Number.isInteger(seconds) || seconds < 30 || seconds > 24 * 60 * 60) {
1657
- throw new AgentClaimError("Claim ttl must be between 30 seconds and 24 hours");
1658
- }
1659
- return seconds;
1660
- }
1661
- function randomId(prefix) {
1662
- return `${prefix}_${Date.now().toString(36)}_${randomBytes(6).toString("base64url").toLowerCase()}`;
1663
- }
1664
- function tokenHash(token) {
1665
- return createHash2("sha256").update(token).digest("hex");
2000
+ function isLineageTaskError(error) {
2001
+ return error instanceof LineageTaskError;
1666
2002
  }
1667
- function safeEqual(a, b) {
1668
- const left = Buffer.from(a);
1669
- const right = Buffer.from(b);
1670
- return left.length === right.length && timingSafeEqual(left, right);
2003
+ var activeStatuses = ["pending", "claimed", "in_progress"];
2004
+ var taskTypes = /* @__PURE__ */ new Set(["iterate", "reroll"]);
2005
+ var taskStatuses = /* @__PURE__ */ new Set(["pending", "claimed", "in_progress", "resolved", "cancelled"]);
2006
+ function taskIdFor(project, rootAssetId, targetAssetId, taskType) {
2007
+ return `${project}:${rootAssetId}:lineage-task:${taskType}:${targetAssetId}`;
1671
2008
  }
1672
- function expiresAtFrom(timestamp, ttlSeconds) {
1673
- return new Date(new Date(timestamp).getTime() + ttlSeconds * 1e3).toISOString();
2009
+ function randomId2(prefix) {
2010
+ return `${prefix}_${Date.now().toString(36)}_${randomBytes2(6).toString("base64url").toLowerCase()}`;
1674
2011
  }
1675
- function metadataJson(metadata2) {
2012
+ function metadataJson2(metadata2) {
1676
2013
  return metadata2 ? JSON.stringify(metadata2) : null;
1677
2014
  }
1678
- function parseMetadata(value) {
2015
+ function metadataWithoutClaim(metadata2) {
2016
+ if (!metadata2) return void 0;
2017
+ const next = { ...metadata2 };
2018
+ delete next.claim_id;
2019
+ return Object.keys(next).length > 0 ? next : void 0;
2020
+ }
2021
+ function parseMetadata2(value) {
1679
2022
  if (typeof value !== "string" || !value) return void 0;
1680
2023
  try {
1681
2024
  const parsed = JSON.parse(value);
@@ -1684,315 +2027,711 @@ function parseMetadata(value) {
1684
2027
  return void 0;
1685
2028
  }
1686
2029
  }
1687
- function derivedState(row, now = /* @__PURE__ */ new Date()) {
1688
- if (row.status !== "active") return row.status === "expired" ? "expired" : "stale";
1689
- if (new Date(row.expires_at).getTime() <= now.getTime()) return "expired";
1690
- const ageSeconds = Math.max(0, Math.floor((now.getTime() - new Date(row.heartbeat_at).getTime()) / 1e3));
1691
- if (ageSeconds >= staleAfterSeconds) return "stale";
1692
- if (ageSeconds >= idleAfterSeconds) return "idle";
1693
- return "active";
2030
+ function normalizeProject(project) {
2031
+ const trimmed = project.trim();
2032
+ if (!trimmed) throw new LineageTaskError("Lineage task requires project");
2033
+ return trimmed;
1694
2034
  }
1695
- function rowToClaim(row, now = /* @__PURE__ */ new Date()) {
1696
- const heartbeatAt = String(row.heartbeat_at);
2035
+ function normalizeTaskType(taskType) {
2036
+ if (!taskTypes.has(taskType)) throw new LineageTaskError(`Unsupported lineage task type: ${taskType}`);
2037
+ return taskType;
2038
+ }
2039
+ function normalizeStatus(status) {
2040
+ if (!taskStatuses.has(status)) throw new LineageTaskError(`Unsupported lineage task status: ${status}`);
2041
+ return status;
2042
+ }
2043
+ function normalizeActor(actor, label) {
2044
+ const trimmed = actor.trim();
2045
+ if (!trimmed) throw new LineageTaskError(`${label} is required`);
2046
+ return trimmed;
2047
+ }
2048
+ function requireAsset(database, project, assetId) {
2049
+ const row = database.prepare("select id from assets where project_id = ? and id = ?").get(project, assetId);
2050
+ if (!row) throw new LineageTaskError(`Unknown indexed asset: ${assetId}`, 404);
2051
+ }
2052
+ function taskFromRow(row) {
2053
+ const metadata2 = parseMetadata2(row.metadata_json);
2054
+ const claimId = typeof metadata2?.claim_id === "string" ? metadata2.claim_id : void 0;
1697
2055
  return {
1698
2056
  id: String(row.id),
1699
- project: String(row.project_id),
1700
- channel: typeof row.channel === "string" ? row.channel : void 0,
1701
- scope_type: String(row.scope_type),
1702
- target_id: String(row.target_id),
1703
- target_title: typeof row.target_title === "string" ? row.target_title : void 0,
1704
- agent_id: typeof row.agent_id === "string" ? row.agent_id : void 0,
1705
- agent_name: String(row.agent_name),
1706
- agent_kind: String(row.agent_kind),
1707
- thread_id: typeof row.thread_id === "string" ? row.thread_id : void 0,
2057
+ project_id: String(row.project_id),
2058
+ root_asset_id: String(row.root_asset_id),
2059
+ target_asset_id: String(row.target_asset_id),
2060
+ task_type: String(row.task_type),
1708
2061
  status: String(row.status),
2062
+ instructions: typeof row.instructions === "string" ? row.instructions : void 0,
2063
+ created_by: String(row.created_by),
1709
2064
  created_at: String(row.created_at),
1710
- heartbeat_at: heartbeatAt,
1711
- expires_at: String(row.expires_at),
1712
- released_at: typeof row.released_at === "string" ? row.released_at : void 0,
1713
- revoked_at: typeof row.revoked_at === "string" ? row.revoked_at : void 0,
1714
- revoked_by: typeof row.revoked_by === "string" ? row.revoked_by : void 0,
1715
- override_reason: typeof row.override_reason === "string" ? row.override_reason : void 0,
1716
- metadata: parseMetadata(row.metadata_json),
1717
- heartbeat_age_seconds: Math.max(0, Math.floor((now.getTime() - new Date(heartbeatAt).getTime()) / 1e3)),
1718
- derived_state: derivedState({
1719
- expires_at: String(row.expires_at),
1720
- heartbeat_at: heartbeatAt,
1721
- status: String(row.status)
1722
- }, now)
2065
+ updated_at: String(row.updated_at),
2066
+ claimed_at: typeof row.claimed_at === "string" ? row.claimed_at : void 0,
2067
+ started_at: typeof row.started_at === "string" ? row.started_at : void 0,
2068
+ resolved_at: typeof row.resolved_at === "string" ? row.resolved_at : void 0,
2069
+ cancelled_at: typeof row.cancelled_at === "string" ? row.cancelled_at : void 0,
2070
+ resolved_generation_job_id: typeof row.resolved_generation_job_id === "string" ? row.resolved_generation_job_id : void 0,
2071
+ resolved_asset_id: typeof row.resolved_asset_id === "string" ? row.resolved_asset_id : void 0,
2072
+ claimed_by_claim_id: claimId,
2073
+ metadata: metadata2
1723
2074
  };
1724
2075
  }
1725
- function eventToRow(row) {
2076
+ function eventFromRow(row) {
1726
2077
  return {
1727
- claim_id: String(row.claim_id),
2078
+ id: String(row.id),
2079
+ task_id: String(row.task_id),
1728
2080
  event_type: String(row.event_type),
1729
2081
  actor: typeof row.actor === "string" ? row.actor : void 0,
1730
2082
  message: typeof row.message === "string" ? row.message : void 0,
1731
2083
  created_at: String(row.created_at),
1732
- metadata: parseMetadata(row.metadata_json)
2084
+ metadata: parseMetadata2(row.metadata_json)
1733
2085
  };
1734
2086
  }
1735
- function ensureProject3(database, project) {
1736
- const timestamp = nowIso();
1737
- database.prepare(`
1738
- insert into projects (id, product, created_at, updated_at)
1739
- values (?, ?, ?, ?)
1740
- on conflict(id) do update set product = excluded.product, updated_at = excluded.updated_at
1741
- `).run(project, project, timestamp, timestamp);
2087
+ function findTask(database, project, taskId) {
2088
+ const row = database.prepare("select * from lineage_tasks where project_id = ? and id = ?").get(project, taskId);
2089
+ return row ? taskFromRow(row) : null;
1742
2090
  }
1743
- function recordEvent(database, claimId, eventType, actor, message, metadata2) {
2091
+ function requireTask(database, project, taskId) {
2092
+ const task = findTask(database, project, taskId);
2093
+ if (!task) throw new LineageTaskError(`Unknown lineage task: ${taskId}`, 404);
2094
+ return task;
2095
+ }
2096
+ function taskEvents(database, taskId) {
2097
+ const rows = database.prepare("select * from lineage_task_events where task_id = ? order by created_at, rowid").all(taskId);
2098
+ return rows.map(eventFromRow);
2099
+ }
2100
+ function recordEvent2(database, taskId, eventType, actor, message, metadata2) {
1744
2101
  database.prepare(`
1745
- insert into agent_claim_events (id, claim_id, event_type, actor, message, created_at, metadata_json)
2102
+ insert into lineage_task_events (id, task_id, event_type, actor, message, created_at, metadata_json)
1746
2103
  values (?, ?, ?, ?, ?, ?, ?)
1747
- `).run(randomId("claim_event"), claimId, eventType, actor || null, message || null, nowIso(), metadataJson(metadata2));
2104
+ `).run(randomId2("lineage_task_event"), taskId, eventType, actor || null, message || null, nowIso(), metadataJson2(metadata2));
1748
2105
  }
1749
- function expireActiveClaims(database) {
1750
- const timestamp = nowIso();
1751
- const expired = database.prepare(`
1752
- select id from agent_claims where status = 'active' and expires_at <= ?
1753
- `).all(timestamp);
1754
- if (expired.length === 0) return;
1755
- database.prepare(`
1756
- update agent_claims set status = 'expired' where status = 'active' and expires_at <= ?
1757
- `).run(timestamp);
1758
- for (const claim of expired) recordEvent(database, claim.id, "expired", "system", "Claim expired after missed heartbeat.");
2106
+ function taskWithEvents(database, project, taskId) {
2107
+ return { project, ok: true, task: requireTask(database, project, taskId), events: taskEvents(database, taskId) };
1759
2108
  }
1760
- function normalizeScope(scopeType) {
1761
- if (!scopes.has(scopeType)) throw new AgentClaimError(`Unsupported claim scope: ${scopeType}`);
1762
- return scopeType;
2109
+ function assertChanged(result, message) {
2110
+ if (Number(result.changes) !== 1) throw new LineageTaskError(message, 409);
1763
2111
  }
1764
- function channelOverlaps(left, right) {
1765
- return !left || !right || left === right;
2112
+ function transaction(database, callback) {
2113
+ database.exec("BEGIN IMMEDIATE");
2114
+ try {
2115
+ const value = callback();
2116
+ database.exec("COMMIT");
2117
+ return value;
2118
+ } catch (error) {
2119
+ database.exec("ROLLBACK");
2120
+ throw error;
2121
+ }
1766
2122
  }
1767
- function claimOverlaps(candidate, existing) {
1768
- if (candidate.project !== existing.project) return false;
1769
- if (!channelOverlaps(candidate.channel, existing.channel)) return false;
1770
- if (candidate.scope_type === existing.scope_type && candidate.target_id === existing.target_id) return true;
1771
- return candidate.scope_type === "project_channel" || existing.scope_type === "project_channel";
2123
+ function taskReadWithEvents(database, project, taskId) {
2124
+ return { task: requireTask(database, project, taskId), events: taskEvents(database, taskId) };
1772
2125
  }
1773
- function activeClaims(database, project) {
1774
- const rows = project ? database.prepare("select * from agent_claims where project_id = ? and status = 'active' order by heartbeat_at desc").all(project) : database.prepare("select * from agent_claims where status = 'active' order by project_id, heartbeat_at desc").all();
1775
- return rows.map((row) => rowToClaim(row));
2126
+ function listLineageTasks(project, rootAssetId, statuses2 = activeStatuses) {
2127
+ const normalizedProject = normalizeProject(project);
2128
+ const normalizedStatuses = statuses2.map(normalizeStatus);
2129
+ const database = lineageDb();
2130
+ try {
2131
+ requireAsset(database, normalizedProject, rootAssetId);
2132
+ if (normalizedStatuses.length === 0) {
2133
+ return { project: normalizedProject, root_asset_id: rootAssetId, tasks: [], fetchedAt: nowIso() };
2134
+ }
2135
+ const placeholders2 = normalizedStatuses.map(() => "?").join(",");
2136
+ const rows = database.prepare(`
2137
+ select * from lineage_tasks
2138
+ where project_id = ? and root_asset_id = ? and status in (${placeholders2})
2139
+ order by updated_at desc, created_at desc, id
2140
+ `).all(normalizedProject, rootAssetId, ...normalizedStatuses);
2141
+ return { project: normalizedProject, root_asset_id: rootAssetId, tasks: rows.map(taskFromRow), fetchedAt: nowIso() };
2142
+ } finally {
2143
+ database.close();
2144
+ }
1776
2145
  }
1777
- function findClaimById(database, claimId, project) {
1778
- const row = project ? database.prepare("select * from agent_claims where project_id = ? and id = ?").get(project, claimId) : database.prepare("select * from agent_claims where id = ?").get(claimId);
1779
- return row ? rowToClaim(row) : null;
2146
+ function getLineageTask(project, taskId) {
2147
+ const normalizedProject = normalizeProject(project);
2148
+ const database = lineageDb();
2149
+ try {
2150
+ return { project: normalizedProject, ...taskReadWithEvents(database, normalizedProject, taskId) };
2151
+ } finally {
2152
+ database.close();
2153
+ }
1780
2154
  }
1781
- function findClaimRowByToken(database, claimToken) {
1782
- const claimId = claimToken.split(".")[0];
1783
- const row = database.prepare("select * from agent_claims where id = ?").get(claimId);
1784
- if (!row) return null;
1785
- return safeEqual(String(row.token_hash), tokenHash(claimToken)) ? row : null;
2155
+ function upsertLineageTask(project, fields) {
2156
+ const normalizedProject = normalizeProject(project);
2157
+ const taskType = normalizeTaskType(fields.taskType);
2158
+ const taskId = taskIdFor(normalizedProject, fields.rootAssetId, fields.targetAssetId, taskType);
2159
+ const database = lineageDb();
2160
+ try {
2161
+ requireAsset(database, normalizedProject, fields.rootAssetId);
2162
+ requireAsset(database, normalizedProject, fields.targetAssetId);
2163
+ const existing = database.prepare(`
2164
+ select * from lineage_tasks
2165
+ where project_id = ? and root_asset_id = ? and target_asset_id = ? and task_type = ?
2166
+ and status in ('pending', 'claimed', 'in_progress')
2167
+ order by created_at desc limit 1
2168
+ `).get(normalizedProject, fields.rootAssetId, fields.targetAssetId, taskType);
2169
+ const instructions = fields.instructions?.trim() || void 0;
2170
+ if (existing) {
2171
+ const task = taskFromRow(existing);
2172
+ if (task.status !== "pending" && instructions !== void 0 && instructions !== task.instructions) {
2173
+ throw new LineageTaskError("Only pending lineage tasks can update instructions.", 409);
2174
+ }
2175
+ if (task.status === "pending" && instructions !== void 0 && instructions !== task.instructions) {
2176
+ const timestamp2 = nowIso();
2177
+ transaction(database, () => {
2178
+ const result = database.prepare(`
2179
+ update lineage_tasks
2180
+ set instructions = ?, updated_at = ?
2181
+ where id = ? and status = 'pending'
2182
+ `).run(instructions, timestamp2, task.id);
2183
+ assertChanged(result, "Only pending lineage tasks can update instructions.");
2184
+ recordEvent2(database, task.id, "instructions_updated", fields.createdBy, "Instructions updated.");
2185
+ });
2186
+ }
2187
+ return taskWithEvents(database, normalizedProject, task.id);
2188
+ }
2189
+ const closedTask = findTask(database, normalizedProject, taskId);
2190
+ if (closedTask) {
2191
+ const timestamp2 = nowIso();
2192
+ transaction(database, () => {
2193
+ const result = database.prepare(`
2194
+ update lineage_tasks
2195
+ set status = 'pending', instructions = ?, created_by = ?, updated_at = ?,
2196
+ claimed_at = null, started_at = null, resolved_at = null, cancelled_at = null,
2197
+ resolved_generation_job_id = null, resolved_asset_id = null, metadata_json = null
2198
+ where id = ? and status not in ('pending', 'claimed', 'in_progress')
2199
+ `).run(instructions || null, fields.createdBy, timestamp2, taskId);
2200
+ assertChanged(result, "Lineage task changed while reopening.");
2201
+ recordEvent2(database, taskId, "created", fields.createdBy, "Lineage task created.");
2202
+ });
2203
+ return taskWithEvents(database, normalizedProject, taskId);
2204
+ }
2205
+ const timestamp = nowIso();
2206
+ transaction(database, () => {
2207
+ database.prepare(`
2208
+ insert into lineage_tasks (
2209
+ id, project_id, root_asset_id, target_asset_id, task_type, status, instructions,
2210
+ created_by, created_at, updated_at, metadata_json
2211
+ ) values (?, ?, ?, ?, ?, 'pending', ?, ?, ?, ?, null)
2212
+ `).run(taskId, normalizedProject, fields.rootAssetId, fields.targetAssetId, taskType, instructions || null, fields.createdBy, timestamp, timestamp);
2213
+ recordEvent2(database, taskId, "created", fields.createdBy, "Lineage task created.");
2214
+ });
2215
+ return taskWithEvents(database, normalizedProject, taskId);
2216
+ } finally {
2217
+ database.close();
2218
+ }
1786
2219
  }
1787
- function denied(code, message, conflicts = []) {
1788
- return { ok: false, code, message, conflicts };
2220
+ function updateLineageTaskInstructions(project, fields) {
2221
+ const normalizedProject = normalizeProject(project);
2222
+ const instructions = fields.instructions.trim();
2223
+ const database = lineageDb();
2224
+ try {
2225
+ const task = requireTask(database, normalizedProject, fields.taskId);
2226
+ if (task.status !== "pending") throw new LineageTaskError("Only pending lineage tasks can update instructions.", 409);
2227
+ const timestamp = nowIso();
2228
+ transaction(database, () => {
2229
+ const result = database.prepare(`
2230
+ update lineage_tasks
2231
+ set instructions = ?, updated_at = ?
2232
+ where id = ? and status = 'pending'
2233
+ `).run(instructions || null, timestamp, task.id);
2234
+ assertChanged(result, "Only pending lineage tasks can update instructions.");
2235
+ recordEvent2(database, task.id, "instructions_updated", "human", "Instructions updated.");
2236
+ });
2237
+ return taskWithEvents(database, normalizedProject, task.id);
2238
+ } finally {
2239
+ database.close();
2240
+ }
1789
2241
  }
1790
- function scopeAllowsWrite(claim, scopeType, targetId, writeKind) {
1791
- if (claim.scope_type === scopeType && claim.target_id === targetId) return true;
1792
- if (claim.scope_type === "project_channel") return true;
1793
- if (claim.scope_type === "lineage_workspace" && scopeType === "lineage_workspace") return claim.target_id === targetId;
1794
- if (claim.scope_type === "content_queue_lane" && writeKind === "content_queue_next") return true;
1795
- return false;
2242
+ function addLineageTaskComment(project, fields) {
2243
+ const normalizedProject = normalizeProject(project);
2244
+ const actor = normalizeActor(fields.actor, "Comment actor");
2245
+ const message = fields.message.trim();
2246
+ if (!message) throw new LineageTaskError("Comment message is required");
2247
+ const database = lineageDb();
2248
+ try {
2249
+ const task = requireTask(database, normalizedProject, fields.taskId);
2250
+ transaction(database, () => {
2251
+ recordEvent2(database, task.id, "comment_added", actor, message);
2252
+ });
2253
+ return taskWithEvents(database, normalizedProject, task.id);
2254
+ } finally {
2255
+ database.close();
2256
+ }
1796
2257
  }
1797
- function createAgentClaim(fields) {
1798
- const project = fields.project.trim();
1799
- const targetId = fields.targetId.trim();
1800
- const agentName = fields.agentName.trim();
1801
- if (!project) throw new AgentClaimError("Agent claim requires project");
1802
- if (!targetId) throw new AgentClaimError("Agent claim requires target");
1803
- if (!agentName) throw new AgentClaimError("Agent claim requires agent name");
1804
- const scopeType = normalizeScope(fields.scopeType);
1805
- const ttlSeconds = fields.ttlSeconds || defaultTtlSeconds;
2258
+ function claimLineageTask(project, fields) {
2259
+ const normalizedProject = normalizeProject(project);
2260
+ const agentName = normalizeActor(fields.agentName, "Agent name");
2261
+ const beforeClaimDb = lineageDb();
2262
+ try {
2263
+ const task = requireTask(beforeClaimDb, normalizedProject, fields.taskId);
2264
+ if (task.status !== "pending") throw new LineageTaskError("Only pending lineage tasks can be claimed.", 409);
2265
+ } finally {
2266
+ beforeClaimDb.close();
2267
+ }
2268
+ const claimResult = createAgentClaim({
2269
+ agentName,
2270
+ project: normalizedProject,
2271
+ scopeType: "lineage_task",
2272
+ targetId: fields.taskId,
2273
+ targetTitle: fields.taskId
2274
+ });
2275
+ if (!claimResult.claim) throw new LineageTaskError("Unable to create lineage task claim.", 500);
2276
+ const claim = claimResult.claim;
1806
2277
  const database = lineageDb();
1807
2278
  try {
1808
- ensureProject3(database, project);
1809
- expireActiveClaims(database);
1810
- const candidate = { project, channel: fields.channel?.trim() || void 0, scope_type: scopeType, target_id: targetId };
1811
- const conflicts = activeClaims(database, project).filter((claim2) => claimOverlaps(candidate, claim2));
1812
- if (conflicts.length > 0 && !fields.force) {
1813
- throw new AgentClaimError("Target already has an active overlapping agent claim.", 409, "target_already_claimed", conflicts);
1814
- }
1815
- if (conflicts.length > 0 && !fields.reason?.trim()) {
1816
- throw new AgentClaimError("Overriding an active claim requires --reason.", 400, "override_reason_required", conflicts);
2279
+ const task = requireTask(database, normalizedProject, fields.taskId);
2280
+ if (task.status !== "pending") {
2281
+ releaseAgentClaim(claimResult.claim_token);
2282
+ throw new LineageTaskError("Only pending lineage tasks can be claimed.", 409);
1817
2283
  }
1818
2284
  const timestamp = nowIso();
1819
- for (const conflict of conflicts) {
1820
- database.prepare(`
1821
- update agent_claims
1822
- set status = 'revoked', revoked_at = ?, revoked_by = ?, override_reason = ?
1823
- where id = ? and status = 'active'
1824
- `).run(timestamp, agentName, fields.reason || null, conflict.id);
1825
- recordEvent(database, conflict.id, "revoked", agentName, fields.reason || "Revoked by forced claim takeover.");
1826
- recordEvent(database, conflict.id, "conflict", agentName, `Overridden by ${agentName}.`, { new_claim_target: targetId });
2285
+ const metadata2 = { ...task.metadata || {}, claim_id: claim.id };
2286
+ try {
2287
+ transaction(database, () => {
2288
+ const result = database.prepare(`
2289
+ update lineage_tasks
2290
+ set status = 'claimed', claimed_at = ?, updated_at = ?, metadata_json = ?
2291
+ where id = ? and status = 'pending'
2292
+ `).run(timestamp, timestamp, metadataJson2(metadata2), task.id);
2293
+ assertChanged(result, "Only pending lineage tasks can be claimed.");
2294
+ recordEvent2(database, task.id, "claimed", agentName, "Lineage task claimed.", { claim_id: claim.id });
2295
+ });
2296
+ } catch (error) {
2297
+ releaseAgentClaim(claimResult.claim_token);
2298
+ throw error;
1827
2299
  }
1828
- const id = randomId("claim");
1829
- const secret = randomBytes(24).toString("base64url");
1830
- const claimToken = `${id}.${secret}`;
1831
- const expiresAt = expiresAtFrom(timestamp, ttlSeconds);
1832
- database.prepare(`
1833
- insert into agent_claims (
1834
- id, token_hash, project_id, channel, scope_type, target_id, target_title, agent_id, agent_name,
1835
- agent_kind, thread_id, status, created_at, heartbeat_at, expires_at, metadata_json
1836
- ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', ?, ?, ?, ?)
1837
- `).run(
1838
- id,
1839
- tokenHash(claimToken),
1840
- project,
1841
- candidate.channel || null,
1842
- scopeType,
1843
- targetId,
1844
- fields.targetTitle?.trim() || null,
1845
- fields.agentId?.trim() || null,
1846
- agentName,
1847
- fields.agentKind?.trim() || "codex",
1848
- fields.threadId?.trim() || null,
1849
- timestamp,
1850
- timestamp,
1851
- expiresAt,
1852
- metadataJson(fields.metadata)
1853
- );
1854
- recordEvent(database, id, "created", agentName, `Claimed ${scopeType} ${targetId}.`, { ttl_seconds: ttlSeconds });
1855
- const claim = findClaimById(database, id);
1856
- return { ok: true, claim, claim_token: claimToken, conflicts_revoked: conflicts.map((conflict) => conflict.id) };
2300
+ return {
2301
+ ...taskWithEvents(database, normalizedProject, task.id),
2302
+ claim,
2303
+ claim_token: claimResult.claim_token
2304
+ };
1857
2305
  } finally {
1858
2306
  database.close();
1859
2307
  }
1860
2308
  }
1861
- function listAgentClaims(project) {
2309
+ function startLineageTask(project, fields) {
2310
+ const normalizedProject = normalizeProject(project);
2311
+ const precheckDatabase = lineageDb();
2312
+ try {
2313
+ const task = requireTask(precheckDatabase, normalizedProject, fields.taskId);
2314
+ if (task.status !== "claimed") throw new LineageTaskError("Only claimed lineage tasks can be started.", 409);
2315
+ } finally {
2316
+ precheckDatabase.close();
2317
+ }
2318
+ const validation = validateAgentClaimForWrite({
2319
+ claimToken: fields.claimToken,
2320
+ dangerLevel: "enforce",
2321
+ project: normalizedProject,
2322
+ recordEvent: false,
2323
+ scopeType: "lineage_task",
2324
+ targetId: fields.taskId,
2325
+ writeKind: "lineage_task_start"
2326
+ });
2327
+ if (!validation.ok) throw new LineageTaskError(validation.message, 409);
1862
2328
  const database = lineageDb();
1863
2329
  try {
1864
- expireActiveClaims(database);
1865
- const rows = project ? database.prepare("select * from agent_claims where project_id = ? order by status, heartbeat_at desc").all(project) : database.prepare("select * from agent_claims order by project_id, status, heartbeat_at desc").all();
1866
- return { ok: true, claims: rows.map((row) => rowToClaim(row)), fetchedAt: nowIso() };
2330
+ const task = requireTask(database, normalizedProject, fields.taskId);
2331
+ if (task.status !== "claimed") throw new LineageTaskError("Only claimed lineage tasks can be started.", 409);
2332
+ if (task.claimed_by_claim_id && task.claimed_by_claim_id !== validation.claim.id) {
2333
+ throw new LineageTaskError("Claim token does not match the task claim.", 409);
2334
+ }
2335
+ const timestamp = nowIso();
2336
+ const metadata2 = { ...task.metadata || {}, claim_id: validation.claim.id };
2337
+ transaction(database, () => {
2338
+ const result = database.prepare(`
2339
+ update lineage_tasks
2340
+ set status = 'in_progress', started_at = ?, updated_at = ?, metadata_json = ?
2341
+ where id = ? and status = 'claimed'
2342
+ `).run(timestamp, timestamp, metadataJson2(metadata2), task.id);
2343
+ assertChanged(result, "Only claimed lineage tasks can be started.");
2344
+ recordEvent2(database, task.id, "started", validation.claim.agent_name, "Lineage task started.", { claim_id: validation.claim.id });
2345
+ });
2346
+ recordAgentClaimWriteAllowed(validation.claim, {
2347
+ dangerLevel: "enforce",
2348
+ targetId: fields.taskId,
2349
+ writeKind: "lineage_task_start"
2350
+ });
2351
+ return taskWithEvents(database, normalizedProject, task.id);
1867
2352
  } finally {
1868
2353
  database.close();
1869
2354
  }
1870
2355
  }
1871
- function inspectAgentClaim(claimId, project) {
2356
+ function overrideLineageTask(project, fields) {
2357
+ const normalizedProject = normalizeProject(project);
2358
+ const actor = normalizeActor(fields.actor, "Override actor");
2359
+ const reason = fields.reason.trim();
2360
+ if (!reason) throw new LineageTaskError("Override reason is required");
1872
2361
  const database = lineageDb();
1873
2362
  try {
1874
- expireActiveClaims(database);
1875
- const claim = findClaimById(database, claimId, project);
1876
- if (!claim) throw new AgentClaimError(`Unknown agent claim: ${claimId}`, 404, "claim_not_found");
1877
- const events = database.prepare("select * from agent_claim_events where claim_id = ? order by created_at").all(claim.id);
1878
- return { ok: true, claim, events: events.map(eventToRow) };
2363
+ const task = requireTask(database, normalizedProject, fields.taskId);
2364
+ if (!["claimed", "in_progress"].includes(task.status)) {
2365
+ throw new LineageTaskError(`Only claimed or in-progress lineage tasks can be overridden; task is ${task.status}.`, 409);
2366
+ }
2367
+ const timestamp = nowIso();
2368
+ const instructions = fields.instructions === void 0 ? task.instructions : fields.instructions.trim() || void 0;
2369
+ const metadata2 = metadataWithoutClaim(task.metadata);
2370
+ transaction(database, () => {
2371
+ const result = database.prepare(`
2372
+ update lineage_tasks
2373
+ set status = 'pending', instructions = ?, claimed_at = null, started_at = null,
2374
+ updated_at = ?, metadata_json = ?
2375
+ where project_id = ? and id = ? and status in ('claimed', 'in_progress')
2376
+ `).run(instructions || null, timestamp, metadataJson2(metadata2), normalizedProject, task.id);
2377
+ assertChanged(result, `Only active lineage task ${task.id} could be overridden.`);
2378
+ if (task.claimed_by_claim_id) {
2379
+ revokeAgentClaimInDatabase(database, normalizedProject, task.claimed_by_claim_id, {
2380
+ actor,
2381
+ reason
2382
+ });
2383
+ }
2384
+ recordEvent2(database, task.id, "human_override", actor, reason, {
2385
+ previous_claim_id: task.claimed_by_claim_id,
2386
+ previous_status: task.status
2387
+ });
2388
+ });
2389
+ return taskWithEvents(database, normalizedProject, task.id);
1879
2390
  } finally {
1880
2391
  database.close();
1881
2392
  }
1882
2393
  }
1883
- function heartbeatAgentClaim(claimToken, ttlSeconds = defaultTtlSeconds) {
2394
+ function cancelLineageTask(project, fields) {
2395
+ const normalizedProject = normalizeProject(project);
2396
+ const actor = normalizeActor(fields.actor, "Cancel actor");
1884
2397
  const database = lineageDb();
1885
2398
  try {
1886
- expireActiveClaims(database);
1887
- const row = findClaimRowByToken(database, claimToken);
1888
- if (!row) throw new AgentClaimError("Unknown or invalid agent claim token.", 401, "claim_token_invalid");
1889
- const claim = rowToClaim(row);
1890
- if (claim.status !== "active") throw new AgentClaimError(`Agent claim is ${claim.status}.`, 409, "claim_not_active");
2399
+ const task = requireTask(database, normalizedProject, fields.taskId);
2400
+ if (task.status === "cancelled") return { project: normalizedProject, ok: true, task, events: taskEvents(database, task.id) };
2401
+ if (!["pending", "claimed", "in_progress"].includes(task.status)) {
2402
+ throw new LineageTaskError(`Only open lineage tasks can be cancelled; task is ${task.status}.`, 409);
2403
+ }
2404
+ if (task.status !== "pending" && !fields.override) {
2405
+ throw new LineageTaskError("Cancelling an active lineage task requires override=true.", 409);
2406
+ }
2407
+ const overridingActiveTask = fields.override === true && task.status !== "pending";
2408
+ const metadata2 = overridingActiveTask ? metadataWithoutClaim(task.metadata) : task.metadata;
1891
2409
  const timestamp = nowIso();
1892
- database.prepare("update agent_claims set heartbeat_at = ?, expires_at = ? where id = ?").run(timestamp, expiresAtFrom(timestamp, ttlSeconds), claim.id);
1893
- recordEvent(database, claim.id, "heartbeat", claim.agent_name, "Claim heartbeat received.");
1894
- return { ok: true, claim: findClaimById(database, claim.id) };
2410
+ const cancelledTask = {
2411
+ ...task,
2412
+ cancelled_at: timestamp,
2413
+ claimed_at: overridingActiveTask ? void 0 : task.claimed_at,
2414
+ claimed_by_claim_id: overridingActiveTask ? void 0 : task.claimed_by_claim_id,
2415
+ started_at: overridingActiveTask ? void 0 : task.started_at,
2416
+ status: "cancelled",
2417
+ updated_at: timestamp,
2418
+ metadata: metadata2
2419
+ };
2420
+ if (!fields.confirmWrite) return { project: normalizedProject, ok: true, dryRun: true, task: cancelledTask, events: taskEvents(database, task.id) };
2421
+ transaction(database, () => {
2422
+ const result = database.prepare(`
2423
+ update lineage_tasks
2424
+ set status = 'cancelled', cancelled_at = ?, claimed_at = ?, started_at = ?,
2425
+ updated_at = ?, metadata_json = ?
2426
+ where id = ? and status = ?
2427
+ `).run(
2428
+ timestamp,
2429
+ overridingActiveTask ? null : task.claimed_at || null,
2430
+ overridingActiveTask ? null : task.started_at || null,
2431
+ timestamp,
2432
+ metadataJson2(metadata2),
2433
+ task.id,
2434
+ task.status
2435
+ );
2436
+ assertChanged(result, `Only ${task.status} lineage task ${task.id} could be cancelled.`);
2437
+ if (overridingActiveTask) {
2438
+ if (task.claimed_by_claim_id) {
2439
+ revokeAgentClaimInDatabase(database, normalizedProject, task.claimed_by_claim_id, {
2440
+ actor,
2441
+ reason: "Lineage task cancelled by human override."
2442
+ });
2443
+ }
2444
+ recordEvent2(database, task.id, "human_override", actor, "Lineage task cancelled by human override.", {
2445
+ previous_claim_id: task.claimed_by_claim_id,
2446
+ previous_status: task.status
2447
+ });
2448
+ }
2449
+ recordEvent2(database, task.id, "cancelled", actor, "Lineage task cancelled.");
2450
+ });
2451
+ return taskWithEvents(database, normalizedProject, task.id);
2452
+ } finally {
2453
+ database.close();
2454
+ }
2455
+ }
2456
+ function cancelLineageIterateTasksForAssets(project, fields) {
2457
+ const targetIds = fields.assetIds ? new Set(fields.assetIds) : void 0;
2458
+ return listLineageTasks(project, fields.rootAssetId, ["pending"]).tasks.filter((task) => task.task_type === "iterate").filter((task) => !targetIds || targetIds.has(task.target_asset_id)).map((task) => cancelLineageTask(project, {
2459
+ actor: fields.actor,
2460
+ confirmWrite: fields.confirmWrite,
2461
+ taskId: task.id
2462
+ }));
2463
+ }
2464
+
2465
+ // src/server/assetLineageWorkspaces.ts
2466
+ var actors = /* @__PURE__ */ new Set(["human", "agent", "system"]);
2467
+ var statuses = /* @__PURE__ */ new Set(["active", "paused", "archived"]);
2468
+ var LineageWorkspaceError = class extends Error {
2469
+ constructor(message, status = 400) {
2470
+ super(message);
2471
+ this.status = status;
2472
+ }
2473
+ status;
2474
+ };
2475
+ function isLineageWorkspaceError(error) {
2476
+ return error instanceof LineageWorkspaceError;
2477
+ }
2478
+ function lineageWorkspaceId(project, rootAssetId) {
2479
+ return `${project}:lineage-workspace:${rootAssetId}`;
2480
+ }
2481
+ function ensureProject3(database, project) {
2482
+ const timestamp = nowIso();
2483
+ database.prepare(`
2484
+ insert into projects (id, product, created_at, updated_at)
2485
+ values (?, ?, ?, ?)
2486
+ on conflict(id) do update set product = excluded.product, updated_at = excluded.updated_at
2487
+ `).run(project, project, timestamp, timestamp);
2488
+ }
2489
+ function normalizeStatus2(value, fallback) {
2490
+ const status = value || fallback;
2491
+ if (!statuses.has(status)) throw new LineageWorkspaceError(`Unsupported lineage workspace status: ${status}`);
2492
+ return status;
2493
+ }
2494
+ function normalizeActor2(value) {
2495
+ const actor = value || "human";
2496
+ if (!actors.has(actor)) throw new LineageWorkspaceError(`Unsupported lineage workspace actor: ${actor}`);
2497
+ return actor;
2498
+ }
2499
+ function requireAsset2(database, project, assetId) {
2500
+ const row = database.prepare("select id, title from assets where project_id = ? and id = ?").get(project, assetId);
2501
+ if (!row) throw new LineageWorkspaceError(`Unknown indexed asset: ${assetId}`, 404);
2502
+ return row;
2503
+ }
2504
+ function rowToWorkspace(row) {
2505
+ return {
2506
+ id: String(row.id),
2507
+ project: String(row.project_id),
2508
+ root_asset_id: String(row.root_asset_id),
2509
+ title: String(row.title),
2510
+ status: String(row.status),
2511
+ notes: typeof row.notes === "string" ? row.notes : void 0,
2512
+ created_by: String(row.created_by),
2513
+ active_at: typeof row.active_at === "string" ? row.active_at : void 0,
2514
+ created_at: String(row.created_at),
2515
+ updated_at: String(row.updated_at)
2516
+ };
2517
+ }
2518
+ function workspaceById(database, project, id) {
2519
+ const row = database.prepare("select * from lineage_workspaces where project_id = ? and id = ?").get(project, id);
2520
+ return row ? rowToWorkspace(row) : null;
2521
+ }
2522
+ function workspaceByRoot(database, project, rootAssetId) {
2523
+ const row = database.prepare("select * from lineage_workspaces where project_id = ? and root_asset_id = ?").get(project, rootAssetId);
2524
+ return row ? rowToWorkspace(row) : null;
2525
+ }
2526
+ function knownRoots(database, project) {
2527
+ const rows = database.prepare(`
2528
+ select root_asset_id, max(selected_at) selected_at from asset_selections where project_id = ? group by root_asset_id
2529
+ union
2530
+ select root_asset_id, null selected_at from asset_layouts where project_id = ? group by root_asset_id
2531
+ union
2532
+ select parent_asset_id root_asset_id, null selected_at
2533
+ from asset_edges
2534
+ where project_id = ?
2535
+ and parent_asset_id not in (select child_asset_id from asset_edges where project_id = ?)
2536
+ group by parent_asset_id
2537
+ `).all(project, project, project, project);
2538
+ return rows.map((row) => ({ root_asset_id: row.root_asset_id, selected_at: row.selected_at || void 0 }));
2539
+ }
2540
+ function seedLegacyWorkspaces(database, project) {
2541
+ ensureProject3(database, project);
2542
+ const timestamp = nowIso();
2543
+ const statement = database.prepare(`
2544
+ insert into lineage_workspaces (
2545
+ id, project_id, root_asset_id, title, status, notes, created_by, active_at, created_at, updated_at
2546
+ )
2547
+ select ?, ?, a.id, a.title || ' lineage', 'active', null, 'system', ?, ?, ?
2548
+ from assets a
2549
+ where a.project_id = ? and a.id = ?
2550
+ on conflict(project_id, root_asset_id) do nothing
2551
+ `);
2552
+ for (const root of knownRoots(database, project)) {
2553
+ statement.run(
2554
+ lineageWorkspaceId(project, root.root_asset_id),
2555
+ project,
2556
+ root.selected_at || null,
2557
+ timestamp,
2558
+ timestamp,
2559
+ project,
2560
+ root.root_asset_id
2561
+ );
2562
+ }
2563
+ }
2564
+ function listRows(database, project) {
2565
+ return database.prepare(`
2566
+ select * from lineage_workspaces
2567
+ where project_id = ?
2568
+ order by
2569
+ case status when 'active' then 0 when 'paused' then 1 else 2 end,
2570
+ active_at desc nulls last,
2571
+ updated_at desc,
2572
+ title
2573
+ `).all(project).map(rowToWorkspace);
2574
+ }
2575
+ function activeWorkspace(database, project) {
2576
+ const row = database.prepare(`
2577
+ select * from lineage_workspaces
2578
+ where project_id = ? and status != 'archived'
2579
+ order by active_at desc nulls last, updated_at desc
2580
+ limit 1
2581
+ `).get(project);
2582
+ return row ? rowToWorkspace(row) : null;
2583
+ }
2584
+ function listLineageWorkspaces(project) {
2585
+ const database = lineageDb();
2586
+ try {
2587
+ seedLegacyWorkspaces(database, project);
2588
+ return {
2589
+ project,
2590
+ active_workspace: activeWorkspace(database, project),
2591
+ workspaces: listRows(database, project),
2592
+ fetchedAt: nowIso()
2593
+ };
1895
2594
  } finally {
1896
2595
  database.close();
1897
2596
  }
1898
2597
  }
1899
- function releaseAgentClaim(claimToken) {
2598
+ function inspectLineageWorkspace(project, workspaceId) {
1900
2599
  const database = lineageDb();
1901
2600
  try {
1902
- expireActiveClaims(database);
1903
- const row = findClaimRowByToken(database, claimToken);
1904
- if (!row) throw new AgentClaimError("Unknown or invalid agent claim token.", 401, "claim_token_invalid");
1905
- const claim = rowToClaim(row);
1906
- if (claim.status !== "active") throw new AgentClaimError(`Agent claim is ${claim.status}.`, 409, "claim_not_active");
1907
- const timestamp = nowIso();
1908
- database.prepare("update agent_claims set status = 'released', released_at = ? where id = ?").run(timestamp, claim.id);
1909
- recordEvent(database, claim.id, "released", claim.agent_name, "Claim released by token holder.");
1910
- return { ok: true, claim: findClaimById(database, claim.id) };
2601
+ seedLegacyWorkspaces(database, project);
2602
+ const workspace = workspaceById(database, project, workspaceId) || workspaceByRoot(database, project, workspaceId);
2603
+ if (!workspace) throw new LineageWorkspaceError(`Unknown lineage workspace: ${workspaceId}`, 404);
2604
+ return workspace;
1911
2605
  } finally {
1912
2606
  database.close();
1913
2607
  }
1914
2608
  }
1915
- function releaseStaleAgentClaim(project, claimId, fields) {
1916
- if (!fields.confirmWrite) throw new AgentClaimError("Releasing a stale agent claim requires confirmWrite=true.", 400, "confirm_write_required");
1917
- if (!fields.reason?.trim()) throw new AgentClaimError("Releasing a stale agent claim requires a reason.", 400, "reason_required");
2609
+ function createLineageWorkspace(project, fields) {
2610
+ const rootAssetId = fields.rootAssetId.trim();
2611
+ if (!rootAssetId) throw new LineageWorkspaceError("Lineage workspace requires rootAssetId");
2612
+ const status = normalizeStatus2(fields.status, "active");
2613
+ const actor = normalizeActor2(fields.createdBy);
1918
2614
  const database = lineageDb();
1919
2615
  try {
1920
- expireActiveClaims(database);
1921
- const claim = findClaimById(database, claimId, project);
1922
- if (!claim) throw new AgentClaimError(`Unknown agent claim: ${claimId}`, 404, "claim_not_found");
1923
- if (claim.status !== "active" || claim.derived_state !== "stale") {
1924
- throw new AgentClaimError("Only stale active claims can be released without the claim token.", 409, "claim_not_stale", [claim]);
1925
- }
2616
+ const root = requireAsset2(database, project, rootAssetId);
1926
2617
  const timestamp = nowIso();
1927
- database.prepare("update agent_claims set status = 'released', released_at = ?, revoked_by = ?, override_reason = ? where id = ?").run(timestamp, fields.actor || "human", fields.reason, claim.id);
1928
- recordEvent(database, claim.id, "released", fields.actor || "human", fields.reason);
1929
- return { ok: true, claim: findClaimById(database, claim.id) };
2618
+ const workspace = {
2619
+ id: lineageWorkspaceId(project, rootAssetId),
2620
+ project,
2621
+ root_asset_id: rootAssetId,
2622
+ title: fields.title?.trim() || `${root.title} lineage`,
2623
+ status,
2624
+ notes: fields.notes?.trim() || void 0,
2625
+ created_by: actor,
2626
+ active_at: fields.activate !== false && status !== "archived" ? timestamp : void 0,
2627
+ created_at: timestamp,
2628
+ updated_at: timestamp
2629
+ };
2630
+ if (!fields.confirmWrite) return { ok: true, dryRun: true, workspace };
2631
+ ensureProject3(database, project);
2632
+ database.prepare(`
2633
+ insert into lineage_workspaces (
2634
+ id, project_id, root_asset_id, title, status, notes, created_by, active_at, created_at, updated_at
2635
+ ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2636
+ on conflict(project_id, root_asset_id) do update set
2637
+ title = excluded.title,
2638
+ status = excluded.status,
2639
+ notes = excluded.notes,
2640
+ active_at = coalesce(excluded.active_at, lineage_workspaces.active_at),
2641
+ updated_at = excluded.updated_at
2642
+ `).run(
2643
+ workspace.id,
2644
+ project,
2645
+ workspace.root_asset_id,
2646
+ workspace.title,
2647
+ workspace.status,
2648
+ workspace.notes || null,
2649
+ workspace.created_by,
2650
+ workspace.active_at || null,
2651
+ workspace.created_at,
2652
+ workspace.updated_at
2653
+ );
2654
+ return {
2655
+ ok: true,
2656
+ message: `Saved lineage workspace ${workspace.title}`,
2657
+ workspace: workspaceById(database, project, workspace.id)
2658
+ };
1930
2659
  } finally {
1931
2660
  database.close();
1932
2661
  }
1933
2662
  }
1934
- function revokeAgentClaim(project, claimId, fields) {
1935
- if (!fields.confirmWrite) throw new AgentClaimError("Revoking an agent claim requires confirmWrite=true.", 400, "confirm_write_required");
1936
- if (!fields.reason?.trim()) throw new AgentClaimError("Revoking an agent claim requires a reason.", 400, "reason_required");
2663
+ function updateLineageWorkspace(project, workspaceId, fields) {
1937
2664
  const database = lineageDb();
1938
2665
  try {
1939
- expireActiveClaims(database);
1940
- const claim = findClaimById(database, claimId, project);
1941
- if (!claim) throw new AgentClaimError(`Unknown agent claim: ${claimId}`, 404, "claim_not_found");
2666
+ seedLegacyWorkspaces(database, project);
2667
+ const current = workspaceById(database, project, workspaceId) || workspaceByRoot(database, project, workspaceId);
2668
+ if (!current) throw new LineageWorkspaceError(`Unknown lineage workspace: ${workspaceId}`, 404);
1942
2669
  const timestamp = nowIso();
2670
+ const next = {
2671
+ ...current,
2672
+ title: fields.title?.trim() || current.title,
2673
+ status: normalizeStatus2(fields.status, current.status),
2674
+ notes: fields.notes === void 0 ? current.notes : fields.notes.trim() || void 0,
2675
+ active_at: fields.activate ? timestamp : current.active_at,
2676
+ updated_at: timestamp
2677
+ };
2678
+ if (!fields.confirmWrite) return { ok: true, dryRun: true, workspace: next };
1943
2679
  database.prepare(`
1944
- update agent_claims set status = 'revoked', revoked_at = ?, revoked_by = ?, override_reason = ?
1945
- where id = ?
1946
- `).run(timestamp, fields.actor || "human", fields.reason, claim.id);
1947
- recordEvent(database, claim.id, "revoked", fields.actor || "human", fields.reason);
1948
- return { ok: true, claim: findClaimById(database, claim.id) };
2680
+ update lineage_workspaces
2681
+ set title = ?, status = ?, notes = ?, active_at = ?, updated_at = ?
2682
+ where project_id = ? and id = ?
2683
+ `).run(next.title, next.status, next.notes || null, next.active_at || null, timestamp, project, current.id);
2684
+ return {
2685
+ ok: true,
2686
+ message: `Updated lineage workspace ${next.title}`,
2687
+ workspace: workspaceById(database, project, current.id)
2688
+ };
1949
2689
  } finally {
1950
2690
  database.close();
1951
2691
  }
1952
2692
  }
1953
- function transferAgentClaim(project, claimId, fields) {
1954
- if (!fields.confirmWrite) throw new AgentClaimError("Transferring an agent claim requires confirmWrite=true.", 400, "confirm_write_required");
1955
- const toAgentName = fields.toAgentName.trim();
1956
- if (!toAgentName) throw new AgentClaimError("Transfer requires toAgentName.", 400, "agent_name_required");
2693
+ function activateLineageWorkspace(project, workspaceId, confirmWrite) {
2694
+ return updateLineageWorkspace(project, workspaceId, { activate: true, status: "active", confirmWrite });
2695
+ }
2696
+ function archiveLineageWorkspace(project, workspaceId, confirmWrite) {
1957
2697
  const database = lineageDb();
1958
2698
  try {
1959
- expireActiveClaims(database);
1960
- const claim = findClaimById(database, claimId, project);
1961
- if (!claim) throw new AgentClaimError(`Unknown agent claim: ${claimId}`, 404, "claim_not_found");
1962
- if (claim.status !== "active") throw new AgentClaimError(`Agent claim is ${claim.status}.`, 409, "claim_not_active");
1963
- database.prepare("update agent_claims set agent_name = ? where id = ?").run(toAgentName, claim.id);
1964
- recordEvent(database, claim.id, "transferred", fields.actor || "human", fields.reason || `Transferred claim to ${toAgentName}.`, { to_agent_name: toAgentName });
1965
- return { ok: true, claim: findClaimById(database, claim.id) };
2699
+ seedLegacyWorkspaces(database, project);
2700
+ const current = workspaceById(database, project, workspaceId) || workspaceByRoot(database, project, workspaceId);
2701
+ if (!current) throw new LineageWorkspaceError(`Unknown lineage workspace: ${workspaceId}`, 404);
2702
+ const timestamp = nowIso();
2703
+ const next = {
2704
+ ...current,
2705
+ status: "archived",
2706
+ active_at: void 0,
2707
+ updated_at: timestamp
2708
+ };
2709
+ if (!confirmWrite) return { ok: true, dryRun: true, workspace: next };
2710
+ cancelLineageIterateTasksForAssets(project, {
2711
+ actor: "human",
2712
+ confirmWrite: true,
2713
+ rootAssetId: current.root_asset_id
2714
+ });
2715
+ database.prepare(`
2716
+ update lineage_workspaces
2717
+ set status = 'archived', active_at = null, updated_at = ?
2718
+ where project_id = ? and id = ?
2719
+ `).run(timestamp, project, current.id);
2720
+ database.prepare("delete from asset_selections where project_id = ? and root_asset_id = ?").run(project, current.root_asset_id);
2721
+ return {
2722
+ ok: true,
2723
+ message: `Archived lineage workspace ${current.title}`,
2724
+ workspace: workspaceById(database, project, current.id)
2725
+ };
1966
2726
  } finally {
1967
2727
  database.close();
1968
2728
  }
1969
2729
  }
1970
- function validateAgentClaimForWrite(fields) {
1971
- if (fields.dangerLevel === "danger" && !fields.confirmWrite) {
1972
- return denied("human_confirmation_required", "Dangerous write requires explicit human confirmation.");
1973
- }
1974
- if (!fields.claimToken) return denied("claim_required", "Mutating agent write requires a matching claim token.");
2730
+ function activeLineageWorkspaceRoot(project) {
1975
2731
  const database = lineageDb();
1976
2732
  try {
1977
- expireActiveClaims(database);
1978
- const row = findClaimRowByToken(database, fields.claimToken);
1979
- if (!row) return denied("claim_token_invalid", "Unknown or invalid claim token.");
1980
- const claim = rowToClaim(row);
1981
- if (claim.status !== "active") return denied("claim_not_active", `Agent claim is ${claim.status}.`);
1982
- if (new Date(claim.expires_at).getTime() <= Date.now()) return denied("claim_expired", "Agent claim has expired.");
1983
- if (claim.project !== fields.project) return denied("claim_project_mismatch", `Claim project ${claim.project} does not match ${fields.project}.`, [claim]);
1984
- if (fields.channel && claim.channel && claim.channel !== fields.channel) {
1985
- return denied("claim_channel_mismatch", `Claim channel ${claim.channel} does not match ${fields.channel}.`, [claim]);
1986
- }
1987
- if (!scopeAllowsWrite(claim, fields.scopeType, fields.targetId, fields.writeKind)) {
1988
- return denied("claim_scope_mismatch", `Claim does not cover ${fields.scopeType} ${fields.targetId}.`, [claim]);
1989
- }
1990
- recordEvent(database, claim.id, "write_allowed", claim.agent_name, `${fields.writeKind} allowed.`, {
1991
- danger_level: fields.dangerLevel,
1992
- target_id: fields.targetId,
1993
- write_kind: fields.writeKind
1994
- });
1995
- return { ok: true, claim, warnings: [] };
2733
+ seedLegacyWorkspaces(database, project);
2734
+ return activeWorkspace(database, project)?.root_asset_id;
1996
2735
  } finally {
1997
2736
  database.close();
1998
2737
  }
@@ -2104,7 +2843,7 @@ function indexLineageAssets(project = defaultProject) {
2104
2843
  database.close();
2105
2844
  return { catalog: catalog.length, local: local.length, total: catalog.length + local.length, database: lineageDbPath() };
2106
2845
  }
2107
- function requireAsset2(database, project, assetId) {
2846
+ function requireAsset3(database, project, assetId) {
2108
2847
  const row = database.prepare("select id from assets where project_id = ? and id = ?").get(project, assetId);
2109
2848
  if (!row) throw new LineageError(`Unknown indexed asset: ${assetId}`, 404);
2110
2849
  }
@@ -2124,7 +2863,7 @@ function rootFor(database, project, assetId) {
2124
2863
  return assetId;
2125
2864
  }
2126
2865
  function assertCanonicalRoot(database, project, root) {
2127
- requireAsset2(database, project, root);
2866
+ requireAsset3(database, project, root);
2128
2867
  const canonicalRoot = rootFor(database, project, root);
2129
2868
  if (canonicalRoot !== root) throw new LineageError(`Asset ${root} is not a lineage root`, 400);
2130
2869
  }
@@ -2161,7 +2900,7 @@ function lineageWriteClaimContext(database, project, assetId) {
2161
2900
  function getLineageWriteClaimContext(project, assetId) {
2162
2901
  const database = lineageDb();
2163
2902
  try {
2164
- requireAsset2(database, project, assetId);
2903
+ requireAsset3(database, project, assetId);
2165
2904
  return lineageWriteClaimContext(database, project, assetId);
2166
2905
  } finally {
2167
2906
  database.close();
@@ -2173,12 +2912,12 @@ function latestSelectedRoot(database, project) {
2173
2912
  }
2174
2913
  function resolveRoot(database, project, rootAssetId) {
2175
2914
  if (rootAssetId) {
2176
- requireAsset2(database, project, rootAssetId);
2915
+ requireAsset3(database, project, rootAssetId);
2177
2916
  return rootAssetId;
2178
2917
  }
2179
2918
  const root = activeLineageWorkspaceRoot(project) || latestSelectedRoot(database, project);
2180
2919
  if (!root) throw new LineageError("Lineage command requires --root unless a project selection exists");
2181
- requireAsset2(database, project, root);
2920
+ requireAsset3(database, project, root);
2182
2921
  return root;
2183
2922
  }
2184
2923
  function edgeId(project, parent, child) {
@@ -2203,6 +2942,33 @@ function rerollRequestFrom(row) {
2203
2942
  resolved_at: rowString(row.resolved_at)
2204
2943
  };
2205
2944
  }
2945
+ function tasksByTarget(tasks) {
2946
+ const byTarget = /* @__PURE__ */ new Map();
2947
+ for (const task of tasks) {
2948
+ if (task.task_type !== "iterate" && task.task_type !== "reroll") continue;
2949
+ const existing = byTarget.get(task.target_asset_id) || {};
2950
+ existing[task.task_type] = task;
2951
+ byTarget.set(task.target_asset_id, existing);
2952
+ }
2953
+ return byTarget;
2954
+ }
2955
+ function withRerollTask(request, task) {
2956
+ return task ? { ...request, task_id: task.id, task } : request;
2957
+ }
2958
+ function taskBackedRerollRequest(project, rootAssetId, task) {
2959
+ return {
2960
+ id: `${task.id}:request`,
2961
+ project_id: project,
2962
+ root_asset_id: rootAssetId,
2963
+ node_asset_id: task.target_asset_id,
2964
+ status: "pending",
2965
+ requested_by: task.created_by,
2966
+ notes: task.instructions,
2967
+ created_at: task.created_at,
2968
+ task_id: task.id,
2969
+ task
2970
+ };
2971
+ }
2206
2972
  function attemptFrom(row) {
2207
2973
  return {
2208
2974
  id: String(row.id),
@@ -2242,7 +3008,7 @@ function withImplicitAttempt(physicalAttempts, row) {
2242
3008
  }
2243
3009
  function assertNodeInRoot(database, project, root, node) {
2244
3010
  assertCanonicalRoot(database, project, root);
2245
- requireAsset2(database, project, node);
3011
+ requireAsset3(database, project, node);
2246
3012
  const nodeRoot = rootFor(database, project, node);
2247
3013
  if (nodeRoot !== root) throw new LineageError(`Asset ${node} is not in lineage rooted at ${root}`);
2248
3014
  }
@@ -2256,8 +3022,8 @@ function localPreviewUrl(project, localPath) {
2256
3022
  }
2257
3023
  function linkLineageAssets(project, fields) {
2258
3024
  const database = lineageDb();
2259
- requireAsset2(database, project, fields.parentAssetId);
2260
- requireAsset2(database, project, fields.childAssetId);
3025
+ requireAsset3(database, project, fields.parentAssetId);
3026
+ requireAsset3(database, project, fields.childAssetId);
2261
3027
  if (fields.parentAssetId === fields.childAssetId) throw new LineageError("Lineage link cannot point to itself");
2262
3028
  const claimContext = lineageWriteClaimContext(database, project, fields.parentAssetId);
2263
3029
  try {
@@ -2308,10 +3074,17 @@ function descendants(database, project, root) {
2308
3074
  }
2309
3075
  function getLineageSnapshot(project, assetId) {
2310
3076
  const database = lineageDb();
2311
- requireAsset2(database, project, assetId);
3077
+ requireAsset3(database, project, assetId);
2312
3078
  const root = explicitWorkspaceRoot(database, project, assetId) || rootFor(database, project, assetId);
2313
3079
  const edges = descendants(database, project, root);
2314
- const ids = [.../* @__PURE__ */ new Set([root, ...edges.flatMap((edge) => [edge.parent_asset_id, edge.child_asset_id])])];
3080
+ const selected = selectedRows(database, project, root);
3081
+ const tasks = listLineageTasks(project, root).tasks;
3082
+ const ids = [.../* @__PURE__ */ new Set([
3083
+ root,
3084
+ ...edges.flatMap((edge) => [edge.parent_asset_id, edge.child_asset_id]),
3085
+ ...selected.map((row) => row.asset_id),
3086
+ ...tasks.map((task) => task.target_asset_id)
3087
+ ])];
2315
3088
  const placeholders2 = ids.map(() => "?").join(",");
2316
3089
  const rows = database.prepare(`
2317
3090
  select a.id asset_id, a.project_id project, a.source, a.title, a.media_type, a.status, a.channel, a.campaign,
@@ -2326,12 +3099,12 @@ function getLineageSnapshot(project, assetId) {
2326
3099
  for (const attempt of attemptRows.map(attemptFrom)) {
2327
3100
  attemptsByNode.set(attempt.node_asset_id, [...attemptsByNode.get(attempt.node_asset_id) || [], attempt]);
2328
3101
  }
3102
+ const lineageTasksByNode = tasksByTarget(tasks);
2329
3103
  const rerollRows = ids.length > 0 ? database.prepare(`select * from asset_reroll_requests where project_id = ? and root_asset_id = ? and status = 'pending' and node_asset_id in (${placeholders2}) order by created_at`).all(project, root, ...ids) : [];
2330
3104
  const rerollsByNode = new Map(rerollRows.map((row) => {
2331
3105
  const request = rerollRequestFrom(row);
2332
- return [request.node_asset_id, request];
3106
+ return [request.node_asset_id, withRerollTask(request, lineageTasksByNode.get(request.node_asset_id)?.reroll)];
2333
3107
  }));
2334
- const selected = selectedRows(database, project, root);
2335
3108
  const childIds = new Set(edges.map((edge) => edge.parent_asset_id));
2336
3109
  const selectedIds = new Set(selected.map((row) => row.asset_id));
2337
3110
  const selections = selected.map((row) => ({
@@ -2348,11 +3121,13 @@ function getLineageSnapshot(project, assetId) {
2348
3121
  const attempts = withImplicitAttempt(attemptsByNode.get(row.asset_id) || [], row);
2349
3122
  const currentAttempt = attempts.find((attempt) => attempt.is_current) || attempts[0];
2350
3123
  const previewPath = currentAttempt?.file_path || row.local_path;
3124
+ const lineageTasks = lineageTasksByNode.get(row.asset_id);
2351
3125
  return {
2352
3126
  ...node,
2353
3127
  attempt_count: attempts.length,
2354
3128
  current_attempt: currentAttempt,
2355
3129
  is_latest: !childIds.has(row.asset_id),
3130
+ lineage_tasks: lineageTasks && Object.keys(lineageTasks).length > 0 ? lineageTasks : void 0,
2356
3131
  position,
2357
3132
  preview_url: canPreviewLocally(row.media_type, previewPath) ? localPreviewUrl(project, previewPath) : void 0,
2358
3133
  reroll_request: rerollsByNode.get(row.asset_id),
@@ -2368,6 +3143,7 @@ function getLineageSnapshot(project, assetId) {
2368
3143
  selected: selections.map((row) => row.asset_id),
2369
3144
  selection,
2370
3145
  selections,
3146
+ tasks,
2371
3147
  latest: nodes.filter((node) => node.is_latest).map((node) => node.asset_id),
2372
3148
  nodes,
2373
3149
  edges,
@@ -2377,8 +3153,8 @@ function getLineageSnapshot(project, assetId) {
2377
3153
  function updateLineageLayout(project, fields) {
2378
3154
  if (fields.positions.length === 0) throw new LineageError("Lineage layout requires at least one position");
2379
3155
  const database = lineageDb();
2380
- requireAsset2(database, project, fields.rootAssetId);
2381
- for (const position of fields.positions) requireAsset2(database, project, position.assetId);
3156
+ requireAsset3(database, project, fields.rootAssetId);
3157
+ for (const position of fields.positions) requireAsset3(database, project, position.assetId);
2382
3158
  try {
2383
3159
  requireLineageWorkspaceClaimForWrite({
2384
3160
  channel: assetChannel(database, project, fields.rootAssetId),
@@ -2414,7 +3190,25 @@ function getLineageNextAsset(project, rootAssetId) {
2414
3190
  const root = resolveRoot(database, project, rootAssetId);
2415
3191
  database.close();
2416
3192
  const snapshot = getLineageSnapshot(project, root);
2417
- const selectedNodes = snapshot.selected.map((assetId) => snapshot.nodes.find((node) => node.asset_id === assetId)).filter((node) => Boolean(node));
3193
+ const pendingIterateTasks = (snapshot.tasks || []).filter((task) => task.task_type === "iterate" && task.status === "pending");
3194
+ const taskIdsInSelectionOrder = [
3195
+ ...snapshot.selections.map((selection) => selection.asset_id).filter((assetId) => pendingIterateTasks.some((task) => task.target_asset_id === assetId)),
3196
+ ...pendingIterateTasks.map((task) => task.target_asset_id).filter((assetId) => !snapshot.selections.some((selection) => selection.asset_id === assetId))
3197
+ ];
3198
+ const taskSelectedIds = [...new Set(taskIdsInSelectionOrder)];
3199
+ const selectedIds = taskSelectedIds.length > 0 ? taskSelectedIds : snapshot.selected;
3200
+ const selectedNodes = selectedIds.map((assetId) => snapshot.nodes.find((node) => node.asset_id === assetId)).filter((node) => Boolean(node));
3201
+ const taskSelections = taskSelectedIds.map((assetId, position) => {
3202
+ const task = pendingIterateTasks.find((item) => item.target_asset_id === assetId);
3203
+ return {
3204
+ asset_id: assetId,
3205
+ notes: task?.instructions,
3206
+ position,
3207
+ selected_at: task?.created_at || nowIso()
3208
+ };
3209
+ });
3210
+ const selectedSelection = taskSelections.length > 0 ? taskSelections[0] : snapshot.selection;
3211
+ const selectedSelections = taskSelections.length > 0 ? taskSelections : snapshot.selections;
2418
3212
  const latestNodes = snapshot.nodes.filter((node) => snapshot.latest.includes(node.asset_id));
2419
3213
  const warnings = [];
2420
3214
  for (const selectedNode of selectedNodes) {
@@ -2432,9 +3226,9 @@ function getLineageNextAsset(project, rootAssetId) {
2432
3226
  next_asset: selectedNodes[0],
2433
3227
  next_assets: selectedNodes,
2434
3228
  latest: snapshot.latest,
2435
- selected: snapshot.selected,
2436
- selection: snapshot.selection,
2437
- selections: snapshot.selections,
3229
+ selected: selectedIds,
3230
+ selection: selectedSelection,
3231
+ selections: selectedSelections,
2438
3232
  candidates: latestNodes,
2439
3233
  warnings,
2440
3234
  fetchedAt: nowIso()
@@ -2451,9 +3245,9 @@ function getLineageNextAsset(project, rootAssetId) {
2451
3245
  next_asset: latestNodes[0],
2452
3246
  next_assets: [latestNodes[0]],
2453
3247
  latest: snapshot.latest,
2454
- selected: snapshot.selected,
2455
- selection: snapshot.selection,
2456
- selections: snapshot.selections,
3248
+ selected: selectedIds,
3249
+ selection: selectedSelection,
3250
+ selections: selectedSelections,
2457
3251
  candidates: latestNodes,
2458
3252
  warnings,
2459
3253
  fetchedAt: nowIso()
@@ -2469,9 +3263,9 @@ function getLineageNextAsset(project, rootAssetId) {
2469
3263
  next_asset: null,
2470
3264
  next_assets: [],
2471
3265
  latest: snapshot.latest,
2472
- selected: snapshot.selected,
2473
- selection: snapshot.selection,
2474
- selections: snapshot.selections,
3266
+ selected: selectedIds,
3267
+ selection: selectedSelection,
3268
+ selections: selectedSelections,
2475
3269
  candidates: latestNodes,
2476
3270
  warnings,
2477
3271
  fetchedAt: nowIso()
@@ -2493,12 +3287,23 @@ function listLineageRerollRequests(project, rootAssetId) {
2493
3287
  const database = lineageDb();
2494
3288
  try {
2495
3289
  assertCanonicalRoot(database, project, rootAssetId);
3290
+ const rerollTasks = listLineageTasks(project, rootAssetId).tasks.filter((task) => task.task_type === "reroll");
3291
+ const pendingRerollTasks = listLineageTasks(project, rootAssetId, ["pending"]).tasks.filter((task) => task.task_type === "reroll");
3292
+ const rerollTaskByTarget = new Map(rerollTasks.map((task) => [task.target_asset_id, task]));
2496
3293
  const rows = database.prepare(`
2497
3294
  select * from asset_reroll_requests
2498
3295
  where project_id = ? and root_asset_id = ? and status = 'pending'
2499
3296
  order by created_at, id
2500
3297
  `).all(project, rootAssetId);
2501
- return { project, root_asset_id: rootAssetId, requests: rows.map(rerollRequestFrom), fetchedAt: nowIso() };
3298
+ const requests = rows.map((row) => {
3299
+ const request = rerollRequestFrom(row);
3300
+ return withRerollTask(request, rerollTaskByTarget.get(request.node_asset_id));
3301
+ });
3302
+ const legacyTargets = new Set(requests.map((request) => request.node_asset_id));
3303
+ for (const task of pendingRerollTasks) {
3304
+ if (!legacyTargets.has(task.target_asset_id)) requests.push(taskBackedRerollRequest(project, rootAssetId, task));
3305
+ }
3306
+ return { project, root_asset_id: rootAssetId, requests, fetchedAt: nowIso() };
2502
3307
  } finally {
2503
3308
  database.close();
2504
3309
  }
@@ -2606,6 +3411,13 @@ function markLineageRerollRequest(project, fields) {
2606
3411
  created_at: timestamp
2607
3412
  };
2608
3413
  if (!fields.confirmWrite) return { ok: true, dryRun: true, request };
3414
+ const taskResult = upsertLineageTask(project, {
3415
+ createdBy: request.requested_by,
3416
+ instructions: request.notes,
3417
+ rootAssetId: fields.rootAssetId,
3418
+ targetAssetId: fields.nodeAssetId,
3419
+ taskType: "reroll"
3420
+ });
2609
3421
  if (existing) {
2610
3422
  database.prepare(`
2611
3423
  update asset_reroll_requests
@@ -2625,7 +3437,12 @@ function markLineageRerollRequest(project, fields) {
2625
3437
  review_state = excluded.review_state, reviewed_at = excluded.reviewed_at,
2626
3438
  ignored_at = excluded.ignored_at, notes = coalesce(excluded.notes, asset_reviews.notes), updated_at = excluded.updated_at
2627
3439
  `).run(fields.nodeAssetId, timestamp, request.notes || null, timestamp);
2628
- return { ok: true, request: rerollRequestFrom(database.prepare("select * from asset_reroll_requests where id = ?").get(request.id)) };
3440
+ return {
3441
+ ok: true,
3442
+ request: withRerollTask(rerollRequestFrom(database.prepare("select * from asset_reroll_requests where id = ?").get(request.id)), taskResult.task),
3443
+ task_id: taskResult.task.id,
3444
+ task: taskResult.task
3445
+ };
2629
3446
  } finally {
2630
3447
  database.close();
2631
3448
  }
@@ -2643,59 +3460,104 @@ function clearLineageRerollRequest(project, fields) {
2643
3460
  const timestamp = nowIso();
2644
3461
  const request = { ...rerollRequestFrom(existing), status: "cancelled", resolved_at: timestamp };
2645
3462
  if (!fields.confirmWrite) return { ok: true, dryRun: true, request };
3463
+ const task = listLineageTasks(project, fields.rootAssetId, ["pending"]).tasks.find((item) => item.task_type === "reroll" && item.target_asset_id === fields.nodeAssetId);
3464
+ const cancelledTask = task ? cancelLineageTask(project, {
3465
+ actor: "human",
3466
+ confirmWrite: true,
3467
+ taskId: task.id
3468
+ }).task : void 0;
2646
3469
  database.prepare(`
2647
3470
  update asset_reroll_requests
2648
3471
  set status = 'cancelled', resolved_at = ?
2649
3472
  where id = ?
2650
3473
  `).run(timestamp, request.id);
2651
- return { ok: true, request };
3474
+ return {
3475
+ ok: true,
3476
+ request: withRerollTask(request, cancelledTask),
3477
+ task_id: cancelledTask?.id,
3478
+ task: cancelledTask
3479
+ };
2652
3480
  } finally {
2653
3481
  database.close();
2654
3482
  }
2655
3483
  }
2656
3484
  function updateSelectedAsset(project, fields) {
2657
3485
  const database = lineageDb();
2658
- const inputAssetIds = normalizeSelectionInput(fields);
2659
- const root = fields.rootAssetId || (inputAssetIds[0] ? rootFor(database, project, inputAssetIds[0]) : "");
2660
- if (!root) throw new LineageError("Selection requires rootAssetId or assetId");
2661
- requireAsset2(database, project, root);
2662
- for (const assetId of inputAssetIds) requireAsset2(database, project, assetId);
2663
- const mode = fields.mode || "replace";
2664
- const limit = fields.maxSelections || LINEAGE_NEXT_VARIATION_LIMIT;
2665
- if (!fields.confirmWrite) {
3486
+ let root;
3487
+ let current;
3488
+ let inputAssetIds;
3489
+ let mode;
3490
+ let limit;
3491
+ let nextIds;
3492
+ const notesByAsset = /* @__PURE__ */ new Map();
3493
+ try {
3494
+ inputAssetIds = normalizeSelectionInput(fields);
3495
+ root = fields.rootAssetId || (inputAssetIds[0] ? rootFor(database, project, inputAssetIds[0]) : "");
3496
+ if (!root) throw new LineageError("Selection requires rootAssetId or assetId");
3497
+ requireAsset3(database, project, root);
3498
+ for (const assetId of inputAssetIds) requireAsset3(database, project, assetId);
3499
+ mode = fields.mode || "replace";
3500
+ limit = fields.maxSelections || LINEAGE_NEXT_VARIATION_LIMIT;
3501
+ if (!fields.confirmWrite) {
3502
+ return { ok: true, dryRun: true, root_asset_id: root, asset_ids: inputAssetIds, mode, clear: Boolean(fields.clear), max_selections: limit };
3503
+ }
3504
+ current = selectedRows(database, project, root);
3505
+ nextIds = current.map((row) => row.asset_id);
3506
+ if (fields.clear) {
3507
+ nextIds = [];
3508
+ } else if (mode === "replace") {
3509
+ nextIds = inputAssetIds;
3510
+ } else if (mode === "add") {
3511
+ nextIds = [...nextIds, ...inputAssetIds];
3512
+ } else if (mode === "remove") {
3513
+ nextIds = nextIds.filter((assetId) => !inputAssetIds.includes(assetId));
3514
+ } else if (mode === "toggle") {
3515
+ for (const assetId of inputAssetIds) {
3516
+ nextIds = nextIds.includes(assetId) ? nextIds.filter((id) => id !== assetId) : [...nextIds, assetId];
3517
+ }
3518
+ }
3519
+ nextIds = [...new Set(nextIds)];
3520
+ if (!fields.clear && inputAssetIds.length === 0) throw new LineageError("Selection set requires assetId or assetIds");
3521
+ if (nextIds.length > limit) throw new LineageError(`Select at most ${limit} assets for next variation`);
3522
+ for (const assetId of nextIds) {
3523
+ const existing = current.find((row) => row.asset_id === assetId);
3524
+ notesByAsset.set(assetId, inputAssetIds.includes(assetId) ? fields.notes || existing?.notes : existing?.notes);
3525
+ }
3526
+ } finally {
2666
3527
  database.close();
2667
- return { ok: true, dryRun: true, root_asset_id: root, asset_ids: inputAssetIds, mode, clear: Boolean(fields.clear), max_selections: limit };
2668
3528
  }
2669
- const current = selectedRows(database, project, root);
2670
- let nextIds = current.map((row) => row.asset_id);
2671
- if (fields.clear) {
2672
- nextIds = [];
2673
- } else if (mode === "replace") {
2674
- nextIds = inputAssetIds;
2675
- } else if (mode === "add") {
2676
- nextIds = [...nextIds, ...inputAssetIds];
2677
- } else if (mode === "remove") {
2678
- nextIds = nextIds.filter((assetId) => !inputAssetIds.includes(assetId));
2679
- } else if (mode === "toggle") {
2680
- for (const assetId of inputAssetIds) {
2681
- nextIds = nextIds.includes(assetId) ? nextIds.filter((id) => id !== assetId) : [...nextIds, assetId];
3529
+ const removedIds = current.map((row) => row.asset_id).filter((assetId) => !nextIds.includes(assetId));
3530
+ const openIterateTasks = listLineageTasks(project, root, ["pending"]).tasks.filter((task) => task.task_type === "iterate");
3531
+ for (const assetId of removedIds) {
3532
+ const task = openIterateTasks.find((item) => item.target_asset_id === assetId);
3533
+ if (task) {
3534
+ cancelLineageTask(project, {
3535
+ actor: "human",
3536
+ confirmWrite: true,
3537
+ taskId: task.id
3538
+ });
2682
3539
  }
2683
3540
  }
2684
- nextIds = [...new Set(nextIds)];
2685
- if (!fields.clear && inputAssetIds.length === 0) throw new LineageError("Selection set requires assetId or assetIds");
2686
- if (nextIds.length > limit) throw new LineageError(`Select at most ${limit} assets for next variation`);
2687
- database.prepare("delete from asset_selections where project_id = ? and root_asset_id = ?").run(project, root);
3541
+ for (const assetId of nextIds) {
3542
+ upsertLineageTask(project, {
3543
+ createdBy: "human",
3544
+ instructions: notesByAsset.get(assetId),
3545
+ rootAssetId: root,
3546
+ targetAssetId: assetId,
3547
+ taskType: "iterate"
3548
+ });
3549
+ }
3550
+ const writeDatabase = lineageDb();
3551
+ writeDatabase.prepare("delete from asset_selections where project_id = ? and root_asset_id = ?").run(project, root);
2688
3552
  const timestamp = nowIso();
2689
- const insert = database.prepare(`
3553
+ const insert = writeDatabase.prepare(`
2690
3554
  insert into asset_selections (id, project_id, root_asset_id, asset_id, position, notes, selected_at)
2691
3555
  values (?, ?, ?, ?, ?, ?, ?)
2692
3556
  `);
2693
3557
  nextIds.forEach((assetId, position) => {
2694
- const existing = current.find((row) => row.asset_id === assetId);
2695
- const notes = inputAssetIds.includes(assetId) ? fields.notes || existing?.notes : existing?.notes;
2696
- insert.run(selectionId(project, root, assetId), project, root, assetId, position, notes || null, timestamp);
3558
+ insert.run(selectionId(project, root, assetId), project, root, assetId, position, notesByAsset.get(assetId) || null, timestamp);
2697
3559
  });
2698
- database.close();
3560
+ writeDatabase.close();
2699
3561
  const message = nextIds.length === 0 ? `Cleared selected assets for ${root}` : `Selected ${nextIds.length} asset${nextIds.length === 1 ? "" : "s"} for ${root}`;
2700
3562
  return { ok: true, message, root_asset_id: root, asset_id: nextIds[0] || null, asset_ids: nextIds, mode };
2701
3563
  }
@@ -2703,7 +3565,7 @@ function updateAssetReview(project, fields) {
2703
3565
  const allowed = /* @__PURE__ */ new Set(["unreviewed", "approved", "needs_revision", "rejected", "ignored"]);
2704
3566
  if (!allowed.has(fields.reviewState)) throw new LineageError(`Unsupported review state: ${fields.reviewState}`);
2705
3567
  const database = lineageDb();
2706
- requireAsset2(database, project, fields.assetId);
3568
+ requireAsset3(database, project, fields.assetId);
2707
3569
  if (!fields.confirmWrite) {
2708
3570
  database.close();
2709
3571
  return { ok: true, dryRun: true, asset_id: fields.assetId, review_state: fields.reviewState, notes: fields.notes };
@@ -2731,6 +3593,9 @@ function lineageCommand(command, project, rootAssetId) {
2731
3593
  function linkChildCommand(project, rootAssetId) {
2732
3594
  return `${publicPackageCommand} link-child --project ${shellQuote(project)} --root ${shellQuote(rootAssetId)} --child <asset-id> --confirm-write --db ${shellQuote(lineageDbPath())} --json`;
2733
3595
  }
3596
+ function rerollImportGuidance(rootAssetId, targetAssetId) {
3597
+ return `Use lineage reroll plan --root ${rootAssetId} --target ${targetAssetId} and lineage reroll import instead.`;
3598
+ }
2734
3599
  function getLineageBrief(project, rootAssetId) {
2735
3600
  const next = getLineageNextAsset(project, rootAssetId);
2736
3601
  const assets = next.next_assets;
@@ -2777,6 +3642,14 @@ function getLineageBrief(project, rootAssetId) {
2777
3642
  function linkSelectedLineageChild(project, fields) {
2778
3643
  const next = getLineageNextAsset(project, fields.rootAssetId);
2779
3644
  if (!next.next_asset) throw new LineageError("Cannot link child until a next base is selected or unambiguous");
3645
+ const rerollRequests = listLineageRerollRequests(project, next.root_asset_id).requests;
3646
+ const pendingRerollForParent = rerollRequests.find((request) => request.node_asset_id === next.next_asset?.asset_id);
3647
+ if (pendingRerollForParent && fields.confirmWrite) {
3648
+ throw new LineageError(
3649
+ `Pending re-roll exists for ${pendingRerollForParent.node_asset_id}. lineage link-child creates a visible child variation edge; it does not re-roll the same node. ${rerollImportGuidance(next.root_asset_id, pendingRerollForParent.node_asset_id)} Cancel the re-roll first if you intentionally want a new child variation.`,
3650
+ 409
3651
+ );
3652
+ }
2780
3653
  if (fields.confirmWrite) {
2781
3654
  const claimContext = getLineageWriteClaimContext(project, next.next_asset.asset_id);
2782
3655
  const validation = validateAgentClaimForWrite({
@@ -2803,12 +3676,16 @@ function linkSelectedLineageChild(project, fields) {
2803
3676
  parent_asset_id: next.next_asset.asset_id,
2804
3677
  child_asset_id: fields.childAssetId,
2805
3678
  reference_asset_ids: next.next_assets.map((asset) => asset.asset_id),
2806
- warning: next.next_assets.length > 1 ? "Linked child to the primary selected base; add explicit edges to other selected references if the child derives from them too." : void 0
3679
+ warning: [
3680
+ pendingRerollForParent ? `Pending re-roll exists for ${pendingRerollForParent.node_asset_id}. link-child would create a visible child variation; use reroll plan/import to update the same node attempt.` : void 0,
3681
+ rerollRequests.length > 0 && !pendingRerollForParent ? `This lineage has ${rerollRequests.length} pending re-roll target(s). link-child is only for new visible child variations, not re-roll attempts.` : void 0,
3682
+ next.next_assets.length > 1 ? "Linked child to the primary selected base; add explicit edges to other selected references if the child derives from them too." : void 0
3683
+ ].filter(Boolean).join(" ") || void 0
2807
3684
  };
2808
3685
  }
2809
3686
 
2810
3687
  // src/server/assetLineageRemove.ts
2811
- function requireAsset3(database, project, assetId) {
3688
+ function requireAsset4(database, project, assetId) {
2812
3689
  const row = database.prepare("select id from assets where project_id = ? and id = ?").get(project, assetId);
2813
3690
  if (!row) throw new LineageError(`Unknown indexed asset: ${assetId}`, 404);
2814
3691
  }
@@ -2850,9 +3727,9 @@ function compactSelectionsAfterRemove(database, project, root, assetId, timestam
2850
3727
  }
2851
3728
  function removeLineageNode(project, fields) {
2852
3729
  const database = lineageDb();
2853
- requireAsset3(database, project, fields.assetId);
3730
+ requireAsset4(database, project, fields.assetId);
2854
3731
  const root = fields.rootAssetId || rootFor2(database, project, fields.assetId);
2855
- requireAsset3(database, project, root);
3732
+ requireAsset4(database, project, root);
2856
3733
  if (fields.assetId === root) {
2857
3734
  database.close();
2858
3735
  throw new LineageError("Cannot remove the root lineage node; archive the workspace or create a new root instead.");
@@ -2900,6 +3777,19 @@ function removeLineageNode(project, fields) {
2900
3777
  asset_preserved: true
2901
3778
  };
2902
3779
  }
3780
+ if (selectionRemoved) {
3781
+ try {
3782
+ cancelLineageIterateTasksForAssets(project, {
3783
+ actor: "human",
3784
+ assetIds: [fields.assetId],
3785
+ confirmWrite: true,
3786
+ rootAssetId: root
3787
+ });
3788
+ } catch (error) {
3789
+ database.close();
3790
+ throw error;
3791
+ }
3792
+ }
2903
3793
  try {
2904
3794
  database.exec("begin immediate transaction");
2905
3795
  const deleteEdge = database.prepare("delete from asset_edges where id = ? and project_id = ?");
@@ -3702,7 +4592,7 @@ function requireText(value, label) {
3702
4592
  if (!text) throw new AssetSelectionError(`Selection ${label} is required`);
3703
4593
  return text;
3704
4594
  }
3705
- function normalizeActor2(value, fallback) {
4595
+ function normalizeActor3(value, fallback) {
3706
4596
  const actor = value || fallback;
3707
4597
  if (!actors2.has(actor)) throw new AssetSelectionError(`Unsupported selection actor: ${actor}`);
3708
4598
  return actor;
@@ -3834,7 +4724,7 @@ function getAssetSelectionSnapshot(project) {
3834
4724
  }
3835
4725
  function selectCurrentAssets(project, fields) {
3836
4726
  const assetIds = [...new Set(fields.assetIds.map((assetId) => assetId.trim()).filter(Boolean))];
3837
- const actor = normalizeActor2(fields.selectedBy, "human");
4727
+ const actor = normalizeActor3(fields.selectedBy, "human");
3838
4728
  const preview = { assetIds, notes: fields.notes, project, selectedBy: actor };
3839
4729
  if (!fields.confirmWrite) return { ok: true, dryRun: true, message: `Would select ${assetIds.length} assets`, preview };
3840
4730
  const database = lineageDb();
@@ -3871,7 +4761,7 @@ function createReviewSet(project, fields) {
3871
4761
  const label = requireText(fields.label, "review set label");
3872
4762
  const key = fields.key?.trim() || `${slug(label)}-${Date.now().toString(36)}`;
3873
4763
  const assetIds = [...new Set((fields.assetIds || []).map((assetId) => assetId.trim()).filter(Boolean))];
3874
- const actor = normalizeActor2(fields.createdBy, "agent");
4764
+ const actor = normalizeActor3(fields.createdBy, "agent");
3875
4765
  const preview = { assetIds, createdBy: actor, key, label, notes: fields.notes, project };
3876
4766
  if (!fields.confirmWrite) return { ok: true, dryRun: true, message: `Would create review set ${label}`, preview };
3877
4767
  const database = lineageDb();
@@ -3908,7 +4798,7 @@ function activeReviewSet(database, project, setId2) {
3908
4798
  function chooseReviewSetLabels(project, fields) {
3909
4799
  const labels = [...new Set(fields.labels.map((label) => label.trim().toUpperCase()).filter(Boolean))];
3910
4800
  if (labels.length === 0) throw new AssetSelectionError("At least one variation label is required");
3911
- const actor = normalizeActor2(fields.selectedBy, "human");
4801
+ const actor = normalizeActor3(fields.selectedBy, "human");
3912
4802
  const database = lineageDb();
3913
4803
  try {
3914
4804
  const reviewSet2 = activeReviewSet(database, project, fields.setId);
@@ -5467,6 +6357,64 @@ function listImageGenerationJobs(project = defaultProject, fields = {}) {
5467
6357
  }
5468
6358
  }
5469
6359
 
6360
+ // src/server/lineageTaskRoutes.ts
6361
+ function stringBody3(req, key) {
6362
+ const value = req.body[key];
6363
+ return typeof value === "string" ? value : void 0;
6364
+ }
6365
+ function boolBody3(req, key) {
6366
+ return req.body[key] === true;
6367
+ }
6368
+ function registerLineageTaskRoutes(app2, projectFrom2, asyncRoute2) {
6369
+ app2.get("/api/lineage/:rootAssetId/tasks", asyncRoute2((req, res) => {
6370
+ res.json(listLineageTasks(projectFrom2(req), req.params.rootAssetId));
6371
+ }));
6372
+ app2.get("/api/lineage/tasks/:taskId", asyncRoute2((req, res) => {
6373
+ res.json(getLineageTask(projectFrom2(req), req.params.taskId));
6374
+ }));
6375
+ app2.post("/api/lineage/tasks/:taskId/instructions", asyncRoute2((req, res) => {
6376
+ res.json(updateLineageTaskInstructions(projectFrom2(req), {
6377
+ instructions: stringBody3(req, "instructions") || "",
6378
+ taskId: req.params.taskId
6379
+ }));
6380
+ }));
6381
+ app2.post("/api/lineage/tasks/:taskId/comment", asyncRoute2((req, res) => {
6382
+ res.json(addLineageTaskComment(projectFrom2(req), {
6383
+ actor: stringBody3(req, "actor") || "",
6384
+ message: stringBody3(req, "message") || stringBody3(req, "comment") || "",
6385
+ taskId: req.params.taskId
6386
+ }));
6387
+ }));
6388
+ app2.post("/api/lineage/tasks/:taskId/claim", asyncRoute2((req, res) => {
6389
+ res.json(claimLineageTask(projectFrom2(req), {
6390
+ agentName: stringBody3(req, "agentName") || stringBody3(req, "agent_name") || "",
6391
+ taskId: req.params.taskId
6392
+ }));
6393
+ }));
6394
+ app2.post("/api/lineage/tasks/:taskId/start", asyncRoute2((req, res) => {
6395
+ res.json(startLineageTask(projectFrom2(req), {
6396
+ claimToken: stringBody3(req, "claimToken") || stringBody3(req, "claim_token") || "",
6397
+ taskId: req.params.taskId
6398
+ }));
6399
+ }));
6400
+ app2.post("/api/lineage/tasks/:taskId/cancel", asyncRoute2((req, res) => {
6401
+ res.json(cancelLineageTask(projectFrom2(req), {
6402
+ actor: stringBody3(req, "actor") || "",
6403
+ confirmWrite: boolBody3(req, "confirmWrite") || boolBody3(req, "confirm_write"),
6404
+ override: boolBody3(req, "override"),
6405
+ taskId: req.params.taskId
6406
+ }));
6407
+ }));
6408
+ app2.post("/api/lineage/tasks/:taskId/override", asyncRoute2((req, res) => {
6409
+ res.json(overrideLineageTask(projectFrom2(req), {
6410
+ actor: stringBody3(req, "actor") || "",
6411
+ instructions: stringBody3(req, "instructions"),
6412
+ reason: stringBody3(req, "reason") || "",
6413
+ taskId: req.params.taskId
6414
+ }));
6415
+ }));
6416
+ }
6417
+
5470
6418
  // src/server/assetLineageDemo.ts
5471
6419
  import { createHash as createHash3 } from "node:crypto";
5472
6420
  import { copyFileSync as copyFileSync2, existsSync as existsSync5, mkdirSync as mkdirSync5, mkdtempSync, readFileSync as readFileSync4, rmSync, writeFileSync as writeFileSync3 } from "node:fs";
@@ -6339,6 +7287,7 @@ app.get(
6339
7287
  })
6340
7288
  );
6341
7289
  registerLineageWorkspaceRoutes(app, projectFrom, asyncRoute);
7290
+ registerLineageTaskRoutes(app, projectFrom, asyncRoute);
6342
7291
  app.get("/api/lineage/:rootAssetId/rerolls", asyncRoute((req, res) => {
6343
7292
  res.json(listLineageRerollRequests(projectFrom(req), req.params.rootAssetId));
6344
7293
  }));
@@ -6646,6 +7595,10 @@ app.use((error, _req, res, _next) => {
6646
7595
  res.status(error.status).json({ error: error.message });
6647
7596
  return;
6648
7597
  }
7598
+ if (isLineageTaskError(error)) {
7599
+ res.status(error.status).json({ error: error.message });
7600
+ return;
7601
+ }
6649
7602
  if (isLineageError(error)) {
6650
7603
  res.status(error.status).json({ error: error.message });
6651
7604
  return;