@mean-weasel/lineage 0.1.4 → 0.1.6
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 +23 -3
- package/dist/cli/lineage-dev.js +1001 -19
- package/dist/cli/lineage-dev.js.map +4 -4
- package/dist/cli/lineage.js +1001 -19
- package/dist/cli/lineage.js.map +4 -4
- package/dist/server.js +1197 -631
- package/dist/server.js.map +4 -4
- package/dist/web/assets/index-EfT3Ues-.css +1 -0
- package/dist/web/assets/index-NDba9I2v.js +23 -0
- package/dist/web/index.html +2 -2
- package/fixtures/demo-project/lineage/swissifier-rerolls/swissifier-drill-mint-diagonal-reroll-v2.png +0 -0
- package/fixtures/demo-project/lineage/swissifier-rerolls/swissifier-vertical-before-after-reroll-v2.png +0 -0
- package/fixtures/demo-project/lineage/swissifier-rerolls/swissifier-vertical-before-after-reroll-v3.png +0 -0
- package/fixtures/demo-project/lineage/swissifier-rich-demo.json +38 -1
- package/package.json +2 -1
- package/dist/web/assets/index-DI7dn5M6.css +0 -1
- package/dist/web/assets/index-dJy71x2a.js +0 -21
package/dist/server.js
CHANGED
|
@@ -339,6 +339,44 @@ function lineageDb() {
|
|
|
339
339
|
);
|
|
340
340
|
create index if not exists edges_parent on asset_edges(project_id, parent_asset_id);
|
|
341
341
|
create index if not exists edges_child on asset_edges(project_id, child_asset_id);
|
|
342
|
+
create table if not exists asset_attempts (
|
|
343
|
+
id text primary key,
|
|
344
|
+
project_id text not null references projects(id),
|
|
345
|
+
node_asset_id text not null references assets(id),
|
|
346
|
+
asset_id text not null references assets(id),
|
|
347
|
+
attempt_index integer not null check (attempt_index > 0),
|
|
348
|
+
source text not null check (source in ('initial', 'generated_child', 'reroll')),
|
|
349
|
+
prompt text,
|
|
350
|
+
generation_job_id text,
|
|
351
|
+
file_path text,
|
|
352
|
+
checksum_sha256 text,
|
|
353
|
+
created_at text not null,
|
|
354
|
+
promoted_at text,
|
|
355
|
+
is_current integer not null check (is_current in (0, 1)),
|
|
356
|
+
unique(project_id, node_asset_id, attempt_index),
|
|
357
|
+
unique(project_id, node_asset_id, asset_id, source)
|
|
358
|
+
);
|
|
359
|
+
create unique index if not exists asset_attempts_one_current
|
|
360
|
+
on asset_attempts(project_id, node_asset_id)
|
|
361
|
+
where is_current = 1;
|
|
362
|
+
create index if not exists asset_attempts_node_created
|
|
363
|
+
on asset_attempts(project_id, node_asset_id, created_at);
|
|
364
|
+
create table if not exists asset_reroll_requests (
|
|
365
|
+
id text primary key,
|
|
366
|
+
project_id text not null references projects(id),
|
|
367
|
+
root_asset_id text not null references assets(id),
|
|
368
|
+
node_asset_id text not null references assets(id),
|
|
369
|
+
status text not null check (status in ('pending', 'resolved', 'cancelled')),
|
|
370
|
+
requested_by text not null check (requested_by in ('human', 'agent', 'system')),
|
|
371
|
+
notes text,
|
|
372
|
+
created_at text not null,
|
|
373
|
+
resolved_at text
|
|
374
|
+
);
|
|
375
|
+
create unique index if not exists asset_reroll_requests_one_pending
|
|
376
|
+
on asset_reroll_requests(project_id, root_asset_id, node_asset_id)
|
|
377
|
+
where status = 'pending';
|
|
378
|
+
create index if not exists asset_reroll_requests_root_status
|
|
379
|
+
on asset_reroll_requests(project_id, root_asset_id, status, created_at);
|
|
342
380
|
create table if not exists asset_reviews (
|
|
343
381
|
asset_id text primary key references assets(id),
|
|
344
382
|
review_state text not null check (review_state in ('unreviewed', 'approved', 'needs_revision', 'rejected', 'ignored')),
|
|
@@ -543,7 +581,7 @@ function lineageDb() {
|
|
|
543
581
|
project_id text not null references projects(id),
|
|
544
582
|
provider text not null default 'codex-handoff',
|
|
545
583
|
adapter_version text not null,
|
|
546
|
-
source_mode text not null check (source_mode in ('lineage_selection')),
|
|
584
|
+
source_mode text not null check (source_mode in ('lineage_selection', 'lineage_reroll')),
|
|
547
585
|
root_asset_id text not null references assets(id),
|
|
548
586
|
prompt text not null,
|
|
549
587
|
expected_output_count integer not null check (expected_output_count > 0),
|
|
@@ -561,7 +599,7 @@ function lineageDb() {
|
|
|
561
599
|
project_id text not null references projects(id),
|
|
562
600
|
asset_id text not null references assets(id),
|
|
563
601
|
root_asset_id text not null references assets(id),
|
|
564
|
-
role text not null check (role in ('lineage_next_base', 'reference')),
|
|
602
|
+
role text not null check (role in ('lineage_next_base', 'reference', 'reroll_target')),
|
|
565
603
|
position integer not null,
|
|
566
604
|
selection_strategy text not null,
|
|
567
605
|
selection_snapshot_json text not null,
|
|
@@ -1587,755 +1625,1099 @@ function activeLineageWorkspaceRoot(project) {
|
|
|
1587
1625
|
}
|
|
1588
1626
|
}
|
|
1589
1627
|
|
|
1590
|
-
// src/server/
|
|
1591
|
-
|
|
1592
|
-
|
|
1628
|
+
// src/server/agentClaims.ts
|
|
1629
|
+
import { createHash as createHash2, randomBytes, timingSafeEqual } from "node:crypto";
|
|
1630
|
+
var defaultTtlSeconds = 20 * 60;
|
|
1631
|
+
var idleAfterSeconds = 5 * 60;
|
|
1632
|
+
var staleAfterSeconds = 15 * 60;
|
|
1633
|
+
var scopes = /* @__PURE__ */ new Set(["lineage_workspace", "content_post", "content_queue_lane", "selection_set", "project_channel"]);
|
|
1634
|
+
var AgentClaimError = class extends Error {
|
|
1635
|
+
constructor(message, status = 400, code = "agent_claim_error", conflicts = []) {
|
|
1593
1636
|
super(message);
|
|
1594
1637
|
this.status = status;
|
|
1638
|
+
this.code = code;
|
|
1639
|
+
this.conflicts = conflicts;
|
|
1595
1640
|
}
|
|
1596
1641
|
status;
|
|
1642
|
+
code;
|
|
1643
|
+
conflicts;
|
|
1597
1644
|
};
|
|
1598
|
-
function
|
|
1599
|
-
return error instanceof
|
|
1645
|
+
function isAgentClaimError(error) {
|
|
1646
|
+
return error instanceof AgentClaimError;
|
|
1600
1647
|
}
|
|
1601
|
-
function
|
|
1602
|
-
|
|
1603
|
-
const
|
|
1604
|
-
|
|
1605
|
-
|
|
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");
|
|
1606
1658
|
}
|
|
1607
|
-
return
|
|
1659
|
+
return seconds;
|
|
1608
1660
|
}
|
|
1609
|
-
function
|
|
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");
|
|
1666
|
+
}
|
|
1667
|
+
function safeEqual(a, b) {
|
|
1668
|
+
const left = Buffer.from(a);
|
|
1669
|
+
const right = Buffer.from(b);
|
|
1670
|
+
return left.length === right.length && timingSafeEqual(left, right);
|
|
1671
|
+
}
|
|
1672
|
+
function expiresAtFrom(timestamp, ttlSeconds) {
|
|
1673
|
+
return new Date(new Date(timestamp).getTime() + ttlSeconds * 1e3).toISOString();
|
|
1674
|
+
}
|
|
1675
|
+
function metadataJson(metadata2) {
|
|
1676
|
+
return metadata2 ? JSON.stringify(metadata2) : null;
|
|
1677
|
+
}
|
|
1678
|
+
function parseMetadata(value) {
|
|
1679
|
+
if (typeof value !== "string" || !value) return void 0;
|
|
1680
|
+
try {
|
|
1681
|
+
const parsed = JSON.parse(value);
|
|
1682
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
|
|
1683
|
+
} catch {
|
|
1684
|
+
return void 0;
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
function derivedState(row, now = /* @__PURE__ */ new Date()) {
|
|
1688
|
+
if (row.status !== "active") return row.status === "expired" ? "expired" : "stale";
|
|
1689
|
+
if (new Date(row.expires_at).getTime() <= now.getTime()) return "expired";
|
|
1690
|
+
const ageSeconds = Math.max(0, Math.floor((now.getTime() - new Date(row.heartbeat_at).getTime()) / 1e3));
|
|
1691
|
+
if (ageSeconds >= staleAfterSeconds) return "stale";
|
|
1692
|
+
if (ageSeconds >= idleAfterSeconds) return "idle";
|
|
1693
|
+
return "active";
|
|
1694
|
+
}
|
|
1695
|
+
function rowToClaim(row, now = /* @__PURE__ */ new Date()) {
|
|
1696
|
+
const heartbeatAt = String(row.heartbeat_at);
|
|
1697
|
+
return {
|
|
1698
|
+
id: String(row.id),
|
|
1699
|
+
project: String(row.project_id),
|
|
1700
|
+
channel: typeof row.channel === "string" ? row.channel : void 0,
|
|
1701
|
+
scope_type: String(row.scope_type),
|
|
1702
|
+
target_id: String(row.target_id),
|
|
1703
|
+
target_title: typeof row.target_title === "string" ? row.target_title : void 0,
|
|
1704
|
+
agent_id: typeof row.agent_id === "string" ? row.agent_id : void 0,
|
|
1705
|
+
agent_name: String(row.agent_name),
|
|
1706
|
+
agent_kind: String(row.agent_kind),
|
|
1707
|
+
thread_id: typeof row.thread_id === "string" ? row.thread_id : void 0,
|
|
1708
|
+
status: String(row.status),
|
|
1709
|
+
created_at: String(row.created_at),
|
|
1710
|
+
heartbeat_at: heartbeatAt,
|
|
1711
|
+
expires_at: String(row.expires_at),
|
|
1712
|
+
released_at: typeof row.released_at === "string" ? row.released_at : void 0,
|
|
1713
|
+
revoked_at: typeof row.revoked_at === "string" ? row.revoked_at : void 0,
|
|
1714
|
+
revoked_by: typeof row.revoked_by === "string" ? row.revoked_by : void 0,
|
|
1715
|
+
override_reason: typeof row.override_reason === "string" ? row.override_reason : void 0,
|
|
1716
|
+
metadata: parseMetadata(row.metadata_json),
|
|
1717
|
+
heartbeat_age_seconds: Math.max(0, Math.floor((now.getTime() - new Date(heartbeatAt).getTime()) / 1e3)),
|
|
1718
|
+
derived_state: derivedState({
|
|
1719
|
+
expires_at: String(row.expires_at),
|
|
1720
|
+
heartbeat_at: heartbeatAt,
|
|
1721
|
+
status: String(row.status)
|
|
1722
|
+
}, now)
|
|
1723
|
+
};
|
|
1724
|
+
}
|
|
1725
|
+
function eventToRow(row) {
|
|
1726
|
+
return {
|
|
1727
|
+
claim_id: String(row.claim_id),
|
|
1728
|
+
event_type: String(row.event_type),
|
|
1729
|
+
actor: typeof row.actor === "string" ? row.actor : void 0,
|
|
1730
|
+
message: typeof row.message === "string" ? row.message : void 0,
|
|
1731
|
+
created_at: String(row.created_at),
|
|
1732
|
+
metadata: parseMetadata(row.metadata_json)
|
|
1733
|
+
};
|
|
1734
|
+
}
|
|
1735
|
+
function ensureProject3(database, project) {
|
|
1610
1736
|
const timestamp = nowIso();
|
|
1611
1737
|
database.prepare(`
|
|
1612
|
-
insert into projects (id, product,
|
|
1613
|
-
values (?, ?, ?,
|
|
1738
|
+
insert into projects (id, product, created_at, updated_at)
|
|
1739
|
+
values (?, ?, ?, ?)
|
|
1614
1740
|
on conflict(id) do update set product = excluded.product, updated_at = excluded.updated_at
|
|
1615
|
-
`).run(project, project,
|
|
1741
|
+
`).run(project, project, timestamp, timestamp);
|
|
1616
1742
|
}
|
|
1617
|
-
function
|
|
1618
|
-
const timestamp = nowIso();
|
|
1619
|
-
const source = asset.source === "local" ? "local" : "catalog";
|
|
1620
|
-
database.prepare(`
|
|
1621
|
-
insert into assets (
|
|
1622
|
-
id, project_id, source, local_path, s3_key, checksum_sha256, media_type, title, status,
|
|
1623
|
-
channel, campaign, audience, size_bytes, content_type, created_at, updated_at, last_seen_at
|
|
1624
|
-
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1625
|
-
on conflict(id) do update set
|
|
1626
|
-
source = excluded.source, local_path = excluded.local_path, s3_key = excluded.s3_key,
|
|
1627
|
-
checksum_sha256 = excluded.checksum_sha256, media_type = excluded.media_type,
|
|
1628
|
-
title = excluded.title, status = excluded.status, channel = excluded.channel,
|
|
1629
|
-
campaign = excluded.campaign, audience = excluded.audience, size_bytes = excluded.size_bytes,
|
|
1630
|
-
content_type = excluded.content_type, updated_at = excluded.updated_at, last_seen_at = excluded.last_seen_at
|
|
1631
|
-
`).run(
|
|
1632
|
-
asset.asset_id,
|
|
1633
|
-
project,
|
|
1634
|
-
source,
|
|
1635
|
-
asset.local?.relative_path || null,
|
|
1636
|
-
asset.s3?.key || null,
|
|
1637
|
-
asset.local?.checksum_sha256 || asset.s3?.checksum_sha256 || null,
|
|
1638
|
-
asset.content_type,
|
|
1639
|
-
asset.title,
|
|
1640
|
-
asset.status,
|
|
1641
|
-
asset.channel || null,
|
|
1642
|
-
asset.campaign || null,
|
|
1643
|
-
asset.audience || null,
|
|
1644
|
-
asset.local?.size_bytes || asset.s3?.size_bytes || null,
|
|
1645
|
-
asset.local?.content_type || asset.s3?.content_type || null,
|
|
1646
|
-
timestamp,
|
|
1647
|
-
timestamp,
|
|
1648
|
-
timestamp
|
|
1649
|
-
);
|
|
1743
|
+
function recordEvent(database, claimId, eventType, actor, message, metadata2) {
|
|
1650
1744
|
database.prepare(`
|
|
1651
|
-
insert into
|
|
1652
|
-
values (?,
|
|
1653
|
-
|
|
1654
|
-
`).run(asset.asset_id, timestamp);
|
|
1745
|
+
insert into agent_claim_events (id, claim_id, event_type, actor, message, created_at, metadata_json)
|
|
1746
|
+
values (?, ?, ?, ?, ?, ?, ?)
|
|
1747
|
+
`).run(randomId("claim_event"), claimId, eventType, actor || null, message || null, nowIso(), metadataJson(metadata2));
|
|
1655
1748
|
}
|
|
1656
|
-
function
|
|
1657
|
-
const
|
|
1658
|
-
const
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
database.
|
|
1663
|
-
|
|
1749
|
+
function expireActiveClaims(database) {
|
|
1750
|
+
const timestamp = nowIso();
|
|
1751
|
+
const expired = database.prepare(`
|
|
1752
|
+
select id from agent_claims where status = 'active' and expires_at <= ?
|
|
1753
|
+
`).all(timestamp);
|
|
1754
|
+
if (expired.length === 0) return;
|
|
1755
|
+
database.prepare(`
|
|
1756
|
+
update agent_claims set status = 'expired' where status = 'active' and expires_at <= ?
|
|
1757
|
+
`).run(timestamp);
|
|
1758
|
+
for (const claim of expired) recordEvent(database, claim.id, "expired", "system", "Claim expired after missed heartbeat.");
|
|
1664
1759
|
}
|
|
1665
|
-
function
|
|
1666
|
-
|
|
1667
|
-
|
|
1760
|
+
function normalizeScope(scopeType) {
|
|
1761
|
+
if (!scopes.has(scopeType)) throw new AgentClaimError(`Unsupported claim scope: ${scopeType}`);
|
|
1762
|
+
return scopeType;
|
|
1668
1763
|
}
|
|
1669
|
-
function
|
|
1670
|
-
|
|
1671
|
-
return row?.parent_asset_id;
|
|
1764
|
+
function channelOverlaps(left, right) {
|
|
1765
|
+
return !left || !right || left === right;
|
|
1672
1766
|
}
|
|
1673
|
-
function
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
const parent = parentOf(database, project, root);
|
|
1679
|
-
if (!parent) return root;
|
|
1680
|
-
root = parent;
|
|
1681
|
-
}
|
|
1682
|
-
return assetId;
|
|
1767
|
+
function claimOverlaps(candidate, existing) {
|
|
1768
|
+
if (candidate.project !== existing.project) return false;
|
|
1769
|
+
if (!channelOverlaps(candidate.channel, existing.channel)) return false;
|
|
1770
|
+
if (candidate.scope_type === existing.scope_type && candidate.target_id === existing.target_id) return true;
|
|
1771
|
+
return candidate.scope_type === "project_channel" || existing.scope_type === "project_channel";
|
|
1683
1772
|
}
|
|
1684
|
-
function
|
|
1685
|
-
const
|
|
1686
|
-
return row
|
|
1773
|
+
function activeClaims(database, project) {
|
|
1774
|
+
const rows = project ? database.prepare("select * from agent_claims where project_id = ? and status = 'active' order by heartbeat_at desc").all(project) : database.prepare("select * from agent_claims where status = 'active' order by project_id, heartbeat_at desc").all();
|
|
1775
|
+
return rows.map((row) => rowToClaim(row));
|
|
1687
1776
|
}
|
|
1688
|
-
function
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
return rootAssetId;
|
|
1692
|
-
}
|
|
1693
|
-
const root = activeLineageWorkspaceRoot(project) || latestSelectedRoot(database, project);
|
|
1694
|
-
if (!root) throw new LineageError("Lineage command requires --root unless a project selection exists");
|
|
1695
|
-
requireAsset2(database, project, root);
|
|
1696
|
-
return root;
|
|
1777
|
+
function findClaimById(database, claimId, project) {
|
|
1778
|
+
const row = project ? database.prepare("select * from agent_claims where project_id = ? and id = ?").get(project, claimId) : database.prepare("select * from agent_claims where id = ?").get(claimId);
|
|
1779
|
+
return row ? rowToClaim(row) : null;
|
|
1697
1780
|
}
|
|
1698
|
-
function
|
|
1699
|
-
|
|
1781
|
+
function findClaimRowByToken(database, claimToken) {
|
|
1782
|
+
const claimId = claimToken.split(".")[0];
|
|
1783
|
+
const row = database.prepare("select * from agent_claims where id = ?").get(claimId);
|
|
1784
|
+
if (!row) return null;
|
|
1785
|
+
return safeEqual(String(row.token_hash), tokenHash(claimToken)) ? row : null;
|
|
1700
1786
|
}
|
|
1701
|
-
function
|
|
1702
|
-
return
|
|
1787
|
+
function denied(code, message, conflicts = []) {
|
|
1788
|
+
return { ok: false, code, message, conflicts };
|
|
1703
1789
|
}
|
|
1704
|
-
function
|
|
1705
|
-
if (
|
|
1706
|
-
|
|
1707
|
-
return
|
|
1790
|
+
function scopeAllowsWrite(claim, scopeType, targetId, writeKind) {
|
|
1791
|
+
if (claim.scope_type === scopeType && claim.target_id === targetId) return true;
|
|
1792
|
+
if (claim.scope_type === "project_channel") return true;
|
|
1793
|
+
if (claim.scope_type === "lineage_workspace" && scopeType === "lineage_workspace") return claim.target_id === targetId;
|
|
1794
|
+
if (claim.scope_type === "content_queue_lane" && writeKind === "content_queue_next") return true;
|
|
1795
|
+
return false;
|
|
1708
1796
|
}
|
|
1709
|
-
function
|
|
1710
|
-
const
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
if (
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1797
|
+
function createAgentClaim(fields) {
|
|
1798
|
+
const project = fields.project.trim();
|
|
1799
|
+
const targetId = fields.targetId.trim();
|
|
1800
|
+
const agentName = fields.agentName.trim();
|
|
1801
|
+
if (!project) throw new AgentClaimError("Agent claim requires project");
|
|
1802
|
+
if (!targetId) throw new AgentClaimError("Agent claim requires target");
|
|
1803
|
+
if (!agentName) throw new AgentClaimError("Agent claim requires agent name");
|
|
1804
|
+
const scopeType = normalizeScope(fields.scopeType);
|
|
1805
|
+
const ttlSeconds = fields.ttlSeconds || defaultTtlSeconds;
|
|
1806
|
+
const database = lineageDb();
|
|
1807
|
+
try {
|
|
1808
|
+
ensureProject3(database, project);
|
|
1809
|
+
expireActiveClaims(database);
|
|
1810
|
+
const candidate = { project, channel: fields.channel?.trim() || void 0, scope_type: scopeType, target_id: targetId };
|
|
1811
|
+
const conflicts = activeClaims(database, project).filter((claim2) => claimOverlaps(candidate, claim2));
|
|
1812
|
+
if (conflicts.length > 0 && !fields.force) {
|
|
1813
|
+
throw new AgentClaimError("Target already has an active overlapping agent claim.", 409, "target_already_claimed", conflicts);
|
|
1814
|
+
}
|
|
1815
|
+
if (conflicts.length > 0 && !fields.reason?.trim()) {
|
|
1816
|
+
throw new AgentClaimError("Overriding an active claim requires --reason.", 400, "override_reason_required", conflicts);
|
|
1817
|
+
}
|
|
1818
|
+
const timestamp = nowIso();
|
|
1819
|
+
for (const conflict of conflicts) {
|
|
1820
|
+
database.prepare(`
|
|
1821
|
+
update agent_claims
|
|
1822
|
+
set status = 'revoked', revoked_at = ?, revoked_by = ?, override_reason = ?
|
|
1823
|
+
where id = ? and status = 'active'
|
|
1824
|
+
`).run(timestamp, agentName, fields.reason || null, conflict.id);
|
|
1825
|
+
recordEvent(database, conflict.id, "revoked", agentName, fields.reason || "Revoked by forced claim takeover.");
|
|
1826
|
+
recordEvent(database, conflict.id, "conflict", agentName, `Overridden by ${agentName}.`, { new_claim_target: targetId });
|
|
1827
|
+
}
|
|
1828
|
+
const id = randomId("claim");
|
|
1829
|
+
const secret = randomBytes(24).toString("base64url");
|
|
1830
|
+
const claimToken = `${id}.${secret}`;
|
|
1831
|
+
const expiresAt = expiresAtFrom(timestamp, ttlSeconds);
|
|
1832
|
+
database.prepare(`
|
|
1833
|
+
insert into agent_claims (
|
|
1834
|
+
id, token_hash, project_id, channel, scope_type, target_id, target_title, agent_id, agent_name,
|
|
1835
|
+
agent_kind, thread_id, status, created_at, heartbeat_at, expires_at, metadata_json
|
|
1836
|
+
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', ?, ?, ?, ?)
|
|
1837
|
+
`).run(
|
|
1838
|
+
id,
|
|
1839
|
+
tokenHash(claimToken),
|
|
1840
|
+
project,
|
|
1841
|
+
candidate.channel || null,
|
|
1842
|
+
scopeType,
|
|
1843
|
+
targetId,
|
|
1844
|
+
fields.targetTitle?.trim() || null,
|
|
1845
|
+
fields.agentId?.trim() || null,
|
|
1846
|
+
agentName,
|
|
1847
|
+
fields.agentKind?.trim() || "codex",
|
|
1848
|
+
fields.threadId?.trim() || null,
|
|
1849
|
+
timestamp,
|
|
1850
|
+
timestamp,
|
|
1851
|
+
expiresAt,
|
|
1852
|
+
metadataJson(fields.metadata)
|
|
1853
|
+
);
|
|
1854
|
+
recordEvent(database, id, "created", agentName, `Claimed ${scopeType} ${targetId}.`, { ttl_seconds: ttlSeconds });
|
|
1855
|
+
const claim = findClaimById(database, id);
|
|
1856
|
+
return { ok: true, claim, claim_token: claimToken, conflicts_revoked: conflicts.map((conflict) => conflict.id) };
|
|
1857
|
+
} finally {
|
|
1722
1858
|
database.close();
|
|
1723
|
-
return { ok: true, dryRun: true, edge };
|
|
1724
1859
|
}
|
|
1725
|
-
database.prepare(`
|
|
1726
|
-
insert into asset_edges (id, project_id, parent_asset_id, child_asset_id, relation_type, created_at)
|
|
1727
|
-
values (?, ?, ?, ?, 'derived_from', ?)
|
|
1728
|
-
on conflict(project_id, parent_asset_id, child_asset_id, relation_type) do nothing
|
|
1729
|
-
`).run(edge.id, project, edge.parent_asset_id, edge.child_asset_id, edge.created_at);
|
|
1730
|
-
database.close();
|
|
1731
|
-
return { ok: true, message: `Linked ${edge.child_asset_id} from ${edge.parent_asset_id}`, edge };
|
|
1732
1860
|
}
|
|
1733
|
-
function
|
|
1734
|
-
const
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
const rows = database.prepare("select id, parent_asset_id, child_asset_id, relation_type, created_at from asset_edges where project_id = ? and parent_asset_id = ? order by created_at").all(project, parent);
|
|
1742
|
-
edges.push(...rows);
|
|
1743
|
-
queue.push(...rows.map((row) => row.child_asset_id));
|
|
1861
|
+
function listAgentClaims(project) {
|
|
1862
|
+
const database = lineageDb();
|
|
1863
|
+
try {
|
|
1864
|
+
expireActiveClaims(database);
|
|
1865
|
+
const rows = project ? database.prepare("select * from agent_claims where project_id = ? order by status, heartbeat_at desc").all(project) : database.prepare("select * from agent_claims order by project_id, status, heartbeat_at desc").all();
|
|
1866
|
+
return { ok: true, claims: rows.map((row) => rowToClaim(row)), fetchedAt: nowIso() };
|
|
1867
|
+
} finally {
|
|
1868
|
+
database.close();
|
|
1744
1869
|
}
|
|
1745
|
-
return edges;
|
|
1746
1870
|
}
|
|
1747
|
-
function
|
|
1871
|
+
function inspectAgentClaim(claimId, project) {
|
|
1748
1872
|
const database = lineageDb();
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
from assets a left join asset_reviews r on r.asset_id = a.id
|
|
1759
|
-
left join asset_layouts l on l.project_id = a.project_id and l.root_asset_id = ? and l.asset_id = a.id
|
|
1760
|
-
where a.project_id = ? and a.id in (${placeholders2})
|
|
1761
|
-
`).all(root, project, ...ids);
|
|
1762
|
-
const selected = selectedRows(database, project, root);
|
|
1763
|
-
const childIds = new Set(edges.map((edge) => edge.parent_asset_id));
|
|
1764
|
-
const selectedIds = new Set(selected.map((row) => row.asset_id));
|
|
1765
|
-
const selections = selected.map((row) => ({
|
|
1766
|
-
asset_id: row.asset_id,
|
|
1767
|
-
notes: row.notes || void 0,
|
|
1768
|
-
position: Number(row.position || 0),
|
|
1769
|
-
selected_at: row.selected_at
|
|
1770
|
-
}));
|
|
1771
|
-
const selection = selections[0] || null;
|
|
1772
|
-
const nodes = rows.map((row) => {
|
|
1773
|
-
const position = typeof row.layout_x === "number" && typeof row.layout_y === "number" ? { x: row.layout_x, y: row.layout_y } : void 0;
|
|
1774
|
-
const { layout_x: _layoutX, layout_y: _layoutY, ...node } = row;
|
|
1775
|
-
const nodeSelection = selections.find((item) => item.asset_id === row.asset_id);
|
|
1776
|
-
return {
|
|
1777
|
-
...node,
|
|
1778
|
-
is_latest: !childIds.has(row.asset_id),
|
|
1779
|
-
position,
|
|
1780
|
-
preview_url: canPreviewLocally(row.media_type, row.local_path) ? localPreviewUrl(project, row.local_path) : void 0,
|
|
1781
|
-
selection_note: nodeSelection?.notes,
|
|
1782
|
-
user_selected: selectedIds.has(row.asset_id)
|
|
1783
|
-
};
|
|
1784
|
-
});
|
|
1785
|
-
database.close();
|
|
1786
|
-
return {
|
|
1787
|
-
project,
|
|
1788
|
-
root_asset_id: root,
|
|
1789
|
-
active_asset_id: assetId,
|
|
1790
|
-
selected: selections.map((row) => row.asset_id),
|
|
1791
|
-
selection,
|
|
1792
|
-
selections,
|
|
1793
|
-
latest: nodes.filter((node) => node.is_latest).map((node) => node.asset_id),
|
|
1794
|
-
nodes,
|
|
1795
|
-
edges,
|
|
1796
|
-
fetchedAt: nowIso()
|
|
1797
|
-
};
|
|
1873
|
+
try {
|
|
1874
|
+
expireActiveClaims(database);
|
|
1875
|
+
const claim = findClaimById(database, claimId, project);
|
|
1876
|
+
if (!claim) throw new AgentClaimError(`Unknown agent claim: ${claimId}`, 404, "claim_not_found");
|
|
1877
|
+
const events = database.prepare("select * from agent_claim_events where claim_id = ? order by created_at").all(claim.id);
|
|
1878
|
+
return { ok: true, claim, events: events.map(eventToRow) };
|
|
1879
|
+
} finally {
|
|
1880
|
+
database.close();
|
|
1881
|
+
}
|
|
1798
1882
|
}
|
|
1799
|
-
function
|
|
1800
|
-
if (fields.positions.length === 0) throw new LineageError("Lineage layout requires at least one position");
|
|
1883
|
+
function heartbeatAgentClaim(claimToken, ttlSeconds = defaultTtlSeconds) {
|
|
1801
1884
|
const database = lineageDb();
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1885
|
+
try {
|
|
1886
|
+
expireActiveClaims(database);
|
|
1887
|
+
const row = findClaimRowByToken(database, claimToken);
|
|
1888
|
+
if (!row) throw new AgentClaimError("Unknown or invalid agent claim token.", 401, "claim_token_invalid");
|
|
1889
|
+
const claim = rowToClaim(row);
|
|
1890
|
+
if (claim.status !== "active") throw new AgentClaimError(`Agent claim is ${claim.status}.`, 409, "claim_not_active");
|
|
1891
|
+
const timestamp = nowIso();
|
|
1892
|
+
database.prepare("update agent_claims set heartbeat_at = ?, expires_at = ? where id = ?").run(timestamp, expiresAtFrom(timestamp, ttlSeconds), claim.id);
|
|
1893
|
+
recordEvent(database, claim.id, "heartbeat", claim.agent_name, "Claim heartbeat received.");
|
|
1894
|
+
return { ok: true, claim: findClaimById(database, claim.id) };
|
|
1895
|
+
} finally {
|
|
1805
1896
|
database.close();
|
|
1806
|
-
return { ok: true, dryRun: true, root_asset_id: fields.rootAssetId, positions: fields.positions };
|
|
1807
|
-
}
|
|
1808
|
-
const timestamp = nowIso();
|
|
1809
|
-
const statement = database.prepare(`
|
|
1810
|
-
insert into asset_layouts (id, project_id, root_asset_id, asset_id, x, y, updated_at)
|
|
1811
|
-
values (?, ?, ?, ?, ?, ?, ?)
|
|
1812
|
-
on conflict(project_id, root_asset_id, asset_id) do update set
|
|
1813
|
-
x = excluded.x, y = excluded.y, updated_at = excluded.updated_at
|
|
1814
|
-
`);
|
|
1815
|
-
for (const position of fields.positions) {
|
|
1816
|
-
statement.run(`${project}:${fields.rootAssetId}:layout:${position.assetId}`, project, fields.rootAssetId, position.assetId, position.x, position.y, timestamp);
|
|
1817
1897
|
}
|
|
1818
|
-
database.close();
|
|
1819
|
-
return { ok: true, message: `Saved ${fields.positions.length} lineage positions`, root_asset_id: fields.rootAssetId, positions: fields.positions };
|
|
1820
1898
|
}
|
|
1821
|
-
function
|
|
1899
|
+
function releaseAgentClaim(claimToken) {
|
|
1822
1900
|
const database = lineageDb();
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1901
|
+
try {
|
|
1902
|
+
expireActiveClaims(database);
|
|
1903
|
+
const row = findClaimRowByToken(database, claimToken);
|
|
1904
|
+
if (!row) throw new AgentClaimError("Unknown or invalid agent claim token.", 401, "claim_token_invalid");
|
|
1905
|
+
const claim = rowToClaim(row);
|
|
1906
|
+
if (claim.status !== "active") throw new AgentClaimError(`Agent claim is ${claim.status}.`, 409, "claim_not_active");
|
|
1907
|
+
const timestamp = nowIso();
|
|
1908
|
+
database.prepare("update agent_claims set status = 'released', released_at = ? where id = ?").run(timestamp, claim.id);
|
|
1909
|
+
recordEvent(database, claim.id, "released", claim.agent_name, "Claim released by token holder.");
|
|
1910
|
+
return { ok: true, claim: findClaimById(database, claim.id) };
|
|
1911
|
+
} finally {
|
|
1912
|
+
database.close();
|
|
1832
1913
|
}
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1914
|
+
}
|
|
1915
|
+
function releaseStaleAgentClaim(project, claimId, fields) {
|
|
1916
|
+
if (!fields.confirmWrite) throw new AgentClaimError("Releasing a stale agent claim requires confirmWrite=true.", 400, "confirm_write_required");
|
|
1917
|
+
if (!fields.reason?.trim()) throw new AgentClaimError("Releasing a stale agent claim requires a reason.", 400, "reason_required");
|
|
1918
|
+
const database = lineageDb();
|
|
1919
|
+
try {
|
|
1920
|
+
expireActiveClaims(database);
|
|
1921
|
+
const claim = findClaimById(database, claimId, project);
|
|
1922
|
+
if (!claim) throw new AgentClaimError(`Unknown agent claim: ${claimId}`, 404, "claim_not_found");
|
|
1923
|
+
if (claim.status !== "active" || claim.derived_state !== "stale") {
|
|
1924
|
+
throw new AgentClaimError("Only stale active claims can be released without the claim token.", 409, "claim_not_stale", [claim]);
|
|
1925
|
+
}
|
|
1926
|
+
const timestamp = nowIso();
|
|
1927
|
+
database.prepare("update agent_claims set status = 'released', released_at = ?, revoked_by = ?, override_reason = ? where id = ?").run(timestamp, fields.actor || "human", fields.reason, claim.id);
|
|
1928
|
+
recordEvent(database, claim.id, "released", fields.actor || "human", fields.reason);
|
|
1929
|
+
return { ok: true, claim: findClaimById(database, claim.id) };
|
|
1930
|
+
} finally {
|
|
1931
|
+
database.close();
|
|
1851
1932
|
}
|
|
1852
|
-
if (latestNodes.length === 1) {
|
|
1853
|
-
return {
|
|
1854
|
-
project,
|
|
1855
|
-
root_asset_id: snapshot.root_asset_id,
|
|
1856
|
-
strategy: "single_latest",
|
|
1857
|
-
selection_mode: "fallback",
|
|
1858
|
-
recommended_action: "evolve_variations",
|
|
1859
|
-
reason: "single_latest_fallback",
|
|
1860
|
-
next_asset: latestNodes[0],
|
|
1861
|
-
next_assets: [latestNodes[0]],
|
|
1862
|
-
latest: snapshot.latest,
|
|
1863
|
-
selected: snapshot.selected,
|
|
1864
|
-
selection: snapshot.selection,
|
|
1865
|
-
selections: snapshot.selections,
|
|
1866
|
-
candidates: latestNodes,
|
|
1867
|
-
warnings,
|
|
1868
|
-
fetchedAt: nowIso()
|
|
1869
|
-
};
|
|
1870
|
-
}
|
|
1871
|
-
return {
|
|
1872
|
-
project,
|
|
1873
|
-
root_asset_id: snapshot.root_asset_id,
|
|
1874
|
-
strategy: latestNodes.length > 1 ? "ambiguous_latest" : "empty",
|
|
1875
|
-
selection_mode: "none",
|
|
1876
|
-
recommended_action: latestNodes.length > 1 ? "choose_next_base" : "none",
|
|
1877
|
-
reason: latestNodes.length > 1 ? "multiple_latest_no_selection" : "no_lineage_candidates",
|
|
1878
|
-
next_asset: null,
|
|
1879
|
-
next_assets: [],
|
|
1880
|
-
latest: snapshot.latest,
|
|
1881
|
-
selected: snapshot.selected,
|
|
1882
|
-
selection: snapshot.selection,
|
|
1883
|
-
selections: snapshot.selections,
|
|
1884
|
-
candidates: latestNodes,
|
|
1885
|
-
warnings,
|
|
1886
|
-
fetchedAt: nowIso()
|
|
1887
|
-
};
|
|
1888
1933
|
}
|
|
1889
|
-
function
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
const
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1934
|
+
function revokeAgentClaim(project, claimId, fields) {
|
|
1935
|
+
if (!fields.confirmWrite) throw new AgentClaimError("Revoking an agent claim requires confirmWrite=true.", 400, "confirm_write_required");
|
|
1936
|
+
if (!fields.reason?.trim()) throw new AgentClaimError("Revoking an agent claim requires a reason.", 400, "reason_required");
|
|
1937
|
+
const database = lineageDb();
|
|
1938
|
+
try {
|
|
1939
|
+
expireActiveClaims(database);
|
|
1940
|
+
const claim = findClaimById(database, claimId, project);
|
|
1941
|
+
if (!claim) throw new AgentClaimError(`Unknown agent claim: ${claimId}`, 404, "claim_not_found");
|
|
1942
|
+
const timestamp = nowIso();
|
|
1943
|
+
database.prepare(`
|
|
1944
|
+
update agent_claims set status = 'revoked', revoked_at = ?, revoked_by = ?, override_reason = ?
|
|
1945
|
+
where id = ?
|
|
1946
|
+
`).run(timestamp, fields.actor || "human", fields.reason, claim.id);
|
|
1947
|
+
recordEvent(database, claim.id, "revoked", fields.actor || "human", fields.reason);
|
|
1948
|
+
return { ok: true, claim: findClaimById(database, claim.id) };
|
|
1949
|
+
} finally {
|
|
1950
|
+
database.close();
|
|
1951
|
+
}
|
|
1900
1952
|
}
|
|
1901
|
-
function
|
|
1953
|
+
function transferAgentClaim(project, claimId, fields) {
|
|
1954
|
+
if (!fields.confirmWrite) throw new AgentClaimError("Transferring an agent claim requires confirmWrite=true.", 400, "confirm_write_required");
|
|
1955
|
+
const toAgentName = fields.toAgentName.trim();
|
|
1956
|
+
if (!toAgentName) throw new AgentClaimError("Transfer requires toAgentName.", 400, "agent_name_required");
|
|
1902
1957
|
const database = lineageDb();
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1958
|
+
try {
|
|
1959
|
+
expireActiveClaims(database);
|
|
1960
|
+
const claim = findClaimById(database, claimId, project);
|
|
1961
|
+
if (!claim) throw new AgentClaimError(`Unknown agent claim: ${claimId}`, 404, "claim_not_found");
|
|
1962
|
+
if (claim.status !== "active") throw new AgentClaimError(`Agent claim is ${claim.status}.`, 409, "claim_not_active");
|
|
1963
|
+
database.prepare("update agent_claims set agent_name = ? where id = ?").run(toAgentName, claim.id);
|
|
1964
|
+
recordEvent(database, claim.id, "transferred", fields.actor || "human", fields.reason || `Transferred claim to ${toAgentName}.`, { to_agent_name: toAgentName });
|
|
1965
|
+
return { ok: true, claim: findClaimById(database, claim.id) };
|
|
1966
|
+
} finally {
|
|
1911
1967
|
database.close();
|
|
1912
|
-
return { ok: true, dryRun: true, root_asset_id: root, asset_ids: inputAssetIds, mode, clear: Boolean(fields.clear), max_selections: limit };
|
|
1913
1968
|
}
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
if (fields.
|
|
1917
|
-
|
|
1918
|
-
}
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1969
|
+
}
|
|
1970
|
+
function validateAgentClaimForWrite(fields) {
|
|
1971
|
+
if (fields.dangerLevel === "danger" && !fields.confirmWrite) {
|
|
1972
|
+
return denied("human_confirmation_required", "Dangerous write requires explicit human confirmation.");
|
|
1973
|
+
}
|
|
1974
|
+
if (!fields.claimToken) return denied("claim_required", "Mutating agent write requires a matching claim token.");
|
|
1975
|
+
const database = lineageDb();
|
|
1976
|
+
try {
|
|
1977
|
+
expireActiveClaims(database);
|
|
1978
|
+
const row = findClaimRowByToken(database, fields.claimToken);
|
|
1979
|
+
if (!row) return denied("claim_token_invalid", "Unknown or invalid claim token.");
|
|
1980
|
+
const claim = rowToClaim(row);
|
|
1981
|
+
if (claim.status !== "active") return denied("claim_not_active", `Agent claim is ${claim.status}.`);
|
|
1982
|
+
if (new Date(claim.expires_at).getTime() <= Date.now()) return denied("claim_expired", "Agent claim has expired.");
|
|
1983
|
+
if (claim.project !== fields.project) return denied("claim_project_mismatch", `Claim project ${claim.project} does not match ${fields.project}.`, [claim]);
|
|
1984
|
+
if (fields.channel && claim.channel && claim.channel !== fields.channel) {
|
|
1985
|
+
return denied("claim_channel_mismatch", `Claim channel ${claim.channel} does not match ${fields.channel}.`, [claim]);
|
|
1986
|
+
}
|
|
1987
|
+
if (!scopeAllowsWrite(claim, fields.scopeType, fields.targetId, fields.writeKind)) {
|
|
1988
|
+
return denied("claim_scope_mismatch", `Claim does not cover ${fields.scopeType} ${fields.targetId}.`, [claim]);
|
|
1927
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: [] };
|
|
1996
|
+
} finally {
|
|
1997
|
+
database.close();
|
|
1928
1998
|
}
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
const notes = inputAssetIds.includes(assetId) ? fields.notes || existing?.notes : existing?.notes;
|
|
1941
|
-
insert.run(selectionId(project, root, assetId), project, root, assetId, position, notes || null, timestamp);
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
// src/server/lineageClaimGuards.ts
|
|
2002
|
+
function channelOverlaps2(left, right) {
|
|
2003
|
+
return !left || !right || left === right;
|
|
2004
|
+
}
|
|
2005
|
+
function hasActiveLineageWorkspaceClaim(project, targetId, channel) {
|
|
2006
|
+
return listAgentClaims(project).claims.some((claim) => {
|
|
2007
|
+
if (claim.project !== project || claim.status !== "active" || claim.derived_state === "expired") return false;
|
|
2008
|
+
if (claim.scope_type === "lineage_workspace") return claim.target_id === targetId;
|
|
2009
|
+
return claim.scope_type === "project_channel" && channelOverlaps2(channel, claim.channel);
|
|
1942
2010
|
});
|
|
1943
|
-
database.close();
|
|
1944
|
-
const message = nextIds.length === 0 ? `Cleared selected assets for ${root}` : `Selected ${nextIds.length} asset${nextIds.length === 1 ? "" : "s"} for ${root}`;
|
|
1945
|
-
return { ok: true, message, root_asset_id: root, asset_id: nextIds[0] || null, asset_ids: nextIds, mode };
|
|
1946
2011
|
}
|
|
1947
|
-
function
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
2012
|
+
function requireLineageWorkspaceClaimForWrite(fields) {
|
|
2013
|
+
if (!fields.confirmWrite) return;
|
|
2014
|
+
const targetId = lineageWorkspaceId(fields.project, fields.rootAssetId);
|
|
2015
|
+
if (!fields.claimToken && !hasActiveLineageWorkspaceClaim(fields.project, targetId, fields.channel)) return;
|
|
2016
|
+
const validation = validateAgentClaimForWrite({
|
|
2017
|
+
channel: fields.channel,
|
|
2018
|
+
claimToken: fields.claimToken,
|
|
2019
|
+
confirmWrite: fields.confirmWrite,
|
|
2020
|
+
dangerLevel: "enforce",
|
|
2021
|
+
project: fields.project,
|
|
2022
|
+
scopeType: "lineage_workspace",
|
|
2023
|
+
targetId,
|
|
2024
|
+
writeKind: fields.writeKind
|
|
2025
|
+
});
|
|
2026
|
+
if (!validation.ok) {
|
|
2027
|
+
const status = validation.code === "claim_required" || validation.code === "claim_token_invalid" ? 401 : 409;
|
|
2028
|
+
throw new AgentClaimError(validation.message, status, validation.code, validation.conflicts);
|
|
1955
2029
|
}
|
|
1956
|
-
const timestamp = nowIso();
|
|
1957
|
-
database.prepare(`
|
|
1958
|
-
insert into asset_reviews (asset_id, review_state, reviewed_at, ignored_at, notes, updated_at)
|
|
1959
|
-
values (?, ?, ?, ?, ?, ?)
|
|
1960
|
-
on conflict(asset_id) do update set
|
|
1961
|
-
review_state = excluded.review_state, reviewed_at = excluded.reviewed_at,
|
|
1962
|
-
ignored_at = excluded.ignored_at, notes = excluded.notes, updated_at = excluded.updated_at
|
|
1963
|
-
`).run(fields.assetId, fields.reviewState, timestamp, fields.reviewState === "ignored" ? timestamp : null, fields.notes || null, timestamp);
|
|
1964
|
-
database.close();
|
|
1965
|
-
return { ok: true, message: `Marked ${fields.assetId} ${fields.reviewState}`, asset_id: fields.assetId, review_state: fields.reviewState };
|
|
1966
2030
|
}
|
|
1967
2031
|
|
|
1968
|
-
// src/server/
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
var idleAfterSeconds = 5 * 60;
|
|
1972
|
-
var staleAfterSeconds = 15 * 60;
|
|
1973
|
-
var scopes = /* @__PURE__ */ new Set(["lineage_workspace", "content_post", "content_queue_lane", "selection_set", "project_channel"]);
|
|
1974
|
-
var AgentClaimError = class extends Error {
|
|
1975
|
-
constructor(message, status = 400, code = "agent_claim_error", conflicts = []) {
|
|
2032
|
+
// src/server/assetLineage.ts
|
|
2033
|
+
var LineageError = class extends Error {
|
|
2034
|
+
constructor(message, status = 400) {
|
|
1976
2035
|
super(message);
|
|
1977
2036
|
this.status = status;
|
|
1978
|
-
this.code = code;
|
|
1979
|
-
this.conflicts = conflicts;
|
|
1980
2037
|
}
|
|
1981
2038
|
status;
|
|
1982
|
-
code;
|
|
1983
|
-
conflicts;
|
|
1984
2039
|
};
|
|
1985
|
-
function
|
|
1986
|
-
return error instanceof
|
|
2040
|
+
function isLineageError(error) {
|
|
2041
|
+
return error instanceof LineageError;
|
|
1987
2042
|
}
|
|
1988
|
-
function
|
|
1989
|
-
|
|
1990
|
-
const
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
const unit = match[2] || "s";
|
|
1994
|
-
const multiplier = unit === "h" ? 3600 : unit === "m" ? 60 : 1;
|
|
1995
|
-
const seconds = amount * multiplier;
|
|
1996
|
-
if (!Number.isInteger(seconds) || seconds < 30 || seconds > 24 * 60 * 60) {
|
|
1997
|
-
throw new AgentClaimError("Claim ttl must be between 30 seconds and 24 hours");
|
|
2043
|
+
function collectAssets(project, source) {
|
|
2044
|
+
const first = listAssets(project, { source, page: 1, pageSize: 100 });
|
|
2045
|
+
const assets = [...first.assets];
|
|
2046
|
+
for (let page = 2; page <= first.pagination.totalPages; page += 1) {
|
|
2047
|
+
assets.push(...listAssets(project, { source, page, pageSize: 100 }).assets);
|
|
1998
2048
|
}
|
|
1999
|
-
return
|
|
2049
|
+
return assets;
|
|
2000
2050
|
}
|
|
2001
|
-
function
|
|
2002
|
-
|
|
2051
|
+
function upsertProject(database, project) {
|
|
2052
|
+
const timestamp = nowIso();
|
|
2053
|
+
database.prepare(`
|
|
2054
|
+
insert into projects (id, product, catalog_path, created_at, updated_at)
|
|
2055
|
+
values (?, ?, ?, ?, ?)
|
|
2056
|
+
on conflict(id) do update set product = excluded.product, updated_at = excluded.updated_at
|
|
2057
|
+
`).run(project, project, join5(repoRoot, project, "assets", "catalog.json"), timestamp, timestamp);
|
|
2003
2058
|
}
|
|
2004
|
-
function
|
|
2005
|
-
|
|
2059
|
+
function upsertAsset(database, project, asset) {
|
|
2060
|
+
const timestamp = nowIso();
|
|
2061
|
+
const source = asset.source === "local" ? "local" : "catalog";
|
|
2062
|
+
database.prepare(`
|
|
2063
|
+
insert into assets (
|
|
2064
|
+
id, project_id, source, local_path, s3_key, checksum_sha256, media_type, title, status,
|
|
2065
|
+
channel, campaign, audience, size_bytes, content_type, created_at, updated_at, last_seen_at
|
|
2066
|
+
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2067
|
+
on conflict(id) do update set
|
|
2068
|
+
source = excluded.source, local_path = excluded.local_path, s3_key = excluded.s3_key,
|
|
2069
|
+
checksum_sha256 = excluded.checksum_sha256, media_type = excluded.media_type,
|
|
2070
|
+
title = excluded.title, status = excluded.status, channel = excluded.channel,
|
|
2071
|
+
campaign = excluded.campaign, audience = excluded.audience, size_bytes = excluded.size_bytes,
|
|
2072
|
+
content_type = excluded.content_type, updated_at = excluded.updated_at, last_seen_at = excluded.last_seen_at
|
|
2073
|
+
`).run(
|
|
2074
|
+
asset.asset_id,
|
|
2075
|
+
project,
|
|
2076
|
+
source,
|
|
2077
|
+
asset.local?.relative_path || null,
|
|
2078
|
+
asset.s3?.key || null,
|
|
2079
|
+
asset.local?.checksum_sha256 || asset.s3?.checksum_sha256 || null,
|
|
2080
|
+
asset.content_type,
|
|
2081
|
+
asset.title,
|
|
2082
|
+
asset.status,
|
|
2083
|
+
asset.channel || null,
|
|
2084
|
+
asset.campaign || null,
|
|
2085
|
+
asset.audience || null,
|
|
2086
|
+
asset.local?.size_bytes || asset.s3?.size_bytes || null,
|
|
2087
|
+
asset.local?.content_type || asset.s3?.content_type || null,
|
|
2088
|
+
timestamp,
|
|
2089
|
+
timestamp,
|
|
2090
|
+
timestamp
|
|
2091
|
+
);
|
|
2092
|
+
database.prepare(`
|
|
2093
|
+
insert into asset_reviews (asset_id, review_state, updated_at)
|
|
2094
|
+
values (?, 'unreviewed', ?)
|
|
2095
|
+
on conflict(asset_id) do nothing
|
|
2096
|
+
`).run(asset.asset_id, timestamp);
|
|
2006
2097
|
}
|
|
2007
|
-
function
|
|
2008
|
-
const
|
|
2009
|
-
const
|
|
2010
|
-
|
|
2098
|
+
function indexLineageAssets(project = defaultProject) {
|
|
2099
|
+
const database = lineageDb();
|
|
2100
|
+
const catalog = collectAssets(project, "catalog");
|
|
2101
|
+
const local = collectAssets(project, "local");
|
|
2102
|
+
upsertProject(database, project);
|
|
2103
|
+
for (const asset of [...catalog, ...local]) upsertAsset(database, project, asset);
|
|
2104
|
+
database.close();
|
|
2105
|
+
return { catalog: catalog.length, local: local.length, total: catalog.length + local.length, database: lineageDbPath() };
|
|
2011
2106
|
}
|
|
2012
|
-
function
|
|
2013
|
-
|
|
2107
|
+
function requireAsset2(database, project, assetId) {
|
|
2108
|
+
const row = database.prepare("select id from assets where project_id = ? and id = ?").get(project, assetId);
|
|
2109
|
+
if (!row) throw new LineageError(`Unknown indexed asset: ${assetId}`, 404);
|
|
2014
2110
|
}
|
|
2015
|
-
function
|
|
2016
|
-
|
|
2111
|
+
function parentOf(database, project, assetId) {
|
|
2112
|
+
const row = database.prepare("select parent_asset_id from asset_edges where project_id = ? and child_asset_id = ? order by created_at limit 1").get(project, assetId);
|
|
2113
|
+
return row?.parent_asset_id;
|
|
2017
2114
|
}
|
|
2018
|
-
function
|
|
2019
|
-
|
|
2115
|
+
function rootFor(database, project, assetId) {
|
|
2116
|
+
let root = assetId;
|
|
2117
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2118
|
+
while (!seen.has(root)) {
|
|
2119
|
+
seen.add(root);
|
|
2120
|
+
const parent = parentOf(database, project, root);
|
|
2121
|
+
if (!parent) return root;
|
|
2122
|
+
root = parent;
|
|
2123
|
+
}
|
|
2124
|
+
return assetId;
|
|
2125
|
+
}
|
|
2126
|
+
function assertCanonicalRoot(database, project, root) {
|
|
2127
|
+
requireAsset2(database, project, root);
|
|
2128
|
+
const canonicalRoot = rootFor(database, project, root);
|
|
2129
|
+
if (canonicalRoot !== root) throw new LineageError(`Asset ${root} is not a lineage root`, 400);
|
|
2130
|
+
}
|
|
2131
|
+
function explicitWorkspaceRoot(database, project, assetId) {
|
|
2132
|
+
const row = database.prepare(`
|
|
2133
|
+
select root_asset_id from lineage_workspaces
|
|
2134
|
+
where project_id = ? and root_asset_id = ? and status != 'archived'
|
|
2135
|
+
`).get(project, assetId);
|
|
2136
|
+
return row?.root_asset_id;
|
|
2137
|
+
}
|
|
2138
|
+
function nearestWorkspaceRoot(database, project, assetId) {
|
|
2139
|
+
let current = assetId;
|
|
2140
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2141
|
+
while (!seen.has(current)) {
|
|
2142
|
+
seen.add(current);
|
|
2143
|
+
const explicit = explicitWorkspaceRoot(database, project, current);
|
|
2144
|
+
if (explicit) return explicit;
|
|
2145
|
+
const parent = parentOf(database, project, current);
|
|
2146
|
+
if (!parent) return current;
|
|
2147
|
+
current = parent;
|
|
2148
|
+
}
|
|
2149
|
+
return assetId;
|
|
2150
|
+
}
|
|
2151
|
+
function assetChannel(database, project, assetId) {
|
|
2152
|
+
const row = database.prepare("select channel from assets where project_id = ? and id = ?").get(project, assetId);
|
|
2153
|
+
return row?.channel;
|
|
2154
|
+
}
|
|
2155
|
+
function lineageWriteClaimContext(database, project, assetId) {
|
|
2156
|
+
return {
|
|
2157
|
+
channel: assetChannel(database, project, assetId),
|
|
2158
|
+
rootAssetId: nearestWorkspaceRoot(database, project, assetId)
|
|
2159
|
+
};
|
|
2160
|
+
}
|
|
2161
|
+
function getLineageWriteClaimContext(project, assetId) {
|
|
2162
|
+
const database = lineageDb();
|
|
2020
2163
|
try {
|
|
2021
|
-
|
|
2022
|
-
return
|
|
2023
|
-
}
|
|
2024
|
-
|
|
2164
|
+
requireAsset2(database, project, assetId);
|
|
2165
|
+
return lineageWriteClaimContext(database, project, assetId);
|
|
2166
|
+
} finally {
|
|
2167
|
+
database.close();
|
|
2025
2168
|
}
|
|
2026
2169
|
}
|
|
2027
|
-
function
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
const ageSeconds = Math.max(0, Math.floor((now.getTime() - new Date(row.heartbeat_at).getTime()) / 1e3));
|
|
2031
|
-
if (ageSeconds >= staleAfterSeconds) return "stale";
|
|
2032
|
-
if (ageSeconds >= idleAfterSeconds) return "idle";
|
|
2033
|
-
return "active";
|
|
2170
|
+
function latestSelectedRoot(database, project) {
|
|
2171
|
+
const row = database.prepare("select root_asset_id from asset_selections where project_id = ? order by selected_at desc limit 1").get(project);
|
|
2172
|
+
return row?.root_asset_id;
|
|
2034
2173
|
}
|
|
2035
|
-
function
|
|
2036
|
-
|
|
2174
|
+
function resolveRoot(database, project, rootAssetId) {
|
|
2175
|
+
if (rootAssetId) {
|
|
2176
|
+
requireAsset2(database, project, rootAssetId);
|
|
2177
|
+
return rootAssetId;
|
|
2178
|
+
}
|
|
2179
|
+
const root = activeLineageWorkspaceRoot(project) || latestSelectedRoot(database, project);
|
|
2180
|
+
if (!root) throw new LineageError("Lineage command requires --root unless a project selection exists");
|
|
2181
|
+
requireAsset2(database, project, root);
|
|
2182
|
+
return root;
|
|
2183
|
+
}
|
|
2184
|
+
function edgeId(project, parent, child) {
|
|
2185
|
+
return `${project}:${parent}:derived_from:${child}`;
|
|
2186
|
+
}
|
|
2187
|
+
function rerollRequestId(project, root, node, timestamp) {
|
|
2188
|
+
return `${project}:${root}:reroll:${node}:${Date.parse(timestamp) || Date.now()}`;
|
|
2189
|
+
}
|
|
2190
|
+
function rowString(value) {
|
|
2191
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
2192
|
+
}
|
|
2193
|
+
function rerollRequestFrom(row) {
|
|
2037
2194
|
return {
|
|
2038
2195
|
id: String(row.id),
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
agent_name: String(row.agent_name),
|
|
2046
|
-
agent_kind: String(row.agent_kind),
|
|
2047
|
-
thread_id: typeof row.thread_id === "string" ? row.thread_id : void 0,
|
|
2048
|
-
status: String(row.status),
|
|
2196
|
+
project_id: String(row.project_id),
|
|
2197
|
+
root_asset_id: String(row.root_asset_id),
|
|
2198
|
+
node_asset_id: String(row.node_asset_id),
|
|
2199
|
+
status: row.status,
|
|
2200
|
+
requested_by: row.requested_by,
|
|
2201
|
+
notes: rowString(row.notes),
|
|
2049
2202
|
created_at: String(row.created_at),
|
|
2050
|
-
|
|
2051
|
-
expires_at: String(row.expires_at),
|
|
2052
|
-
released_at: typeof row.released_at === "string" ? row.released_at : void 0,
|
|
2053
|
-
revoked_at: typeof row.revoked_at === "string" ? row.revoked_at : void 0,
|
|
2054
|
-
revoked_by: typeof row.revoked_by === "string" ? row.revoked_by : void 0,
|
|
2055
|
-
override_reason: typeof row.override_reason === "string" ? row.override_reason : void 0,
|
|
2056
|
-
metadata: parseMetadata(row.metadata_json),
|
|
2057
|
-
heartbeat_age_seconds: Math.max(0, Math.floor((now.getTime() - new Date(heartbeatAt).getTime()) / 1e3)),
|
|
2058
|
-
derived_state: derivedState({
|
|
2059
|
-
expires_at: String(row.expires_at),
|
|
2060
|
-
heartbeat_at: heartbeatAt,
|
|
2061
|
-
status: String(row.status)
|
|
2062
|
-
}, now)
|
|
2203
|
+
resolved_at: rowString(row.resolved_at)
|
|
2063
2204
|
};
|
|
2064
2205
|
}
|
|
2065
|
-
function
|
|
2206
|
+
function attemptFrom(row) {
|
|
2066
2207
|
return {
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2208
|
+
id: String(row.id),
|
|
2209
|
+
project_id: String(row.project_id),
|
|
2210
|
+
node_asset_id: String(row.node_asset_id),
|
|
2211
|
+
asset_id: String(row.asset_id),
|
|
2212
|
+
attempt_index: Number(row.attempt_index),
|
|
2213
|
+
source: row.source,
|
|
2214
|
+
prompt: rowString(row.prompt),
|
|
2215
|
+
generation_job_id: rowString(row.generation_job_id),
|
|
2216
|
+
file_path: rowString(row.file_path),
|
|
2217
|
+
checksum_sha256: rowString(row.checksum_sha256),
|
|
2071
2218
|
created_at: String(row.created_at),
|
|
2072
|
-
|
|
2219
|
+
promoted_at: rowString(row.promoted_at),
|
|
2220
|
+
is_current: Boolean(Number(row.is_current))
|
|
2073
2221
|
};
|
|
2074
2222
|
}
|
|
2075
|
-
function
|
|
2076
|
-
const
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
const timestamp = nowIso();
|
|
2091
|
-
const expired = database.prepare(`
|
|
2092
|
-
select id from agent_claims where status = 'active' and expires_at <= ?
|
|
2093
|
-
`).all(timestamp);
|
|
2094
|
-
if (expired.length === 0) return;
|
|
2095
|
-
database.prepare(`
|
|
2096
|
-
update agent_claims set status = 'expired' where status = 'active' and expires_at <= ?
|
|
2097
|
-
`).run(timestamp);
|
|
2098
|
-
for (const claim of expired) recordEvent(database, claim.id, "expired", "system", "Claim expired after missed heartbeat.");
|
|
2099
|
-
}
|
|
2100
|
-
function normalizeScope(scopeType) {
|
|
2101
|
-
if (!scopes.has(scopeType)) throw new AgentClaimError(`Unsupported claim scope: ${scopeType}`);
|
|
2102
|
-
return scopeType;
|
|
2223
|
+
function implicitAttempt(row, isCurrent = true) {
|
|
2224
|
+
const createdAt = row.asset_created_at || nowIso();
|
|
2225
|
+
return {
|
|
2226
|
+
id: `${row.project}:${row.asset_id}:attempt:implicit`,
|
|
2227
|
+
project_id: row.project,
|
|
2228
|
+
node_asset_id: row.asset_id,
|
|
2229
|
+
asset_id: row.asset_id,
|
|
2230
|
+
attempt_index: 1,
|
|
2231
|
+
source: "initial",
|
|
2232
|
+
file_path: row.local_path,
|
|
2233
|
+
checksum_sha256: row.checksum_sha256,
|
|
2234
|
+
created_at: createdAt,
|
|
2235
|
+
promoted_at: createdAt,
|
|
2236
|
+
is_current: isCurrent
|
|
2237
|
+
};
|
|
2103
2238
|
}
|
|
2104
|
-
function
|
|
2105
|
-
|
|
2239
|
+
function withImplicitAttempt(physicalAttempts, row) {
|
|
2240
|
+
if (physicalAttempts.some((attempt) => attempt.source === "initial")) return physicalAttempts;
|
|
2241
|
+
return [...physicalAttempts, implicitAttempt(row, !physicalAttempts.some((attempt) => attempt.is_current))];
|
|
2106
2242
|
}
|
|
2107
|
-
function
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2243
|
+
function assertNodeInRoot(database, project, root, node) {
|
|
2244
|
+
assertCanonicalRoot(database, project, root);
|
|
2245
|
+
requireAsset2(database, project, node);
|
|
2246
|
+
const nodeRoot = rootFor(database, project, node);
|
|
2247
|
+
if (nodeRoot !== root) throw new LineageError(`Asset ${node} is not in lineage rooted at ${root}`);
|
|
2112
2248
|
}
|
|
2113
|
-
function
|
|
2114
|
-
|
|
2115
|
-
return rows.map((row) => rowToClaim(row));
|
|
2249
|
+
function canPreviewLocally(mediaType, localPath) {
|
|
2250
|
+
return Boolean(localPath && ["image", "video", "gif"].includes(mediaType));
|
|
2116
2251
|
}
|
|
2117
|
-
function
|
|
2118
|
-
|
|
2119
|
-
|
|
2252
|
+
function localPreviewUrl(project, localPath) {
|
|
2253
|
+
if (!localPath) return void 0;
|
|
2254
|
+
const params = new URLSearchParams({ project, path: localPath });
|
|
2255
|
+
return `/api/assets/local-preview?${params.toString()}`;
|
|
2120
2256
|
}
|
|
2121
|
-
function
|
|
2122
|
-
const
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2257
|
+
function linkLineageAssets(project, fields) {
|
|
2258
|
+
const database = lineageDb();
|
|
2259
|
+
requireAsset2(database, project, fields.parentAssetId);
|
|
2260
|
+
requireAsset2(database, project, fields.childAssetId);
|
|
2261
|
+
if (fields.parentAssetId === fields.childAssetId) throw new LineageError("Lineage link cannot point to itself");
|
|
2262
|
+
const claimContext = lineageWriteClaimContext(database, project, fields.parentAssetId);
|
|
2263
|
+
try {
|
|
2264
|
+
requireLineageWorkspaceClaimForWrite({
|
|
2265
|
+
channel: claimContext.channel,
|
|
2266
|
+
claimToken: fields.claimToken,
|
|
2267
|
+
confirmWrite: fields.confirmWrite,
|
|
2268
|
+
project,
|
|
2269
|
+
rootAssetId: claimContext.rootAssetId,
|
|
2270
|
+
writeKind: "lineage_link"
|
|
2271
|
+
});
|
|
2272
|
+
} catch (error) {
|
|
2273
|
+
database.close();
|
|
2274
|
+
throw error;
|
|
2275
|
+
}
|
|
2276
|
+
const edge = {
|
|
2277
|
+
id: edgeId(project, fields.parentAssetId, fields.childAssetId),
|
|
2278
|
+
parent_asset_id: fields.parentAssetId,
|
|
2279
|
+
child_asset_id: fields.childAssetId,
|
|
2280
|
+
relation_type: "derived_from",
|
|
2281
|
+
created_at: nowIso()
|
|
2282
|
+
};
|
|
2283
|
+
if (!fields.confirmWrite) {
|
|
2284
|
+
database.close();
|
|
2285
|
+
return { ok: true, dryRun: true, edge };
|
|
2286
|
+
}
|
|
2287
|
+
database.prepare(`
|
|
2288
|
+
insert into asset_edges (id, project_id, parent_asset_id, child_asset_id, relation_type, created_at)
|
|
2289
|
+
values (?, ?, ?, ?, 'derived_from', ?)
|
|
2290
|
+
on conflict(project_id, parent_asset_id, child_asset_id, relation_type) do nothing
|
|
2291
|
+
`).run(edge.id, project, edge.parent_asset_id, edge.child_asset_id, edge.created_at);
|
|
2292
|
+
database.close();
|
|
2293
|
+
return { ok: true, message: `Linked ${edge.child_asset_id} from ${edge.parent_asset_id}`, edge };
|
|
2126
2294
|
}
|
|
2127
|
-
function
|
|
2128
|
-
|
|
2295
|
+
function descendants(database, project, root) {
|
|
2296
|
+
const edges = [];
|
|
2297
|
+
const queue = [root];
|
|
2298
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2299
|
+
while (queue.length > 0) {
|
|
2300
|
+
const parent = queue.shift();
|
|
2301
|
+
if (seen.has(parent)) continue;
|
|
2302
|
+
seen.add(parent);
|
|
2303
|
+
const rows = database.prepare("select id, parent_asset_id, child_asset_id, relation_type, created_at from asset_edges where project_id = ? and parent_asset_id = ? order by created_at").all(project, parent);
|
|
2304
|
+
edges.push(...rows);
|
|
2305
|
+
queue.push(...rows.map((row) => row.child_asset_id));
|
|
2306
|
+
}
|
|
2307
|
+
return edges;
|
|
2129
2308
|
}
|
|
2130
|
-
function
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2309
|
+
function getLineageSnapshot(project, assetId) {
|
|
2310
|
+
const database = lineageDb();
|
|
2311
|
+
requireAsset2(database, project, assetId);
|
|
2312
|
+
const root = explicitWorkspaceRoot(database, project, assetId) || rootFor(database, project, assetId);
|
|
2313
|
+
const edges = descendants(database, project, root);
|
|
2314
|
+
const ids = [.../* @__PURE__ */ new Set([root, ...edges.flatMap((edge) => [edge.parent_asset_id, edge.child_asset_id])])];
|
|
2315
|
+
const placeholders2 = ids.map(() => "?").join(",");
|
|
2316
|
+
const rows = database.prepare(`
|
|
2317
|
+
select a.id asset_id, a.project_id project, a.source, a.title, a.media_type, a.status, a.channel, a.campaign,
|
|
2318
|
+
a.local_path, a.s3_key, a.checksum_sha256, coalesce(r.review_state, 'unreviewed') review_state,
|
|
2319
|
+
r.notes review_notes, a.created_at asset_created_at, l.x layout_x, l.y layout_y
|
|
2320
|
+
from assets a left join asset_reviews r on r.asset_id = a.id
|
|
2321
|
+
left join asset_layouts l on l.project_id = a.project_id and l.root_asset_id = ? and l.asset_id = a.id
|
|
2322
|
+
where a.project_id = ? and a.id in (${placeholders2})
|
|
2323
|
+
`).all(root, project, ...ids);
|
|
2324
|
+
const attemptRows = ids.length > 0 ? database.prepare(`select * from asset_attempts where project_id = ? and node_asset_id in (${placeholders2}) order by node_asset_id, attempt_index desc`).all(project, ...ids) : [];
|
|
2325
|
+
const attemptsByNode = /* @__PURE__ */ new Map();
|
|
2326
|
+
for (const attempt of attemptRows.map(attemptFrom)) {
|
|
2327
|
+
attemptsByNode.set(attempt.node_asset_id, [...attemptsByNode.get(attempt.node_asset_id) || [], attempt]);
|
|
2328
|
+
}
|
|
2329
|
+
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
|
+
const rerollsByNode = new Map(rerollRows.map((row) => {
|
|
2331
|
+
const request = rerollRequestFrom(row);
|
|
2332
|
+
return [request.node_asset_id, request];
|
|
2333
|
+
}));
|
|
2334
|
+
const selected = selectedRows(database, project, root);
|
|
2335
|
+
const childIds = new Set(edges.map((edge) => edge.parent_asset_id));
|
|
2336
|
+
const selectedIds = new Set(selected.map((row) => row.asset_id));
|
|
2337
|
+
const selections = selected.map((row) => ({
|
|
2338
|
+
asset_id: row.asset_id,
|
|
2339
|
+
notes: row.notes || void 0,
|
|
2340
|
+
position: Number(row.position || 0),
|
|
2341
|
+
selected_at: row.selected_at
|
|
2342
|
+
}));
|
|
2343
|
+
const selection = selections[0] || null;
|
|
2344
|
+
const nodes = rows.map((row) => {
|
|
2345
|
+
const position = typeof row.layout_x === "number" && typeof row.layout_y === "number" ? { x: row.layout_x, y: row.layout_y } : void 0;
|
|
2346
|
+
const { asset_created_at: _assetCreatedAt, layout_x: _layoutX, layout_y: _layoutY, ...node } = row;
|
|
2347
|
+
const nodeSelection = selections.find((item) => item.asset_id === row.asset_id);
|
|
2348
|
+
const attempts = withImplicitAttempt(attemptsByNode.get(row.asset_id) || [], row);
|
|
2349
|
+
const currentAttempt = attempts.find((attempt) => attempt.is_current) || attempts[0];
|
|
2350
|
+
const previewPath = currentAttempt?.file_path || row.local_path;
|
|
2351
|
+
return {
|
|
2352
|
+
...node,
|
|
2353
|
+
attempt_count: attempts.length,
|
|
2354
|
+
current_attempt: currentAttempt,
|
|
2355
|
+
is_latest: !childIds.has(row.asset_id),
|
|
2356
|
+
position,
|
|
2357
|
+
preview_url: canPreviewLocally(row.media_type, previewPath) ? localPreviewUrl(project, previewPath) : void 0,
|
|
2358
|
+
reroll_request: rerollsByNode.get(row.asset_id),
|
|
2359
|
+
selection_note: nodeSelection?.notes,
|
|
2360
|
+
user_selected: selectedIds.has(row.asset_id)
|
|
2361
|
+
};
|
|
2362
|
+
});
|
|
2363
|
+
database.close();
|
|
2364
|
+
return {
|
|
2365
|
+
project,
|
|
2366
|
+
root_asset_id: root,
|
|
2367
|
+
active_asset_id: assetId,
|
|
2368
|
+
selected: selections.map((row) => row.asset_id),
|
|
2369
|
+
selection,
|
|
2370
|
+
selections,
|
|
2371
|
+
latest: nodes.filter((node) => node.is_latest).map((node) => node.asset_id),
|
|
2372
|
+
nodes,
|
|
2373
|
+
edges,
|
|
2374
|
+
fetchedAt: nowIso()
|
|
2375
|
+
};
|
|
2136
2376
|
}
|
|
2137
|
-
function
|
|
2138
|
-
|
|
2139
|
-
const targetId = fields.targetId.trim();
|
|
2140
|
-
const agentName = fields.agentName.trim();
|
|
2141
|
-
if (!project) throw new AgentClaimError("Agent claim requires project");
|
|
2142
|
-
if (!targetId) throw new AgentClaimError("Agent claim requires target");
|
|
2143
|
-
if (!agentName) throw new AgentClaimError("Agent claim requires agent name");
|
|
2144
|
-
const scopeType = normalizeScope(fields.scopeType);
|
|
2145
|
-
const ttlSeconds = fields.ttlSeconds || defaultTtlSeconds;
|
|
2377
|
+
function updateLineageLayout(project, fields) {
|
|
2378
|
+
if (fields.positions.length === 0) throw new LineageError("Lineage layout requires at least one position");
|
|
2146
2379
|
const database = lineageDb();
|
|
2380
|
+
requireAsset2(database, project, fields.rootAssetId);
|
|
2381
|
+
for (const position of fields.positions) requireAsset2(database, project, position.assetId);
|
|
2147
2382
|
try {
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
if (conflicts.length > 0 && !fields.force) {
|
|
2153
|
-
throw new AgentClaimError("Target already has an active overlapping agent claim.", 409, "target_already_claimed", conflicts);
|
|
2154
|
-
}
|
|
2155
|
-
if (conflicts.length > 0 && !fields.reason?.trim()) {
|
|
2156
|
-
throw new AgentClaimError("Overriding an active claim requires --reason.", 400, "override_reason_required", conflicts);
|
|
2157
|
-
}
|
|
2158
|
-
const timestamp = nowIso();
|
|
2159
|
-
for (const conflict of conflicts) {
|
|
2160
|
-
database.prepare(`
|
|
2161
|
-
update agent_claims
|
|
2162
|
-
set status = 'revoked', revoked_at = ?, revoked_by = ?, override_reason = ?
|
|
2163
|
-
where id = ? and status = 'active'
|
|
2164
|
-
`).run(timestamp, agentName, fields.reason || null, conflict.id);
|
|
2165
|
-
recordEvent(database, conflict.id, "revoked", agentName, fields.reason || "Revoked by forced claim takeover.");
|
|
2166
|
-
recordEvent(database, conflict.id, "conflict", agentName, `Overridden by ${agentName}.`, { new_claim_target: targetId });
|
|
2167
|
-
}
|
|
2168
|
-
const id = randomId("claim");
|
|
2169
|
-
const secret = randomBytes(24).toString("base64url");
|
|
2170
|
-
const claimToken = `${id}.${secret}`;
|
|
2171
|
-
const expiresAt = expiresAtFrom(timestamp, ttlSeconds);
|
|
2172
|
-
database.prepare(`
|
|
2173
|
-
insert into agent_claims (
|
|
2174
|
-
id, token_hash, project_id, channel, scope_type, target_id, target_title, agent_id, agent_name,
|
|
2175
|
-
agent_kind, thread_id, status, created_at, heartbeat_at, expires_at, metadata_json
|
|
2176
|
-
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', ?, ?, ?, ?)
|
|
2177
|
-
`).run(
|
|
2178
|
-
id,
|
|
2179
|
-
tokenHash(claimToken),
|
|
2383
|
+
requireLineageWorkspaceClaimForWrite({
|
|
2384
|
+
channel: assetChannel(database, project, fields.rootAssetId),
|
|
2385
|
+
claimToken: fields.claimToken,
|
|
2386
|
+
confirmWrite: fields.confirmWrite,
|
|
2180
2387
|
project,
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
timestamp,
|
|
2190
|
-
timestamp,
|
|
2191
|
-
expiresAt,
|
|
2192
|
-
metadataJson(fields.metadata)
|
|
2193
|
-
);
|
|
2194
|
-
recordEvent(database, id, "created", agentName, `Claimed ${scopeType} ${targetId}.`, { ttl_seconds: ttlSeconds });
|
|
2195
|
-
const claim = findClaimById(database, id);
|
|
2196
|
-
return { ok: true, claim, claim_token: claimToken, conflicts_revoked: conflicts.map((conflict) => conflict.id) };
|
|
2197
|
-
} finally {
|
|
2388
|
+
rootAssetId: fields.rootAssetId,
|
|
2389
|
+
writeKind: "lineage_layout"
|
|
2390
|
+
});
|
|
2391
|
+
} catch (error) {
|
|
2392
|
+
database.close();
|
|
2393
|
+
throw error;
|
|
2394
|
+
}
|
|
2395
|
+
if (!fields.confirmWrite) {
|
|
2198
2396
|
database.close();
|
|
2397
|
+
return { ok: true, dryRun: true, root_asset_id: fields.rootAssetId, positions: fields.positions };
|
|
2398
|
+
}
|
|
2399
|
+
const timestamp = nowIso();
|
|
2400
|
+
const statement = database.prepare(`
|
|
2401
|
+
insert into asset_layouts (id, project_id, root_asset_id, asset_id, x, y, updated_at)
|
|
2402
|
+
values (?, ?, ?, ?, ?, ?, ?)
|
|
2403
|
+
on conflict(project_id, root_asset_id, asset_id) do update set
|
|
2404
|
+
x = excluded.x, y = excluded.y, updated_at = excluded.updated_at
|
|
2405
|
+
`);
|
|
2406
|
+
for (const position of fields.positions) {
|
|
2407
|
+
statement.run(`${project}:${fields.rootAssetId}:layout:${position.assetId}`, project, fields.rootAssetId, position.assetId, position.x, position.y, timestamp);
|
|
2408
|
+
}
|
|
2409
|
+
database.close();
|
|
2410
|
+
return { ok: true, message: `Saved ${fields.positions.length} lineage positions`, root_asset_id: fields.rootAssetId, positions: fields.positions };
|
|
2411
|
+
}
|
|
2412
|
+
function getLineageNextAsset(project, rootAssetId) {
|
|
2413
|
+
const database = lineageDb();
|
|
2414
|
+
const root = resolveRoot(database, project, rootAssetId);
|
|
2415
|
+
database.close();
|
|
2416
|
+
const snapshot = getLineageSnapshot(project, root);
|
|
2417
|
+
const selectedNodes = snapshot.selected.map((assetId) => snapshot.nodes.find((node) => node.asset_id === assetId)).filter((node) => Boolean(node));
|
|
2418
|
+
const latestNodes = snapshot.nodes.filter((node) => snapshot.latest.includes(node.asset_id));
|
|
2419
|
+
const warnings = [];
|
|
2420
|
+
for (const selectedNode of selectedNodes) {
|
|
2421
|
+
if (selectedNode.is_latest) continue;
|
|
2422
|
+
warnings.push("Selected asset is not a latest leaf; agents should treat this as an intentional branch choice.");
|
|
2423
|
+
}
|
|
2424
|
+
if (selectedNodes.length > 0) {
|
|
2425
|
+
return {
|
|
2426
|
+
project,
|
|
2427
|
+
root_asset_id: snapshot.root_asset_id,
|
|
2428
|
+
strategy: "selected",
|
|
2429
|
+
selection_mode: selectedNodes.length > 1 ? "multiple" : "single",
|
|
2430
|
+
recommended_action: "evolve_variations",
|
|
2431
|
+
reason: "user_selected",
|
|
2432
|
+
next_asset: selectedNodes[0],
|
|
2433
|
+
next_assets: selectedNodes,
|
|
2434
|
+
latest: snapshot.latest,
|
|
2435
|
+
selected: snapshot.selected,
|
|
2436
|
+
selection: snapshot.selection,
|
|
2437
|
+
selections: snapshot.selections,
|
|
2438
|
+
candidates: latestNodes,
|
|
2439
|
+
warnings,
|
|
2440
|
+
fetchedAt: nowIso()
|
|
2441
|
+
};
|
|
2442
|
+
}
|
|
2443
|
+
if (latestNodes.length === 1) {
|
|
2444
|
+
return {
|
|
2445
|
+
project,
|
|
2446
|
+
root_asset_id: snapshot.root_asset_id,
|
|
2447
|
+
strategy: "single_latest",
|
|
2448
|
+
selection_mode: "fallback",
|
|
2449
|
+
recommended_action: "evolve_variations",
|
|
2450
|
+
reason: "single_latest_fallback",
|
|
2451
|
+
next_asset: latestNodes[0],
|
|
2452
|
+
next_assets: [latestNodes[0]],
|
|
2453
|
+
latest: snapshot.latest,
|
|
2454
|
+
selected: snapshot.selected,
|
|
2455
|
+
selection: snapshot.selection,
|
|
2456
|
+
selections: snapshot.selections,
|
|
2457
|
+
candidates: latestNodes,
|
|
2458
|
+
warnings,
|
|
2459
|
+
fetchedAt: nowIso()
|
|
2460
|
+
};
|
|
2199
2461
|
}
|
|
2462
|
+
return {
|
|
2463
|
+
project,
|
|
2464
|
+
root_asset_id: snapshot.root_asset_id,
|
|
2465
|
+
strategy: latestNodes.length > 1 ? "ambiguous_latest" : "empty",
|
|
2466
|
+
selection_mode: "none",
|
|
2467
|
+
recommended_action: latestNodes.length > 1 ? "choose_next_base" : "none",
|
|
2468
|
+
reason: latestNodes.length > 1 ? "multiple_latest_no_selection" : "no_lineage_candidates",
|
|
2469
|
+
next_asset: null,
|
|
2470
|
+
next_assets: [],
|
|
2471
|
+
latest: snapshot.latest,
|
|
2472
|
+
selected: snapshot.selected,
|
|
2473
|
+
selection: snapshot.selection,
|
|
2474
|
+
selections: snapshot.selections,
|
|
2475
|
+
candidates: latestNodes,
|
|
2476
|
+
warnings,
|
|
2477
|
+
fetchedAt: nowIso()
|
|
2478
|
+
};
|
|
2200
2479
|
}
|
|
2201
|
-
function
|
|
2202
|
-
const
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2480
|
+
function getLineageChildren(project, parentAssetId) {
|
|
2481
|
+
const snapshot = getLineageSnapshot(project, parentAssetId);
|
|
2482
|
+
const edges = snapshot.edges.filter((edge) => edge.parent_asset_id === parentAssetId);
|
|
2483
|
+
const childIds = new Set(edges.map((edge) => edge.child_asset_id));
|
|
2484
|
+
return {
|
|
2485
|
+
project,
|
|
2486
|
+
parent_asset_id: parentAssetId,
|
|
2487
|
+
children: snapshot.nodes.filter((node) => childIds.has(node.asset_id)),
|
|
2488
|
+
edges,
|
|
2489
|
+
fetchedAt: nowIso()
|
|
2490
|
+
};
|
|
2210
2491
|
}
|
|
2211
|
-
function
|
|
2492
|
+
function listLineageRerollRequests(project, rootAssetId) {
|
|
2212
2493
|
const database = lineageDb();
|
|
2213
2494
|
try {
|
|
2214
|
-
|
|
2215
|
-
const
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2495
|
+
assertCanonicalRoot(database, project, rootAssetId);
|
|
2496
|
+
const rows = database.prepare(`
|
|
2497
|
+
select * from asset_reroll_requests
|
|
2498
|
+
where project_id = ? and root_asset_id = ? and status = 'pending'
|
|
2499
|
+
order by created_at, id
|
|
2500
|
+
`).all(project, rootAssetId);
|
|
2501
|
+
return { project, root_asset_id: rootAssetId, requests: rows.map(rerollRequestFrom), fetchedAt: nowIso() };
|
|
2219
2502
|
} finally {
|
|
2220
2503
|
database.close();
|
|
2221
2504
|
}
|
|
2222
2505
|
}
|
|
2223
|
-
function
|
|
2506
|
+
function getLineageAttempts(project, rootAssetId, nodeAssetId) {
|
|
2224
2507
|
const database = lineageDb();
|
|
2225
2508
|
try {
|
|
2226
|
-
|
|
2227
|
-
const
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
database.prepare("
|
|
2233
|
-
|
|
2234
|
-
return { ok: true, claim: findClaimById(database, claim.id) };
|
|
2509
|
+
assertNodeInRoot(database, project, rootAssetId, nodeAssetId);
|
|
2510
|
+
const rows = database.prepare(`
|
|
2511
|
+
select * from asset_attempts
|
|
2512
|
+
where project_id = ? and node_asset_id = ?
|
|
2513
|
+
order by attempt_index desc, created_at desc
|
|
2514
|
+
`).all(project, nodeAssetId);
|
|
2515
|
+
const asset = database.prepare("select id asset_id, project_id project, local_path, checksum_sha256, created_at asset_created_at from assets where project_id = ? and id = ?").get(project, nodeAssetId);
|
|
2516
|
+
return { project, root_asset_id: rootAssetId, node_asset_id: nodeAssetId, attempts: withImplicitAttempt(rows.map(attemptFrom), asset), fetchedAt: nowIso() };
|
|
2235
2517
|
} finally {
|
|
2236
2518
|
database.close();
|
|
2237
2519
|
}
|
|
2238
2520
|
}
|
|
2239
|
-
function
|
|
2521
|
+
function promoteLineageAttempt(project, fields) {
|
|
2240
2522
|
const database = lineageDb();
|
|
2241
2523
|
try {
|
|
2242
|
-
|
|
2243
|
-
const
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2524
|
+
assertNodeInRoot(database, project, fields.rootAssetId, fields.nodeAssetId);
|
|
2525
|
+
const asset = database.prepare("select id asset_id, project_id project, local_path, checksum_sha256, created_at asset_created_at from assets where project_id = ? and id = ?").get(project, fields.nodeAssetId);
|
|
2526
|
+
const rows = database.prepare(`
|
|
2527
|
+
select * from asset_attempts
|
|
2528
|
+
where project_id = ? and node_asset_id = ?
|
|
2529
|
+
order by attempt_index desc, created_at desc
|
|
2530
|
+
`).all(project, fields.nodeAssetId);
|
|
2531
|
+
const attempts = withImplicitAttempt(rows.map(attemptFrom), asset);
|
|
2532
|
+
const target = attempts.find((attempt2) => attempt2.id === fields.attemptId);
|
|
2533
|
+
if (!target) throw new LineageError(`Attempt ${fields.attemptId} is not in ${fields.nodeAssetId}`, 404);
|
|
2247
2534
|
const timestamp = nowIso();
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2535
|
+
const promotedAttempt = { ...target, is_current: true, promoted_at: timestamp };
|
|
2536
|
+
if (!fields.confirmWrite) {
|
|
2537
|
+
return {
|
|
2538
|
+
ok: true,
|
|
2539
|
+
dryRun: true,
|
|
2540
|
+
project,
|
|
2541
|
+
root_asset_id: fields.rootAssetId,
|
|
2542
|
+
node_asset_id: fields.nodeAssetId,
|
|
2543
|
+
attempt: promotedAttempt,
|
|
2544
|
+
attempts: attempts.map((attempt2) => ({ ...attempt2, is_current: attempt2.id === target.id, promoted_at: attempt2.id === target.id ? timestamp : attempt2.promoted_at })),
|
|
2545
|
+
fetchedAt: timestamp
|
|
2546
|
+
};
|
|
2547
|
+
}
|
|
2548
|
+
database.exec("BEGIN IMMEDIATE");
|
|
2549
|
+
try {
|
|
2550
|
+
database.prepare("update asset_attempts set is_current = 0 where project_id = ? and node_asset_id = ?").run(project, fields.nodeAssetId);
|
|
2551
|
+
if (target.source !== "initial") {
|
|
2552
|
+
const result = database.prepare(`
|
|
2553
|
+
update asset_attempts
|
|
2554
|
+
set is_current = 1, promoted_at = ?
|
|
2555
|
+
where project_id = ? and node_asset_id = ? and id = ?
|
|
2556
|
+
`).run(timestamp, project, fields.nodeAssetId, target.id);
|
|
2557
|
+
if (result.changes !== 1) throw new LineageError(`Attempt ${fields.attemptId} is not in ${fields.nodeAssetId}`, 404);
|
|
2558
|
+
}
|
|
2559
|
+
database.exec("COMMIT");
|
|
2560
|
+
} catch (error) {
|
|
2561
|
+
database.exec("ROLLBACK");
|
|
2562
|
+
throw error;
|
|
2563
|
+
}
|
|
2564
|
+
const refreshedRows = database.prepare(`
|
|
2565
|
+
select * from asset_attempts
|
|
2566
|
+
where project_id = ? and node_asset_id = ?
|
|
2567
|
+
order by attempt_index desc, created_at desc
|
|
2568
|
+
`).all(project, fields.nodeAssetId);
|
|
2569
|
+
const refreshedAttempts = withImplicitAttempt(refreshedRows.map(attemptFrom), asset);
|
|
2570
|
+
const attempt = refreshedAttempts.find((item) => item.id === target.id) || promotedAttempt;
|
|
2571
|
+
return {
|
|
2572
|
+
ok: true,
|
|
2573
|
+
project,
|
|
2574
|
+
root_asset_id: fields.rootAssetId,
|
|
2575
|
+
node_asset_id: fields.nodeAssetId,
|
|
2576
|
+
attempt,
|
|
2577
|
+
attempts: refreshedAttempts,
|
|
2578
|
+
fetchedAt: nowIso()
|
|
2579
|
+
};
|
|
2251
2580
|
} finally {
|
|
2252
2581
|
database.close();
|
|
2253
2582
|
}
|
|
2254
2583
|
}
|
|
2255
|
-
function
|
|
2256
|
-
if (!fields.confirmWrite) throw new AgentClaimError("Releasing a stale agent claim requires confirmWrite=true.", 400, "confirm_write_required");
|
|
2257
|
-
if (!fields.reason?.trim()) throw new AgentClaimError("Releasing a stale agent claim requires a reason.", 400, "reason_required");
|
|
2584
|
+
function markLineageRerollRequest(project, fields) {
|
|
2258
2585
|
const database = lineageDb();
|
|
2259
2586
|
try {
|
|
2260
|
-
|
|
2261
|
-
const
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2587
|
+
assertNodeInRoot(database, project, fields.rootAssetId, fields.nodeAssetId);
|
|
2588
|
+
const existing = database.prepare(`
|
|
2589
|
+
select * from asset_reroll_requests
|
|
2590
|
+
where project_id = ? and root_asset_id = ? and node_asset_id = ? and status = 'pending'
|
|
2591
|
+
order by created_at desc limit 1
|
|
2592
|
+
`).get(project, fields.rootAssetId, fields.nodeAssetId);
|
|
2266
2593
|
const timestamp = nowIso();
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2594
|
+
const request = existing ? {
|
|
2595
|
+
...rerollRequestFrom(existing),
|
|
2596
|
+
notes: fields.notes || rerollRequestFrom(existing).notes,
|
|
2597
|
+
requested_by: fields.requestedBy || rerollRequestFrom(existing).requested_by
|
|
2598
|
+
} : {
|
|
2599
|
+
id: rerollRequestId(project, fields.rootAssetId, fields.nodeAssetId, timestamp),
|
|
2600
|
+
project_id: project,
|
|
2601
|
+
root_asset_id: fields.rootAssetId,
|
|
2602
|
+
node_asset_id: fields.nodeAssetId,
|
|
2603
|
+
status: "pending",
|
|
2604
|
+
requested_by: fields.requestedBy || "human",
|
|
2605
|
+
notes: fields.notes,
|
|
2606
|
+
created_at: timestamp
|
|
2607
|
+
};
|
|
2608
|
+
if (!fields.confirmWrite) return { ok: true, dryRun: true, request };
|
|
2609
|
+
if (existing) {
|
|
2610
|
+
database.prepare(`
|
|
2611
|
+
update asset_reroll_requests
|
|
2612
|
+
set requested_by = ?, notes = ?
|
|
2613
|
+
where id = ?
|
|
2614
|
+
`).run(request.requested_by, request.notes || null, request.id);
|
|
2615
|
+
} else {
|
|
2616
|
+
database.prepare(`
|
|
2617
|
+
insert into asset_reroll_requests (id, project_id, root_asset_id, node_asset_id, status, requested_by, notes, created_at, resolved_at)
|
|
2618
|
+
values (?, ?, ?, ?, 'pending', ?, ?, ?, null)
|
|
2619
|
+
`).run(request.id, project, fields.rootAssetId, fields.nodeAssetId, request.requested_by, request.notes || null, request.created_at);
|
|
2620
|
+
}
|
|
2621
|
+
database.prepare(`
|
|
2622
|
+
insert into asset_reviews (asset_id, review_state, reviewed_at, ignored_at, notes, updated_at)
|
|
2623
|
+
values (?, 'needs_revision', ?, null, ?, ?)
|
|
2624
|
+
on conflict(asset_id) do update set
|
|
2625
|
+
review_state = excluded.review_state, reviewed_at = excluded.reviewed_at,
|
|
2626
|
+
ignored_at = excluded.ignored_at, notes = coalesce(excluded.notes, asset_reviews.notes), updated_at = excluded.updated_at
|
|
2627
|
+
`).run(fields.nodeAssetId, timestamp, request.notes || null, timestamp);
|
|
2628
|
+
return { ok: true, request: rerollRequestFrom(database.prepare("select * from asset_reroll_requests where id = ?").get(request.id)) };
|
|
2270
2629
|
} finally {
|
|
2271
2630
|
database.close();
|
|
2272
2631
|
}
|
|
2273
2632
|
}
|
|
2274
|
-
function
|
|
2275
|
-
if (!fields.confirmWrite) throw new AgentClaimError("Revoking an agent claim requires confirmWrite=true.", 400, "confirm_write_required");
|
|
2276
|
-
if (!fields.reason?.trim()) throw new AgentClaimError("Revoking an agent claim requires a reason.", 400, "reason_required");
|
|
2633
|
+
function clearLineageRerollRequest(project, fields) {
|
|
2277
2634
|
const database = lineageDb();
|
|
2278
2635
|
try {
|
|
2279
|
-
|
|
2280
|
-
const
|
|
2281
|
-
|
|
2636
|
+
assertNodeInRoot(database, project, fields.rootAssetId, fields.nodeAssetId);
|
|
2637
|
+
const existing = database.prepare(`
|
|
2638
|
+
select * from asset_reroll_requests
|
|
2639
|
+
where project_id = ? and root_asset_id = ? and node_asset_id = ? and status = 'pending'
|
|
2640
|
+
order by created_at desc limit 1
|
|
2641
|
+
`).get(project, fields.rootAssetId, fields.nodeAssetId);
|
|
2642
|
+
if (!existing) throw new LineageError(`No pending re-roll request for ${fields.nodeAssetId}`, 404);
|
|
2282
2643
|
const timestamp = nowIso();
|
|
2644
|
+
const request = { ...rerollRequestFrom(existing), status: "cancelled", resolved_at: timestamp };
|
|
2645
|
+
if (!fields.confirmWrite) return { ok: true, dryRun: true, request };
|
|
2283
2646
|
database.prepare(`
|
|
2284
|
-
update
|
|
2647
|
+
update asset_reroll_requests
|
|
2648
|
+
set status = 'cancelled', resolved_at = ?
|
|
2285
2649
|
where id = ?
|
|
2286
|
-
`).run(timestamp,
|
|
2287
|
-
|
|
2288
|
-
return { ok: true, claim: findClaimById(database, claim.id) };
|
|
2650
|
+
`).run(timestamp, request.id);
|
|
2651
|
+
return { ok: true, request };
|
|
2289
2652
|
} finally {
|
|
2290
2653
|
database.close();
|
|
2291
2654
|
}
|
|
2292
2655
|
}
|
|
2293
|
-
function
|
|
2294
|
-
if (!fields.confirmWrite) throw new AgentClaimError("Transferring an agent claim requires confirmWrite=true.", 400, "confirm_write_required");
|
|
2295
|
-
const toAgentName = fields.toAgentName.trim();
|
|
2296
|
-
if (!toAgentName) throw new AgentClaimError("Transfer requires toAgentName.", 400, "agent_name_required");
|
|
2656
|
+
function updateSelectedAsset(project, fields) {
|
|
2297
2657
|
const database = lineageDb();
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
} finally {
|
|
2658
|
+
const inputAssetIds = normalizeSelectionInput(fields);
|
|
2659
|
+
const root = fields.rootAssetId || (inputAssetIds[0] ? rootFor(database, project, inputAssetIds[0]) : "");
|
|
2660
|
+
if (!root) throw new LineageError("Selection requires rootAssetId or assetId");
|
|
2661
|
+
requireAsset2(database, project, root);
|
|
2662
|
+
for (const assetId of inputAssetIds) requireAsset2(database, project, assetId);
|
|
2663
|
+
const mode = fields.mode || "replace";
|
|
2664
|
+
const limit = fields.maxSelections || LINEAGE_NEXT_VARIATION_LIMIT;
|
|
2665
|
+
if (!fields.confirmWrite) {
|
|
2307
2666
|
database.close();
|
|
2667
|
+
return { ok: true, dryRun: true, root_asset_id: root, asset_ids: inputAssetIds, mode, clear: Boolean(fields.clear), max_selections: limit };
|
|
2308
2668
|
}
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
if (fields.
|
|
2312
|
-
|
|
2669
|
+
const current = selectedRows(database, project, root);
|
|
2670
|
+
let nextIds = current.map((row) => row.asset_id);
|
|
2671
|
+
if (fields.clear) {
|
|
2672
|
+
nextIds = [];
|
|
2673
|
+
} else if (mode === "replace") {
|
|
2674
|
+
nextIds = inputAssetIds;
|
|
2675
|
+
} else if (mode === "add") {
|
|
2676
|
+
nextIds = [...nextIds, ...inputAssetIds];
|
|
2677
|
+
} else if (mode === "remove") {
|
|
2678
|
+
nextIds = nextIds.filter((assetId) => !inputAssetIds.includes(assetId));
|
|
2679
|
+
} else if (mode === "toggle") {
|
|
2680
|
+
for (const assetId of inputAssetIds) {
|
|
2681
|
+
nextIds = nextIds.includes(assetId) ? nextIds.filter((id) => id !== assetId) : [...nextIds, assetId];
|
|
2682
|
+
}
|
|
2313
2683
|
}
|
|
2314
|
-
|
|
2684
|
+
nextIds = [...new Set(nextIds)];
|
|
2685
|
+
if (!fields.clear && inputAssetIds.length === 0) throw new LineageError("Selection set requires assetId or assetIds");
|
|
2686
|
+
if (nextIds.length > limit) throw new LineageError(`Select at most ${limit} assets for next variation`);
|
|
2687
|
+
database.prepare("delete from asset_selections where project_id = ? and root_asset_id = ?").run(project, root);
|
|
2688
|
+
const timestamp = nowIso();
|
|
2689
|
+
const insert = database.prepare(`
|
|
2690
|
+
insert into asset_selections (id, project_id, root_asset_id, asset_id, position, notes, selected_at)
|
|
2691
|
+
values (?, ?, ?, ?, ?, ?, ?)
|
|
2692
|
+
`);
|
|
2693
|
+
nextIds.forEach((assetId, position) => {
|
|
2694
|
+
const existing = current.find((row) => row.asset_id === assetId);
|
|
2695
|
+
const notes = inputAssetIds.includes(assetId) ? fields.notes || existing?.notes : existing?.notes;
|
|
2696
|
+
insert.run(selectionId(project, root, assetId), project, root, assetId, position, notes || null, timestamp);
|
|
2697
|
+
});
|
|
2698
|
+
database.close();
|
|
2699
|
+
const message = nextIds.length === 0 ? `Cleared selected assets for ${root}` : `Selected ${nextIds.length} asset${nextIds.length === 1 ? "" : "s"} for ${root}`;
|
|
2700
|
+
return { ok: true, message, root_asset_id: root, asset_id: nextIds[0] || null, asset_ids: nextIds, mode };
|
|
2701
|
+
}
|
|
2702
|
+
function updateAssetReview(project, fields) {
|
|
2703
|
+
const allowed = /* @__PURE__ */ new Set(["unreviewed", "approved", "needs_revision", "rejected", "ignored"]);
|
|
2704
|
+
if (!allowed.has(fields.reviewState)) throw new LineageError(`Unsupported review state: ${fields.reviewState}`);
|
|
2315
2705
|
const database = lineageDb();
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
const row = findClaimRowByToken(database, fields.claimToken);
|
|
2319
|
-
if (!row) return denied("claim_token_invalid", "Unknown or invalid claim token.");
|
|
2320
|
-
const claim = rowToClaim(row);
|
|
2321
|
-
if (claim.status !== "active") return denied("claim_not_active", `Agent claim is ${claim.status}.`);
|
|
2322
|
-
if (new Date(claim.expires_at).getTime() <= Date.now()) return denied("claim_expired", "Agent claim has expired.");
|
|
2323
|
-
if (claim.project !== fields.project) return denied("claim_project_mismatch", `Claim project ${claim.project} does not match ${fields.project}.`, [claim]);
|
|
2324
|
-
if (fields.channel && claim.channel && claim.channel !== fields.channel) {
|
|
2325
|
-
return denied("claim_channel_mismatch", `Claim channel ${claim.channel} does not match ${fields.channel}.`, [claim]);
|
|
2326
|
-
}
|
|
2327
|
-
if (!scopeAllowsWrite(claim, fields.scopeType, fields.targetId, fields.writeKind)) {
|
|
2328
|
-
return denied("claim_scope_mismatch", `Claim does not cover ${fields.scopeType} ${fields.targetId}.`, [claim]);
|
|
2329
|
-
}
|
|
2330
|
-
recordEvent(database, claim.id, "write_allowed", claim.agent_name, `${fields.writeKind} allowed.`, {
|
|
2331
|
-
danger_level: fields.dangerLevel,
|
|
2332
|
-
target_id: fields.targetId,
|
|
2333
|
-
write_kind: fields.writeKind
|
|
2334
|
-
});
|
|
2335
|
-
return { ok: true, claim, warnings: [] };
|
|
2336
|
-
} finally {
|
|
2706
|
+
requireAsset2(database, project, fields.assetId);
|
|
2707
|
+
if (!fields.confirmWrite) {
|
|
2337
2708
|
database.close();
|
|
2709
|
+
return { ok: true, dryRun: true, asset_id: fields.assetId, review_state: fields.reviewState, notes: fields.notes };
|
|
2338
2710
|
}
|
|
2711
|
+
const timestamp = nowIso();
|
|
2712
|
+
database.prepare(`
|
|
2713
|
+
insert into asset_reviews (asset_id, review_state, reviewed_at, ignored_at, notes, updated_at)
|
|
2714
|
+
values (?, ?, ?, ?, ?, ?)
|
|
2715
|
+
on conflict(asset_id) do update set
|
|
2716
|
+
review_state = excluded.review_state, reviewed_at = excluded.reviewed_at,
|
|
2717
|
+
ignored_at = excluded.ignored_at, notes = excluded.notes, updated_at = excluded.updated_at
|
|
2718
|
+
`).run(fields.assetId, fields.reviewState, timestamp, fields.reviewState === "ignored" ? timestamp : null, fields.notes || null, timestamp);
|
|
2719
|
+
database.close();
|
|
2720
|
+
return { ok: true, message: `Marked ${fields.assetId} ${fields.reviewState}`, asset_id: fields.assetId, review_state: fields.reviewState };
|
|
2339
2721
|
}
|
|
2340
2722
|
|
|
2341
2723
|
// src/server/assetLineageHandoff.ts
|
|
@@ -2396,14 +2778,15 @@ function linkSelectedLineageChild(project, fields) {
|
|
|
2396
2778
|
const next = getLineageNextAsset(project, fields.rootAssetId);
|
|
2397
2779
|
if (!next.next_asset) throw new LineageError("Cannot link child until a next base is selected or unambiguous");
|
|
2398
2780
|
if (fields.confirmWrite) {
|
|
2781
|
+
const claimContext = getLineageWriteClaimContext(project, next.next_asset.asset_id);
|
|
2399
2782
|
const validation = validateAgentClaimForWrite({
|
|
2400
|
-
channel:
|
|
2783
|
+
channel: claimContext.channel,
|
|
2401
2784
|
claimToken: fields.claimToken,
|
|
2402
2785
|
confirmWrite: fields.confirmWrite,
|
|
2403
2786
|
dangerLevel: "enforce",
|
|
2404
2787
|
project,
|
|
2405
2788
|
scopeType: "lineage_workspace",
|
|
2406
|
-
targetId: lineageWorkspaceId(project,
|
|
2789
|
+
targetId: lineageWorkspaceId(project, claimContext.rootAssetId),
|
|
2407
2790
|
writeKind: "link_child"
|
|
2408
2791
|
});
|
|
2409
2792
|
if (!validation.ok) throw new AgentClaimError(validation.message, validation.code === "claim_required" ? 401 : 403, validation.code, validation.conflicts);
|
|
@@ -2429,6 +2812,10 @@ function requireAsset3(database, project, assetId) {
|
|
|
2429
2812
|
const row = database.prepare("select id from assets where project_id = ? and id = ?").get(project, assetId);
|
|
2430
2813
|
if (!row) throw new LineageError(`Unknown indexed asset: ${assetId}`, 404);
|
|
2431
2814
|
}
|
|
2815
|
+
function assetChannel2(database, project, assetId) {
|
|
2816
|
+
const row = database.prepare("select channel from assets where project_id = ? and id = ?").get(project, assetId);
|
|
2817
|
+
return row?.channel;
|
|
2818
|
+
}
|
|
2432
2819
|
function parentOf2(database, project, assetId) {
|
|
2433
2820
|
const row = database.prepare("select parent_asset_id from asset_edges where project_id = ? and child_asset_id = ? order by created_at limit 1").get(project, assetId);
|
|
2434
2821
|
return row?.parent_asset_id;
|
|
@@ -2475,6 +2862,19 @@ function removeLineageNode(project, fields) {
|
|
|
2475
2862
|
database.close();
|
|
2476
2863
|
throw new LineageError(`Asset ${fields.assetId} is not in lineage rooted at ${root}`, 404);
|
|
2477
2864
|
}
|
|
2865
|
+
try {
|
|
2866
|
+
requireLineageWorkspaceClaimForWrite({
|
|
2867
|
+
channel: assetChannel2(database, project, root),
|
|
2868
|
+
claimToken: fields.claimToken,
|
|
2869
|
+
confirmWrite: fields.confirmWrite,
|
|
2870
|
+
project,
|
|
2871
|
+
rootAssetId: root,
|
|
2872
|
+
writeKind: "lineage_remove_node"
|
|
2873
|
+
});
|
|
2874
|
+
} catch (error) {
|
|
2875
|
+
database.close();
|
|
2876
|
+
throw error;
|
|
2877
|
+
}
|
|
2478
2878
|
const parentEdges = snapshot.edges.filter((edge) => edge.child_asset_id === fields.assetId);
|
|
2479
2879
|
const childEdges = snapshot.edges.filter((edge) => edge.parent_asset_id === fields.assetId);
|
|
2480
2880
|
const removedIds = [...parentEdges, ...childEdges].map((edge) => edge.id);
|
|
@@ -4215,7 +4615,7 @@ function updateContentPost(project, fields) {
|
|
|
4215
4615
|
const timestamp = nowIso();
|
|
4216
4616
|
try {
|
|
4217
4617
|
const current = findContentPost(database, project, postId);
|
|
4218
|
-
|
|
4618
|
+
requireContentPostClaim(project, current, fields.claimToken, fields.phase ? "content_post_phase" : "content_post_update");
|
|
4219
4619
|
if (fields.batchId) {
|
|
4220
4620
|
const batch = database.prepare("select id from content_batches where project_id = ? and id = ?").get(project, fields.batchId);
|
|
4221
4621
|
if (!batch) throw new ContentBatchError(`Unknown content batch: ${fields.batchId}`, 404);
|
|
@@ -4389,7 +4789,8 @@ function setContentTarget(project, fields) {
|
|
|
4389
4789
|
const database = lineageDb();
|
|
4390
4790
|
const timestamp = nowIso();
|
|
4391
4791
|
try {
|
|
4392
|
-
findPost(database, project, postId);
|
|
4792
|
+
const post = findPost(database, project, postId);
|
|
4793
|
+
requireContentPostClaim(project, post, fields.claimToken, "content_target_set");
|
|
4393
4794
|
database.prepare(`
|
|
4394
4795
|
insert into content_targets (project_id, post_id, notes, selected_at, updated_at)
|
|
4395
4796
|
values (?, ?, ?, ?, ?)
|
|
@@ -4402,10 +4803,19 @@ function setContentTarget(project, fields) {
|
|
|
4402
4803
|
}
|
|
4403
4804
|
return { ok: true, message: `Selected content target ${postId}`, ...getContentTarget(project) };
|
|
4404
4805
|
}
|
|
4405
|
-
function clearContentTarget(project, confirmWrite) {
|
|
4806
|
+
function clearContentTarget(project, confirmWrite, claimToken) {
|
|
4406
4807
|
if (!confirmWrite) return { ok: true, dryRun: true, message: "Would clear selected content target" };
|
|
4407
4808
|
const database = lineageDb();
|
|
4408
4809
|
try {
|
|
4810
|
+
const row = database.prepare("select post_id from content_targets where project_id = ?").get(project);
|
|
4811
|
+
if (row?.post_id) {
|
|
4812
|
+
try {
|
|
4813
|
+
const post = findPost(database, project, String(row.post_id));
|
|
4814
|
+
requireContentPostClaim(project, post, claimToken, "content_target_clear");
|
|
4815
|
+
} catch (error) {
|
|
4816
|
+
if (!(error instanceof ContentBatchError && error.status === 404)) throw error;
|
|
4817
|
+
}
|
|
4818
|
+
}
|
|
4409
4819
|
database.prepare("delete from content_targets where project_id = ?").run(project);
|
|
4410
4820
|
} finally {
|
|
4411
4821
|
database.close();
|
|
@@ -4609,7 +5019,7 @@ function itemFromFile(file) {
|
|
|
4609
5019
|
};
|
|
4610
5020
|
}
|
|
4611
5021
|
function demoMarkdownItems(kind) {
|
|
4612
|
-
const root = join7(repoRoot, "demo-project", "channels");
|
|
5022
|
+
const root = process.env.LINEAGE_CONTENT_SOURCE_ROOT || join7(repoRoot, "demo-project", "channels");
|
|
4613
5023
|
if (!existsSync4(root)) return [];
|
|
4614
5024
|
return walk(root).map(itemFromFile).filter((item) => Boolean(item)).filter((item) => kind === "all" || `${item.kind}s` === kind).sort((a, b) => a.sourcePath.localeCompare(b.sourcePath));
|
|
4615
5025
|
}
|
|
@@ -4873,13 +5283,14 @@ function contentBatchRouter(projectFrom2) {
|
|
|
4873
5283
|
});
|
|
4874
5284
|
router.post("/target", (req, res) => {
|
|
4875
5285
|
res.json(setContentTarget(projectFrom2(bodyProjectShape(req)), {
|
|
5286
|
+
claimToken: claimTokenFromRequest2(req),
|
|
4876
5287
|
confirmWrite: boolBody2(req, "confirmWrite"),
|
|
4877
5288
|
notes: stringBody2(req, "notes"),
|
|
4878
5289
|
postId: stringBody2(req, "postId") || ""
|
|
4879
5290
|
}));
|
|
4880
5291
|
});
|
|
4881
5292
|
router.post("/target/clear", (req, res) => {
|
|
4882
|
-
res.json(clearContentTarget(projectFrom2(bodyProjectShape(req)), boolBody2(req, "confirmWrite")));
|
|
5293
|
+
res.json(clearContentTarget(projectFrom2(bodyProjectShape(req)), boolBody2(req, "confirmWrite"), claimTokenFromRequest2(req)));
|
|
4883
5294
|
});
|
|
4884
5295
|
router.get("/batches/:batchId", (req, res) => {
|
|
4885
5296
|
res.json(getContentBatch(projectFrom2(req), req.params.batchId));
|
|
@@ -5010,7 +5421,7 @@ function loadGenerationJob(database, project, id) {
|
|
|
5010
5421
|
project_id: String(row.project_id),
|
|
5011
5422
|
provider: row.provider,
|
|
5012
5423
|
adapter_version: String(row.adapter_version),
|
|
5013
|
-
source_mode:
|
|
5424
|
+
source_mode: String(row.source_mode),
|
|
5014
5425
|
root_asset_id: String(row.root_asset_id),
|
|
5015
5426
|
prompt: String(row.prompt),
|
|
5016
5427
|
expected_output_count: Number(row.expected_output_count),
|
|
@@ -5119,6 +5530,15 @@ function swissifierRelativePath(manifest, asset) {
|
|
|
5119
5530
|
function swissifierFilePath(manifest, asset) {
|
|
5120
5531
|
return join8(repoRoot, ".asset-scratch", swissifierRelativePath(manifest, asset));
|
|
5121
5532
|
}
|
|
5533
|
+
function swissifierRerollRelativePath(manifest, attempt) {
|
|
5534
|
+
return join8(manifest.media.target_dir, "reroll-attempts", attempt.file);
|
|
5535
|
+
}
|
|
5536
|
+
function swissifierRerollFilePath(manifest, attempt) {
|
|
5537
|
+
return join8(repoRoot, ".asset-scratch", swissifierRerollRelativePath(manifest, attempt));
|
|
5538
|
+
}
|
|
5539
|
+
function swissifierRerollFixturePath(attempt) {
|
|
5540
|
+
return join8(dirname3(swissifierManifestPath), "swissifier-rerolls", attempt.file);
|
|
5541
|
+
}
|
|
5122
5542
|
function swissifierSourcePath(manifest, asset, sourceDir) {
|
|
5123
5543
|
const direct = join8(sourceDir, asset.file);
|
|
5124
5544
|
if (existsSync5(direct)) return direct;
|
|
@@ -5416,6 +5836,27 @@ function writeDemoFiles(project) {
|
|
|
5416
5836
|
for (const asset of demoAssets) writeDemoFile(project, asset);
|
|
5417
5837
|
return ids;
|
|
5418
5838
|
}
|
|
5839
|
+
function swissifierRerollAsset(attempt) {
|
|
5840
|
+
const source = swissifierRerollFixturePath(attempt);
|
|
5841
|
+
if (!existsSync5(source)) throw new Error(`Missing Swissifier re-roll fixture media: ${attempt.file}`);
|
|
5842
|
+
const body = readFileSync4(source);
|
|
5843
|
+
if (body.length !== attempt.size_bytes) throw new Error(`Unexpected Swissifier re-roll fixture size for ${attempt.file}`);
|
|
5844
|
+
const checksumSha256 = sha256Hex(body);
|
|
5845
|
+
if (checksumSha256 !== attempt.checksum_sha256) throw new Error(`Checksum mismatch for Swissifier re-roll fixture: ${attempt.file}`);
|
|
5846
|
+
return {
|
|
5847
|
+
assetId: `local-${checksumSha256.slice(0, 12)}`,
|
|
5848
|
+
checksumSha256,
|
|
5849
|
+
sizeBytes: body.length
|
|
5850
|
+
};
|
|
5851
|
+
}
|
|
5852
|
+
function writeSwissifierRerollFiles(manifest) {
|
|
5853
|
+
for (const attempt of manifest.reroll_attempts || []) {
|
|
5854
|
+
swissifierRerollAsset(attempt);
|
|
5855
|
+
const target = swissifierRerollFilePath(manifest, attempt);
|
|
5856
|
+
mkdirSync5(dirname3(target), { recursive: true });
|
|
5857
|
+
copyFileSync2(swissifierRerollFixturePath(attempt), target);
|
|
5858
|
+
}
|
|
5859
|
+
}
|
|
5419
5860
|
function upsertDemoAssets(project, ids) {
|
|
5420
5861
|
const database = lineageDb();
|
|
5421
5862
|
const timestamp = nowIso();
|
|
@@ -5506,10 +5947,100 @@ function upsertSwissifierAssets(project, manifest) {
|
|
|
5506
5947
|
);
|
|
5507
5948
|
reviewStatement.run(asset.asset_id, timestamp);
|
|
5508
5949
|
}
|
|
5950
|
+
for (const attempt of manifest.reroll_attempts || []) {
|
|
5951
|
+
const generated = swissifierRerollAsset(attempt);
|
|
5952
|
+
assetStatement.run(
|
|
5953
|
+
generated.assetId,
|
|
5954
|
+
project,
|
|
5955
|
+
swissifierRerollRelativePath(manifest, attempt),
|
|
5956
|
+
generated.checksumSha256,
|
|
5957
|
+
attempt.title,
|
|
5958
|
+
"planned",
|
|
5959
|
+
"local",
|
|
5960
|
+
manifest.campaign,
|
|
5961
|
+
manifest.audience,
|
|
5962
|
+
generated.sizeBytes,
|
|
5963
|
+
attempt.content_type,
|
|
5964
|
+
timestamp,
|
|
5965
|
+
timestamp,
|
|
5966
|
+
timestamp
|
|
5967
|
+
);
|
|
5968
|
+
reviewStatement.run(generated.assetId, timestamp);
|
|
5969
|
+
}
|
|
5970
|
+
} finally {
|
|
5971
|
+
database.close();
|
|
5972
|
+
}
|
|
5973
|
+
const attemptCount = manifest.reroll_attempts?.length || 0;
|
|
5974
|
+
return { catalog: 0, local: manifest.assets.length + attemptCount, total: manifest.assets.length + attemptCount };
|
|
5975
|
+
}
|
|
5976
|
+
function upsertSwissifierRerollAttempts(project, manifest) {
|
|
5977
|
+
const attempts = manifest.reroll_attempts || [];
|
|
5978
|
+
if (attempts.length === 0) return { total: 0 };
|
|
5979
|
+
const database = lineageDb();
|
|
5980
|
+
const timestamp = nowIso();
|
|
5981
|
+
try {
|
|
5982
|
+
database.exec("BEGIN IMMEDIATE");
|
|
5983
|
+
try {
|
|
5984
|
+
const nodes = new Set(attempts.map((attempt) => attempt.node_asset_id));
|
|
5985
|
+
for (const nodeAssetId of nodes) {
|
|
5986
|
+
database.prepare("update asset_attempts set is_current = 0 where project_id = ? and node_asset_id = ?").run(project, nodeAssetId);
|
|
5987
|
+
}
|
|
5988
|
+
const statement = database.prepare(`
|
|
5989
|
+
insert into asset_attempts (
|
|
5990
|
+
id, project_id, node_asset_id, asset_id, attempt_index, source, prompt, generation_job_id,
|
|
5991
|
+
file_path, checksum_sha256, created_at, promoted_at, is_current
|
|
5992
|
+
) values (?, ?, ?, ?, ?, 'reroll', ?, ?, ?, ?, ?, ?, ?)
|
|
5993
|
+
on conflict(id) do update set
|
|
5994
|
+
asset_id = excluded.asset_id,
|
|
5995
|
+
source = excluded.source,
|
|
5996
|
+
prompt = excluded.prompt,
|
|
5997
|
+
generation_job_id = excluded.generation_job_id,
|
|
5998
|
+
file_path = excluded.file_path,
|
|
5999
|
+
checksum_sha256 = excluded.checksum_sha256,
|
|
6000
|
+
promoted_at = excluded.promoted_at,
|
|
6001
|
+
is_current = excluded.is_current
|
|
6002
|
+
`);
|
|
6003
|
+
for (const attempt of attempts) {
|
|
6004
|
+
const generated = swissifierRerollAsset(attempt);
|
|
6005
|
+
statement.run(
|
|
6006
|
+
`${project}:${attempt.node_asset_id}:attempt:${attempt.attempt_index}`,
|
|
6007
|
+
project,
|
|
6008
|
+
attempt.node_asset_id,
|
|
6009
|
+
generated.assetId,
|
|
6010
|
+
attempt.attempt_index,
|
|
6011
|
+
attempt.prompt,
|
|
6012
|
+
attempt.generation_job_id,
|
|
6013
|
+
swissifierRerollRelativePath(manifest, attempt),
|
|
6014
|
+
generated.checksumSha256,
|
|
6015
|
+
timestamp,
|
|
6016
|
+
timestamp,
|
|
6017
|
+
attempt.current ? 1 : 0
|
|
6018
|
+
);
|
|
6019
|
+
}
|
|
6020
|
+
for (const nodeAssetId of nodes) {
|
|
6021
|
+
const currentCount = database.prepare("select count(*) count from asset_attempts where project_id = ? and node_asset_id = ? and is_current = 1").get(project, nodeAssetId);
|
|
6022
|
+
if (currentCount.count === 0) {
|
|
6023
|
+
database.prepare(`
|
|
6024
|
+
update asset_attempts
|
|
6025
|
+
set is_current = 1, promoted_at = ?
|
|
6026
|
+
where id = (
|
|
6027
|
+
select id from asset_attempts
|
|
6028
|
+
where project_id = ? and node_asset_id = ?
|
|
6029
|
+
order by attempt_index desc
|
|
6030
|
+
limit 1
|
|
6031
|
+
)
|
|
6032
|
+
`).run(timestamp, project, nodeAssetId);
|
|
6033
|
+
}
|
|
6034
|
+
}
|
|
6035
|
+
database.exec("COMMIT");
|
|
6036
|
+
} catch (error) {
|
|
6037
|
+
database.exec("ROLLBACK");
|
|
6038
|
+
throw error;
|
|
6039
|
+
}
|
|
5509
6040
|
} finally {
|
|
5510
6041
|
database.close();
|
|
5511
6042
|
}
|
|
5512
|
-
return {
|
|
6043
|
+
return { total: attempts.length };
|
|
5513
6044
|
}
|
|
5514
6045
|
function seedDemoLineageWorkspace(project, fields) {
|
|
5515
6046
|
const ids = fields.confirmWrite ? writeDemoFiles(project) : demoAssetIds();
|
|
@@ -5568,10 +6099,12 @@ function seedSwissifierRichDemoWorkspace(project, fields) {
|
|
|
5568
6099
|
media_status: swissifierRichDemoMediaStatus(project)
|
|
5569
6100
|
};
|
|
5570
6101
|
}
|
|
6102
|
+
writeSwissifierRerollFiles(manifest);
|
|
5571
6103
|
const summary = upsertSwissifierAssets(project, manifest);
|
|
5572
6104
|
for (const edge of manifest.edges) {
|
|
5573
6105
|
linkLineageAssets(project, { parentAssetId: edge.parent, childAssetId: edge.child, confirmWrite: true });
|
|
5574
6106
|
}
|
|
6107
|
+
const reroll_attempts = upsertSwissifierRerollAttempts(project, manifest);
|
|
5575
6108
|
updateSelectedAsset(project, {
|
|
5576
6109
|
assetIds: manifest.selected_asset_ids,
|
|
5577
6110
|
confirmWrite: true,
|
|
@@ -5599,6 +6132,7 @@ function seedSwissifierRichDemoWorkspace(project, fields) {
|
|
|
5599
6132
|
media_status: swissifierRichDemoMediaStatus(project),
|
|
5600
6133
|
root_asset_id: manifest.root_asset_id,
|
|
5601
6134
|
selected_asset_ids: manifest.selected_asset_ids,
|
|
6135
|
+
reroll_attempts,
|
|
5602
6136
|
summary,
|
|
5603
6137
|
workspace
|
|
5604
6138
|
};
|
|
@@ -5805,6 +6339,36 @@ app.get(
|
|
|
5805
6339
|
})
|
|
5806
6340
|
);
|
|
5807
6341
|
registerLineageWorkspaceRoutes(app, projectFrom, asyncRoute);
|
|
6342
|
+
app.get("/api/lineage/:rootAssetId/rerolls", asyncRoute((req, res) => {
|
|
6343
|
+
res.json(listLineageRerollRequests(projectFrom(req), req.params.rootAssetId));
|
|
6344
|
+
}));
|
|
6345
|
+
app.post("/api/lineage/:rootAssetId/rerolls/:nodeAssetId", asyncRoute((req, res) => {
|
|
6346
|
+
res.json(markLineageRerollRequest(projectFrom(req), {
|
|
6347
|
+
rootAssetId: req.params.rootAssetId,
|
|
6348
|
+
nodeAssetId: req.params.nodeAssetId,
|
|
6349
|
+
notes: typeof req.body.notes === "string" ? req.body.notes : void 0,
|
|
6350
|
+
requestedBy: req.body.requestedBy === "agent" || req.body.requestedBy === "system" ? req.body.requestedBy : "human",
|
|
6351
|
+
confirmWrite: req.body.confirmWrite === true
|
|
6352
|
+
}));
|
|
6353
|
+
}));
|
|
6354
|
+
app.post("/api/lineage/:rootAssetId/rerolls/:nodeAssetId/cancel", asyncRoute((req, res) => {
|
|
6355
|
+
res.json(clearLineageRerollRequest(projectFrom(req), {
|
|
6356
|
+
rootAssetId: req.params.rootAssetId,
|
|
6357
|
+
nodeAssetId: req.params.nodeAssetId,
|
|
6358
|
+
confirmWrite: req.body.confirmWrite === true
|
|
6359
|
+
}));
|
|
6360
|
+
}));
|
|
6361
|
+
app.get("/api/lineage/:rootAssetId/attempts/:nodeAssetId", asyncRoute((req, res) => {
|
|
6362
|
+
res.json(getLineageAttempts(projectFrom(req), req.params.rootAssetId, req.params.nodeAssetId));
|
|
6363
|
+
}));
|
|
6364
|
+
app.post("/api/lineage/:rootAssetId/attempts/:nodeAssetId/promote", asyncRoute((req, res) => {
|
|
6365
|
+
res.json(promoteLineageAttempt(projectFrom(req), {
|
|
6366
|
+
rootAssetId: req.params.rootAssetId,
|
|
6367
|
+
nodeAssetId: req.params.nodeAssetId,
|
|
6368
|
+
attemptId: String(req.body.attemptId || ""),
|
|
6369
|
+
confirmWrite: req.body.confirmWrite === true
|
|
6370
|
+
}));
|
|
6371
|
+
}));
|
|
5808
6372
|
app.get("/api/lineage/:assetId", asyncRoute((req, res) => {
|
|
5809
6373
|
res.json(getLineageSnapshot(projectFrom(req), req.params.assetId));
|
|
5810
6374
|
}));
|
|
@@ -5815,13 +6379,14 @@ app.post(
|
|
|
5815
6379
|
linkLineageAssets(projectFrom(req), {
|
|
5816
6380
|
parentAssetId: String(req.body.parentAssetId || ""),
|
|
5817
6381
|
childAssetId: String(req.body.childAssetId || ""),
|
|
5818
|
-
confirmWrite: req.body.confirmWrite === true
|
|
6382
|
+
confirmWrite: req.body.confirmWrite === true,
|
|
6383
|
+
claimToken: claimTokenFromRequest(req)
|
|
5819
6384
|
})
|
|
5820
6385
|
);
|
|
5821
6386
|
})
|
|
5822
6387
|
);
|
|
5823
6388
|
app.post("/api/lineage/remove-node", asyncRoute((req, res) => {
|
|
5824
|
-
res.json(removeLineageNode(projectFrom(req), { assetId: String(req.body.assetId || ""), rootAssetId: typeof req.body.rootAssetId === "string" ? req.body.rootAssetId : void 0, confirmWrite: req.body.confirmWrite === true }));
|
|
6389
|
+
res.json(removeLineageNode(projectFrom(req), { assetId: String(req.body.assetId || ""), rootAssetId: typeof req.body.rootAssetId === "string" ? req.body.rootAssetId : void 0, confirmWrite: req.body.confirmWrite === true, claimToken: claimTokenFromRequest(req) }));
|
|
5825
6390
|
}));
|
|
5826
6391
|
app.post(
|
|
5827
6392
|
"/api/lineage/layout",
|
|
@@ -5835,7 +6400,8 @@ app.post(
|
|
|
5835
6400
|
x: Number(position.x),
|
|
5836
6401
|
y: Number(position.y)
|
|
5837
6402
|
})),
|
|
5838
|
-
confirmWrite: req.body.confirmWrite === true
|
|
6403
|
+
confirmWrite: req.body.confirmWrite === true,
|
|
6404
|
+
claimToken: claimTokenFromRequest(req)
|
|
5839
6405
|
})
|
|
5840
6406
|
);
|
|
5841
6407
|
})
|