@mean-weasel/lineage 0.1.5 → 0.1.7
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/CHANGELOG.md +14 -0
- package/README.md +35 -2
- package/dist/cli/lineage-dev.js +1873 -46
- package/dist/cli/lineage-dev.js.map +4 -4
- package/dist/cli/lineage.js +1873 -46
- package/dist/cli/lineage.js.map +4 -4
- package/dist/server.js +1960 -569
- package/dist/server.js.map +4 -4
- package/dist/web/assets/{index-DkLQJqxa.css → index-38XlcMya.css} +1 -1
- package/dist/web/assets/index-CtsYIUyG.js +23 -0
- package/dist/web/index.html +2 -2
- package/fixtures/demo-project/lineage/swissifier-rerolls/swissifier-drill-mint-diagonal-reroll-v2.png +0 -0
- package/fixtures/demo-project/lineage/swissifier-rerolls/swissifier-vertical-before-after-reroll-v2.png +0 -0
- package/fixtures/demo-project/lineage/swissifier-rerolls/swissifier-vertical-before-after-reroll-v3.png +0 -0
- package/fixtures/demo-project/lineage/swissifier-rich-demo.json +38 -1
- package/package.json +3 -1
- package/dist/web/assets/index-C54JeMzP.js +0 -22
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),
|
|
@@ -339,6 +343,44 @@ function lineageDb() {
|
|
|
339
343
|
);
|
|
340
344
|
create index if not exists edges_parent on asset_edges(project_id, parent_asset_id);
|
|
341
345
|
create index if not exists edges_child on asset_edges(project_id, child_asset_id);
|
|
346
|
+
create table if not exists asset_attempts (
|
|
347
|
+
id text primary key,
|
|
348
|
+
project_id text not null references projects(id),
|
|
349
|
+
node_asset_id text not null references assets(id),
|
|
350
|
+
asset_id text not null references assets(id),
|
|
351
|
+
attempt_index integer not null check (attempt_index > 0),
|
|
352
|
+
source text not null check (source in ('initial', 'generated_child', 'reroll')),
|
|
353
|
+
prompt text,
|
|
354
|
+
generation_job_id text,
|
|
355
|
+
file_path text,
|
|
356
|
+
checksum_sha256 text,
|
|
357
|
+
created_at text not null,
|
|
358
|
+
promoted_at text,
|
|
359
|
+
is_current integer not null check (is_current in (0, 1)),
|
|
360
|
+
unique(project_id, node_asset_id, attempt_index),
|
|
361
|
+
unique(project_id, node_asset_id, asset_id, source)
|
|
362
|
+
);
|
|
363
|
+
create unique index if not exists asset_attempts_one_current
|
|
364
|
+
on asset_attempts(project_id, node_asset_id)
|
|
365
|
+
where is_current = 1;
|
|
366
|
+
create index if not exists asset_attempts_node_created
|
|
367
|
+
on asset_attempts(project_id, node_asset_id, created_at);
|
|
368
|
+
create table if not exists asset_reroll_requests (
|
|
369
|
+
id text primary key,
|
|
370
|
+
project_id text not null references projects(id),
|
|
371
|
+
root_asset_id text not null references assets(id),
|
|
372
|
+
node_asset_id text not null references assets(id),
|
|
373
|
+
status text not null check (status in ('pending', 'resolved', 'cancelled')),
|
|
374
|
+
requested_by text not null check (requested_by in ('human', 'agent', 'system')),
|
|
375
|
+
notes text,
|
|
376
|
+
created_at text not null,
|
|
377
|
+
resolved_at text
|
|
378
|
+
);
|
|
379
|
+
create unique index if not exists asset_reroll_requests_one_pending
|
|
380
|
+
on asset_reroll_requests(project_id, root_asset_id, node_asset_id)
|
|
381
|
+
where status = 'pending';
|
|
382
|
+
create index if not exists asset_reroll_requests_root_status
|
|
383
|
+
on asset_reroll_requests(project_id, root_asset_id, status, created_at);
|
|
342
384
|
create table if not exists asset_reviews (
|
|
343
385
|
asset_id text primary key references assets(id),
|
|
344
386
|
review_state text not null check (review_state in ('unreviewed', 'approved', 'needs_revision', 'rejected', 'ignored')),
|
|
@@ -543,7 +585,7 @@ function lineageDb() {
|
|
|
543
585
|
project_id text not null references projects(id),
|
|
544
586
|
provider text not null default 'codex-handoff',
|
|
545
587
|
adapter_version text not null,
|
|
546
|
-
source_mode text not null check (source_mode in ('lineage_selection')),
|
|
588
|
+
source_mode text not null check (source_mode in ('lineage_selection', 'lineage_reroll')),
|
|
547
589
|
root_asset_id text not null references assets(id),
|
|
548
590
|
prompt text not null,
|
|
549
591
|
expected_output_count integer not null check (expected_output_count > 0),
|
|
@@ -561,7 +603,7 @@ function lineageDb() {
|
|
|
561
603
|
project_id text not null references projects(id),
|
|
562
604
|
asset_id text not null references assets(id),
|
|
563
605
|
root_asset_id text not null references assets(id),
|
|
564
|
-
role text not null check (role in ('lineage_next_base', 'reference')),
|
|
606
|
+
role text not null check (role in ('lineage_next_base', 'reference', 'reroll_target')),
|
|
565
607
|
position integer not null,
|
|
566
608
|
selection_strategy text not null,
|
|
567
609
|
selection_snapshot_json text not null,
|
|
@@ -595,12 +637,49 @@ function lineageDb() {
|
|
|
595
637
|
);
|
|
596
638
|
create index if not exists generation_job_receipts_job on generation_job_receipts(job_id, created_at);
|
|
597
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);
|
|
598
677
|
create table if not exists agent_claims (
|
|
599
678
|
id text primary key,
|
|
600
679
|
token_hash text not null,
|
|
601
680
|
project_id text not null references projects(id),
|
|
602
681
|
channel text,
|
|
603
|
-
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')),
|
|
604
683
|
target_id text not null,
|
|
605
684
|
target_title text,
|
|
606
685
|
agent_id text,
|
|
@@ -634,13 +713,76 @@ function lineageDb() {
|
|
|
634
713
|
migrateAssetSelections(database);
|
|
635
714
|
dropLegacyAssetSelectionRootUnique(database);
|
|
636
715
|
ensureColumn(database, "asset_selections", "notes", "text");
|
|
716
|
+
backfillLineageTasks(database);
|
|
637
717
|
ensureColumn(database, "asset_ledger_records", "first_seen_at", "text");
|
|
638
718
|
ensureColumn(database, "asset_ledger_records", "indexed_by_run_id", "text");
|
|
639
719
|
ensureColumn(database, "asset_ledger_sources", "first_seen_at", "text");
|
|
640
720
|
ensureColumn(database, "asset_ledger_sources", "indexed_by_run_id", "text");
|
|
641
721
|
ensureReviewStateValues(database);
|
|
722
|
+
ensureAgentClaimScopeValues(database);
|
|
723
|
+
ensureGenerationReceiptCheckValues(database);
|
|
642
724
|
return database;
|
|
643
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
|
+
}
|
|
644
786
|
function ensureColumn(database, table, column, definition) {
|
|
645
787
|
const rows = database.prepare(`pragma table_info(${table})`).all();
|
|
646
788
|
if (!rows.some((row) => row.name === column)) database.exec(`alter table ${table} add column ${column} ${definition}`);
|
|
@@ -704,6 +846,132 @@ function ensureReviewStateValues(database) {
|
|
|
704
846
|
drop table asset_reviews_old;
|
|
705
847
|
`);
|
|
706
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
|
+
}
|
|
707
975
|
|
|
708
976
|
// src/server/assetLedgerWorkflow.ts
|
|
709
977
|
function ensureProject(database, project) {
|
|
@@ -1317,382 +1585,898 @@ function normalizeSelectionInput(fields) {
|
|
|
1317
1585
|
return [...new Set([...fields.assetIds || [], fields.assetId || ""].map((assetId) => assetId.trim()).filter(Boolean))];
|
|
1318
1586
|
}
|
|
1319
1587
|
|
|
1320
|
-
// src/server/
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
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 = []) {
|
|
1325
1599
|
super(message);
|
|
1326
1600
|
this.status = status;
|
|
1601
|
+
this.code = code;
|
|
1602
|
+
this.conflicts = conflicts;
|
|
1327
1603
|
}
|
|
1328
1604
|
status;
|
|
1605
|
+
code;
|
|
1606
|
+
conflicts;
|
|
1329
1607
|
};
|
|
1330
|
-
function
|
|
1331
|
-
return error instanceof
|
|
1608
|
+
function isAgentClaimError(error) {
|
|
1609
|
+
return error instanceof AgentClaimError;
|
|
1332
1610
|
}
|
|
1333
|
-
function
|
|
1334
|
-
return
|
|
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;
|
|
1335
1623
|
}
|
|
1336
|
-
function
|
|
1337
|
-
|
|
1338
|
-
database.prepare(`
|
|
1339
|
-
insert into projects (id, product, created_at, updated_at)
|
|
1340
|
-
values (?, ?, ?, ?)
|
|
1341
|
-
on conflict(id) do update set product = excluded.product, updated_at = excluded.updated_at
|
|
1342
|
-
`).run(project, project, timestamp, timestamp);
|
|
1624
|
+
function randomId(prefix) {
|
|
1625
|
+
return `${prefix}_${Date.now().toString(36)}_${randomBytes(6).toString("base64url").toLowerCase()}`;
|
|
1343
1626
|
}
|
|
1344
|
-
function
|
|
1345
|
-
|
|
1346
|
-
if (!statuses.has(status)) throw new LineageWorkspaceError(`Unsupported lineage workspace status: ${status}`);
|
|
1347
|
-
return status;
|
|
1627
|
+
function tokenHash(token) {
|
|
1628
|
+
return createHash2("sha256").update(token).digest("hex");
|
|
1348
1629
|
}
|
|
1349
|
-
function
|
|
1350
|
-
const
|
|
1351
|
-
|
|
1352
|
-
return
|
|
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);
|
|
1353
1634
|
}
|
|
1354
|
-
function
|
|
1355
|
-
|
|
1356
|
-
if (!row) throw new LineageWorkspaceError(`Unknown indexed asset: ${assetId}`, 404);
|
|
1357
|
-
return row;
|
|
1635
|
+
function expiresAtFrom(timestamp, ttlSeconds) {
|
|
1636
|
+
return new Date(new Date(timestamp).getTime() + ttlSeconds * 1e3).toISOString();
|
|
1358
1637
|
}
|
|
1359
|
-
function
|
|
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);
|
|
1360
1660
|
return {
|
|
1361
1661
|
id: String(row.id),
|
|
1362
1662
|
project: String(row.project_id),
|
|
1363
|
-
|
|
1364
|
-
|
|
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,
|
|
1365
1671
|
status: String(row.status),
|
|
1366
|
-
notes: typeof row.notes === "string" ? row.notes : void 0,
|
|
1367
|
-
created_by: String(row.created_by),
|
|
1368
|
-
active_at: typeof row.active_at === "string" ? row.active_at : void 0,
|
|
1369
1672
|
created_at: String(row.created_at),
|
|
1370
|
-
|
|
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)
|
|
1371
1686
|
};
|
|
1372
1687
|
}
|
|
1373
|
-
function
|
|
1374
|
-
|
|
1375
|
-
|
|
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
|
+
};
|
|
1376
1697
|
}
|
|
1377
|
-
function
|
|
1378
|
-
const
|
|
1379
|
-
|
|
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);
|
|
1380
1705
|
}
|
|
1381
|
-
function
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
union
|
|
1387
|
-
select parent_asset_id root_asset_id, null selected_at
|
|
1388
|
-
from asset_edges
|
|
1389
|
-
where project_id = ?
|
|
1390
|
-
and parent_asset_id not in (select child_asset_id from asset_edges where project_id = ?)
|
|
1391
|
-
group by parent_asset_id
|
|
1392
|
-
`).all(project, project, project, project);
|
|
1393
|
-
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));
|
|
1394
1711
|
}
|
|
1395
|
-
function
|
|
1396
|
-
ensureProject2(database, project);
|
|
1712
|
+
function expireActiveClaims(database) {
|
|
1397
1713
|
const timestamp = nowIso();
|
|
1398
|
-
const
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
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.");
|
|
1722
|
+
}
|
|
1723
|
+
function normalizeScope(scopeType) {
|
|
1724
|
+
if (!scopes.has(scopeType)) throw new AgentClaimError(`Unsupported claim scope: ${scopeType}`);
|
|
1725
|
+
return scopeType;
|
|
1726
|
+
}
|
|
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;
|
|
1769
|
+
const database = lineageDb();
|
|
1770
|
+
try {
|
|
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),
|
|
1410
1803
|
project,
|
|
1411
|
-
|
|
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,
|
|
1412
1812
|
timestamp,
|
|
1413
1813
|
timestamp,
|
|
1414
|
-
|
|
1415
|
-
|
|
1814
|
+
expiresAt,
|
|
1815
|
+
metadataJson(fields.metadata)
|
|
1416
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) };
|
|
1820
|
+
} finally {
|
|
1821
|
+
database.close();
|
|
1417
1822
|
}
|
|
1418
1823
|
}
|
|
1419
|
-
function
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
order by
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
`).all(project).map(rowToWorkspace);
|
|
1824
|
+
function listAgentClaims(project) {
|
|
1825
|
+
const database = lineageDb();
|
|
1826
|
+
try {
|
|
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() };
|
|
1830
|
+
} finally {
|
|
1831
|
+
database.close();
|
|
1832
|
+
}
|
|
1429
1833
|
}
|
|
1430
|
-
function
|
|
1431
|
-
const
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1834
|
+
function inspectAgentClaim(claimId, project) {
|
|
1835
|
+
const database = lineageDb();
|
|
1836
|
+
try {
|
|
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) };
|
|
1842
|
+
} finally {
|
|
1843
|
+
database.close();
|
|
1844
|
+
}
|
|
1438
1845
|
}
|
|
1439
|
-
function
|
|
1846
|
+
function heartbeatAgentClaim(claimToken, ttlSeconds = defaultTtlSeconds) {
|
|
1440
1847
|
const database = lineageDb();
|
|
1441
1848
|
try {
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
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");
|
|
1854
|
+
const timestamp = nowIso();
|
|
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) };
|
|
1449
1858
|
} finally {
|
|
1450
1859
|
database.close();
|
|
1451
1860
|
}
|
|
1452
1861
|
}
|
|
1453
|
-
function
|
|
1862
|
+
function releaseAgentClaim(claimToken) {
|
|
1454
1863
|
const database = lineageDb();
|
|
1455
1864
|
try {
|
|
1456
|
-
|
|
1457
|
-
const
|
|
1458
|
-
if (!
|
|
1459
|
-
|
|
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");
|
|
1870
|
+
const timestamp = nowIso();
|
|
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) };
|
|
1460
1874
|
} finally {
|
|
1461
1875
|
database.close();
|
|
1462
1876
|
}
|
|
1463
1877
|
}
|
|
1464
|
-
function
|
|
1465
|
-
|
|
1466
|
-
if (!
|
|
1467
|
-
const status = normalizeStatus(fields.status, "active");
|
|
1468
|
-
const actor = normalizeActor(fields.createdBy);
|
|
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");
|
|
1469
1881
|
const database = lineageDb();
|
|
1470
1882
|
try {
|
|
1471
|
-
|
|
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
|
+
}
|
|
1472
1889
|
const timestamp = nowIso();
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
root_asset_id: rootAssetId,
|
|
1477
|
-
title: fields.title?.trim() || `${root.title} lineage`,
|
|
1478
|
-
status,
|
|
1479
|
-
notes: fields.notes?.trim() || void 0,
|
|
1480
|
-
created_by: actor,
|
|
1481
|
-
active_at: fields.activate !== false && status !== "archived" ? timestamp : void 0,
|
|
1482
|
-
created_at: timestamp,
|
|
1483
|
-
updated_at: timestamp
|
|
1484
|
-
};
|
|
1485
|
-
if (!fields.confirmWrite) return { ok: true, dryRun: true, workspace };
|
|
1486
|
-
ensureProject2(database, project);
|
|
1487
|
-
database.prepare(`
|
|
1488
|
-
insert into lineage_workspaces (
|
|
1489
|
-
id, project_id, root_asset_id, title, status, notes, created_by, active_at, created_at, updated_at
|
|
1490
|
-
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1491
|
-
on conflict(project_id, root_asset_id) do update set
|
|
1492
|
-
title = excluded.title,
|
|
1493
|
-
status = excluded.status,
|
|
1494
|
-
notes = excluded.notes,
|
|
1495
|
-
active_at = coalesce(excluded.active_at, lineage_workspaces.active_at),
|
|
1496
|
-
updated_at = excluded.updated_at
|
|
1497
|
-
`).run(
|
|
1498
|
-
workspace.id,
|
|
1499
|
-
project,
|
|
1500
|
-
workspace.root_asset_id,
|
|
1501
|
-
workspace.title,
|
|
1502
|
-
workspace.status,
|
|
1503
|
-
workspace.notes || null,
|
|
1504
|
-
workspace.created_by,
|
|
1505
|
-
workspace.active_at || null,
|
|
1506
|
-
workspace.created_at,
|
|
1507
|
-
workspace.updated_at
|
|
1508
|
-
);
|
|
1509
|
-
return {
|
|
1510
|
-
ok: true,
|
|
1511
|
-
message: `Saved lineage workspace ${workspace.title}`,
|
|
1512
|
-
workspace: workspaceById(database, project, workspace.id)
|
|
1513
|
-
};
|
|
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) };
|
|
1514
1893
|
} finally {
|
|
1515
1894
|
database.close();
|
|
1516
1895
|
}
|
|
1517
1896
|
}
|
|
1518
|
-
function
|
|
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");
|
|
1519
1900
|
const database = lineageDb();
|
|
1520
1901
|
try {
|
|
1521
|
-
|
|
1522
|
-
const
|
|
1523
|
-
if (!
|
|
1902
|
+
expireActiveClaims(database);
|
|
1903
|
+
const claim = findClaimById(database, claimId, project);
|
|
1904
|
+
if (!claim) throw new AgentClaimError(`Unknown agent claim: ${claimId}`, 404, "claim_not_found");
|
|
1524
1905
|
const timestamp = nowIso();
|
|
1525
|
-
const next = {
|
|
1526
|
-
...current,
|
|
1527
|
-
title: fields.title?.trim() || current.title,
|
|
1528
|
-
status: normalizeStatus(fields.status, current.status),
|
|
1529
|
-
notes: fields.notes === void 0 ? current.notes : fields.notes.trim() || void 0,
|
|
1530
|
-
active_at: fields.activate ? timestamp : current.active_at,
|
|
1531
|
-
updated_at: timestamp
|
|
1532
|
-
};
|
|
1533
|
-
if (!fields.confirmWrite) return { ok: true, dryRun: true, workspace: next };
|
|
1534
1906
|
database.prepare(`
|
|
1535
|
-
update
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
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: [] };
|
|
1987
|
+
} finally {
|
|
1988
|
+
database.close();
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
// src/server/assetLineageTasks.ts
|
|
1993
|
+
var LineageTaskError = class extends Error {
|
|
1994
|
+
constructor(message, status = 400) {
|
|
1995
|
+
super(message);
|
|
1996
|
+
this.status = status;
|
|
1997
|
+
}
|
|
1998
|
+
status;
|
|
1999
|
+
};
|
|
2000
|
+
function isLineageTaskError(error) {
|
|
2001
|
+
return error instanceof LineageTaskError;
|
|
2002
|
+
}
|
|
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}`;
|
|
2008
|
+
}
|
|
2009
|
+
function randomId2(prefix) {
|
|
2010
|
+
return `${prefix}_${Date.now().toString(36)}_${randomBytes2(6).toString("base64url").toLowerCase()}`;
|
|
2011
|
+
}
|
|
2012
|
+
function metadataJson2(metadata2) {
|
|
2013
|
+
return metadata2 ? JSON.stringify(metadata2) : null;
|
|
2014
|
+
}
|
|
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) {
|
|
2022
|
+
if (typeof value !== "string" || !value) return void 0;
|
|
2023
|
+
try {
|
|
2024
|
+
const parsed = JSON.parse(value);
|
|
2025
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
|
|
2026
|
+
} catch {
|
|
2027
|
+
return void 0;
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
function normalizeProject(project) {
|
|
2031
|
+
const trimmed = project.trim();
|
|
2032
|
+
if (!trimmed) throw new LineageTaskError("Lineage task requires project");
|
|
2033
|
+
return trimmed;
|
|
2034
|
+
}
|
|
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;
|
|
2055
|
+
return {
|
|
2056
|
+
id: String(row.id),
|
|
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),
|
|
2061
|
+
status: String(row.status),
|
|
2062
|
+
instructions: typeof row.instructions === "string" ? row.instructions : void 0,
|
|
2063
|
+
created_by: String(row.created_by),
|
|
2064
|
+
created_at: String(row.created_at),
|
|
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
|
|
2074
|
+
};
|
|
2075
|
+
}
|
|
2076
|
+
function eventFromRow(row) {
|
|
2077
|
+
return {
|
|
2078
|
+
id: String(row.id),
|
|
2079
|
+
task_id: String(row.task_id),
|
|
2080
|
+
event_type: String(row.event_type),
|
|
2081
|
+
actor: typeof row.actor === "string" ? row.actor : void 0,
|
|
2082
|
+
message: typeof row.message === "string" ? row.message : void 0,
|
|
2083
|
+
created_at: String(row.created_at),
|
|
2084
|
+
metadata: parseMetadata2(row.metadata_json)
|
|
2085
|
+
};
|
|
2086
|
+
}
|
|
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;
|
|
2090
|
+
}
|
|
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) {
|
|
2101
|
+
database.prepare(`
|
|
2102
|
+
insert into lineage_task_events (id, task_id, event_type, actor, message, created_at, metadata_json)
|
|
2103
|
+
values (?, ?, ?, ?, ?, ?, ?)
|
|
2104
|
+
`).run(randomId2("lineage_task_event"), taskId, eventType, actor || null, message || null, nowIso(), metadataJson2(metadata2));
|
|
2105
|
+
}
|
|
2106
|
+
function taskWithEvents(database, project, taskId) {
|
|
2107
|
+
return { project, ok: true, task: requireTask(database, project, taskId), events: taskEvents(database, taskId) };
|
|
2108
|
+
}
|
|
2109
|
+
function assertChanged(result, message) {
|
|
2110
|
+
if (Number(result.changes) !== 1) throw new LineageTaskError(message, 409);
|
|
2111
|
+
}
|
|
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
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
function taskReadWithEvents(database, project, taskId) {
|
|
2124
|
+
return { task: requireTask(database, project, taskId), events: taskEvents(database, taskId) };
|
|
2125
|
+
}
|
|
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
|
+
}
|
|
2145
|
+
}
|
|
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
|
+
}
|
|
2154
|
+
}
|
|
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
|
+
}
|
|
2219
|
+
}
|
|
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
|
+
}
|
|
2241
|
+
}
|
|
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
|
+
}
|
|
2257
|
+
}
|
|
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;
|
|
2277
|
+
const database = lineageDb();
|
|
2278
|
+
try {
|
|
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);
|
|
2283
|
+
}
|
|
2284
|
+
const timestamp = nowIso();
|
|
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;
|
|
2299
|
+
}
|
|
1539
2300
|
return {
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
2301
|
+
...taskWithEvents(database, normalizedProject, task.id),
|
|
2302
|
+
claim,
|
|
2303
|
+
claim_token: claimResult.claim_token
|
|
1543
2304
|
};
|
|
1544
2305
|
} finally {
|
|
1545
2306
|
database.close();
|
|
1546
2307
|
}
|
|
1547
2308
|
}
|
|
1548
|
-
function
|
|
1549
|
-
|
|
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);
|
|
2328
|
+
const database = lineageDb();
|
|
2329
|
+
try {
|
|
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);
|
|
2352
|
+
} finally {
|
|
2353
|
+
database.close();
|
|
2354
|
+
}
|
|
1550
2355
|
}
|
|
1551
|
-
function
|
|
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");
|
|
1552
2361
|
const database = lineageDb();
|
|
1553
2362
|
try {
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
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
|
+
}
|
|
1557
2367
|
const timestamp = nowIso();
|
|
1558
|
-
const
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
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);
|
|
1576
2390
|
} finally {
|
|
1577
2391
|
database.close();
|
|
1578
2392
|
}
|
|
1579
2393
|
}
|
|
1580
|
-
function
|
|
2394
|
+
function cancelLineageTask(project, fields) {
|
|
2395
|
+
const normalizedProject = normalizeProject(project);
|
|
2396
|
+
const actor = normalizeActor(fields.actor, "Cancel actor");
|
|
1581
2397
|
const database = lineageDb();
|
|
1582
2398
|
try {
|
|
1583
|
-
|
|
1584
|
-
return
|
|
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;
|
|
2409
|
+
const timestamp = nowIso();
|
|
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);
|
|
1585
2452
|
} finally {
|
|
1586
2453
|
database.close();
|
|
1587
2454
|
}
|
|
1588
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
|
+
}
|
|
1589
2464
|
|
|
1590
|
-
// src/server/
|
|
1591
|
-
|
|
1592
|
-
var
|
|
1593
|
-
var
|
|
1594
|
-
|
|
1595
|
-
var scopes = /* @__PURE__ */ new Set(["lineage_workspace", "content_post", "content_queue_lane", "selection_set", "project_channel"]);
|
|
1596
|
-
var AgentClaimError = class extends Error {
|
|
1597
|
-
constructor(message, status = 400, code = "agent_claim_error", conflicts = []) {
|
|
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) {
|
|
1598
2470
|
super(message);
|
|
1599
2471
|
this.status = status;
|
|
1600
|
-
this.code = code;
|
|
1601
|
-
this.conflicts = conflicts;
|
|
1602
2472
|
}
|
|
1603
2473
|
status;
|
|
1604
|
-
code;
|
|
1605
|
-
conflicts;
|
|
1606
2474
|
};
|
|
1607
|
-
function
|
|
1608
|
-
return error instanceof
|
|
1609
|
-
}
|
|
1610
|
-
function parseClaimTtl(value) {
|
|
1611
|
-
if (!value) return defaultTtlSeconds;
|
|
1612
|
-
const match = value.trim().match(/^(\d+)(s|m|h)?$/);
|
|
1613
|
-
if (!match) throw new AgentClaimError(`Invalid claim ttl: ${value}`);
|
|
1614
|
-
const amount = Number(match[1]);
|
|
1615
|
-
const unit = match[2] || "s";
|
|
1616
|
-
const multiplier = unit === "h" ? 3600 : unit === "m" ? 60 : 1;
|
|
1617
|
-
const seconds = amount * multiplier;
|
|
1618
|
-
if (!Number.isInteger(seconds) || seconds < 30 || seconds > 24 * 60 * 60) {
|
|
1619
|
-
throw new AgentClaimError("Claim ttl must be between 30 seconds and 24 hours");
|
|
1620
|
-
}
|
|
1621
|
-
return seconds;
|
|
1622
|
-
}
|
|
1623
|
-
function randomId(prefix) {
|
|
1624
|
-
return `${prefix}_${Date.now().toString(36)}_${randomBytes(6).toString("base64url").toLowerCase()}`;
|
|
1625
|
-
}
|
|
1626
|
-
function tokenHash(token) {
|
|
1627
|
-
return createHash2("sha256").update(token).digest("hex");
|
|
1628
|
-
}
|
|
1629
|
-
function safeEqual(a, b) {
|
|
1630
|
-
const left = Buffer.from(a);
|
|
1631
|
-
const right = Buffer.from(b);
|
|
1632
|
-
return left.length === right.length && timingSafeEqual(left, right);
|
|
1633
|
-
}
|
|
1634
|
-
function expiresAtFrom(timestamp, ttlSeconds) {
|
|
1635
|
-
return new Date(new Date(timestamp).getTime() + ttlSeconds * 1e3).toISOString();
|
|
1636
|
-
}
|
|
1637
|
-
function metadataJson(metadata2) {
|
|
1638
|
-
return metadata2 ? JSON.stringify(metadata2) : null;
|
|
1639
|
-
}
|
|
1640
|
-
function parseMetadata(value) {
|
|
1641
|
-
if (typeof value !== "string" || !value) return void 0;
|
|
1642
|
-
try {
|
|
1643
|
-
const parsed = JSON.parse(value);
|
|
1644
|
-
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
|
|
1645
|
-
} catch {
|
|
1646
|
-
return void 0;
|
|
1647
|
-
}
|
|
1648
|
-
}
|
|
1649
|
-
function derivedState(row, now = /* @__PURE__ */ new Date()) {
|
|
1650
|
-
if (row.status !== "active") return row.status === "expired" ? "expired" : "stale";
|
|
1651
|
-
if (new Date(row.expires_at).getTime() <= now.getTime()) return "expired";
|
|
1652
|
-
const ageSeconds = Math.max(0, Math.floor((now.getTime() - new Date(row.heartbeat_at).getTime()) / 1e3));
|
|
1653
|
-
if (ageSeconds >= staleAfterSeconds) return "stale";
|
|
1654
|
-
if (ageSeconds >= idleAfterSeconds) return "idle";
|
|
1655
|
-
return "active";
|
|
1656
|
-
}
|
|
1657
|
-
function rowToClaim(row, now = /* @__PURE__ */ new Date()) {
|
|
1658
|
-
const heartbeatAt = String(row.heartbeat_at);
|
|
1659
|
-
return {
|
|
1660
|
-
id: String(row.id),
|
|
1661
|
-
project: String(row.project_id),
|
|
1662
|
-
channel: typeof row.channel === "string" ? row.channel : void 0,
|
|
1663
|
-
scope_type: String(row.scope_type),
|
|
1664
|
-
target_id: String(row.target_id),
|
|
1665
|
-
target_title: typeof row.target_title === "string" ? row.target_title : void 0,
|
|
1666
|
-
agent_id: typeof row.agent_id === "string" ? row.agent_id : void 0,
|
|
1667
|
-
agent_name: String(row.agent_name),
|
|
1668
|
-
agent_kind: String(row.agent_kind),
|
|
1669
|
-
thread_id: typeof row.thread_id === "string" ? row.thread_id : void 0,
|
|
1670
|
-
status: String(row.status),
|
|
1671
|
-
created_at: String(row.created_at),
|
|
1672
|
-
heartbeat_at: heartbeatAt,
|
|
1673
|
-
expires_at: String(row.expires_at),
|
|
1674
|
-
released_at: typeof row.released_at === "string" ? row.released_at : void 0,
|
|
1675
|
-
revoked_at: typeof row.revoked_at === "string" ? row.revoked_at : void 0,
|
|
1676
|
-
revoked_by: typeof row.revoked_by === "string" ? row.revoked_by : void 0,
|
|
1677
|
-
override_reason: typeof row.override_reason === "string" ? row.override_reason : void 0,
|
|
1678
|
-
metadata: parseMetadata(row.metadata_json),
|
|
1679
|
-
heartbeat_age_seconds: Math.max(0, Math.floor((now.getTime() - new Date(heartbeatAt).getTime()) / 1e3)),
|
|
1680
|
-
derived_state: derivedState({
|
|
1681
|
-
expires_at: String(row.expires_at),
|
|
1682
|
-
heartbeat_at: heartbeatAt,
|
|
1683
|
-
status: String(row.status)
|
|
1684
|
-
}, now)
|
|
1685
|
-
};
|
|
2475
|
+
function isLineageWorkspaceError(error) {
|
|
2476
|
+
return error instanceof LineageWorkspaceError;
|
|
1686
2477
|
}
|
|
1687
|
-
function
|
|
1688
|
-
return {
|
|
1689
|
-
claim_id: String(row.claim_id),
|
|
1690
|
-
event_type: String(row.event_type),
|
|
1691
|
-
actor: typeof row.actor === "string" ? row.actor : void 0,
|
|
1692
|
-
message: typeof row.message === "string" ? row.message : void 0,
|
|
1693
|
-
created_at: String(row.created_at),
|
|
1694
|
-
metadata: parseMetadata(row.metadata_json)
|
|
1695
|
-
};
|
|
2478
|
+
function lineageWorkspaceId(project, rootAssetId) {
|
|
2479
|
+
return `${project}:lineage-workspace:${rootAssetId}`;
|
|
1696
2480
|
}
|
|
1697
2481
|
function ensureProject3(database, project) {
|
|
1698
2482
|
const timestamp = nowIso();
|
|
@@ -1702,259 +2486,252 @@ function ensureProject3(database, project) {
|
|
|
1702
2486
|
on conflict(id) do update set product = excluded.product, updated_at = excluded.updated_at
|
|
1703
2487
|
`).run(project, project, timestamp, timestamp);
|
|
1704
2488
|
}
|
|
1705
|
-
function
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
`).run(randomId("claim_event"), claimId, eventType, actor || null, message || null, nowIso(), metadataJson(metadata2));
|
|
1710
|
-
}
|
|
1711
|
-
function expireActiveClaims(database) {
|
|
1712
|
-
const timestamp = nowIso();
|
|
1713
|
-
const expired = database.prepare(`
|
|
1714
|
-
select id from agent_claims where status = 'active' and expires_at <= ?
|
|
1715
|
-
`).all(timestamp);
|
|
1716
|
-
if (expired.length === 0) return;
|
|
1717
|
-
database.prepare(`
|
|
1718
|
-
update agent_claims set status = 'expired' where status = 'active' and expires_at <= ?
|
|
1719
|
-
`).run(timestamp);
|
|
1720
|
-
for (const claim of expired) recordEvent(database, claim.id, "expired", "system", "Claim expired after missed heartbeat.");
|
|
1721
|
-
}
|
|
1722
|
-
function normalizeScope(scopeType) {
|
|
1723
|
-
if (!scopes.has(scopeType)) throw new AgentClaimError(`Unsupported claim scope: ${scopeType}`);
|
|
1724
|
-
return scopeType;
|
|
1725
|
-
}
|
|
1726
|
-
function channelOverlaps(left, right) {
|
|
1727
|
-
return !left || !right || left === right;
|
|
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;
|
|
1728
2493
|
}
|
|
1729
|
-
function
|
|
1730
|
-
|
|
1731
|
-
if (!
|
|
1732
|
-
|
|
1733
|
-
return candidate.scope_type === "project_channel" || existing.scope_type === "project_channel";
|
|
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;
|
|
1734
2498
|
}
|
|
1735
|
-
function
|
|
1736
|
-
const
|
|
1737
|
-
|
|
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;
|
|
1738
2503
|
}
|
|
1739
|
-
function
|
|
1740
|
-
|
|
1741
|
-
|
|
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
|
+
};
|
|
1742
2517
|
}
|
|
1743
|
-
function
|
|
1744
|
-
const
|
|
1745
|
-
|
|
1746
|
-
if (!row) return null;
|
|
1747
|
-
return safeEqual(String(row.token_hash), tokenHash(claimToken)) ? row : null;
|
|
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;
|
|
1748
2521
|
}
|
|
1749
|
-
function
|
|
1750
|
-
|
|
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;
|
|
1751
2525
|
}
|
|
1752
|
-
function
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
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 }));
|
|
1758
2539
|
}
|
|
1759
|
-
function
|
|
1760
|
-
|
|
1761
|
-
const
|
|
1762
|
-
const
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
if (conflicts.length > 0 && !fields.force) {
|
|
1775
|
-
throw new AgentClaimError("Target already has an active overlapping agent claim.", 409, "target_already_claimed", conflicts);
|
|
1776
|
-
}
|
|
1777
|
-
if (conflicts.length > 0 && !fields.reason?.trim()) {
|
|
1778
|
-
throw new AgentClaimError("Overriding an active claim requires --reason.", 400, "override_reason_required", conflicts);
|
|
1779
|
-
}
|
|
1780
|
-
const timestamp = nowIso();
|
|
1781
|
-
for (const conflict of conflicts) {
|
|
1782
|
-
database.prepare(`
|
|
1783
|
-
update agent_claims
|
|
1784
|
-
set status = 'revoked', revoked_at = ?, revoked_by = ?, override_reason = ?
|
|
1785
|
-
where id = ? and status = 'active'
|
|
1786
|
-
`).run(timestamp, agentName, fields.reason || null, conflict.id);
|
|
1787
|
-
recordEvent(database, conflict.id, "revoked", agentName, fields.reason || "Revoked by forced claim takeover.");
|
|
1788
|
-
recordEvent(database, conflict.id, "conflict", agentName, `Overridden by ${agentName}.`, { new_claim_target: targetId });
|
|
1789
|
-
}
|
|
1790
|
-
const id = randomId("claim");
|
|
1791
|
-
const secret = randomBytes(24).toString("base64url");
|
|
1792
|
-
const claimToken = `${id}.${secret}`;
|
|
1793
|
-
const expiresAt = expiresAtFrom(timestamp, ttlSeconds);
|
|
1794
|
-
database.prepare(`
|
|
1795
|
-
insert into agent_claims (
|
|
1796
|
-
id, token_hash, project_id, channel, scope_type, target_id, target_title, agent_id, agent_name,
|
|
1797
|
-
agent_kind, thread_id, status, created_at, heartbeat_at, expires_at, metadata_json
|
|
1798
|
-
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', ?, ?, ?, ?)
|
|
1799
|
-
`).run(
|
|
1800
|
-
id,
|
|
1801
|
-
tokenHash(claimToken),
|
|
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),
|
|
1802
2555
|
project,
|
|
1803
|
-
|
|
1804
|
-
scopeType,
|
|
1805
|
-
targetId,
|
|
1806
|
-
fields.targetTitle?.trim() || null,
|
|
1807
|
-
fields.agentId?.trim() || null,
|
|
1808
|
-
agentName,
|
|
1809
|
-
fields.agentKind?.trim() || "codex",
|
|
1810
|
-
fields.threadId?.trim() || null,
|
|
2556
|
+
root.selected_at || null,
|
|
1811
2557
|
timestamp,
|
|
1812
2558
|
timestamp,
|
|
1813
|
-
|
|
1814
|
-
|
|
2559
|
+
project,
|
|
2560
|
+
root.root_asset_id
|
|
1815
2561
|
);
|
|
1816
|
-
recordEvent(database, id, "created", agentName, `Claimed ${scopeType} ${targetId}.`, { ttl_seconds: ttlSeconds });
|
|
1817
|
-
const claim = findClaimById(database, id);
|
|
1818
|
-
return { ok: true, claim, claim_token: claimToken, conflicts_revoked: conflicts.map((conflict) => conflict.id) };
|
|
1819
|
-
} finally {
|
|
1820
|
-
database.close();
|
|
1821
2562
|
}
|
|
1822
2563
|
}
|
|
1823
|
-
function
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
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);
|
|
1832
2574
|
}
|
|
1833
|
-
function
|
|
1834
|
-
const
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
} finally {
|
|
1842
|
-
database.close();
|
|
1843
|
-
}
|
|
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;
|
|
1844
2583
|
}
|
|
1845
|
-
function
|
|
2584
|
+
function listLineageWorkspaces(project) {
|
|
1846
2585
|
const database = lineageDb();
|
|
1847
2586
|
try {
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
recordEvent(database, claim.id, "heartbeat", claim.agent_name, "Claim heartbeat received.");
|
|
1856
|
-
return { ok: true, claim: findClaimById(database, claim.id) };
|
|
2587
|
+
seedLegacyWorkspaces(database, project);
|
|
2588
|
+
return {
|
|
2589
|
+
project,
|
|
2590
|
+
active_workspace: activeWorkspace(database, project),
|
|
2591
|
+
workspaces: listRows(database, project),
|
|
2592
|
+
fetchedAt: nowIso()
|
|
2593
|
+
};
|
|
1857
2594
|
} finally {
|
|
1858
2595
|
database.close();
|
|
1859
2596
|
}
|
|
1860
2597
|
}
|
|
1861
|
-
function
|
|
2598
|
+
function inspectLineageWorkspace(project, workspaceId) {
|
|
1862
2599
|
const database = lineageDb();
|
|
1863
2600
|
try {
|
|
1864
|
-
|
|
1865
|
-
const
|
|
1866
|
-
if (!
|
|
1867
|
-
|
|
1868
|
-
if (claim.status !== "active") throw new AgentClaimError(`Agent claim is ${claim.status}.`, 409, "claim_not_active");
|
|
1869
|
-
const timestamp = nowIso();
|
|
1870
|
-
database.prepare("update agent_claims set status = 'released', released_at = ? where id = ?").run(timestamp, claim.id);
|
|
1871
|
-
recordEvent(database, claim.id, "released", claim.agent_name, "Claim released by token holder.");
|
|
1872
|
-
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;
|
|
1873
2605
|
} finally {
|
|
1874
2606
|
database.close();
|
|
1875
2607
|
}
|
|
1876
2608
|
}
|
|
1877
|
-
function
|
|
1878
|
-
|
|
1879
|
-
if (!
|
|
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);
|
|
1880
2614
|
const database = lineageDb();
|
|
1881
2615
|
try {
|
|
1882
|
-
|
|
1883
|
-
const claim = findClaimById(database, claimId, project);
|
|
1884
|
-
if (!claim) throw new AgentClaimError(`Unknown agent claim: ${claimId}`, 404, "claim_not_found");
|
|
1885
|
-
if (claim.status !== "active" || claim.derived_state !== "stale") {
|
|
1886
|
-
throw new AgentClaimError("Only stale active claims can be released without the claim token.", 409, "claim_not_stale", [claim]);
|
|
1887
|
-
}
|
|
2616
|
+
const root = requireAsset2(database, project, rootAssetId);
|
|
1888
2617
|
const timestamp = nowIso();
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
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
|
+
};
|
|
1892
2659
|
} finally {
|
|
1893
2660
|
database.close();
|
|
1894
2661
|
}
|
|
1895
2662
|
}
|
|
1896
|
-
function
|
|
1897
|
-
if (!fields.confirmWrite) throw new AgentClaimError("Revoking an agent claim requires confirmWrite=true.", 400, "confirm_write_required");
|
|
1898
|
-
if (!fields.reason?.trim()) throw new AgentClaimError("Revoking an agent claim requires a reason.", 400, "reason_required");
|
|
2663
|
+
function updateLineageWorkspace(project, workspaceId, fields) {
|
|
1899
2664
|
const database = lineageDb();
|
|
1900
2665
|
try {
|
|
1901
|
-
|
|
1902
|
-
const
|
|
1903
|
-
if (!
|
|
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);
|
|
1904
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 };
|
|
1905
2679
|
database.prepare(`
|
|
1906
|
-
update
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
return {
|
|
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
|
+
};
|
|
1911
2689
|
} finally {
|
|
1912
2690
|
database.close();
|
|
1913
2691
|
}
|
|
1914
2692
|
}
|
|
1915
|
-
function
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
2693
|
+
function activateLineageWorkspace(project, workspaceId, confirmWrite) {
|
|
2694
|
+
return updateLineageWorkspace(project, workspaceId, { activate: true, status: "active", confirmWrite });
|
|
2695
|
+
}
|
|
2696
|
+
function archiveLineageWorkspace(project, workspaceId, confirmWrite) {
|
|
1919
2697
|
const database = lineageDb();
|
|
1920
2698
|
try {
|
|
1921
|
-
|
|
1922
|
-
const
|
|
1923
|
-
if (!
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
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
|
+
};
|
|
1928
2726
|
} finally {
|
|
1929
2727
|
database.close();
|
|
1930
2728
|
}
|
|
1931
2729
|
}
|
|
1932
|
-
function
|
|
1933
|
-
if (fields.dangerLevel === "danger" && !fields.confirmWrite) {
|
|
1934
|
-
return denied("human_confirmation_required", "Dangerous write requires explicit human confirmation.");
|
|
1935
|
-
}
|
|
1936
|
-
if (!fields.claimToken) return denied("claim_required", "Mutating agent write requires a matching claim token.");
|
|
2730
|
+
function activeLineageWorkspaceRoot(project) {
|
|
1937
2731
|
const database = lineageDb();
|
|
1938
2732
|
try {
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
if (!row) return denied("claim_token_invalid", "Unknown or invalid claim token.");
|
|
1942
|
-
const claim = rowToClaim(row);
|
|
1943
|
-
if (claim.status !== "active") return denied("claim_not_active", `Agent claim is ${claim.status}.`);
|
|
1944
|
-
if (new Date(claim.expires_at).getTime() <= Date.now()) return denied("claim_expired", "Agent claim has expired.");
|
|
1945
|
-
if (claim.project !== fields.project) return denied("claim_project_mismatch", `Claim project ${claim.project} does not match ${fields.project}.`, [claim]);
|
|
1946
|
-
if (fields.channel && claim.channel && claim.channel !== fields.channel) {
|
|
1947
|
-
return denied("claim_channel_mismatch", `Claim channel ${claim.channel} does not match ${fields.channel}.`, [claim]);
|
|
1948
|
-
}
|
|
1949
|
-
if (!scopeAllowsWrite(claim, fields.scopeType, fields.targetId, fields.writeKind)) {
|
|
1950
|
-
return denied("claim_scope_mismatch", `Claim does not cover ${fields.scopeType} ${fields.targetId}.`, [claim]);
|
|
1951
|
-
}
|
|
1952
|
-
recordEvent(database, claim.id, "write_allowed", claim.agent_name, `${fields.writeKind} allowed.`, {
|
|
1953
|
-
danger_level: fields.dangerLevel,
|
|
1954
|
-
target_id: fields.targetId,
|
|
1955
|
-
write_kind: fields.writeKind
|
|
1956
|
-
});
|
|
1957
|
-
return { ok: true, claim, warnings: [] };
|
|
2733
|
+
seedLegacyWorkspaces(database, project);
|
|
2734
|
+
return activeWorkspace(database, project)?.root_asset_id;
|
|
1958
2735
|
} finally {
|
|
1959
2736
|
database.close();
|
|
1960
2737
|
}
|
|
@@ -2066,7 +2843,7 @@ function indexLineageAssets(project = defaultProject) {
|
|
|
2066
2843
|
database.close();
|
|
2067
2844
|
return { catalog: catalog.length, local: local.length, total: catalog.length + local.length, database: lineageDbPath() };
|
|
2068
2845
|
}
|
|
2069
|
-
function
|
|
2846
|
+
function requireAsset3(database, project, assetId) {
|
|
2070
2847
|
const row = database.prepare("select id from assets where project_id = ? and id = ?").get(project, assetId);
|
|
2071
2848
|
if (!row) throw new LineageError(`Unknown indexed asset: ${assetId}`, 404);
|
|
2072
2849
|
}
|
|
@@ -2085,6 +2862,11 @@ function rootFor(database, project, assetId) {
|
|
|
2085
2862
|
}
|
|
2086
2863
|
return assetId;
|
|
2087
2864
|
}
|
|
2865
|
+
function assertCanonicalRoot(database, project, root) {
|
|
2866
|
+
requireAsset3(database, project, root);
|
|
2867
|
+
const canonicalRoot = rootFor(database, project, root);
|
|
2868
|
+
if (canonicalRoot !== root) throw new LineageError(`Asset ${root} is not a lineage root`, 400);
|
|
2869
|
+
}
|
|
2088
2870
|
function explicitWorkspaceRoot(database, project, assetId) {
|
|
2089
2871
|
const row = database.prepare(`
|
|
2090
2872
|
select root_asset_id from lineage_workspaces
|
|
@@ -2118,7 +2900,7 @@ function lineageWriteClaimContext(database, project, assetId) {
|
|
|
2118
2900
|
function getLineageWriteClaimContext(project, assetId) {
|
|
2119
2901
|
const database = lineageDb();
|
|
2120
2902
|
try {
|
|
2121
|
-
|
|
2903
|
+
requireAsset3(database, project, assetId);
|
|
2122
2904
|
return lineageWriteClaimContext(database, project, assetId);
|
|
2123
2905
|
} finally {
|
|
2124
2906
|
database.close();
|
|
@@ -2130,17 +2912,106 @@ function latestSelectedRoot(database, project) {
|
|
|
2130
2912
|
}
|
|
2131
2913
|
function resolveRoot(database, project, rootAssetId) {
|
|
2132
2914
|
if (rootAssetId) {
|
|
2133
|
-
|
|
2915
|
+
requireAsset3(database, project, rootAssetId);
|
|
2134
2916
|
return rootAssetId;
|
|
2135
2917
|
}
|
|
2136
2918
|
const root = activeLineageWorkspaceRoot(project) || latestSelectedRoot(database, project);
|
|
2137
2919
|
if (!root) throw new LineageError("Lineage command requires --root unless a project selection exists");
|
|
2138
|
-
|
|
2920
|
+
requireAsset3(database, project, root);
|
|
2139
2921
|
return root;
|
|
2140
2922
|
}
|
|
2141
2923
|
function edgeId(project, parent, child) {
|
|
2142
2924
|
return `${project}:${parent}:derived_from:${child}`;
|
|
2143
2925
|
}
|
|
2926
|
+
function rerollRequestId(project, root, node, timestamp) {
|
|
2927
|
+
return `${project}:${root}:reroll:${node}:${Date.parse(timestamp) || Date.now()}`;
|
|
2928
|
+
}
|
|
2929
|
+
function rowString(value) {
|
|
2930
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
2931
|
+
}
|
|
2932
|
+
function rerollRequestFrom(row) {
|
|
2933
|
+
return {
|
|
2934
|
+
id: String(row.id),
|
|
2935
|
+
project_id: String(row.project_id),
|
|
2936
|
+
root_asset_id: String(row.root_asset_id),
|
|
2937
|
+
node_asset_id: String(row.node_asset_id),
|
|
2938
|
+
status: row.status,
|
|
2939
|
+
requested_by: row.requested_by,
|
|
2940
|
+
notes: rowString(row.notes),
|
|
2941
|
+
created_at: String(row.created_at),
|
|
2942
|
+
resolved_at: rowString(row.resolved_at)
|
|
2943
|
+
};
|
|
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
|
+
}
|
|
2972
|
+
function attemptFrom(row) {
|
|
2973
|
+
return {
|
|
2974
|
+
id: String(row.id),
|
|
2975
|
+
project_id: String(row.project_id),
|
|
2976
|
+
node_asset_id: String(row.node_asset_id),
|
|
2977
|
+
asset_id: String(row.asset_id),
|
|
2978
|
+
attempt_index: Number(row.attempt_index),
|
|
2979
|
+
source: row.source,
|
|
2980
|
+
prompt: rowString(row.prompt),
|
|
2981
|
+
generation_job_id: rowString(row.generation_job_id),
|
|
2982
|
+
file_path: rowString(row.file_path),
|
|
2983
|
+
checksum_sha256: rowString(row.checksum_sha256),
|
|
2984
|
+
created_at: String(row.created_at),
|
|
2985
|
+
promoted_at: rowString(row.promoted_at),
|
|
2986
|
+
is_current: Boolean(Number(row.is_current))
|
|
2987
|
+
};
|
|
2988
|
+
}
|
|
2989
|
+
function implicitAttempt(row, isCurrent = true) {
|
|
2990
|
+
const createdAt = row.asset_created_at || nowIso();
|
|
2991
|
+
return {
|
|
2992
|
+
id: `${row.project}:${row.asset_id}:attempt:implicit`,
|
|
2993
|
+
project_id: row.project,
|
|
2994
|
+
node_asset_id: row.asset_id,
|
|
2995
|
+
asset_id: row.asset_id,
|
|
2996
|
+
attempt_index: 1,
|
|
2997
|
+
source: "initial",
|
|
2998
|
+
file_path: row.local_path,
|
|
2999
|
+
checksum_sha256: row.checksum_sha256,
|
|
3000
|
+
created_at: createdAt,
|
|
3001
|
+
promoted_at: createdAt,
|
|
3002
|
+
is_current: isCurrent
|
|
3003
|
+
};
|
|
3004
|
+
}
|
|
3005
|
+
function withImplicitAttempt(physicalAttempts, row) {
|
|
3006
|
+
if (physicalAttempts.some((attempt) => attempt.source === "initial")) return physicalAttempts;
|
|
3007
|
+
return [...physicalAttempts, implicitAttempt(row, !physicalAttempts.some((attempt) => attempt.is_current))];
|
|
3008
|
+
}
|
|
3009
|
+
function assertNodeInRoot(database, project, root, node) {
|
|
3010
|
+
assertCanonicalRoot(database, project, root);
|
|
3011
|
+
requireAsset3(database, project, node);
|
|
3012
|
+
const nodeRoot = rootFor(database, project, node);
|
|
3013
|
+
if (nodeRoot !== root) throw new LineageError(`Asset ${node} is not in lineage rooted at ${root}`);
|
|
3014
|
+
}
|
|
2144
3015
|
function canPreviewLocally(mediaType, localPath) {
|
|
2145
3016
|
return Boolean(localPath && ["image", "video", "gif"].includes(mediaType));
|
|
2146
3017
|
}
|
|
@@ -2151,8 +3022,8 @@ function localPreviewUrl(project, localPath) {
|
|
|
2151
3022
|
}
|
|
2152
3023
|
function linkLineageAssets(project, fields) {
|
|
2153
3024
|
const database = lineageDb();
|
|
2154
|
-
|
|
2155
|
-
|
|
3025
|
+
requireAsset3(database, project, fields.parentAssetId);
|
|
3026
|
+
requireAsset3(database, project, fields.childAssetId);
|
|
2156
3027
|
if (fields.parentAssetId === fields.childAssetId) throw new LineageError("Lineage link cannot point to itself");
|
|
2157
3028
|
const claimContext = lineageWriteClaimContext(database, project, fields.parentAssetId);
|
|
2158
3029
|
try {
|
|
@@ -2203,20 +3074,37 @@ function descendants(database, project, root) {
|
|
|
2203
3074
|
}
|
|
2204
3075
|
function getLineageSnapshot(project, assetId) {
|
|
2205
3076
|
const database = lineageDb();
|
|
2206
|
-
|
|
3077
|
+
requireAsset3(database, project, assetId);
|
|
2207
3078
|
const root = explicitWorkspaceRoot(database, project, assetId) || rootFor(database, project, assetId);
|
|
2208
3079
|
const edges = descendants(database, project, root);
|
|
2209
|
-
const
|
|
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
|
+
])];
|
|
2210
3088
|
const placeholders2 = ids.map(() => "?").join(",");
|
|
2211
3089
|
const rows = database.prepare(`
|
|
2212
3090
|
select a.id asset_id, a.project_id project, a.source, a.title, a.media_type, a.status, a.channel, a.campaign,
|
|
2213
3091
|
a.local_path, a.s3_key, a.checksum_sha256, coalesce(r.review_state, 'unreviewed') review_state,
|
|
2214
|
-
r.notes review_notes, l.x layout_x, l.y layout_y
|
|
3092
|
+
r.notes review_notes, a.created_at asset_created_at, l.x layout_x, l.y layout_y
|
|
2215
3093
|
from assets a left join asset_reviews r on r.asset_id = a.id
|
|
2216
3094
|
left join asset_layouts l on l.project_id = a.project_id and l.root_asset_id = ? and l.asset_id = a.id
|
|
2217
3095
|
where a.project_id = ? and a.id in (${placeholders2})
|
|
2218
3096
|
`).all(root, project, ...ids);
|
|
2219
|
-
const
|
|
3097
|
+
const attemptRows = ids.length > 0 ? database.prepare(`select * from asset_attempts where project_id = ? and node_asset_id in (${placeholders2}) order by node_asset_id, attempt_index desc`).all(project, ...ids) : [];
|
|
3098
|
+
const attemptsByNode = /* @__PURE__ */ new Map();
|
|
3099
|
+
for (const attempt of attemptRows.map(attemptFrom)) {
|
|
3100
|
+
attemptsByNode.set(attempt.node_asset_id, [...attemptsByNode.get(attempt.node_asset_id) || [], attempt]);
|
|
3101
|
+
}
|
|
3102
|
+
const lineageTasksByNode = tasksByTarget(tasks);
|
|
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) : [];
|
|
3104
|
+
const rerollsByNode = new Map(rerollRows.map((row) => {
|
|
3105
|
+
const request = rerollRequestFrom(row);
|
|
3106
|
+
return [request.node_asset_id, withRerollTask(request, lineageTasksByNode.get(request.node_asset_id)?.reroll)];
|
|
3107
|
+
}));
|
|
2220
3108
|
const childIds = new Set(edges.map((edge) => edge.parent_asset_id));
|
|
2221
3109
|
const selectedIds = new Set(selected.map((row) => row.asset_id));
|
|
2222
3110
|
const selections = selected.map((row) => ({
|
|
@@ -2228,13 +3116,21 @@ function getLineageSnapshot(project, assetId) {
|
|
|
2228
3116
|
const selection = selections[0] || null;
|
|
2229
3117
|
const nodes = rows.map((row) => {
|
|
2230
3118
|
const position = typeof row.layout_x === "number" && typeof row.layout_y === "number" ? { x: row.layout_x, y: row.layout_y } : void 0;
|
|
2231
|
-
const { layout_x: _layoutX, layout_y: _layoutY, ...node } = row;
|
|
3119
|
+
const { asset_created_at: _assetCreatedAt, layout_x: _layoutX, layout_y: _layoutY, ...node } = row;
|
|
2232
3120
|
const nodeSelection = selections.find((item) => item.asset_id === row.asset_id);
|
|
3121
|
+
const attempts = withImplicitAttempt(attemptsByNode.get(row.asset_id) || [], row);
|
|
3122
|
+
const currentAttempt = attempts.find((attempt) => attempt.is_current) || attempts[0];
|
|
3123
|
+
const previewPath = currentAttempt?.file_path || row.local_path;
|
|
3124
|
+
const lineageTasks = lineageTasksByNode.get(row.asset_id);
|
|
2233
3125
|
return {
|
|
2234
3126
|
...node,
|
|
3127
|
+
attempt_count: attempts.length,
|
|
3128
|
+
current_attempt: currentAttempt,
|
|
2235
3129
|
is_latest: !childIds.has(row.asset_id),
|
|
3130
|
+
lineage_tasks: lineageTasks && Object.keys(lineageTasks).length > 0 ? lineageTasks : void 0,
|
|
2236
3131
|
position,
|
|
2237
|
-
preview_url: canPreviewLocally(row.media_type,
|
|
3132
|
+
preview_url: canPreviewLocally(row.media_type, previewPath) ? localPreviewUrl(project, previewPath) : void 0,
|
|
3133
|
+
reroll_request: rerollsByNode.get(row.asset_id),
|
|
2238
3134
|
selection_note: nodeSelection?.notes,
|
|
2239
3135
|
user_selected: selectedIds.has(row.asset_id)
|
|
2240
3136
|
};
|
|
@@ -2247,6 +3143,7 @@ function getLineageSnapshot(project, assetId) {
|
|
|
2247
3143
|
selected: selections.map((row) => row.asset_id),
|
|
2248
3144
|
selection,
|
|
2249
3145
|
selections,
|
|
3146
|
+
tasks,
|
|
2250
3147
|
latest: nodes.filter((node) => node.is_latest).map((node) => node.asset_id),
|
|
2251
3148
|
nodes,
|
|
2252
3149
|
edges,
|
|
@@ -2256,8 +3153,8 @@ function getLineageSnapshot(project, assetId) {
|
|
|
2256
3153
|
function updateLineageLayout(project, fields) {
|
|
2257
3154
|
if (fields.positions.length === 0) throw new LineageError("Lineage layout requires at least one position");
|
|
2258
3155
|
const database = lineageDb();
|
|
2259
|
-
|
|
2260
|
-
for (const position of fields.positions)
|
|
3156
|
+
requireAsset3(database, project, fields.rootAssetId);
|
|
3157
|
+
for (const position of fields.positions) requireAsset3(database, project, position.assetId);
|
|
2261
3158
|
try {
|
|
2262
3159
|
requireLineageWorkspaceClaimForWrite({
|
|
2263
3160
|
channel: assetChannel(database, project, fields.rootAssetId),
|
|
@@ -2293,7 +3190,25 @@ function getLineageNextAsset(project, rootAssetId) {
|
|
|
2293
3190
|
const root = resolveRoot(database, project, rootAssetId);
|
|
2294
3191
|
database.close();
|
|
2295
3192
|
const snapshot = getLineageSnapshot(project, root);
|
|
2296
|
-
const
|
|
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;
|
|
2297
3212
|
const latestNodes = snapshot.nodes.filter((node) => snapshot.latest.includes(node.asset_id));
|
|
2298
3213
|
const warnings = [];
|
|
2299
3214
|
for (const selectedNode of selectedNodes) {
|
|
@@ -2311,9 +3226,9 @@ function getLineageNextAsset(project, rootAssetId) {
|
|
|
2311
3226
|
next_asset: selectedNodes[0],
|
|
2312
3227
|
next_assets: selectedNodes,
|
|
2313
3228
|
latest: snapshot.latest,
|
|
2314
|
-
selected:
|
|
2315
|
-
selection:
|
|
2316
|
-
selections:
|
|
3229
|
+
selected: selectedIds,
|
|
3230
|
+
selection: selectedSelection,
|
|
3231
|
+
selections: selectedSelections,
|
|
2317
3232
|
candidates: latestNodes,
|
|
2318
3233
|
warnings,
|
|
2319
3234
|
fetchedAt: nowIso()
|
|
@@ -2330,9 +3245,9 @@ function getLineageNextAsset(project, rootAssetId) {
|
|
|
2330
3245
|
next_asset: latestNodes[0],
|
|
2331
3246
|
next_assets: [latestNodes[0]],
|
|
2332
3247
|
latest: snapshot.latest,
|
|
2333
|
-
selected:
|
|
2334
|
-
selection:
|
|
2335
|
-
selections:
|
|
3248
|
+
selected: selectedIds,
|
|
3249
|
+
selection: selectedSelection,
|
|
3250
|
+
selections: selectedSelections,
|
|
2336
3251
|
candidates: latestNodes,
|
|
2337
3252
|
warnings,
|
|
2338
3253
|
fetchedAt: nowIso()
|
|
@@ -2348,9 +3263,9 @@ function getLineageNextAsset(project, rootAssetId) {
|
|
|
2348
3263
|
next_asset: null,
|
|
2349
3264
|
next_assets: [],
|
|
2350
3265
|
latest: snapshot.latest,
|
|
2351
|
-
selected:
|
|
2352
|
-
selection:
|
|
2353
|
-
selections:
|
|
3266
|
+
selected: selectedIds,
|
|
3267
|
+
selection: selectedSelection,
|
|
3268
|
+
selections: selectedSelections,
|
|
2354
3269
|
candidates: latestNodes,
|
|
2355
3270
|
warnings,
|
|
2356
3271
|
fetchedAt: nowIso()
|
|
@@ -2368,49 +3283,281 @@ function getLineageChildren(project, parentAssetId) {
|
|
|
2368
3283
|
fetchedAt: nowIso()
|
|
2369
3284
|
};
|
|
2370
3285
|
}
|
|
3286
|
+
function listLineageRerollRequests(project, rootAssetId) {
|
|
3287
|
+
const database = lineageDb();
|
|
3288
|
+
try {
|
|
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]));
|
|
3293
|
+
const rows = database.prepare(`
|
|
3294
|
+
select * from asset_reroll_requests
|
|
3295
|
+
where project_id = ? and root_asset_id = ? and status = 'pending'
|
|
3296
|
+
order by created_at, id
|
|
3297
|
+
`).all(project, rootAssetId);
|
|
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() };
|
|
3307
|
+
} finally {
|
|
3308
|
+
database.close();
|
|
3309
|
+
}
|
|
3310
|
+
}
|
|
3311
|
+
function getLineageAttempts(project, rootAssetId, nodeAssetId) {
|
|
3312
|
+
const database = lineageDb();
|
|
3313
|
+
try {
|
|
3314
|
+
assertNodeInRoot(database, project, rootAssetId, nodeAssetId);
|
|
3315
|
+
const rows = database.prepare(`
|
|
3316
|
+
select * from asset_attempts
|
|
3317
|
+
where project_id = ? and node_asset_id = ?
|
|
3318
|
+
order by attempt_index desc, created_at desc
|
|
3319
|
+
`).all(project, nodeAssetId);
|
|
3320
|
+
const asset = database.prepare("select id asset_id, project_id project, local_path, checksum_sha256, created_at asset_created_at from assets where project_id = ? and id = ?").get(project, nodeAssetId);
|
|
3321
|
+
return { project, root_asset_id: rootAssetId, node_asset_id: nodeAssetId, attempts: withImplicitAttempt(rows.map(attemptFrom), asset), fetchedAt: nowIso() };
|
|
3322
|
+
} finally {
|
|
3323
|
+
database.close();
|
|
3324
|
+
}
|
|
3325
|
+
}
|
|
3326
|
+
function promoteLineageAttempt(project, fields) {
|
|
3327
|
+
const database = lineageDb();
|
|
3328
|
+
try {
|
|
3329
|
+
assertNodeInRoot(database, project, fields.rootAssetId, fields.nodeAssetId);
|
|
3330
|
+
const asset = database.prepare("select id asset_id, project_id project, local_path, checksum_sha256, created_at asset_created_at from assets where project_id = ? and id = ?").get(project, fields.nodeAssetId);
|
|
3331
|
+
const rows = database.prepare(`
|
|
3332
|
+
select * from asset_attempts
|
|
3333
|
+
where project_id = ? and node_asset_id = ?
|
|
3334
|
+
order by attempt_index desc, created_at desc
|
|
3335
|
+
`).all(project, fields.nodeAssetId);
|
|
3336
|
+
const attempts = withImplicitAttempt(rows.map(attemptFrom), asset);
|
|
3337
|
+
const target = attempts.find((attempt2) => attempt2.id === fields.attemptId);
|
|
3338
|
+
if (!target) throw new LineageError(`Attempt ${fields.attemptId} is not in ${fields.nodeAssetId}`, 404);
|
|
3339
|
+
const timestamp = nowIso();
|
|
3340
|
+
const promotedAttempt = { ...target, is_current: true, promoted_at: timestamp };
|
|
3341
|
+
if (!fields.confirmWrite) {
|
|
3342
|
+
return {
|
|
3343
|
+
ok: true,
|
|
3344
|
+
dryRun: true,
|
|
3345
|
+
project,
|
|
3346
|
+
root_asset_id: fields.rootAssetId,
|
|
3347
|
+
node_asset_id: fields.nodeAssetId,
|
|
3348
|
+
attempt: promotedAttempt,
|
|
3349
|
+
attempts: attempts.map((attempt2) => ({ ...attempt2, is_current: attempt2.id === target.id, promoted_at: attempt2.id === target.id ? timestamp : attempt2.promoted_at })),
|
|
3350
|
+
fetchedAt: timestamp
|
|
3351
|
+
};
|
|
3352
|
+
}
|
|
3353
|
+
database.exec("BEGIN IMMEDIATE");
|
|
3354
|
+
try {
|
|
3355
|
+
database.prepare("update asset_attempts set is_current = 0 where project_id = ? and node_asset_id = ?").run(project, fields.nodeAssetId);
|
|
3356
|
+
if (target.source !== "initial") {
|
|
3357
|
+
const result = database.prepare(`
|
|
3358
|
+
update asset_attempts
|
|
3359
|
+
set is_current = 1, promoted_at = ?
|
|
3360
|
+
where project_id = ? and node_asset_id = ? and id = ?
|
|
3361
|
+
`).run(timestamp, project, fields.nodeAssetId, target.id);
|
|
3362
|
+
if (result.changes !== 1) throw new LineageError(`Attempt ${fields.attemptId} is not in ${fields.nodeAssetId}`, 404);
|
|
3363
|
+
}
|
|
3364
|
+
database.exec("COMMIT");
|
|
3365
|
+
} catch (error) {
|
|
3366
|
+
database.exec("ROLLBACK");
|
|
3367
|
+
throw error;
|
|
3368
|
+
}
|
|
3369
|
+
const refreshedRows = database.prepare(`
|
|
3370
|
+
select * from asset_attempts
|
|
3371
|
+
where project_id = ? and node_asset_id = ?
|
|
3372
|
+
order by attempt_index desc, created_at desc
|
|
3373
|
+
`).all(project, fields.nodeAssetId);
|
|
3374
|
+
const refreshedAttempts = withImplicitAttempt(refreshedRows.map(attemptFrom), asset);
|
|
3375
|
+
const attempt = refreshedAttempts.find((item) => item.id === target.id) || promotedAttempt;
|
|
3376
|
+
return {
|
|
3377
|
+
ok: true,
|
|
3378
|
+
project,
|
|
3379
|
+
root_asset_id: fields.rootAssetId,
|
|
3380
|
+
node_asset_id: fields.nodeAssetId,
|
|
3381
|
+
attempt,
|
|
3382
|
+
attempts: refreshedAttempts,
|
|
3383
|
+
fetchedAt: nowIso()
|
|
3384
|
+
};
|
|
3385
|
+
} finally {
|
|
3386
|
+
database.close();
|
|
3387
|
+
}
|
|
3388
|
+
}
|
|
3389
|
+
function markLineageRerollRequest(project, fields) {
|
|
3390
|
+
const database = lineageDb();
|
|
3391
|
+
try {
|
|
3392
|
+
assertNodeInRoot(database, project, fields.rootAssetId, fields.nodeAssetId);
|
|
3393
|
+
const existing = database.prepare(`
|
|
3394
|
+
select * from asset_reroll_requests
|
|
3395
|
+
where project_id = ? and root_asset_id = ? and node_asset_id = ? and status = 'pending'
|
|
3396
|
+
order by created_at desc limit 1
|
|
3397
|
+
`).get(project, fields.rootAssetId, fields.nodeAssetId);
|
|
3398
|
+
const timestamp = nowIso();
|
|
3399
|
+
const request = existing ? {
|
|
3400
|
+
...rerollRequestFrom(existing),
|
|
3401
|
+
notes: fields.notes || rerollRequestFrom(existing).notes,
|
|
3402
|
+
requested_by: fields.requestedBy || rerollRequestFrom(existing).requested_by
|
|
3403
|
+
} : {
|
|
3404
|
+
id: rerollRequestId(project, fields.rootAssetId, fields.nodeAssetId, timestamp),
|
|
3405
|
+
project_id: project,
|
|
3406
|
+
root_asset_id: fields.rootAssetId,
|
|
3407
|
+
node_asset_id: fields.nodeAssetId,
|
|
3408
|
+
status: "pending",
|
|
3409
|
+
requested_by: fields.requestedBy || "human",
|
|
3410
|
+
notes: fields.notes,
|
|
3411
|
+
created_at: timestamp
|
|
3412
|
+
};
|
|
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
|
+
});
|
|
3421
|
+
if (existing) {
|
|
3422
|
+
database.prepare(`
|
|
3423
|
+
update asset_reroll_requests
|
|
3424
|
+
set requested_by = ?, notes = ?
|
|
3425
|
+
where id = ?
|
|
3426
|
+
`).run(request.requested_by, request.notes || null, request.id);
|
|
3427
|
+
} else {
|
|
3428
|
+
database.prepare(`
|
|
3429
|
+
insert into asset_reroll_requests (id, project_id, root_asset_id, node_asset_id, status, requested_by, notes, created_at, resolved_at)
|
|
3430
|
+
values (?, ?, ?, ?, 'pending', ?, ?, ?, null)
|
|
3431
|
+
`).run(request.id, project, fields.rootAssetId, fields.nodeAssetId, request.requested_by, request.notes || null, request.created_at);
|
|
3432
|
+
}
|
|
3433
|
+
database.prepare(`
|
|
3434
|
+
insert into asset_reviews (asset_id, review_state, reviewed_at, ignored_at, notes, updated_at)
|
|
3435
|
+
values (?, 'needs_revision', ?, null, ?, ?)
|
|
3436
|
+
on conflict(asset_id) do update set
|
|
3437
|
+
review_state = excluded.review_state, reviewed_at = excluded.reviewed_at,
|
|
3438
|
+
ignored_at = excluded.ignored_at, notes = coalesce(excluded.notes, asset_reviews.notes), updated_at = excluded.updated_at
|
|
3439
|
+
`).run(fields.nodeAssetId, timestamp, request.notes || null, timestamp);
|
|
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
|
+
};
|
|
3446
|
+
} finally {
|
|
3447
|
+
database.close();
|
|
3448
|
+
}
|
|
3449
|
+
}
|
|
3450
|
+
function clearLineageRerollRequest(project, fields) {
|
|
3451
|
+
const database = lineageDb();
|
|
3452
|
+
try {
|
|
3453
|
+
assertNodeInRoot(database, project, fields.rootAssetId, fields.nodeAssetId);
|
|
3454
|
+
const existing = database.prepare(`
|
|
3455
|
+
select * from asset_reroll_requests
|
|
3456
|
+
where project_id = ? and root_asset_id = ? and node_asset_id = ? and status = 'pending'
|
|
3457
|
+
order by created_at desc limit 1
|
|
3458
|
+
`).get(project, fields.rootAssetId, fields.nodeAssetId);
|
|
3459
|
+
if (!existing) throw new LineageError(`No pending re-roll request for ${fields.nodeAssetId}`, 404);
|
|
3460
|
+
const timestamp = nowIso();
|
|
3461
|
+
const request = { ...rerollRequestFrom(existing), status: "cancelled", resolved_at: timestamp };
|
|
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;
|
|
3469
|
+
database.prepare(`
|
|
3470
|
+
update asset_reroll_requests
|
|
3471
|
+
set status = 'cancelled', resolved_at = ?
|
|
3472
|
+
where id = ?
|
|
3473
|
+
`).run(timestamp, request.id);
|
|
3474
|
+
return {
|
|
3475
|
+
ok: true,
|
|
3476
|
+
request: withRerollTask(request, cancelledTask),
|
|
3477
|
+
task_id: cancelledTask?.id,
|
|
3478
|
+
task: cancelledTask
|
|
3479
|
+
};
|
|
3480
|
+
} finally {
|
|
3481
|
+
database.close();
|
|
3482
|
+
}
|
|
3483
|
+
}
|
|
2371
3484
|
function updateSelectedAsset(project, fields) {
|
|
2372
3485
|
const database = lineageDb();
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
const
|
|
2380
|
-
|
|
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 {
|
|
2381
3527
|
database.close();
|
|
2382
|
-
return { ok: true, dryRun: true, root_asset_id: root, asset_ids: inputAssetIds, mode, clear: Boolean(fields.clear), max_selections: limit };
|
|
2383
3528
|
}
|
|
2384
|
-
const
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
} else if (mode === "toggle") {
|
|
2395
|
-
for (const assetId of inputAssetIds) {
|
|
2396
|
-
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
|
+
});
|
|
2397
3539
|
}
|
|
2398
3540
|
}
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
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);
|
|
2403
3552
|
const timestamp = nowIso();
|
|
2404
|
-
const insert =
|
|
3553
|
+
const insert = writeDatabase.prepare(`
|
|
2405
3554
|
insert into asset_selections (id, project_id, root_asset_id, asset_id, position, notes, selected_at)
|
|
2406
3555
|
values (?, ?, ?, ?, ?, ?, ?)
|
|
2407
3556
|
`);
|
|
2408
3557
|
nextIds.forEach((assetId, position) => {
|
|
2409
|
-
|
|
2410
|
-
const notes = inputAssetIds.includes(assetId) ? fields.notes || existing?.notes : existing?.notes;
|
|
2411
|
-
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);
|
|
2412
3559
|
});
|
|
2413
|
-
|
|
3560
|
+
writeDatabase.close();
|
|
2414
3561
|
const message = nextIds.length === 0 ? `Cleared selected assets for ${root}` : `Selected ${nextIds.length} asset${nextIds.length === 1 ? "" : "s"} for ${root}`;
|
|
2415
3562
|
return { ok: true, message, root_asset_id: root, asset_id: nextIds[0] || null, asset_ids: nextIds, mode };
|
|
2416
3563
|
}
|
|
@@ -2418,7 +3565,7 @@ function updateAssetReview(project, fields) {
|
|
|
2418
3565
|
const allowed = /* @__PURE__ */ new Set(["unreviewed", "approved", "needs_revision", "rejected", "ignored"]);
|
|
2419
3566
|
if (!allowed.has(fields.reviewState)) throw new LineageError(`Unsupported review state: ${fields.reviewState}`);
|
|
2420
3567
|
const database = lineageDb();
|
|
2421
|
-
|
|
3568
|
+
requireAsset3(database, project, fields.assetId);
|
|
2422
3569
|
if (!fields.confirmWrite) {
|
|
2423
3570
|
database.close();
|
|
2424
3571
|
return { ok: true, dryRun: true, asset_id: fields.assetId, review_state: fields.reviewState, notes: fields.notes };
|
|
@@ -2446,6 +3593,9 @@ function lineageCommand(command, project, rootAssetId) {
|
|
|
2446
3593
|
function linkChildCommand(project, rootAssetId) {
|
|
2447
3594
|
return `${publicPackageCommand} link-child --project ${shellQuote(project)} --root ${shellQuote(rootAssetId)} --child <asset-id> --confirm-write --db ${shellQuote(lineageDbPath())} --json`;
|
|
2448
3595
|
}
|
|
3596
|
+
function rerollImportGuidance(rootAssetId, targetAssetId) {
|
|
3597
|
+
return `Use lineage reroll plan --root ${rootAssetId} --target ${targetAssetId} and lineage reroll import instead.`;
|
|
3598
|
+
}
|
|
2449
3599
|
function getLineageBrief(project, rootAssetId) {
|
|
2450
3600
|
const next = getLineageNextAsset(project, rootAssetId);
|
|
2451
3601
|
const assets = next.next_assets;
|
|
@@ -2492,6 +3642,14 @@ function getLineageBrief(project, rootAssetId) {
|
|
|
2492
3642
|
function linkSelectedLineageChild(project, fields) {
|
|
2493
3643
|
const next = getLineageNextAsset(project, fields.rootAssetId);
|
|
2494
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
|
+
}
|
|
2495
3653
|
if (fields.confirmWrite) {
|
|
2496
3654
|
const claimContext = getLineageWriteClaimContext(project, next.next_asset.asset_id);
|
|
2497
3655
|
const validation = validateAgentClaimForWrite({
|
|
@@ -2518,12 +3676,16 @@ function linkSelectedLineageChild(project, fields) {
|
|
|
2518
3676
|
parent_asset_id: next.next_asset.asset_id,
|
|
2519
3677
|
child_asset_id: fields.childAssetId,
|
|
2520
3678
|
reference_asset_ids: next.next_assets.map((asset) => asset.asset_id),
|
|
2521
|
-
warning:
|
|
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
|
|
2522
3684
|
};
|
|
2523
3685
|
}
|
|
2524
3686
|
|
|
2525
3687
|
// src/server/assetLineageRemove.ts
|
|
2526
|
-
function
|
|
3688
|
+
function requireAsset4(database, project, assetId) {
|
|
2527
3689
|
const row = database.prepare("select id from assets where project_id = ? and id = ?").get(project, assetId);
|
|
2528
3690
|
if (!row) throw new LineageError(`Unknown indexed asset: ${assetId}`, 404);
|
|
2529
3691
|
}
|
|
@@ -2565,9 +3727,9 @@ function compactSelectionsAfterRemove(database, project, root, assetId, timestam
|
|
|
2565
3727
|
}
|
|
2566
3728
|
function removeLineageNode(project, fields) {
|
|
2567
3729
|
const database = lineageDb();
|
|
2568
|
-
|
|
3730
|
+
requireAsset4(database, project, fields.assetId);
|
|
2569
3731
|
const root = fields.rootAssetId || rootFor2(database, project, fields.assetId);
|
|
2570
|
-
|
|
3732
|
+
requireAsset4(database, project, root);
|
|
2571
3733
|
if (fields.assetId === root) {
|
|
2572
3734
|
database.close();
|
|
2573
3735
|
throw new LineageError("Cannot remove the root lineage node; archive the workspace or create a new root instead.");
|
|
@@ -2615,6 +3777,19 @@ function removeLineageNode(project, fields) {
|
|
|
2615
3777
|
asset_preserved: true
|
|
2616
3778
|
};
|
|
2617
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
|
+
}
|
|
2618
3793
|
try {
|
|
2619
3794
|
database.exec("begin immediate transaction");
|
|
2620
3795
|
const deleteEdge = database.prepare("delete from asset_edges where id = ? and project_id = ?");
|
|
@@ -3417,7 +4592,7 @@ function requireText(value, label) {
|
|
|
3417
4592
|
if (!text) throw new AssetSelectionError(`Selection ${label} is required`);
|
|
3418
4593
|
return text;
|
|
3419
4594
|
}
|
|
3420
|
-
function
|
|
4595
|
+
function normalizeActor3(value, fallback) {
|
|
3421
4596
|
const actor = value || fallback;
|
|
3422
4597
|
if (!actors2.has(actor)) throw new AssetSelectionError(`Unsupported selection actor: ${actor}`);
|
|
3423
4598
|
return actor;
|
|
@@ -3549,7 +4724,7 @@ function getAssetSelectionSnapshot(project) {
|
|
|
3549
4724
|
}
|
|
3550
4725
|
function selectCurrentAssets(project, fields) {
|
|
3551
4726
|
const assetIds = [...new Set(fields.assetIds.map((assetId) => assetId.trim()).filter(Boolean))];
|
|
3552
|
-
const actor =
|
|
4727
|
+
const actor = normalizeActor3(fields.selectedBy, "human");
|
|
3553
4728
|
const preview = { assetIds, notes: fields.notes, project, selectedBy: actor };
|
|
3554
4729
|
if (!fields.confirmWrite) return { ok: true, dryRun: true, message: `Would select ${assetIds.length} assets`, preview };
|
|
3555
4730
|
const database = lineageDb();
|
|
@@ -3586,7 +4761,7 @@ function createReviewSet(project, fields) {
|
|
|
3586
4761
|
const label = requireText(fields.label, "review set label");
|
|
3587
4762
|
const key = fields.key?.trim() || `${slug(label)}-${Date.now().toString(36)}`;
|
|
3588
4763
|
const assetIds = [...new Set((fields.assetIds || []).map((assetId) => assetId.trim()).filter(Boolean))];
|
|
3589
|
-
const actor =
|
|
4764
|
+
const actor = normalizeActor3(fields.createdBy, "agent");
|
|
3590
4765
|
const preview = { assetIds, createdBy: actor, key, label, notes: fields.notes, project };
|
|
3591
4766
|
if (!fields.confirmWrite) return { ok: true, dryRun: true, message: `Would create review set ${label}`, preview };
|
|
3592
4767
|
const database = lineageDb();
|
|
@@ -3623,7 +4798,7 @@ function activeReviewSet(database, project, setId2) {
|
|
|
3623
4798
|
function chooseReviewSetLabels(project, fields) {
|
|
3624
4799
|
const labels = [...new Set(fields.labels.map((label) => label.trim().toUpperCase()).filter(Boolean))];
|
|
3625
4800
|
if (labels.length === 0) throw new AssetSelectionError("At least one variation label is required");
|
|
3626
|
-
const actor =
|
|
4801
|
+
const actor = normalizeActor3(fields.selectedBy, "human");
|
|
3627
4802
|
const database = lineageDb();
|
|
3628
4803
|
try {
|
|
3629
4804
|
const reviewSet2 = activeReviewSet(database, project, fields.setId);
|
|
@@ -4734,7 +5909,7 @@ function itemFromFile(file) {
|
|
|
4734
5909
|
};
|
|
4735
5910
|
}
|
|
4736
5911
|
function demoMarkdownItems(kind) {
|
|
4737
|
-
const root = join7(repoRoot, "demo-project", "channels");
|
|
5912
|
+
const root = process.env.LINEAGE_CONTENT_SOURCE_ROOT || join7(repoRoot, "demo-project", "channels");
|
|
4738
5913
|
if (!existsSync4(root)) return [];
|
|
4739
5914
|
return walk(root).map(itemFromFile).filter((item) => Boolean(item)).filter((item) => kind === "all" || `${item.kind}s` === kind).sort((a, b) => a.sourcePath.localeCompare(b.sourcePath));
|
|
4740
5915
|
}
|
|
@@ -5136,7 +6311,7 @@ function loadGenerationJob(database, project, id) {
|
|
|
5136
6311
|
project_id: String(row.project_id),
|
|
5137
6312
|
provider: row.provider,
|
|
5138
6313
|
adapter_version: String(row.adapter_version),
|
|
5139
|
-
source_mode:
|
|
6314
|
+
source_mode: String(row.source_mode),
|
|
5140
6315
|
root_asset_id: String(row.root_asset_id),
|
|
5141
6316
|
prompt: String(row.prompt),
|
|
5142
6317
|
expected_output_count: Number(row.expected_output_count),
|
|
@@ -5182,6 +6357,64 @@ function listImageGenerationJobs(project = defaultProject, fields = {}) {
|
|
|
5182
6357
|
}
|
|
5183
6358
|
}
|
|
5184
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
|
+
|
|
5185
6418
|
// src/server/assetLineageDemo.ts
|
|
5186
6419
|
import { createHash as createHash3 } from "node:crypto";
|
|
5187
6420
|
import { copyFileSync as copyFileSync2, existsSync as existsSync5, mkdirSync as mkdirSync5, mkdtempSync, readFileSync as readFileSync4, rmSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
@@ -5245,6 +6478,15 @@ function swissifierRelativePath(manifest, asset) {
|
|
|
5245
6478
|
function swissifierFilePath(manifest, asset) {
|
|
5246
6479
|
return join8(repoRoot, ".asset-scratch", swissifierRelativePath(manifest, asset));
|
|
5247
6480
|
}
|
|
6481
|
+
function swissifierRerollRelativePath(manifest, attempt) {
|
|
6482
|
+
return join8(manifest.media.target_dir, "reroll-attempts", attempt.file);
|
|
6483
|
+
}
|
|
6484
|
+
function swissifierRerollFilePath(manifest, attempt) {
|
|
6485
|
+
return join8(repoRoot, ".asset-scratch", swissifierRerollRelativePath(manifest, attempt));
|
|
6486
|
+
}
|
|
6487
|
+
function swissifierRerollFixturePath(attempt) {
|
|
6488
|
+
return join8(dirname3(swissifierManifestPath), "swissifier-rerolls", attempt.file);
|
|
6489
|
+
}
|
|
5248
6490
|
function swissifierSourcePath(manifest, asset, sourceDir) {
|
|
5249
6491
|
const direct = join8(sourceDir, asset.file);
|
|
5250
6492
|
if (existsSync5(direct)) return direct;
|
|
@@ -5542,6 +6784,27 @@ function writeDemoFiles(project) {
|
|
|
5542
6784
|
for (const asset of demoAssets) writeDemoFile(project, asset);
|
|
5543
6785
|
return ids;
|
|
5544
6786
|
}
|
|
6787
|
+
function swissifierRerollAsset(attempt) {
|
|
6788
|
+
const source = swissifierRerollFixturePath(attempt);
|
|
6789
|
+
if (!existsSync5(source)) throw new Error(`Missing Swissifier re-roll fixture media: ${attempt.file}`);
|
|
6790
|
+
const body = readFileSync4(source);
|
|
6791
|
+
if (body.length !== attempt.size_bytes) throw new Error(`Unexpected Swissifier re-roll fixture size for ${attempt.file}`);
|
|
6792
|
+
const checksumSha256 = sha256Hex(body);
|
|
6793
|
+
if (checksumSha256 !== attempt.checksum_sha256) throw new Error(`Checksum mismatch for Swissifier re-roll fixture: ${attempt.file}`);
|
|
6794
|
+
return {
|
|
6795
|
+
assetId: `local-${checksumSha256.slice(0, 12)}`,
|
|
6796
|
+
checksumSha256,
|
|
6797
|
+
sizeBytes: body.length
|
|
6798
|
+
};
|
|
6799
|
+
}
|
|
6800
|
+
function writeSwissifierRerollFiles(manifest) {
|
|
6801
|
+
for (const attempt of manifest.reroll_attempts || []) {
|
|
6802
|
+
swissifierRerollAsset(attempt);
|
|
6803
|
+
const target = swissifierRerollFilePath(manifest, attempt);
|
|
6804
|
+
mkdirSync5(dirname3(target), { recursive: true });
|
|
6805
|
+
copyFileSync2(swissifierRerollFixturePath(attempt), target);
|
|
6806
|
+
}
|
|
6807
|
+
}
|
|
5545
6808
|
function upsertDemoAssets(project, ids) {
|
|
5546
6809
|
const database = lineageDb();
|
|
5547
6810
|
const timestamp = nowIso();
|
|
@@ -5632,10 +6895,100 @@ function upsertSwissifierAssets(project, manifest) {
|
|
|
5632
6895
|
);
|
|
5633
6896
|
reviewStatement.run(asset.asset_id, timestamp);
|
|
5634
6897
|
}
|
|
6898
|
+
for (const attempt of manifest.reroll_attempts || []) {
|
|
6899
|
+
const generated = swissifierRerollAsset(attempt);
|
|
6900
|
+
assetStatement.run(
|
|
6901
|
+
generated.assetId,
|
|
6902
|
+
project,
|
|
6903
|
+
swissifierRerollRelativePath(manifest, attempt),
|
|
6904
|
+
generated.checksumSha256,
|
|
6905
|
+
attempt.title,
|
|
6906
|
+
"planned",
|
|
6907
|
+
"local",
|
|
6908
|
+
manifest.campaign,
|
|
6909
|
+
manifest.audience,
|
|
6910
|
+
generated.sizeBytes,
|
|
6911
|
+
attempt.content_type,
|
|
6912
|
+
timestamp,
|
|
6913
|
+
timestamp,
|
|
6914
|
+
timestamp
|
|
6915
|
+
);
|
|
6916
|
+
reviewStatement.run(generated.assetId, timestamp);
|
|
6917
|
+
}
|
|
6918
|
+
} finally {
|
|
6919
|
+
database.close();
|
|
6920
|
+
}
|
|
6921
|
+
const attemptCount = manifest.reroll_attempts?.length || 0;
|
|
6922
|
+
return { catalog: 0, local: manifest.assets.length + attemptCount, total: manifest.assets.length + attemptCount };
|
|
6923
|
+
}
|
|
6924
|
+
function upsertSwissifierRerollAttempts(project, manifest) {
|
|
6925
|
+
const attempts = manifest.reroll_attempts || [];
|
|
6926
|
+
if (attempts.length === 0) return { total: 0 };
|
|
6927
|
+
const database = lineageDb();
|
|
6928
|
+
const timestamp = nowIso();
|
|
6929
|
+
try {
|
|
6930
|
+
database.exec("BEGIN IMMEDIATE");
|
|
6931
|
+
try {
|
|
6932
|
+
const nodes = new Set(attempts.map((attempt) => attempt.node_asset_id));
|
|
6933
|
+
for (const nodeAssetId of nodes) {
|
|
6934
|
+
database.prepare("update asset_attempts set is_current = 0 where project_id = ? and node_asset_id = ?").run(project, nodeAssetId);
|
|
6935
|
+
}
|
|
6936
|
+
const statement = database.prepare(`
|
|
6937
|
+
insert into asset_attempts (
|
|
6938
|
+
id, project_id, node_asset_id, asset_id, attempt_index, source, prompt, generation_job_id,
|
|
6939
|
+
file_path, checksum_sha256, created_at, promoted_at, is_current
|
|
6940
|
+
) values (?, ?, ?, ?, ?, 'reroll', ?, ?, ?, ?, ?, ?, ?)
|
|
6941
|
+
on conflict(id) do update set
|
|
6942
|
+
asset_id = excluded.asset_id,
|
|
6943
|
+
source = excluded.source,
|
|
6944
|
+
prompt = excluded.prompt,
|
|
6945
|
+
generation_job_id = excluded.generation_job_id,
|
|
6946
|
+
file_path = excluded.file_path,
|
|
6947
|
+
checksum_sha256 = excluded.checksum_sha256,
|
|
6948
|
+
promoted_at = excluded.promoted_at,
|
|
6949
|
+
is_current = excluded.is_current
|
|
6950
|
+
`);
|
|
6951
|
+
for (const attempt of attempts) {
|
|
6952
|
+
const generated = swissifierRerollAsset(attempt);
|
|
6953
|
+
statement.run(
|
|
6954
|
+
`${project}:${attempt.node_asset_id}:attempt:${attempt.attempt_index}`,
|
|
6955
|
+
project,
|
|
6956
|
+
attempt.node_asset_id,
|
|
6957
|
+
generated.assetId,
|
|
6958
|
+
attempt.attempt_index,
|
|
6959
|
+
attempt.prompt,
|
|
6960
|
+
attempt.generation_job_id,
|
|
6961
|
+
swissifierRerollRelativePath(manifest, attempt),
|
|
6962
|
+
generated.checksumSha256,
|
|
6963
|
+
timestamp,
|
|
6964
|
+
timestamp,
|
|
6965
|
+
attempt.current ? 1 : 0
|
|
6966
|
+
);
|
|
6967
|
+
}
|
|
6968
|
+
for (const nodeAssetId of nodes) {
|
|
6969
|
+
const currentCount = database.prepare("select count(*) count from asset_attempts where project_id = ? and node_asset_id = ? and is_current = 1").get(project, nodeAssetId);
|
|
6970
|
+
if (currentCount.count === 0) {
|
|
6971
|
+
database.prepare(`
|
|
6972
|
+
update asset_attempts
|
|
6973
|
+
set is_current = 1, promoted_at = ?
|
|
6974
|
+
where id = (
|
|
6975
|
+
select id from asset_attempts
|
|
6976
|
+
where project_id = ? and node_asset_id = ?
|
|
6977
|
+
order by attempt_index desc
|
|
6978
|
+
limit 1
|
|
6979
|
+
)
|
|
6980
|
+
`).run(timestamp, project, nodeAssetId);
|
|
6981
|
+
}
|
|
6982
|
+
}
|
|
6983
|
+
database.exec("COMMIT");
|
|
6984
|
+
} catch (error) {
|
|
6985
|
+
database.exec("ROLLBACK");
|
|
6986
|
+
throw error;
|
|
6987
|
+
}
|
|
5635
6988
|
} finally {
|
|
5636
6989
|
database.close();
|
|
5637
6990
|
}
|
|
5638
|
-
return {
|
|
6991
|
+
return { total: attempts.length };
|
|
5639
6992
|
}
|
|
5640
6993
|
function seedDemoLineageWorkspace(project, fields) {
|
|
5641
6994
|
const ids = fields.confirmWrite ? writeDemoFiles(project) : demoAssetIds();
|
|
@@ -5694,10 +7047,12 @@ function seedSwissifierRichDemoWorkspace(project, fields) {
|
|
|
5694
7047
|
media_status: swissifierRichDemoMediaStatus(project)
|
|
5695
7048
|
};
|
|
5696
7049
|
}
|
|
7050
|
+
writeSwissifierRerollFiles(manifest);
|
|
5697
7051
|
const summary = upsertSwissifierAssets(project, manifest);
|
|
5698
7052
|
for (const edge of manifest.edges) {
|
|
5699
7053
|
linkLineageAssets(project, { parentAssetId: edge.parent, childAssetId: edge.child, confirmWrite: true });
|
|
5700
7054
|
}
|
|
7055
|
+
const reroll_attempts = upsertSwissifierRerollAttempts(project, manifest);
|
|
5701
7056
|
updateSelectedAsset(project, {
|
|
5702
7057
|
assetIds: manifest.selected_asset_ids,
|
|
5703
7058
|
confirmWrite: true,
|
|
@@ -5725,6 +7080,7 @@ function seedSwissifierRichDemoWorkspace(project, fields) {
|
|
|
5725
7080
|
media_status: swissifierRichDemoMediaStatus(project),
|
|
5726
7081
|
root_asset_id: manifest.root_asset_id,
|
|
5727
7082
|
selected_asset_ids: manifest.selected_asset_ids,
|
|
7083
|
+
reroll_attempts,
|
|
5728
7084
|
summary,
|
|
5729
7085
|
workspace
|
|
5730
7086
|
};
|
|
@@ -5931,6 +7287,37 @@ app.get(
|
|
|
5931
7287
|
})
|
|
5932
7288
|
);
|
|
5933
7289
|
registerLineageWorkspaceRoutes(app, projectFrom, asyncRoute);
|
|
7290
|
+
registerLineageTaskRoutes(app, projectFrom, asyncRoute);
|
|
7291
|
+
app.get("/api/lineage/:rootAssetId/rerolls", asyncRoute((req, res) => {
|
|
7292
|
+
res.json(listLineageRerollRequests(projectFrom(req), req.params.rootAssetId));
|
|
7293
|
+
}));
|
|
7294
|
+
app.post("/api/lineage/:rootAssetId/rerolls/:nodeAssetId", asyncRoute((req, res) => {
|
|
7295
|
+
res.json(markLineageRerollRequest(projectFrom(req), {
|
|
7296
|
+
rootAssetId: req.params.rootAssetId,
|
|
7297
|
+
nodeAssetId: req.params.nodeAssetId,
|
|
7298
|
+
notes: typeof req.body.notes === "string" ? req.body.notes : void 0,
|
|
7299
|
+
requestedBy: req.body.requestedBy === "agent" || req.body.requestedBy === "system" ? req.body.requestedBy : "human",
|
|
7300
|
+
confirmWrite: req.body.confirmWrite === true
|
|
7301
|
+
}));
|
|
7302
|
+
}));
|
|
7303
|
+
app.post("/api/lineage/:rootAssetId/rerolls/:nodeAssetId/cancel", asyncRoute((req, res) => {
|
|
7304
|
+
res.json(clearLineageRerollRequest(projectFrom(req), {
|
|
7305
|
+
rootAssetId: req.params.rootAssetId,
|
|
7306
|
+
nodeAssetId: req.params.nodeAssetId,
|
|
7307
|
+
confirmWrite: req.body.confirmWrite === true
|
|
7308
|
+
}));
|
|
7309
|
+
}));
|
|
7310
|
+
app.get("/api/lineage/:rootAssetId/attempts/:nodeAssetId", asyncRoute((req, res) => {
|
|
7311
|
+
res.json(getLineageAttempts(projectFrom(req), req.params.rootAssetId, req.params.nodeAssetId));
|
|
7312
|
+
}));
|
|
7313
|
+
app.post("/api/lineage/:rootAssetId/attempts/:nodeAssetId/promote", asyncRoute((req, res) => {
|
|
7314
|
+
res.json(promoteLineageAttempt(projectFrom(req), {
|
|
7315
|
+
rootAssetId: req.params.rootAssetId,
|
|
7316
|
+
nodeAssetId: req.params.nodeAssetId,
|
|
7317
|
+
attemptId: String(req.body.attemptId || ""),
|
|
7318
|
+
confirmWrite: req.body.confirmWrite === true
|
|
7319
|
+
}));
|
|
7320
|
+
}));
|
|
5934
7321
|
app.get("/api/lineage/:assetId", asyncRoute((req, res) => {
|
|
5935
7322
|
res.json(getLineageSnapshot(projectFrom(req), req.params.assetId));
|
|
5936
7323
|
}));
|
|
@@ -6208,6 +7595,10 @@ app.use((error, _req, res, _next) => {
|
|
|
6208
7595
|
res.status(error.status).json({ error: error.message });
|
|
6209
7596
|
return;
|
|
6210
7597
|
}
|
|
7598
|
+
if (isLineageTaskError(error)) {
|
|
7599
|
+
res.status(error.status).json({ error: error.message });
|
|
7600
|
+
return;
|
|
7601
|
+
}
|
|
6211
7602
|
if (isLineageError(error)) {
|
|
6212
7603
|
res.status(error.status).json({ error: error.message });
|
|
6213
7604
|
return;
|