@mean-weasel/lineage 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/README.md +20 -0
- package/dist/cli/lineage-dev.js +1006 -41
- package/dist/cli/lineage-dev.js.map +4 -4
- package/dist/cli/lineage.js +1006 -41
- 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-EfT3Ues-.css → index-38XlcMya.css} +1 -1
- package/dist/web/assets/index-CtsYIUyG.js +23 -0
- package/dist/web/index.html +2 -2
- package/package.json +3 -1
- package/dist/web/assets/index-NDba9I2v.js +0 -23
package/dist/cli/lineage.js
CHANGED
|
@@ -615,6 +615,10 @@ function lineageDb() {
|
|
|
615
615
|
create index if not exists assets_project_source_seen on assets(project_id, source, last_seen_at);
|
|
616
616
|
create index if not exists assets_project_checksum on assets(project_id, checksum_sha256);
|
|
617
617
|
create index if not exists assets_project_channel_campaign on assets(project_id, channel, campaign);
|
|
618
|
+
create table if not exists lineage_schema_migrations (
|
|
619
|
+
key text primary key,
|
|
620
|
+
applied_at text not null
|
|
621
|
+
);
|
|
618
622
|
create table if not exists asset_edges (
|
|
619
623
|
id text primary key,
|
|
620
624
|
project_id text not null references projects(id),
|
|
@@ -920,12 +924,49 @@ function lineageDb() {
|
|
|
920
924
|
);
|
|
921
925
|
create index if not exists generation_job_receipts_job on generation_job_receipts(job_id, created_at);
|
|
922
926
|
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);
|
|
927
|
+
create table if not exists lineage_tasks (
|
|
928
|
+
id text primary key,
|
|
929
|
+
project_id text not null references projects(id),
|
|
930
|
+
root_asset_id text not null references assets(id),
|
|
931
|
+
target_asset_id text not null references assets(id),
|
|
932
|
+
task_type text not null check (task_type in ('iterate', 'reroll')),
|
|
933
|
+
status text not null check (status in ('pending', 'claimed', 'in_progress', 'resolved', 'cancelled')),
|
|
934
|
+
instructions text,
|
|
935
|
+
created_by text not null check (created_by in ('human', 'agent', 'system')),
|
|
936
|
+
created_at text not null,
|
|
937
|
+
updated_at text not null,
|
|
938
|
+
claimed_at text,
|
|
939
|
+
started_at text,
|
|
940
|
+
resolved_at text,
|
|
941
|
+
cancelled_at text,
|
|
942
|
+
resolved_generation_job_id text,
|
|
943
|
+
resolved_asset_id text references assets(id),
|
|
944
|
+
metadata_json text
|
|
945
|
+
);
|
|
946
|
+
create unique index if not exists lineage_tasks_one_open
|
|
947
|
+
on lineage_tasks(project_id, root_asset_id, target_asset_id, task_type)
|
|
948
|
+
where status in ('pending', 'claimed', 'in_progress');
|
|
949
|
+
create index if not exists lineage_tasks_root_status
|
|
950
|
+
on lineage_tasks(project_id, root_asset_id, status, updated_at);
|
|
951
|
+
create index if not exists lineage_tasks_target
|
|
952
|
+
on lineage_tasks(project_id, root_asset_id, target_asset_id, task_type, status);
|
|
953
|
+
create table if not exists lineage_task_events (
|
|
954
|
+
id text primary key,
|
|
955
|
+
task_id text not null references lineage_tasks(id) on delete cascade,
|
|
956
|
+
event_type text not null,
|
|
957
|
+
actor text,
|
|
958
|
+
message text,
|
|
959
|
+
created_at text not null,
|
|
960
|
+
metadata_json text
|
|
961
|
+
);
|
|
962
|
+
create index if not exists lineage_task_events_task_created
|
|
963
|
+
on lineage_task_events(task_id, created_at);
|
|
923
964
|
create table if not exists agent_claims (
|
|
924
965
|
id text primary key,
|
|
925
966
|
token_hash text not null,
|
|
926
967
|
project_id text not null references projects(id),
|
|
927
968
|
channel text,
|
|
928
|
-
scope_type text not null check (scope_type in ('lineage_workspace', 'content_post', 'content_queue_lane', 'selection_set', 'project_channel')),
|
|
969
|
+
scope_type text not null check (scope_type in ('lineage_workspace', 'lineage_task', 'content_post', 'content_queue_lane', 'selection_set', 'project_channel')),
|
|
929
970
|
target_id text not null,
|
|
930
971
|
target_title text,
|
|
931
972
|
agent_id text,
|
|
@@ -959,13 +1000,76 @@ function lineageDb() {
|
|
|
959
1000
|
migrateAssetSelections(database);
|
|
960
1001
|
dropLegacyAssetSelectionRootUnique(database);
|
|
961
1002
|
ensureColumn(database, "asset_selections", "notes", "text");
|
|
1003
|
+
backfillLineageTasks(database);
|
|
962
1004
|
ensureColumn(database, "asset_ledger_records", "first_seen_at", "text");
|
|
963
1005
|
ensureColumn(database, "asset_ledger_records", "indexed_by_run_id", "text");
|
|
964
1006
|
ensureColumn(database, "asset_ledger_sources", "first_seen_at", "text");
|
|
965
1007
|
ensureColumn(database, "asset_ledger_sources", "indexed_by_run_id", "text");
|
|
966
1008
|
ensureReviewStateValues(database);
|
|
1009
|
+
ensureAgentClaimScopeValues(database);
|
|
1010
|
+
ensureGenerationReceiptCheckValues(database);
|
|
967
1011
|
return database;
|
|
968
1012
|
}
|
|
1013
|
+
function backfillLineageTasks(database) {
|
|
1014
|
+
database.exec(`
|
|
1015
|
+
create table if not exists lineage_schema_migrations (
|
|
1016
|
+
key text primary key,
|
|
1017
|
+
applied_at text not null
|
|
1018
|
+
)
|
|
1019
|
+
`);
|
|
1020
|
+
const marker = database.prepare("select key from lineage_schema_migrations where key = 'lineage_tasks_backfilled_v1'").get();
|
|
1021
|
+
if (marker) return;
|
|
1022
|
+
database.exec("BEGIN IMMEDIATE");
|
|
1023
|
+
try {
|
|
1024
|
+
database.exec(`
|
|
1025
|
+
insert or ignore into lineage_tasks (
|
|
1026
|
+
id, project_id, root_asset_id, target_asset_id, task_type, status, instructions,
|
|
1027
|
+
created_by, created_at, updated_at, metadata_json
|
|
1028
|
+
)
|
|
1029
|
+
select
|
|
1030
|
+
s.project_id || ':' || s.root_asset_id || ':lineage-task:iterate:' || s.asset_id,
|
|
1031
|
+
s.project_id,
|
|
1032
|
+
s.root_asset_id,
|
|
1033
|
+
s.asset_id,
|
|
1034
|
+
'iterate',
|
|
1035
|
+
'pending',
|
|
1036
|
+
s.notes,
|
|
1037
|
+
'human',
|
|
1038
|
+
s.selected_at,
|
|
1039
|
+
s.selected_at,
|
|
1040
|
+
null
|
|
1041
|
+
from asset_selections s;
|
|
1042
|
+
|
|
1043
|
+
insert or ignore into lineage_tasks (
|
|
1044
|
+
id, project_id, root_asset_id, target_asset_id, task_type, status, instructions,
|
|
1045
|
+
created_by, created_at, updated_at, metadata_json
|
|
1046
|
+
)
|
|
1047
|
+
select
|
|
1048
|
+
r.project_id || ':' || r.root_asset_id || ':lineage-task:reroll:' || r.node_asset_id,
|
|
1049
|
+
r.project_id,
|
|
1050
|
+
r.root_asset_id,
|
|
1051
|
+
r.node_asset_id,
|
|
1052
|
+
'reroll',
|
|
1053
|
+
'pending',
|
|
1054
|
+
r.notes,
|
|
1055
|
+
r.requested_by,
|
|
1056
|
+
r.created_at,
|
|
1057
|
+
r.created_at,
|
|
1058
|
+
null
|
|
1059
|
+
from asset_reroll_requests r
|
|
1060
|
+
where r.status = 'pending';
|
|
1061
|
+
`);
|
|
1062
|
+
database.prepare(`
|
|
1063
|
+
insert into lineage_schema_migrations (key, applied_at)
|
|
1064
|
+
values ('lineage_tasks_backfilled_v1', ?)
|
|
1065
|
+
on conflict(key) do nothing
|
|
1066
|
+
`).run(nowIso());
|
|
1067
|
+
database.exec("COMMIT");
|
|
1068
|
+
} catch (error) {
|
|
1069
|
+
database.exec("ROLLBACK");
|
|
1070
|
+
throw error;
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
969
1073
|
function ensureColumn(database, table, column, definition) {
|
|
970
1074
|
const rows = database.prepare(`pragma table_info(${table})`).all();
|
|
971
1075
|
if (!rows.some((row) => row.name === column)) database.exec(`alter table ${table} add column ${column} ${definition}`);
|
|
@@ -1029,12 +1133,138 @@ function ensureReviewStateValues(database) {
|
|
|
1029
1133
|
drop table asset_reviews_old;
|
|
1030
1134
|
`);
|
|
1031
1135
|
}
|
|
1136
|
+
function ensureAgentClaimScopeValues(database) {
|
|
1137
|
+
const createSql = database.prepare("select sql from sqlite_master where type = 'table' and name = 'agent_claims'").get();
|
|
1138
|
+
if (createSql?.sql?.includes("'lineage_task'")) return;
|
|
1139
|
+
database.exec("PRAGMA foreign_keys = OFF; PRAGMA legacy_alter_table = ON; BEGIN IMMEDIATE");
|
|
1140
|
+
try {
|
|
1141
|
+
database.exec(`
|
|
1142
|
+
alter table agent_claims rename to agent_claims_old;
|
|
1143
|
+
create table agent_claims (
|
|
1144
|
+
id text primary key,
|
|
1145
|
+
token_hash text not null,
|
|
1146
|
+
project_id text not null references projects(id),
|
|
1147
|
+
channel text,
|
|
1148
|
+
scope_type text not null check (scope_type in ('lineage_workspace', 'lineage_task', 'content_post', 'content_queue_lane', 'selection_set', 'project_channel')),
|
|
1149
|
+
target_id text not null,
|
|
1150
|
+
target_title text,
|
|
1151
|
+
agent_id text,
|
|
1152
|
+
agent_name text not null,
|
|
1153
|
+
agent_kind text not null,
|
|
1154
|
+
thread_id text,
|
|
1155
|
+
status text not null check (status in ('active', 'expired', 'released', 'revoked', 'transferred')),
|
|
1156
|
+
created_at text not null,
|
|
1157
|
+
heartbeat_at text not null,
|
|
1158
|
+
expires_at text not null,
|
|
1159
|
+
released_at text,
|
|
1160
|
+
revoked_at text,
|
|
1161
|
+
revoked_by text,
|
|
1162
|
+
override_reason text,
|
|
1163
|
+
metadata_json text
|
|
1164
|
+
);
|
|
1165
|
+
insert into agent_claims
|
|
1166
|
+
select * from agent_claims_old;
|
|
1167
|
+
drop table agent_claims_old;
|
|
1168
|
+
create unique index if not exists agent_claims_token_hash on agent_claims(token_hash);
|
|
1169
|
+
create index if not exists agent_claims_project_status on agent_claims(project_id, status, heartbeat_at);
|
|
1170
|
+
create index if not exists agent_claims_target on agent_claims(project_id, channel, scope_type, target_id, status);
|
|
1171
|
+
`);
|
|
1172
|
+
const violations = database.prepare("pragma foreign_key_check").all();
|
|
1173
|
+
if (violations.length > 0) throw new Error(`Agent claim scope migration failed foreign key check: ${JSON.stringify(violations)}`);
|
|
1174
|
+
database.exec("COMMIT");
|
|
1175
|
+
} catch (error) {
|
|
1176
|
+
database.exec("ROLLBACK");
|
|
1177
|
+
throw error;
|
|
1178
|
+
} finally {
|
|
1179
|
+
database.exec("PRAGMA legacy_alter_table = OFF; PRAGMA foreign_keys = ON");
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
function tableCreateSql(database, table) {
|
|
1183
|
+
const row = database.prepare("select sql from sqlite_master where type = 'table' and name = ?").get(table);
|
|
1184
|
+
return row?.sql || "";
|
|
1185
|
+
}
|
|
1186
|
+
function ensureGenerationReceiptCheckValues(database) {
|
|
1187
|
+
const jobsSql = tableCreateSql(database, "generation_jobs");
|
|
1188
|
+
const inputsSql = tableCreateSql(database, "generation_job_inputs");
|
|
1189
|
+
const needsJobsMigration = Boolean(jobsSql && !jobsSql.includes("'lineage_reroll'"));
|
|
1190
|
+
const needsInputsMigration = Boolean(inputsSql && !inputsSql.includes("'reroll_target'"));
|
|
1191
|
+
if (!needsJobsMigration && !needsInputsMigration) return;
|
|
1192
|
+
database.exec("PRAGMA foreign_keys = OFF; PRAGMA legacy_alter_table = ON; BEGIN IMMEDIATE");
|
|
1193
|
+
try {
|
|
1194
|
+
if (needsJobsMigration) migrateGenerationJobsCheck(database);
|
|
1195
|
+
if (needsInputsMigration) migrateGenerationJobInputsCheck(database);
|
|
1196
|
+
const violations = database.prepare("pragma foreign_key_check").all();
|
|
1197
|
+
if (violations.length > 0) throw new Error(`Generation receipt migration failed foreign key check: ${JSON.stringify(violations)}`);
|
|
1198
|
+
database.exec("COMMIT");
|
|
1199
|
+
} catch (error) {
|
|
1200
|
+
database.exec("ROLLBACK");
|
|
1201
|
+
throw error;
|
|
1202
|
+
} finally {
|
|
1203
|
+
database.exec("PRAGMA legacy_alter_table = OFF; PRAGMA foreign_keys = ON");
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
function migrateGenerationJobsCheck(database) {
|
|
1207
|
+
database.exec(`
|
|
1208
|
+
alter table generation_jobs rename to generation_jobs_legacy_check;
|
|
1209
|
+
create table generation_jobs (
|
|
1210
|
+
id text primary key,
|
|
1211
|
+
project_id text not null references projects(id),
|
|
1212
|
+
provider text not null default 'codex-handoff',
|
|
1213
|
+
adapter_version text not null,
|
|
1214
|
+
source_mode text not null check (source_mode in ('lineage_selection', 'lineage_reroll')),
|
|
1215
|
+
root_asset_id text not null references assets(id),
|
|
1216
|
+
prompt text not null,
|
|
1217
|
+
expected_output_count integer not null check (expected_output_count > 0),
|
|
1218
|
+
status text not null check (status in ('planned', 'imported', 'failed', 'cancelled')),
|
|
1219
|
+
output_dir text,
|
|
1220
|
+
handoff_json text,
|
|
1221
|
+
created_at text not null,
|
|
1222
|
+
updated_at text not null,
|
|
1223
|
+
imported_at text
|
|
1224
|
+
);
|
|
1225
|
+
insert into generation_jobs (
|
|
1226
|
+
id, project_id, provider, adapter_version, source_mode, root_asset_id, prompt,
|
|
1227
|
+
expected_output_count, status, output_dir, handoff_json, created_at, updated_at, imported_at
|
|
1228
|
+
)
|
|
1229
|
+
select
|
|
1230
|
+
id, project_id, provider, adapter_version, source_mode, root_asset_id, prompt,
|
|
1231
|
+
expected_output_count, status, output_dir, handoff_json, created_at, updated_at, imported_at
|
|
1232
|
+
from generation_jobs_legacy_check;
|
|
1233
|
+
drop table generation_jobs_legacy_check;
|
|
1234
|
+
create index if not exists generation_jobs_project_created on generation_jobs(project_id, created_at);
|
|
1235
|
+
`);
|
|
1236
|
+
}
|
|
1237
|
+
function migrateGenerationJobInputsCheck(database) {
|
|
1238
|
+
database.exec(`
|
|
1239
|
+
alter table generation_job_inputs rename to generation_job_inputs_legacy_check;
|
|
1240
|
+
create table generation_job_inputs (
|
|
1241
|
+
id text primary key,
|
|
1242
|
+
job_id text not null references generation_jobs(id) on delete cascade,
|
|
1243
|
+
project_id text not null references projects(id),
|
|
1244
|
+
asset_id text not null references assets(id),
|
|
1245
|
+
root_asset_id text not null references assets(id),
|
|
1246
|
+
role text not null check (role in ('lineage_next_base', 'reference', 'reroll_target')),
|
|
1247
|
+
position integer not null,
|
|
1248
|
+
selection_strategy text not null,
|
|
1249
|
+
selection_snapshot_json text not null,
|
|
1250
|
+
unique(job_id, asset_id, role)
|
|
1251
|
+
);
|
|
1252
|
+
insert into generation_job_inputs (
|
|
1253
|
+
id, job_id, project_id, asset_id, root_asset_id, role, position, selection_strategy, selection_snapshot_json
|
|
1254
|
+
)
|
|
1255
|
+
select
|
|
1256
|
+
id, job_id, project_id, asset_id, root_asset_id, role, position, selection_strategy, selection_snapshot_json
|
|
1257
|
+
from generation_job_inputs_legacy_check;
|
|
1258
|
+
drop table generation_job_inputs_legacy_check;
|
|
1259
|
+
create index if not exists generation_job_inputs_job on generation_job_inputs(job_id, position);
|
|
1260
|
+
`);
|
|
1261
|
+
}
|
|
1032
1262
|
|
|
1033
1263
|
// src/server/agentClaims.ts
|
|
1034
1264
|
var defaultTtlSeconds = 20 * 60;
|
|
1035
1265
|
var idleAfterSeconds = 5 * 60;
|
|
1036
1266
|
var staleAfterSeconds = 15 * 60;
|
|
1037
|
-
var scopes = /* @__PURE__ */ new Set(["lineage_workspace", "content_post", "content_queue_lane", "selection_set", "project_channel"]);
|
|
1267
|
+
var scopes = /* @__PURE__ */ new Set(["lineage_workspace", "lineage_task", "content_post", "content_queue_lane", "selection_set", "project_channel"]);
|
|
1038
1268
|
var claimTokenPattern = /claim_[a-z0-9_-]+\.[A-Za-z0-9_-]+/g;
|
|
1039
1269
|
var AgentClaimError = class extends Error {
|
|
1040
1270
|
constructor(message, status = 400, code = "agent_claim_error", conflicts = []) {
|
|
@@ -1339,6 +1569,19 @@ function revokeAgentClaim(project, claimId, fields) {
|
|
|
1339
1569
|
database.close();
|
|
1340
1570
|
}
|
|
1341
1571
|
}
|
|
1572
|
+
function revokeAgentClaimInDatabase(database, project, claimId, fields) {
|
|
1573
|
+
expireActiveClaims(database);
|
|
1574
|
+
const claim = findClaimById(database, claimId, project);
|
|
1575
|
+
if (!claim) throw new AgentClaimError(`Unknown agent claim: ${claimId}`, 404, "claim_not_found");
|
|
1576
|
+
if (claim.status !== "active") return claim;
|
|
1577
|
+
const timestamp = nowIso();
|
|
1578
|
+
database.prepare(`
|
|
1579
|
+
update agent_claims set status = 'revoked', revoked_at = ?, revoked_by = ?, override_reason = ?
|
|
1580
|
+
where id = ? and status = 'active'
|
|
1581
|
+
`).run(timestamp, fields.actor || "human", fields.reason || null, claim.id);
|
|
1582
|
+
recordEvent(database, claim.id, "revoked", fields.actor || "human", fields.reason);
|
|
1583
|
+
return findClaimById(database, claim.id, project);
|
|
1584
|
+
}
|
|
1342
1585
|
function transferAgentClaim(project, claimId, fields) {
|
|
1343
1586
|
if (!fields.confirmWrite) throw new AgentClaimError("Transferring an agent claim requires confirmWrite=true.", 400, "confirm_write_required");
|
|
1344
1587
|
const toAgentName = fields.toAgentName.trim();
|
|
@@ -1356,6 +1599,19 @@ function transferAgentClaim(project, claimId, fields) {
|
|
|
1356
1599
|
database.close();
|
|
1357
1600
|
}
|
|
1358
1601
|
}
|
|
1602
|
+
function recordAgentClaimWriteAllowed(claim, fields) {
|
|
1603
|
+
const database = lineageDb();
|
|
1604
|
+
try {
|
|
1605
|
+
recordEvent(database, claim.id, "write_allowed", claim.agent_name, `${fields.writeKind} allowed.`, {
|
|
1606
|
+
danger_level: fields.dangerLevel,
|
|
1607
|
+
target_id: fields.targetId,
|
|
1608
|
+
write_kind: fields.writeKind
|
|
1609
|
+
});
|
|
1610
|
+
return { ok: true };
|
|
1611
|
+
} finally {
|
|
1612
|
+
database.close();
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1359
1615
|
function validateAgentClaimForWrite(fields) {
|
|
1360
1616
|
if (fields.dangerLevel === "danger" && !fields.confirmWrite) {
|
|
1361
1617
|
return denied("human_confirmation_required", "Dangerous write requires explicit human confirmation.");
|
|
@@ -1376,11 +1632,13 @@ function validateAgentClaimForWrite(fields) {
|
|
|
1376
1632
|
if (!scopeAllowsWrite(claim, fields.scopeType, fields.targetId, fields.writeKind)) {
|
|
1377
1633
|
return denied("claim_scope_mismatch", `Claim does not cover ${fields.scopeType} ${fields.targetId}.`, [claim]);
|
|
1378
1634
|
}
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1635
|
+
if (fields.recordEvent !== false) {
|
|
1636
|
+
recordEvent(database, claim.id, "write_allowed", claim.agent_name, `${fields.writeKind} allowed.`, {
|
|
1637
|
+
danger_level: fields.dangerLevel,
|
|
1638
|
+
target_id: fields.targetId,
|
|
1639
|
+
write_kind: fields.writeKind
|
|
1640
|
+
});
|
|
1641
|
+
}
|
|
1384
1642
|
return { ok: true, claim, warnings: [] };
|
|
1385
1643
|
} finally {
|
|
1386
1644
|
database.close();
|
|
@@ -1400,6 +1658,507 @@ function selectedRows(database, project, root) {
|
|
|
1400
1658
|
`).all(project, root);
|
|
1401
1659
|
}
|
|
1402
1660
|
|
|
1661
|
+
// src/server/assetLineageTasks.ts
|
|
1662
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
1663
|
+
var LineageTaskError = class extends Error {
|
|
1664
|
+
constructor(message, status = 400) {
|
|
1665
|
+
super(message);
|
|
1666
|
+
this.status = status;
|
|
1667
|
+
}
|
|
1668
|
+
status;
|
|
1669
|
+
};
|
|
1670
|
+
var activeStatuses = ["pending", "claimed", "in_progress"];
|
|
1671
|
+
var taskTypes = /* @__PURE__ */ new Set(["iterate", "reroll"]);
|
|
1672
|
+
var taskStatuses = /* @__PURE__ */ new Set(["pending", "claimed", "in_progress", "resolved", "cancelled"]);
|
|
1673
|
+
function taskIdFor(project, rootAssetId, targetAssetId, taskType) {
|
|
1674
|
+
return `${project}:${rootAssetId}:lineage-task:${taskType}:${targetAssetId}`;
|
|
1675
|
+
}
|
|
1676
|
+
function randomId2(prefix) {
|
|
1677
|
+
return `${prefix}_${Date.now().toString(36)}_${randomBytes2(6).toString("base64url").toLowerCase()}`;
|
|
1678
|
+
}
|
|
1679
|
+
function metadataJson2(metadata) {
|
|
1680
|
+
return metadata ? JSON.stringify(metadata) : null;
|
|
1681
|
+
}
|
|
1682
|
+
function metadataWithoutClaim(metadata) {
|
|
1683
|
+
if (!metadata) return void 0;
|
|
1684
|
+
const next = { ...metadata };
|
|
1685
|
+
delete next.claim_id;
|
|
1686
|
+
return Object.keys(next).length > 0 ? next : void 0;
|
|
1687
|
+
}
|
|
1688
|
+
function parseMetadata2(value) {
|
|
1689
|
+
if (typeof value !== "string" || !value) return void 0;
|
|
1690
|
+
try {
|
|
1691
|
+
const parsed = JSON.parse(value);
|
|
1692
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
|
|
1693
|
+
} catch {
|
|
1694
|
+
return void 0;
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
function normalizeProject(project) {
|
|
1698
|
+
const trimmed = project.trim();
|
|
1699
|
+
if (!trimmed) throw new LineageTaskError("Lineage task requires project");
|
|
1700
|
+
return trimmed;
|
|
1701
|
+
}
|
|
1702
|
+
function normalizeTaskType(taskType) {
|
|
1703
|
+
if (!taskTypes.has(taskType)) throw new LineageTaskError(`Unsupported lineage task type: ${taskType}`);
|
|
1704
|
+
return taskType;
|
|
1705
|
+
}
|
|
1706
|
+
function normalizeStatus(status) {
|
|
1707
|
+
if (!taskStatuses.has(status)) throw new LineageTaskError(`Unsupported lineage task status: ${status}`);
|
|
1708
|
+
return status;
|
|
1709
|
+
}
|
|
1710
|
+
function normalizeActor(actor, label) {
|
|
1711
|
+
const trimmed = actor.trim();
|
|
1712
|
+
if (!trimmed) throw new LineageTaskError(`${label} is required`);
|
|
1713
|
+
return trimmed;
|
|
1714
|
+
}
|
|
1715
|
+
function requireAsset(database, project, assetId) {
|
|
1716
|
+
const row = database.prepare("select id from assets where project_id = ? and id = ?").get(project, assetId);
|
|
1717
|
+
if (!row) throw new LineageTaskError(`Unknown indexed asset: ${assetId}`, 404);
|
|
1718
|
+
}
|
|
1719
|
+
function taskFromRow(row) {
|
|
1720
|
+
const metadata = parseMetadata2(row.metadata_json);
|
|
1721
|
+
const claimId = typeof metadata?.claim_id === "string" ? metadata.claim_id : void 0;
|
|
1722
|
+
return {
|
|
1723
|
+
id: String(row.id),
|
|
1724
|
+
project_id: String(row.project_id),
|
|
1725
|
+
root_asset_id: String(row.root_asset_id),
|
|
1726
|
+
target_asset_id: String(row.target_asset_id),
|
|
1727
|
+
task_type: String(row.task_type),
|
|
1728
|
+
status: String(row.status),
|
|
1729
|
+
instructions: typeof row.instructions === "string" ? row.instructions : void 0,
|
|
1730
|
+
created_by: String(row.created_by),
|
|
1731
|
+
created_at: String(row.created_at),
|
|
1732
|
+
updated_at: String(row.updated_at),
|
|
1733
|
+
claimed_at: typeof row.claimed_at === "string" ? row.claimed_at : void 0,
|
|
1734
|
+
started_at: typeof row.started_at === "string" ? row.started_at : void 0,
|
|
1735
|
+
resolved_at: typeof row.resolved_at === "string" ? row.resolved_at : void 0,
|
|
1736
|
+
cancelled_at: typeof row.cancelled_at === "string" ? row.cancelled_at : void 0,
|
|
1737
|
+
resolved_generation_job_id: typeof row.resolved_generation_job_id === "string" ? row.resolved_generation_job_id : void 0,
|
|
1738
|
+
resolved_asset_id: typeof row.resolved_asset_id === "string" ? row.resolved_asset_id : void 0,
|
|
1739
|
+
claimed_by_claim_id: claimId,
|
|
1740
|
+
metadata
|
|
1741
|
+
};
|
|
1742
|
+
}
|
|
1743
|
+
function eventFromRow(row) {
|
|
1744
|
+
return {
|
|
1745
|
+
id: String(row.id),
|
|
1746
|
+
task_id: String(row.task_id),
|
|
1747
|
+
event_type: String(row.event_type),
|
|
1748
|
+
actor: typeof row.actor === "string" ? row.actor : void 0,
|
|
1749
|
+
message: typeof row.message === "string" ? row.message : void 0,
|
|
1750
|
+
created_at: String(row.created_at),
|
|
1751
|
+
metadata: parseMetadata2(row.metadata_json)
|
|
1752
|
+
};
|
|
1753
|
+
}
|
|
1754
|
+
function findTask(database, project, taskId) {
|
|
1755
|
+
const row = database.prepare("select * from lineage_tasks where project_id = ? and id = ?").get(project, taskId);
|
|
1756
|
+
return row ? taskFromRow(row) : null;
|
|
1757
|
+
}
|
|
1758
|
+
function requireTask(database, project, taskId) {
|
|
1759
|
+
const task = findTask(database, project, taskId);
|
|
1760
|
+
if (!task) throw new LineageTaskError(`Unknown lineage task: ${taskId}`, 404);
|
|
1761
|
+
return task;
|
|
1762
|
+
}
|
|
1763
|
+
function taskEvents(database, taskId) {
|
|
1764
|
+
const rows = database.prepare("select * from lineage_task_events where task_id = ? order by created_at, rowid").all(taskId);
|
|
1765
|
+
return rows.map(eventFromRow);
|
|
1766
|
+
}
|
|
1767
|
+
function recordEvent2(database, taskId, eventType, actor, message, metadata) {
|
|
1768
|
+
database.prepare(`
|
|
1769
|
+
insert into lineage_task_events (id, task_id, event_type, actor, message, created_at, metadata_json)
|
|
1770
|
+
values (?, ?, ?, ?, ?, ?, ?)
|
|
1771
|
+
`).run(randomId2("lineage_task_event"), taskId, eventType, actor || null, message || null, nowIso(), metadataJson2(metadata));
|
|
1772
|
+
}
|
|
1773
|
+
function taskWithEvents(database, project, taskId) {
|
|
1774
|
+
return { project, ok: true, task: requireTask(database, project, taskId), events: taskEvents(database, taskId) };
|
|
1775
|
+
}
|
|
1776
|
+
function assertChanged(result, message) {
|
|
1777
|
+
if (Number(result.changes) !== 1) throw new LineageTaskError(message, 409);
|
|
1778
|
+
}
|
|
1779
|
+
function transaction(database, callback) {
|
|
1780
|
+
database.exec("BEGIN IMMEDIATE");
|
|
1781
|
+
try {
|
|
1782
|
+
const value = callback();
|
|
1783
|
+
database.exec("COMMIT");
|
|
1784
|
+
return value;
|
|
1785
|
+
} catch (error) {
|
|
1786
|
+
database.exec("ROLLBACK");
|
|
1787
|
+
throw error;
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
function taskReadWithEvents(database, project, taskId) {
|
|
1791
|
+
return { task: requireTask(database, project, taskId), events: taskEvents(database, taskId) };
|
|
1792
|
+
}
|
|
1793
|
+
function listLineageTasks(project, rootAssetId, statuses = activeStatuses) {
|
|
1794
|
+
const normalizedProject = normalizeProject(project);
|
|
1795
|
+
const normalizedStatuses = statuses.map(normalizeStatus);
|
|
1796
|
+
const database = lineageDb();
|
|
1797
|
+
try {
|
|
1798
|
+
requireAsset(database, normalizedProject, rootAssetId);
|
|
1799
|
+
if (normalizedStatuses.length === 0) {
|
|
1800
|
+
return { project: normalizedProject, root_asset_id: rootAssetId, tasks: [], fetchedAt: nowIso() };
|
|
1801
|
+
}
|
|
1802
|
+
const placeholders = normalizedStatuses.map(() => "?").join(",");
|
|
1803
|
+
const rows = database.prepare(`
|
|
1804
|
+
select * from lineage_tasks
|
|
1805
|
+
where project_id = ? and root_asset_id = ? and status in (${placeholders})
|
|
1806
|
+
order by updated_at desc, created_at desc, id
|
|
1807
|
+
`).all(normalizedProject, rootAssetId, ...normalizedStatuses);
|
|
1808
|
+
return { project: normalizedProject, root_asset_id: rootAssetId, tasks: rows.map(taskFromRow), fetchedAt: nowIso() };
|
|
1809
|
+
} finally {
|
|
1810
|
+
database.close();
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
function getLineageTask(project, taskId) {
|
|
1814
|
+
const normalizedProject = normalizeProject(project);
|
|
1815
|
+
const database = lineageDb();
|
|
1816
|
+
try {
|
|
1817
|
+
return { project: normalizedProject, ...taskReadWithEvents(database, normalizedProject, taskId) };
|
|
1818
|
+
} finally {
|
|
1819
|
+
database.close();
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
function upsertLineageTask(project, fields) {
|
|
1823
|
+
const normalizedProject = normalizeProject(project);
|
|
1824
|
+
const taskType = normalizeTaskType(fields.taskType);
|
|
1825
|
+
const taskId = taskIdFor(normalizedProject, fields.rootAssetId, fields.targetAssetId, taskType);
|
|
1826
|
+
const database = lineageDb();
|
|
1827
|
+
try {
|
|
1828
|
+
requireAsset(database, normalizedProject, fields.rootAssetId);
|
|
1829
|
+
requireAsset(database, normalizedProject, fields.targetAssetId);
|
|
1830
|
+
const existing = database.prepare(`
|
|
1831
|
+
select * from lineage_tasks
|
|
1832
|
+
where project_id = ? and root_asset_id = ? and target_asset_id = ? and task_type = ?
|
|
1833
|
+
and status in ('pending', 'claimed', 'in_progress')
|
|
1834
|
+
order by created_at desc limit 1
|
|
1835
|
+
`).get(normalizedProject, fields.rootAssetId, fields.targetAssetId, taskType);
|
|
1836
|
+
const instructions = fields.instructions?.trim() || void 0;
|
|
1837
|
+
if (existing) {
|
|
1838
|
+
const task = taskFromRow(existing);
|
|
1839
|
+
if (task.status !== "pending" && instructions !== void 0 && instructions !== task.instructions) {
|
|
1840
|
+
throw new LineageTaskError("Only pending lineage tasks can update instructions.", 409);
|
|
1841
|
+
}
|
|
1842
|
+
if (task.status === "pending" && instructions !== void 0 && instructions !== task.instructions) {
|
|
1843
|
+
const timestamp2 = nowIso();
|
|
1844
|
+
transaction(database, () => {
|
|
1845
|
+
const result = database.prepare(`
|
|
1846
|
+
update lineage_tasks
|
|
1847
|
+
set instructions = ?, updated_at = ?
|
|
1848
|
+
where id = ? and status = 'pending'
|
|
1849
|
+
`).run(instructions, timestamp2, task.id);
|
|
1850
|
+
assertChanged(result, "Only pending lineage tasks can update instructions.");
|
|
1851
|
+
recordEvent2(database, task.id, "instructions_updated", fields.createdBy, "Instructions updated.");
|
|
1852
|
+
});
|
|
1853
|
+
}
|
|
1854
|
+
return taskWithEvents(database, normalizedProject, task.id);
|
|
1855
|
+
}
|
|
1856
|
+
const closedTask = findTask(database, normalizedProject, taskId);
|
|
1857
|
+
if (closedTask) {
|
|
1858
|
+
const timestamp2 = nowIso();
|
|
1859
|
+
transaction(database, () => {
|
|
1860
|
+
const result = database.prepare(`
|
|
1861
|
+
update lineage_tasks
|
|
1862
|
+
set status = 'pending', instructions = ?, created_by = ?, updated_at = ?,
|
|
1863
|
+
claimed_at = null, started_at = null, resolved_at = null, cancelled_at = null,
|
|
1864
|
+
resolved_generation_job_id = null, resolved_asset_id = null, metadata_json = null
|
|
1865
|
+
where id = ? and status not in ('pending', 'claimed', 'in_progress')
|
|
1866
|
+
`).run(instructions || null, fields.createdBy, timestamp2, taskId);
|
|
1867
|
+
assertChanged(result, "Lineage task changed while reopening.");
|
|
1868
|
+
recordEvent2(database, taskId, "created", fields.createdBy, "Lineage task created.");
|
|
1869
|
+
});
|
|
1870
|
+
return taskWithEvents(database, normalizedProject, taskId);
|
|
1871
|
+
}
|
|
1872
|
+
const timestamp = nowIso();
|
|
1873
|
+
transaction(database, () => {
|
|
1874
|
+
database.prepare(`
|
|
1875
|
+
insert into lineage_tasks (
|
|
1876
|
+
id, project_id, root_asset_id, target_asset_id, task_type, status, instructions,
|
|
1877
|
+
created_by, created_at, updated_at, metadata_json
|
|
1878
|
+
) values (?, ?, ?, ?, ?, 'pending', ?, ?, ?, ?, null)
|
|
1879
|
+
`).run(taskId, normalizedProject, fields.rootAssetId, fields.targetAssetId, taskType, instructions || null, fields.createdBy, timestamp, timestamp);
|
|
1880
|
+
recordEvent2(database, taskId, "created", fields.createdBy, "Lineage task created.");
|
|
1881
|
+
});
|
|
1882
|
+
return taskWithEvents(database, normalizedProject, taskId);
|
|
1883
|
+
} finally {
|
|
1884
|
+
database.close();
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
function updateLineageTaskInstructions(project, fields) {
|
|
1888
|
+
const normalizedProject = normalizeProject(project);
|
|
1889
|
+
const instructions = fields.instructions.trim();
|
|
1890
|
+
const database = lineageDb();
|
|
1891
|
+
try {
|
|
1892
|
+
const task = requireTask(database, normalizedProject, fields.taskId);
|
|
1893
|
+
if (task.status !== "pending") throw new LineageTaskError("Only pending lineage tasks can update instructions.", 409);
|
|
1894
|
+
const timestamp = nowIso();
|
|
1895
|
+
transaction(database, () => {
|
|
1896
|
+
const result = database.prepare(`
|
|
1897
|
+
update lineage_tasks
|
|
1898
|
+
set instructions = ?, updated_at = ?
|
|
1899
|
+
where id = ? and status = 'pending'
|
|
1900
|
+
`).run(instructions || null, timestamp, task.id);
|
|
1901
|
+
assertChanged(result, "Only pending lineage tasks can update instructions.");
|
|
1902
|
+
recordEvent2(database, task.id, "instructions_updated", "human", "Instructions updated.");
|
|
1903
|
+
});
|
|
1904
|
+
return taskWithEvents(database, normalizedProject, task.id);
|
|
1905
|
+
} finally {
|
|
1906
|
+
database.close();
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
function addLineageTaskComment(project, fields) {
|
|
1910
|
+
const normalizedProject = normalizeProject(project);
|
|
1911
|
+
const actor = normalizeActor(fields.actor, "Comment actor");
|
|
1912
|
+
const message = fields.message.trim();
|
|
1913
|
+
if (!message) throw new LineageTaskError("Comment message is required");
|
|
1914
|
+
const database = lineageDb();
|
|
1915
|
+
try {
|
|
1916
|
+
const task = requireTask(database, normalizedProject, fields.taskId);
|
|
1917
|
+
transaction(database, () => {
|
|
1918
|
+
recordEvent2(database, task.id, "comment_added", actor, message);
|
|
1919
|
+
});
|
|
1920
|
+
return taskWithEvents(database, normalizedProject, task.id);
|
|
1921
|
+
} finally {
|
|
1922
|
+
database.close();
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
function claimLineageTask(project, fields) {
|
|
1926
|
+
const normalizedProject = normalizeProject(project);
|
|
1927
|
+
const agentName = normalizeActor(fields.agentName, "Agent name");
|
|
1928
|
+
const beforeClaimDb = lineageDb();
|
|
1929
|
+
try {
|
|
1930
|
+
const task = requireTask(beforeClaimDb, normalizedProject, fields.taskId);
|
|
1931
|
+
if (task.status !== "pending") throw new LineageTaskError("Only pending lineage tasks can be claimed.", 409);
|
|
1932
|
+
} finally {
|
|
1933
|
+
beforeClaimDb.close();
|
|
1934
|
+
}
|
|
1935
|
+
const claimResult = createAgentClaim({
|
|
1936
|
+
agentName,
|
|
1937
|
+
project: normalizedProject,
|
|
1938
|
+
scopeType: "lineage_task",
|
|
1939
|
+
targetId: fields.taskId,
|
|
1940
|
+
targetTitle: fields.taskId
|
|
1941
|
+
});
|
|
1942
|
+
if (!claimResult.claim) throw new LineageTaskError("Unable to create lineage task claim.", 500);
|
|
1943
|
+
const claim = claimResult.claim;
|
|
1944
|
+
const database = lineageDb();
|
|
1945
|
+
try {
|
|
1946
|
+
const task = requireTask(database, normalizedProject, fields.taskId);
|
|
1947
|
+
if (task.status !== "pending") {
|
|
1948
|
+
releaseAgentClaim(claimResult.claim_token);
|
|
1949
|
+
throw new LineageTaskError("Only pending lineage tasks can be claimed.", 409);
|
|
1950
|
+
}
|
|
1951
|
+
const timestamp = nowIso();
|
|
1952
|
+
const metadata = { ...task.metadata || {}, claim_id: claim.id };
|
|
1953
|
+
try {
|
|
1954
|
+
transaction(database, () => {
|
|
1955
|
+
const result = database.prepare(`
|
|
1956
|
+
update lineage_tasks
|
|
1957
|
+
set status = 'claimed', claimed_at = ?, updated_at = ?, metadata_json = ?
|
|
1958
|
+
where id = ? and status = 'pending'
|
|
1959
|
+
`).run(timestamp, timestamp, metadataJson2(metadata), task.id);
|
|
1960
|
+
assertChanged(result, "Only pending lineage tasks can be claimed.");
|
|
1961
|
+
recordEvent2(database, task.id, "claimed", agentName, "Lineage task claimed.", { claim_id: claim.id });
|
|
1962
|
+
});
|
|
1963
|
+
} catch (error) {
|
|
1964
|
+
releaseAgentClaim(claimResult.claim_token);
|
|
1965
|
+
throw error;
|
|
1966
|
+
}
|
|
1967
|
+
return {
|
|
1968
|
+
...taskWithEvents(database, normalizedProject, task.id),
|
|
1969
|
+
claim,
|
|
1970
|
+
claim_token: claimResult.claim_token
|
|
1971
|
+
};
|
|
1972
|
+
} finally {
|
|
1973
|
+
database.close();
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
function startLineageTask(project, fields) {
|
|
1977
|
+
const normalizedProject = normalizeProject(project);
|
|
1978
|
+
const precheckDatabase = lineageDb();
|
|
1979
|
+
try {
|
|
1980
|
+
const task = requireTask(precheckDatabase, normalizedProject, fields.taskId);
|
|
1981
|
+
if (task.status !== "claimed") throw new LineageTaskError("Only claimed lineage tasks can be started.", 409);
|
|
1982
|
+
} finally {
|
|
1983
|
+
precheckDatabase.close();
|
|
1984
|
+
}
|
|
1985
|
+
const validation = validateAgentClaimForWrite({
|
|
1986
|
+
claimToken: fields.claimToken,
|
|
1987
|
+
dangerLevel: "enforce",
|
|
1988
|
+
project: normalizedProject,
|
|
1989
|
+
recordEvent: false,
|
|
1990
|
+
scopeType: "lineage_task",
|
|
1991
|
+
targetId: fields.taskId,
|
|
1992
|
+
writeKind: "lineage_task_start"
|
|
1993
|
+
});
|
|
1994
|
+
if (!validation.ok) throw new LineageTaskError(validation.message, 409);
|
|
1995
|
+
const database = lineageDb();
|
|
1996
|
+
try {
|
|
1997
|
+
const task = requireTask(database, normalizedProject, fields.taskId);
|
|
1998
|
+
if (task.status !== "claimed") throw new LineageTaskError("Only claimed lineage tasks can be started.", 409);
|
|
1999
|
+
if (task.claimed_by_claim_id && task.claimed_by_claim_id !== validation.claim.id) {
|
|
2000
|
+
throw new LineageTaskError("Claim token does not match the task claim.", 409);
|
|
2001
|
+
}
|
|
2002
|
+
const timestamp = nowIso();
|
|
2003
|
+
const metadata = { ...task.metadata || {}, claim_id: validation.claim.id };
|
|
2004
|
+
transaction(database, () => {
|
|
2005
|
+
const result = database.prepare(`
|
|
2006
|
+
update lineage_tasks
|
|
2007
|
+
set status = 'in_progress', started_at = ?, updated_at = ?, metadata_json = ?
|
|
2008
|
+
where id = ? and status = 'claimed'
|
|
2009
|
+
`).run(timestamp, timestamp, metadataJson2(metadata), task.id);
|
|
2010
|
+
assertChanged(result, "Only claimed lineage tasks can be started.");
|
|
2011
|
+
recordEvent2(database, task.id, "started", validation.claim.agent_name, "Lineage task started.", { claim_id: validation.claim.id });
|
|
2012
|
+
});
|
|
2013
|
+
recordAgentClaimWriteAllowed(validation.claim, {
|
|
2014
|
+
dangerLevel: "enforce",
|
|
2015
|
+
targetId: fields.taskId,
|
|
2016
|
+
writeKind: "lineage_task_start"
|
|
2017
|
+
});
|
|
2018
|
+
return taskWithEvents(database, normalizedProject, task.id);
|
|
2019
|
+
} finally {
|
|
2020
|
+
database.close();
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
function overrideLineageTask(project, fields) {
|
|
2024
|
+
const normalizedProject = normalizeProject(project);
|
|
2025
|
+
const actor = normalizeActor(fields.actor, "Override actor");
|
|
2026
|
+
const reason = fields.reason.trim();
|
|
2027
|
+
if (!reason) throw new LineageTaskError("Override reason is required");
|
|
2028
|
+
const database = lineageDb();
|
|
2029
|
+
try {
|
|
2030
|
+
const task = requireTask(database, normalizedProject, fields.taskId);
|
|
2031
|
+
if (!["claimed", "in_progress"].includes(task.status)) {
|
|
2032
|
+
throw new LineageTaskError(`Only claimed or in-progress lineage tasks can be overridden; task is ${task.status}.`, 409);
|
|
2033
|
+
}
|
|
2034
|
+
const timestamp = nowIso();
|
|
2035
|
+
const instructions = fields.instructions === void 0 ? task.instructions : fields.instructions.trim() || void 0;
|
|
2036
|
+
const metadata = metadataWithoutClaim(task.metadata);
|
|
2037
|
+
transaction(database, () => {
|
|
2038
|
+
const result = database.prepare(`
|
|
2039
|
+
update lineage_tasks
|
|
2040
|
+
set status = 'pending', instructions = ?, claimed_at = null, started_at = null,
|
|
2041
|
+
updated_at = ?, metadata_json = ?
|
|
2042
|
+
where project_id = ? and id = ? and status in ('claimed', 'in_progress')
|
|
2043
|
+
`).run(instructions || null, timestamp, metadataJson2(metadata), normalizedProject, task.id);
|
|
2044
|
+
assertChanged(result, `Only active lineage task ${task.id} could be overridden.`);
|
|
2045
|
+
if (task.claimed_by_claim_id) {
|
|
2046
|
+
revokeAgentClaimInDatabase(database, normalizedProject, task.claimed_by_claim_id, {
|
|
2047
|
+
actor,
|
|
2048
|
+
reason
|
|
2049
|
+
});
|
|
2050
|
+
}
|
|
2051
|
+
recordEvent2(database, task.id, "human_override", actor, reason, {
|
|
2052
|
+
previous_claim_id: task.claimed_by_claim_id,
|
|
2053
|
+
previous_status: task.status
|
|
2054
|
+
});
|
|
2055
|
+
});
|
|
2056
|
+
return taskWithEvents(database, normalizedProject, task.id);
|
|
2057
|
+
} finally {
|
|
2058
|
+
database.close();
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
function cancelLineageTask(project, fields) {
|
|
2062
|
+
const normalizedProject = normalizeProject(project);
|
|
2063
|
+
const actor = normalizeActor(fields.actor, "Cancel actor");
|
|
2064
|
+
const database = lineageDb();
|
|
2065
|
+
try {
|
|
2066
|
+
const task = requireTask(database, normalizedProject, fields.taskId);
|
|
2067
|
+
if (task.status === "cancelled") return { project: normalizedProject, ok: true, task, events: taskEvents(database, task.id) };
|
|
2068
|
+
if (!["pending", "claimed", "in_progress"].includes(task.status)) {
|
|
2069
|
+
throw new LineageTaskError(`Only open lineage tasks can be cancelled; task is ${task.status}.`, 409);
|
|
2070
|
+
}
|
|
2071
|
+
if (task.status !== "pending" && !fields.override) {
|
|
2072
|
+
throw new LineageTaskError("Cancelling an active lineage task requires override=true.", 409);
|
|
2073
|
+
}
|
|
2074
|
+
const overridingActiveTask = fields.override === true && task.status !== "pending";
|
|
2075
|
+
const metadata = overridingActiveTask ? metadataWithoutClaim(task.metadata) : task.metadata;
|
|
2076
|
+
const timestamp = nowIso();
|
|
2077
|
+
const cancelledTask = {
|
|
2078
|
+
...task,
|
|
2079
|
+
cancelled_at: timestamp,
|
|
2080
|
+
claimed_at: overridingActiveTask ? void 0 : task.claimed_at,
|
|
2081
|
+
claimed_by_claim_id: overridingActiveTask ? void 0 : task.claimed_by_claim_id,
|
|
2082
|
+
started_at: overridingActiveTask ? void 0 : task.started_at,
|
|
2083
|
+
status: "cancelled",
|
|
2084
|
+
updated_at: timestamp,
|
|
2085
|
+
metadata
|
|
2086
|
+
};
|
|
2087
|
+
if (!fields.confirmWrite) return { project: normalizedProject, ok: true, dryRun: true, task: cancelledTask, events: taskEvents(database, task.id) };
|
|
2088
|
+
transaction(database, () => {
|
|
2089
|
+
const result = database.prepare(`
|
|
2090
|
+
update lineage_tasks
|
|
2091
|
+
set status = 'cancelled', cancelled_at = ?, claimed_at = ?, started_at = ?,
|
|
2092
|
+
updated_at = ?, metadata_json = ?
|
|
2093
|
+
where id = ? and status = ?
|
|
2094
|
+
`).run(
|
|
2095
|
+
timestamp,
|
|
2096
|
+
overridingActiveTask ? null : task.claimed_at || null,
|
|
2097
|
+
overridingActiveTask ? null : task.started_at || null,
|
|
2098
|
+
timestamp,
|
|
2099
|
+
metadataJson2(metadata),
|
|
2100
|
+
task.id,
|
|
2101
|
+
task.status
|
|
2102
|
+
);
|
|
2103
|
+
assertChanged(result, `Only ${task.status} lineage task ${task.id} could be cancelled.`);
|
|
2104
|
+
if (overridingActiveTask) {
|
|
2105
|
+
if (task.claimed_by_claim_id) {
|
|
2106
|
+
revokeAgentClaimInDatabase(database, normalizedProject, task.claimed_by_claim_id, {
|
|
2107
|
+
actor,
|
|
2108
|
+
reason: "Lineage task cancelled by human override."
|
|
2109
|
+
});
|
|
2110
|
+
}
|
|
2111
|
+
recordEvent2(database, task.id, "human_override", actor, "Lineage task cancelled by human override.", {
|
|
2112
|
+
previous_claim_id: task.claimed_by_claim_id,
|
|
2113
|
+
previous_status: task.status
|
|
2114
|
+
});
|
|
2115
|
+
}
|
|
2116
|
+
recordEvent2(database, task.id, "cancelled", actor, "Lineage task cancelled.");
|
|
2117
|
+
});
|
|
2118
|
+
return taskWithEvents(database, normalizedProject, task.id);
|
|
2119
|
+
} finally {
|
|
2120
|
+
database.close();
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
function resolveLineageTask(project, fields) {
|
|
2124
|
+
const normalizedProject = normalizeProject(project);
|
|
2125
|
+
const actor = normalizeActor(fields.actor, "Resolve actor");
|
|
2126
|
+
const database = lineageDb();
|
|
2127
|
+
try {
|
|
2128
|
+
const task = requireTask(database, normalizedProject, fields.taskId);
|
|
2129
|
+
if (fields.resolvedAssetId) requireAsset(database, normalizedProject, fields.resolvedAssetId);
|
|
2130
|
+
if (task.status === "resolved") return { project: normalizedProject, ok: true, task, events: taskEvents(database, task.id) };
|
|
2131
|
+
if (!["pending", "claimed", "in_progress"].includes(task.status)) {
|
|
2132
|
+
throw new LineageTaskError(`Only open lineage tasks can be resolved; task is ${task.status}.`, 409);
|
|
2133
|
+
}
|
|
2134
|
+
const timestamp = nowIso();
|
|
2135
|
+
const resolvedTask = {
|
|
2136
|
+
...task,
|
|
2137
|
+
status: "resolved",
|
|
2138
|
+
resolved_asset_id: fields.resolvedAssetId,
|
|
2139
|
+
resolved_at: timestamp,
|
|
2140
|
+
resolved_generation_job_id: fields.resolvedGenerationJobId,
|
|
2141
|
+
updated_at: timestamp
|
|
2142
|
+
};
|
|
2143
|
+
if (!fields.confirmWrite) return { project: normalizedProject, ok: true, dryRun: true, task: resolvedTask, events: taskEvents(database, task.id) };
|
|
2144
|
+
transaction(database, () => {
|
|
2145
|
+
const result = database.prepare(`
|
|
2146
|
+
update lineage_tasks
|
|
2147
|
+
set status = 'resolved', resolved_at = ?, resolved_generation_job_id = ?, resolved_asset_id = ?, updated_at = ?
|
|
2148
|
+
where id = ? and status = ?
|
|
2149
|
+
`).run(timestamp, fields.resolvedGenerationJobId || null, fields.resolvedAssetId || null, timestamp, task.id, task.status);
|
|
2150
|
+
assertChanged(result, `Only ${task.status} lineage task ${task.id} could be resolved.`);
|
|
2151
|
+
recordEvent2(database, task.id, "resolved", actor, "Lineage task resolved.", {
|
|
2152
|
+
resolved_asset_id: fields.resolvedAssetId,
|
|
2153
|
+
resolved_generation_job_id: fields.resolvedGenerationJobId
|
|
2154
|
+
});
|
|
2155
|
+
});
|
|
2156
|
+
return taskWithEvents(database, normalizedProject, task.id);
|
|
2157
|
+
} finally {
|
|
2158
|
+
database.close();
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
|
|
1403
2162
|
// src/server/assetLineageWorkspaces.ts
|
|
1404
2163
|
function lineageWorkspaceId(project, rootAssetId) {
|
|
1405
2164
|
return `${project}:lineage-workspace:${rootAssetId}`;
|
|
@@ -1586,7 +2345,7 @@ function indexLineageAssets(project = defaultProject) {
|
|
|
1586
2345
|
database.close();
|
|
1587
2346
|
return { catalog: catalog.length, local: local.length, total: catalog.length + local.length, database: lineageDbPath() };
|
|
1588
2347
|
}
|
|
1589
|
-
function
|
|
2348
|
+
function requireAsset2(database, project, assetId) {
|
|
1590
2349
|
const row = database.prepare("select id from assets where project_id = ? and id = ?").get(project, assetId);
|
|
1591
2350
|
if (!row) throw new LineageError(`Unknown indexed asset: ${assetId}`, 404);
|
|
1592
2351
|
}
|
|
@@ -1606,7 +2365,7 @@ function rootFor(database, project, assetId) {
|
|
|
1606
2365
|
return assetId;
|
|
1607
2366
|
}
|
|
1608
2367
|
function assertCanonicalRoot(database, project, root) {
|
|
1609
|
-
|
|
2368
|
+
requireAsset2(database, project, root);
|
|
1610
2369
|
const canonicalRoot = rootFor(database, project, root);
|
|
1611
2370
|
if (canonicalRoot !== root) throw new LineageError(`Asset ${root} is not a lineage root`, 400);
|
|
1612
2371
|
}
|
|
@@ -1643,7 +2402,7 @@ function lineageWriteClaimContext(database, project, assetId) {
|
|
|
1643
2402
|
function getLineageWriteClaimContext(project, assetId) {
|
|
1644
2403
|
const database = lineageDb();
|
|
1645
2404
|
try {
|
|
1646
|
-
|
|
2405
|
+
requireAsset2(database, project, assetId);
|
|
1647
2406
|
return lineageWriteClaimContext(database, project, assetId);
|
|
1648
2407
|
} finally {
|
|
1649
2408
|
database.close();
|
|
@@ -1655,12 +2414,12 @@ function latestSelectedRoot(database, project) {
|
|
|
1655
2414
|
}
|
|
1656
2415
|
function resolveRoot(database, project, rootAssetId) {
|
|
1657
2416
|
if (rootAssetId) {
|
|
1658
|
-
|
|
2417
|
+
requireAsset2(database, project, rootAssetId);
|
|
1659
2418
|
return rootAssetId;
|
|
1660
2419
|
}
|
|
1661
2420
|
const root = activeLineageWorkspaceRoot(project) || latestSelectedRoot(database, project);
|
|
1662
2421
|
if (!root) throw new LineageError("Lineage command requires --root unless a project selection exists");
|
|
1663
|
-
|
|
2422
|
+
requireAsset2(database, project, root);
|
|
1664
2423
|
return root;
|
|
1665
2424
|
}
|
|
1666
2425
|
function edgeId(project, parent, child) {
|
|
@@ -1685,6 +2444,33 @@ function rerollRequestFrom(row) {
|
|
|
1685
2444
|
resolved_at: rowString(row.resolved_at)
|
|
1686
2445
|
};
|
|
1687
2446
|
}
|
|
2447
|
+
function tasksByTarget(tasks) {
|
|
2448
|
+
const byTarget = /* @__PURE__ */ new Map();
|
|
2449
|
+
for (const task of tasks) {
|
|
2450
|
+
if (task.task_type !== "iterate" && task.task_type !== "reroll") continue;
|
|
2451
|
+
const existing = byTarget.get(task.target_asset_id) || {};
|
|
2452
|
+
existing[task.task_type] = task;
|
|
2453
|
+
byTarget.set(task.target_asset_id, existing);
|
|
2454
|
+
}
|
|
2455
|
+
return byTarget;
|
|
2456
|
+
}
|
|
2457
|
+
function withRerollTask(request, task) {
|
|
2458
|
+
return task ? { ...request, task_id: task.id, task } : request;
|
|
2459
|
+
}
|
|
2460
|
+
function taskBackedRerollRequest(project, rootAssetId, task) {
|
|
2461
|
+
return {
|
|
2462
|
+
id: `${task.id}:request`,
|
|
2463
|
+
project_id: project,
|
|
2464
|
+
root_asset_id: rootAssetId,
|
|
2465
|
+
node_asset_id: task.target_asset_id,
|
|
2466
|
+
status: "pending",
|
|
2467
|
+
requested_by: task.created_by,
|
|
2468
|
+
notes: task.instructions,
|
|
2469
|
+
created_at: task.created_at,
|
|
2470
|
+
task_id: task.id,
|
|
2471
|
+
task
|
|
2472
|
+
};
|
|
2473
|
+
}
|
|
1688
2474
|
function attemptFrom(row) {
|
|
1689
2475
|
return {
|
|
1690
2476
|
id: String(row.id),
|
|
@@ -1724,7 +2510,7 @@ function withImplicitAttempt(physicalAttempts, row) {
|
|
|
1724
2510
|
}
|
|
1725
2511
|
function assertNodeInRoot(database, project, root, node) {
|
|
1726
2512
|
assertCanonicalRoot(database, project, root);
|
|
1727
|
-
|
|
2513
|
+
requireAsset2(database, project, node);
|
|
1728
2514
|
const nodeRoot = rootFor(database, project, node);
|
|
1729
2515
|
if (nodeRoot !== root) throw new LineageError(`Asset ${node} is not in lineage rooted at ${root}`);
|
|
1730
2516
|
}
|
|
@@ -1745,8 +2531,8 @@ function localPreviewUrl(project, localPath) {
|
|
|
1745
2531
|
}
|
|
1746
2532
|
function linkLineageAssets(project, fields) {
|
|
1747
2533
|
const database = lineageDb();
|
|
1748
|
-
|
|
1749
|
-
|
|
2534
|
+
requireAsset2(database, project, fields.parentAssetId);
|
|
2535
|
+
requireAsset2(database, project, fields.childAssetId);
|
|
1750
2536
|
if (fields.parentAssetId === fields.childAssetId) throw new LineageError("Lineage link cannot point to itself");
|
|
1751
2537
|
const claimContext = lineageWriteClaimContext(database, project, fields.parentAssetId);
|
|
1752
2538
|
try {
|
|
@@ -1797,10 +2583,17 @@ function descendants(database, project, root) {
|
|
|
1797
2583
|
}
|
|
1798
2584
|
function getLineageSnapshot(project, assetId) {
|
|
1799
2585
|
const database = lineageDb();
|
|
1800
|
-
|
|
2586
|
+
requireAsset2(database, project, assetId);
|
|
1801
2587
|
const root = explicitWorkspaceRoot(database, project, assetId) || rootFor(database, project, assetId);
|
|
1802
2588
|
const edges = descendants(database, project, root);
|
|
1803
|
-
const
|
|
2589
|
+
const selected = selectedRows(database, project, root);
|
|
2590
|
+
const tasks = listLineageTasks(project, root).tasks;
|
|
2591
|
+
const ids = [.../* @__PURE__ */ new Set([
|
|
2592
|
+
root,
|
|
2593
|
+
...edges.flatMap((edge) => [edge.parent_asset_id, edge.child_asset_id]),
|
|
2594
|
+
...selected.map((row) => row.asset_id),
|
|
2595
|
+
...tasks.map((task) => task.target_asset_id)
|
|
2596
|
+
])];
|
|
1804
2597
|
const placeholders = ids.map(() => "?").join(",");
|
|
1805
2598
|
const rows = database.prepare(`
|
|
1806
2599
|
select a.id asset_id, a.project_id project, a.source, a.title, a.media_type, a.status, a.channel, a.campaign,
|
|
@@ -1815,12 +2608,12 @@ function getLineageSnapshot(project, assetId) {
|
|
|
1815
2608
|
for (const attempt of attemptRows.map(attemptFrom)) {
|
|
1816
2609
|
attemptsByNode.set(attempt.node_asset_id, [...attemptsByNode.get(attempt.node_asset_id) || [], attempt]);
|
|
1817
2610
|
}
|
|
2611
|
+
const lineageTasksByNode = tasksByTarget(tasks);
|
|
1818
2612
|
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 (${placeholders}) order by created_at`).all(project, root, ...ids) : [];
|
|
1819
2613
|
const rerollsByNode = new Map(rerollRows.map((row) => {
|
|
1820
2614
|
const request = rerollRequestFrom(row);
|
|
1821
|
-
return [request.node_asset_id, request];
|
|
2615
|
+
return [request.node_asset_id, withRerollTask(request, lineageTasksByNode.get(request.node_asset_id)?.reroll)];
|
|
1822
2616
|
}));
|
|
1823
|
-
const selected = selectedRows(database, project, root);
|
|
1824
2617
|
const childIds = new Set(edges.map((edge) => edge.parent_asset_id));
|
|
1825
2618
|
const selectedIds = new Set(selected.map((row) => row.asset_id));
|
|
1826
2619
|
const selections = selected.map((row) => ({
|
|
@@ -1837,11 +2630,13 @@ function getLineageSnapshot(project, assetId) {
|
|
|
1837
2630
|
const attempts = withImplicitAttempt(attemptsByNode.get(row.asset_id) || [], row);
|
|
1838
2631
|
const currentAttempt = attempts.find((attempt) => attempt.is_current) || attempts[0];
|
|
1839
2632
|
const previewPath = currentAttempt?.file_path || row.local_path;
|
|
2633
|
+
const lineageTasks = lineageTasksByNode.get(row.asset_id);
|
|
1840
2634
|
return {
|
|
1841
2635
|
...node,
|
|
1842
2636
|
attempt_count: attempts.length,
|
|
1843
2637
|
current_attempt: currentAttempt,
|
|
1844
2638
|
is_latest: !childIds.has(row.asset_id),
|
|
2639
|
+
lineage_tasks: lineageTasks && Object.keys(lineageTasks).length > 0 ? lineageTasks : void 0,
|
|
1845
2640
|
position,
|
|
1846
2641
|
preview_url: canPreviewLocally(row.media_type, previewPath) ? localPreviewUrl(project, previewPath) : void 0,
|
|
1847
2642
|
reroll_request: rerollsByNode.get(row.asset_id),
|
|
@@ -1857,6 +2652,7 @@ function getLineageSnapshot(project, assetId) {
|
|
|
1857
2652
|
selected: selections.map((row) => row.asset_id),
|
|
1858
2653
|
selection,
|
|
1859
2654
|
selections,
|
|
2655
|
+
tasks,
|
|
1860
2656
|
latest: nodes.filter((node) => node.is_latest).map((node) => node.asset_id),
|
|
1861
2657
|
nodes,
|
|
1862
2658
|
edges,
|
|
@@ -1868,7 +2664,25 @@ function getLineageNextAsset(project, rootAssetId) {
|
|
|
1868
2664
|
const root = resolveRoot(database, project, rootAssetId);
|
|
1869
2665
|
database.close();
|
|
1870
2666
|
const snapshot = getLineageSnapshot(project, root);
|
|
1871
|
-
const
|
|
2667
|
+
const pendingIterateTasks = (snapshot.tasks || []).filter((task) => task.task_type === "iterate" && task.status === "pending");
|
|
2668
|
+
const taskIdsInSelectionOrder = [
|
|
2669
|
+
...snapshot.selections.map((selection) => selection.asset_id).filter((assetId) => pendingIterateTasks.some((task) => task.target_asset_id === assetId)),
|
|
2670
|
+
...pendingIterateTasks.map((task) => task.target_asset_id).filter((assetId) => !snapshot.selections.some((selection) => selection.asset_id === assetId))
|
|
2671
|
+
];
|
|
2672
|
+
const taskSelectedIds = [...new Set(taskIdsInSelectionOrder)];
|
|
2673
|
+
const selectedIds = taskSelectedIds.length > 0 ? taskSelectedIds : snapshot.selected;
|
|
2674
|
+
const selectedNodes = selectedIds.map((assetId) => snapshot.nodes.find((node) => node.asset_id === assetId)).filter((node) => Boolean(node));
|
|
2675
|
+
const taskSelections = taskSelectedIds.map((assetId, position) => {
|
|
2676
|
+
const task = pendingIterateTasks.find((item) => item.target_asset_id === assetId);
|
|
2677
|
+
return {
|
|
2678
|
+
asset_id: assetId,
|
|
2679
|
+
notes: task?.instructions,
|
|
2680
|
+
position,
|
|
2681
|
+
selected_at: task?.created_at || nowIso()
|
|
2682
|
+
};
|
|
2683
|
+
});
|
|
2684
|
+
const selectedSelection = taskSelections.length > 0 ? taskSelections[0] : snapshot.selection;
|
|
2685
|
+
const selectedSelections = taskSelections.length > 0 ? taskSelections : snapshot.selections;
|
|
1872
2686
|
const latestNodes = snapshot.nodes.filter((node) => snapshot.latest.includes(node.asset_id));
|
|
1873
2687
|
const warnings = [];
|
|
1874
2688
|
for (const selectedNode of selectedNodes) {
|
|
@@ -1886,9 +2700,9 @@ function getLineageNextAsset(project, rootAssetId) {
|
|
|
1886
2700
|
next_asset: selectedNodes[0],
|
|
1887
2701
|
next_assets: selectedNodes,
|
|
1888
2702
|
latest: snapshot.latest,
|
|
1889
|
-
selected:
|
|
1890
|
-
selection:
|
|
1891
|
-
selections:
|
|
2703
|
+
selected: selectedIds,
|
|
2704
|
+
selection: selectedSelection,
|
|
2705
|
+
selections: selectedSelections,
|
|
1892
2706
|
candidates: latestNodes,
|
|
1893
2707
|
warnings,
|
|
1894
2708
|
fetchedAt: nowIso()
|
|
@@ -1905,9 +2719,9 @@ function getLineageNextAsset(project, rootAssetId) {
|
|
|
1905
2719
|
next_asset: latestNodes[0],
|
|
1906
2720
|
next_assets: [latestNodes[0]],
|
|
1907
2721
|
latest: snapshot.latest,
|
|
1908
|
-
selected:
|
|
1909
|
-
selection:
|
|
1910
|
-
selections:
|
|
2722
|
+
selected: selectedIds,
|
|
2723
|
+
selection: selectedSelection,
|
|
2724
|
+
selections: selectedSelections,
|
|
1911
2725
|
candidates: latestNodes,
|
|
1912
2726
|
warnings,
|
|
1913
2727
|
fetchedAt: nowIso()
|
|
@@ -1923,9 +2737,9 @@ function getLineageNextAsset(project, rootAssetId) {
|
|
|
1923
2737
|
next_asset: null,
|
|
1924
2738
|
next_assets: [],
|
|
1925
2739
|
latest: snapshot.latest,
|
|
1926
|
-
selected:
|
|
1927
|
-
selection:
|
|
1928
|
-
selections:
|
|
2740
|
+
selected: selectedIds,
|
|
2741
|
+
selection: selectedSelection,
|
|
2742
|
+
selections: selectedSelections,
|
|
1929
2743
|
candidates: latestNodes,
|
|
1930
2744
|
warnings,
|
|
1931
2745
|
fetchedAt: nowIso()
|
|
@@ -1935,12 +2749,23 @@ function listLineageRerollRequests(project, rootAssetId) {
|
|
|
1935
2749
|
const database = lineageDb();
|
|
1936
2750
|
try {
|
|
1937
2751
|
assertCanonicalRoot(database, project, rootAssetId);
|
|
2752
|
+
const rerollTasks = listLineageTasks(project, rootAssetId).tasks.filter((task) => task.task_type === "reroll");
|
|
2753
|
+
const pendingRerollTasks = listLineageTasks(project, rootAssetId, ["pending"]).tasks.filter((task) => task.task_type === "reroll");
|
|
2754
|
+
const rerollTaskByTarget = new Map(rerollTasks.map((task) => [task.target_asset_id, task]));
|
|
1938
2755
|
const rows = database.prepare(`
|
|
1939
2756
|
select * from asset_reroll_requests
|
|
1940
2757
|
where project_id = ? and root_asset_id = ? and status = 'pending'
|
|
1941
2758
|
order by created_at, id
|
|
1942
2759
|
`).all(project, rootAssetId);
|
|
1943
|
-
|
|
2760
|
+
const requests = rows.map((row) => {
|
|
2761
|
+
const request = rerollRequestFrom(row);
|
|
2762
|
+
return withRerollTask(request, rerollTaskByTarget.get(request.node_asset_id));
|
|
2763
|
+
});
|
|
2764
|
+
const legacyTargets = new Set(requests.map((request) => request.node_asset_id));
|
|
2765
|
+
for (const task of pendingRerollTasks) {
|
|
2766
|
+
if (!legacyTargets.has(task.target_asset_id)) requests.push(taskBackedRerollRequest(project, rootAssetId, task));
|
|
2767
|
+
}
|
|
2768
|
+
return { project, root_asset_id: rootAssetId, requests, fetchedAt: nowIso() };
|
|
1944
2769
|
} finally {
|
|
1945
2770
|
database.close();
|
|
1946
2771
|
}
|
|
@@ -1949,7 +2774,7 @@ function recordLineageRerollAttempt(project, fields) {
|
|
|
1949
2774
|
const database = lineageDb();
|
|
1950
2775
|
try {
|
|
1951
2776
|
assertNodeInRoot(database, project, fields.rootAssetId, fields.nodeAssetId);
|
|
1952
|
-
|
|
2777
|
+
requireAsset2(database, project, fields.assetId);
|
|
1953
2778
|
assertAttemptAssetNotVisibleLineageNode(database, project, fields.rootAssetId, fields.assetId);
|
|
1954
2779
|
const timestamp = nowIso();
|
|
1955
2780
|
const maxRow = database.prepare("select max(attempt_index) max_index from asset_attempts where project_id = ? and node_asset_id = ?").get(project, fields.nodeAssetId);
|
|
@@ -2038,6 +2863,13 @@ function markLineageRerollRequest(project, fields) {
|
|
|
2038
2863
|
created_at: timestamp
|
|
2039
2864
|
};
|
|
2040
2865
|
if (!fields.confirmWrite) return { ok: true, dryRun: true, request };
|
|
2866
|
+
const taskResult = upsertLineageTask(project, {
|
|
2867
|
+
createdBy: request.requested_by,
|
|
2868
|
+
instructions: request.notes,
|
|
2869
|
+
rootAssetId: fields.rootAssetId,
|
|
2870
|
+
targetAssetId: fields.nodeAssetId,
|
|
2871
|
+
taskType: "reroll"
|
|
2872
|
+
});
|
|
2041
2873
|
if (existing) {
|
|
2042
2874
|
database.prepare(`
|
|
2043
2875
|
update asset_reroll_requests
|
|
@@ -2057,7 +2889,12 @@ function markLineageRerollRequest(project, fields) {
|
|
|
2057
2889
|
review_state = excluded.review_state, reviewed_at = excluded.reviewed_at,
|
|
2058
2890
|
ignored_at = excluded.ignored_at, notes = coalesce(excluded.notes, asset_reviews.notes), updated_at = excluded.updated_at
|
|
2059
2891
|
`).run(fields.nodeAssetId, timestamp, request.notes || null, timestamp);
|
|
2060
|
-
return {
|
|
2892
|
+
return {
|
|
2893
|
+
ok: true,
|
|
2894
|
+
request: withRerollTask(rerollRequestFrom(database.prepare("select * from asset_reroll_requests where id = ?").get(request.id)), taskResult.task),
|
|
2895
|
+
task_id: taskResult.task.id,
|
|
2896
|
+
task: taskResult.task
|
|
2897
|
+
};
|
|
2061
2898
|
} finally {
|
|
2062
2899
|
database.close();
|
|
2063
2900
|
}
|
|
@@ -2075,12 +2912,23 @@ function clearLineageRerollRequest(project, fields) {
|
|
|
2075
2912
|
const timestamp = nowIso();
|
|
2076
2913
|
const request = { ...rerollRequestFrom(existing), status: "cancelled", resolved_at: timestamp };
|
|
2077
2914
|
if (!fields.confirmWrite) return { ok: true, dryRun: true, request };
|
|
2915
|
+
const task = listLineageTasks(project, fields.rootAssetId, ["pending"]).tasks.find((item) => item.task_type === "reroll" && item.target_asset_id === fields.nodeAssetId);
|
|
2916
|
+
const cancelledTask = task ? cancelLineageTask(project, {
|
|
2917
|
+
actor: "human",
|
|
2918
|
+
confirmWrite: true,
|
|
2919
|
+
taskId: task.id
|
|
2920
|
+
}).task : void 0;
|
|
2078
2921
|
database.prepare(`
|
|
2079
2922
|
update asset_reroll_requests
|
|
2080
2923
|
set status = 'cancelled', resolved_at = ?
|
|
2081
2924
|
where id = ?
|
|
2082
2925
|
`).run(timestamp, request.id);
|
|
2083
|
-
return {
|
|
2926
|
+
return {
|
|
2927
|
+
ok: true,
|
|
2928
|
+
request: withRerollTask(request, cancelledTask),
|
|
2929
|
+
task_id: cancelledTask?.id,
|
|
2930
|
+
task: cancelledTask
|
|
2931
|
+
};
|
|
2084
2932
|
} finally {
|
|
2085
2933
|
database.close();
|
|
2086
2934
|
}
|
|
@@ -2097,6 +2945,9 @@ function lineageCommand(command, project, rootAssetId) {
|
|
|
2097
2945
|
function linkChildCommand(project, rootAssetId) {
|
|
2098
2946
|
return `${publicPackageCommand} link-child --project ${shellQuote(project)} --root ${shellQuote(rootAssetId)} --child <asset-id> --confirm-write --db ${shellQuote(lineageDbPath())} --json`;
|
|
2099
2947
|
}
|
|
2948
|
+
function rerollImportGuidance(rootAssetId, targetAssetId) {
|
|
2949
|
+
return `Use lineage reroll plan --root ${rootAssetId} --target ${targetAssetId} and lineage reroll import instead.`;
|
|
2950
|
+
}
|
|
2100
2951
|
function getLineageBrief(project, rootAssetId) {
|
|
2101
2952
|
const next = getLineageNextAsset(project, rootAssetId);
|
|
2102
2953
|
const assets = next.next_assets;
|
|
@@ -2143,6 +2994,14 @@ function getLineageBrief(project, rootAssetId) {
|
|
|
2143
2994
|
function linkSelectedLineageChild(project, fields) {
|
|
2144
2995
|
const next = getLineageNextAsset(project, fields.rootAssetId);
|
|
2145
2996
|
if (!next.next_asset) throw new LineageError("Cannot link child until a next base is selected or unambiguous");
|
|
2997
|
+
const rerollRequests = listLineageRerollRequests(project, next.root_asset_id).requests;
|
|
2998
|
+
const pendingRerollForParent = rerollRequests.find((request) => request.node_asset_id === next.next_asset?.asset_id);
|
|
2999
|
+
if (pendingRerollForParent && fields.confirmWrite) {
|
|
3000
|
+
throw new LineageError(
|
|
3001
|
+
`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.`,
|
|
3002
|
+
409
|
|
3003
|
+
);
|
|
3004
|
+
}
|
|
2146
3005
|
if (fields.confirmWrite) {
|
|
2147
3006
|
const claimContext = getLineageWriteClaimContext(project, next.next_asset.asset_id);
|
|
2148
3007
|
const validation = validateAgentClaimForWrite({
|
|
@@ -2169,7 +3028,11 @@ function linkSelectedLineageChild(project, fields) {
|
|
|
2169
3028
|
parent_asset_id: next.next_asset.asset_id,
|
|
2170
3029
|
child_asset_id: fields.childAssetId,
|
|
2171
3030
|
reference_asset_ids: next.next_assets.map((asset) => asset.asset_id),
|
|
2172
|
-
warning:
|
|
3031
|
+
warning: [
|
|
3032
|
+
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,
|
|
3033
|
+
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,
|
|
3034
|
+
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
|
|
3035
|
+
].filter(Boolean).join(" ") || void 0
|
|
2173
3036
|
};
|
|
2174
3037
|
}
|
|
2175
3038
|
|
|
@@ -2359,7 +3222,7 @@ function planImageReroll(project = defaultProject, fields) {
|
|
|
2359
3222
|
created_at: timestamp
|
|
2360
3223
|
}]
|
|
2361
3224
|
};
|
|
2362
|
-
if (fields.dryRun) return { ok: true, command: "
|
|
3225
|
+
if (fields.dryRun) return { ok: true, command: "reroll plan", project, dryRun: true, wouldWrite: true, job: preview };
|
|
2363
3226
|
const database = lineageDb();
|
|
2364
3227
|
try {
|
|
2365
3228
|
database.exec("BEGIN IMMEDIATE");
|
|
@@ -2377,7 +3240,7 @@ function planImageReroll(project = defaultProject, fields) {
|
|
|
2377
3240
|
database.exec("ROLLBACK");
|
|
2378
3241
|
throw error;
|
|
2379
3242
|
}
|
|
2380
|
-
return { ok: true, command: "
|
|
3243
|
+
return { ok: true, command: "reroll plan", project, job: loadGenerationJob(database, project, id) };
|
|
2381
3244
|
} finally {
|
|
2382
3245
|
database.close();
|
|
2383
3246
|
}
|
|
@@ -2456,8 +3319,18 @@ function importImageRerollOutput(project = defaultProject, fields) {
|
|
|
2456
3319
|
checksumSha256: resolved.checksum,
|
|
2457
3320
|
confirmWrite: true
|
|
2458
3321
|
});
|
|
3322
|
+
const rerollTask = listLineageTasks(project, job.root_asset_id).tasks.find((task) => task.task_type === "reroll" && task.target_asset_id === target[0].asset_id);
|
|
3323
|
+
if (rerollTask) {
|
|
3324
|
+
resolveLineageTask(project, {
|
|
3325
|
+
actor: "agent",
|
|
3326
|
+
confirmWrite: true,
|
|
3327
|
+
resolvedAssetId: resolved.assetId,
|
|
3328
|
+
resolvedGenerationJobId: fields.jobId,
|
|
3329
|
+
taskId: rerollTask.id
|
|
3330
|
+
});
|
|
3331
|
+
}
|
|
2459
3332
|
const importedJob = loadGenerationJob(writeDb, project, fields.jobId);
|
|
2460
|
-
return { ok: true, command: "
|
|
3333
|
+
return { ok: true, command: "reroll import", project, job: importedJob, imported: importedJob.outputs };
|
|
2461
3334
|
} finally {
|
|
2462
3335
|
writeDb.close();
|
|
2463
3336
|
}
|
|
@@ -2508,8 +3381,8 @@ function resolveStartOptions(config, args) {
|
|
|
2508
3381
|
port
|
|
2509
3382
|
};
|
|
2510
3383
|
}
|
|
2511
|
-
function
|
|
2512
|
-
|
|
3384
|
+
function formatLineageHelp(config) {
|
|
3385
|
+
return `${config.binName} ${packageVersion()}
|
|
2513
3386
|
|
|
2514
3387
|
Usage:
|
|
2515
3388
|
${config.binName} start [--port <port>] [--host <host>] [--db <path>] [--open] [--json]
|
|
@@ -2522,6 +3395,14 @@ Usage:
|
|
|
2522
3395
|
${config.binName} reroll cancel --root <asset-id> --target <asset-id> [--project <project>] [--confirm-write] [--db <path>] [--json]
|
|
2523
3396
|
${config.binName} reroll plan --root <asset-id> --target <asset-id> --prompt <text> [--project <project>] [--db <path>] [--json]
|
|
2524
3397
|
${config.binName} reroll import --job-id <job-id> --file <scratch-file> --confirm-write [--project <project>] [--db <path>] [--json]
|
|
3398
|
+
${config.binName} tasks list --root <asset-id> [--project <project>] [--db <path>] [--json]
|
|
3399
|
+
${config.binName} tasks inspect --task <task-id> [--project <project>] [--db <path>] [--json]
|
|
3400
|
+
${config.binName} tasks claim --task <task-id> --agent-name <name> [--project <project>] [--db <path>] [--json]
|
|
3401
|
+
${config.binName} tasks start --task <task-id> --claim-token <claim-id.secret> [--project <project>] [--db <path>] [--json]
|
|
3402
|
+
${config.binName} tasks comment --task <task-id> --message <text> [--project <project>] [--db <path>] [--json]
|
|
3403
|
+
${config.binName} tasks cancel --task <task-id> [--confirm-write] [--override] [--project <project>] [--db <path>] [--json]
|
|
3404
|
+
${config.binName} tasks override --task <task-id> --reason <text> [--instructions <text>] [--project <project>] [--db <path>] [--json]
|
|
3405
|
+
${config.binName} tasks instructions --task <task-id> --instructions <text> [--project <project>] [--db <path>] [--json]
|
|
2525
3406
|
${config.binName} agent claim --project <project> --scope <scope> --target <target-id> --agent-name <name> [--channel <channel>] [--ttl 20m] [--json]
|
|
2526
3407
|
${config.binName} agent graph --root <asset-id> [--project <project>] [--db <path>] [--json]
|
|
2527
3408
|
${config.binName} agent status [--project <project>] [--json]
|
|
@@ -2533,7 +3414,14 @@ Usage:
|
|
|
2533
3414
|
${config.binName} --help
|
|
2534
3415
|
${config.binName} --version
|
|
2535
3416
|
|
|
2536
|
-
${config.displayName} runs the bundled Lineage server for the ${config.channel} channel
|
|
3417
|
+
${config.displayName} runs the bundled Lineage server for the ${config.channel} channel.
|
|
3418
|
+
|
|
3419
|
+
Variation vs re-roll:
|
|
3420
|
+
link-child creates a new visible child variation edge.
|
|
3421
|
+
reroll mark -> reroll plan -> reroll import updates the same node with a new attempt.`;
|
|
3422
|
+
}
|
|
3423
|
+
function printHelp(config) {
|
|
3424
|
+
console.log(formatLineageHelp(config));
|
|
2537
3425
|
}
|
|
2538
3426
|
function openBrowser(url) {
|
|
2539
3427
|
const command = platform() === "darwin" ? "open" : platform() === "win32" ? "cmd" : "xdg-open";
|
|
@@ -2636,6 +3524,66 @@ function runLineageDataCommand(command, args) {
|
|
|
2636
3524
|
}
|
|
2637
3525
|
throw new Error(`Unknown reroll command: ${subcommand}`);
|
|
2638
3526
|
}
|
|
3527
|
+
if (command === "tasks") {
|
|
3528
|
+
const subcommand = positionalArgs(args)[0] || "";
|
|
3529
|
+
const taskId = readOption(args, "--task");
|
|
3530
|
+
if (subcommand === "list") {
|
|
3531
|
+
if (!options.rootAssetId) throw new Error("lineage tasks list requires --root");
|
|
3532
|
+
return listLineageTasks(options.project, options.rootAssetId);
|
|
3533
|
+
}
|
|
3534
|
+
if (subcommand === "inspect") {
|
|
3535
|
+
if (!taskId) throw new Error("lineage tasks inspect requires --task");
|
|
3536
|
+
return getLineageTask(options.project, taskId);
|
|
3537
|
+
}
|
|
3538
|
+
if (subcommand === "claim") {
|
|
3539
|
+
const agentName = readOption(args, "--agent-name");
|
|
3540
|
+
if (!taskId) throw new Error("lineage tasks claim requires --task");
|
|
3541
|
+
if (!agentName) throw new Error("lineage tasks claim requires --agent-name");
|
|
3542
|
+
return claimLineageTask(options.project, { taskId, agentName });
|
|
3543
|
+
}
|
|
3544
|
+
if (subcommand === "start") {
|
|
3545
|
+
if (!taskId) throw new Error("lineage tasks start requires --task");
|
|
3546
|
+
if (!options.claimToken) throw new Error("lineage tasks start requires --claim-token");
|
|
3547
|
+
return startLineageTask(options.project, { taskId, claimToken: options.claimToken });
|
|
3548
|
+
}
|
|
3549
|
+
if (subcommand === "comment") {
|
|
3550
|
+
const message = readOption(args, "--message");
|
|
3551
|
+
if (!taskId) throw new Error("lineage tasks comment requires --task");
|
|
3552
|
+
if (!message) throw new Error("lineage tasks comment requires --message");
|
|
3553
|
+
return addLineageTaskComment(options.project, {
|
|
3554
|
+
actor: readOption(args, "--actor") || "human",
|
|
3555
|
+
message,
|
|
3556
|
+
taskId
|
|
3557
|
+
});
|
|
3558
|
+
}
|
|
3559
|
+
if (subcommand === "cancel") {
|
|
3560
|
+
if (!taskId) throw new Error("lineage tasks cancel requires --task");
|
|
3561
|
+
return cancelLineageTask(options.project, {
|
|
3562
|
+
actor: readOption(args, "--actor") || "human",
|
|
3563
|
+
confirmWrite: options.confirmWrite,
|
|
3564
|
+
override: args.includes("--override"),
|
|
3565
|
+
taskId
|
|
3566
|
+
});
|
|
3567
|
+
}
|
|
3568
|
+
if (subcommand === "override") {
|
|
3569
|
+
const reason = readOption(args, "--reason");
|
|
3570
|
+
if (!taskId) throw new Error("lineage tasks override requires --task");
|
|
3571
|
+
if (!reason) throw new Error("lineage tasks override requires --reason");
|
|
3572
|
+
return overrideLineageTask(options.project, {
|
|
3573
|
+
actor: readOption(args, "--actor") || "human",
|
|
3574
|
+
instructions: readOption(args, "--instructions"),
|
|
3575
|
+
reason,
|
|
3576
|
+
taskId
|
|
3577
|
+
});
|
|
3578
|
+
}
|
|
3579
|
+
if (subcommand === "instructions") {
|
|
3580
|
+
const instructions = readOption(args, "--instructions");
|
|
3581
|
+
if (!taskId) throw new Error("lineage tasks instructions requires --task");
|
|
3582
|
+
if (instructions === void 0) throw new Error("lineage tasks instructions requires --instructions");
|
|
3583
|
+
return updateLineageTaskInstructions(options.project, { instructions, taskId });
|
|
3584
|
+
}
|
|
3585
|
+
throw new Error(`Unknown tasks command: ${subcommand}`);
|
|
3586
|
+
}
|
|
2639
3587
|
throw new Error(`Unknown command: ${command}`);
|
|
2640
3588
|
}
|
|
2641
3589
|
function rerollRequestedBy(value) {
|
|
@@ -2668,6 +3616,7 @@ function printDataResult(command, result, json) {
|
|
|
2668
3616
|
if (command === "link-child" && result && typeof result === "object") {
|
|
2669
3617
|
const link = result;
|
|
2670
3618
|
console.log(link.message || `${link.dryRun ? "Dry run: " : ""}Link ${link.edge?.child_asset_id || "child"} from ${link.edge?.parent_asset_id || "parent"}`);
|
|
3619
|
+
if (link.warning) console.log(`Warning: ${link.warning}`);
|
|
2671
3620
|
return;
|
|
2672
3621
|
}
|
|
2673
3622
|
if (command === "reroll" && result && typeof result === "object") {
|
|
@@ -2688,6 +3637,22 @@ function printDataResult(command, result, json) {
|
|
|
2688
3637
|
return;
|
|
2689
3638
|
}
|
|
2690
3639
|
}
|
|
3640
|
+
if (command === "tasks" && result && typeof result === "object") {
|
|
3641
|
+
if ("tasks" in result) {
|
|
3642
|
+
const listed = result;
|
|
3643
|
+
console.log(`${listed.tasks.length} lineage task(s)`);
|
|
3644
|
+
for (const task of listed.tasks) console.log(`${task.id} ${task.task_type} ${task.status} ${task.target_asset_id}`);
|
|
3645
|
+
return;
|
|
3646
|
+
}
|
|
3647
|
+
if ("task" in result) {
|
|
3648
|
+
const mutation = result;
|
|
3649
|
+
const prefix = mutation.dryRun ? "Dry run: " : "";
|
|
3650
|
+
console.log(`${prefix}${mutation.task?.id || "task"} ${mutation.task?.task_type || "task"} ${mutation.task?.status || "unknown"} ${mutation.task?.target_asset_id || ""}`.trim());
|
|
3651
|
+
if ("claim_token" in mutation && typeof mutation.claim_token === "string") console.log(`Token: ${mutation.claim_token}`);
|
|
3652
|
+
if (mutation.events && mutation.events.length > 0) console.log(`Events: ${mutation.events.map((event) => event.event_type).join(", ")}`);
|
|
3653
|
+
return;
|
|
3654
|
+
}
|
|
3655
|
+
}
|
|
2691
3656
|
console.log(String(result));
|
|
2692
3657
|
}
|
|
2693
3658
|
function runLineageAgentCommand(command, args) {
|
|
@@ -2871,7 +3836,7 @@ function runLineageCli(config, args = process.argv.slice(2)) {
|
|
|
2871
3836
|
start(config, normalizedArgs.slice(1));
|
|
2872
3837
|
return;
|
|
2873
3838
|
}
|
|
2874
|
-
if (command === "next" || command === "brief" || command === "inspect" || command === "link-child" || command === "reroll") {
|
|
3839
|
+
if (command === "next" || command === "brief" || command === "inspect" || command === "link-child" || command === "reroll" || command === "tasks") {
|
|
2875
3840
|
const commandArgs = normalizedArgs.slice(1);
|
|
2876
3841
|
const json2 = commandArgs.includes("--json");
|
|
2877
3842
|
try {
|