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