Store and use activity_id for L2 links

- Add activity_id column to l2_shares table
- Store activity_id when publishing to L2
- Link to /activities/{activity_id} instead of /assets/{name}
- Falls back to asset link if no activity_id

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
gilesb
2026-01-10 18:56:23 +00:00
parent 3d45efa90b
commit 9a3d6949fc
2 changed files with 31 additions and 12 deletions

View File

@@ -65,12 +65,19 @@ CREATE TABLE IF NOT EXISTS l2_shares (
actor_id VARCHAR(255) NOT NULL,
l2_server VARCHAR(255) NOT NULL,
asset_name VARCHAR(255) NOT NULL,
activity_id VARCHAR(128),
content_type VARCHAR(50) NOT NULL,
published_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
last_synced_at TIMESTAMP WITH TIME ZONE,
UNIQUE(content_hash, actor_id, l2_server, content_type)
);
-- Add activity_id column if it doesn't exist (for existing databases)
DO $$ BEGIN
ALTER TABLE l2_shares ADD COLUMN IF NOT EXISTS activity_id VARCHAR(128);
EXCEPTION WHEN others THEN NULL;
END $$;
-- Run cache: maps content-addressable run_id to output
-- run_id is a hash of (sorted inputs + recipe), making runs deterministic
CREATE TABLE IF NOT EXISTS run_cache (
@@ -512,7 +519,7 @@ async def get_l2_shares(content_hash: str, actor_id: Optional[str] = None) -> Li
if actor_id:
rows = await conn.fetch(
"""
SELECT id, content_hash, actor_id, l2_server, asset_name, content_type, published_at, last_synced_at
SELECT id, content_hash, actor_id, l2_server, asset_name, activity_id, content_type, published_at, last_synced_at
FROM l2_shares
WHERE content_hash = $1 AND actor_id = $2
ORDER BY published_at
@@ -522,7 +529,7 @@ async def get_l2_shares(content_hash: str, actor_id: Optional[str] = None) -> Li
else:
rows = await conn.fetch(
"""
SELECT id, content_hash, actor_id, l2_server, asset_name, content_type, published_at, last_synced_at
SELECT id, content_hash, actor_id, l2_server, asset_name, activity_id, content_type, published_at, last_synced_at
FROM l2_shares
WHERE content_hash = $1
ORDER BY published_at
@@ -780,7 +787,7 @@ async def load_item_metadata(content_hash: str, actor_id: Optional[str] = None)
if actor_id:
shares = await conn.fetch(
"""
SELECT l2_server, asset_name, content_type, published_at, last_synced_at
SELECT l2_server, asset_name, activity_id, content_type, published_at, last_synced_at
FROM l2_shares WHERE content_hash = $1 AND actor_id = $2
""",
content_hash, actor_id
@@ -788,7 +795,7 @@ async def load_item_metadata(content_hash: str, actor_id: Optional[str] = None)
else:
shares = await conn.fetch(
"""
SELECT l2_server, asset_name, content_type, published_at, last_synced_at
SELECT l2_server, asset_name, activity_id, content_type, published_at, last_synced_at
FROM l2_shares WHERE content_hash = $1
""",
content_hash
@@ -799,6 +806,7 @@ async def load_item_metadata(content_hash: str, actor_id: Optional[str] = None)
{
"l2_server": s["l2_server"],
"asset_name": s["asset_name"],
"activity_id": s["activity_id"],
"content_type": s["content_type"],
"published_at": s["published_at"].isoformat() if s["published_at"] else None,
"last_synced_at": s["last_synced_at"].isoformat() if s["last_synced_at"] else None,
@@ -810,6 +818,7 @@ async def load_item_metadata(content_hash: str, actor_id: Optional[str] = None)
result["published"] = {
"to_l2": True,
"asset_name": shares[0]["asset_name"],
"activity_id": shares[0]["activity_id"],
"l2_server": shares[0]["l2_server"],
}
@@ -944,24 +953,27 @@ async def save_l2_share(
actor_id: str,
l2_server: str,
asset_name: str,
content_type: str = "media"
content_type: str = "media",
activity_id: Optional[str] = None
) -> dict:
"""Save an L2 share and return share info."""
async with pool.acquire() as conn:
row = await conn.fetchrow(
"""
INSERT INTO l2_shares (content_hash, actor_id, l2_server, asset_name, content_type, last_synced_at)
VALUES ($1, $2, $3, $4, $5, NOW())
INSERT INTO l2_shares (content_hash, actor_id, l2_server, asset_name, activity_id, content_type, last_synced_at)
VALUES ($1, $2, $3, $4, $5, $6, NOW())
ON CONFLICT (content_hash, actor_id, l2_server, content_type) DO UPDATE SET
asset_name = EXCLUDED.asset_name,
activity_id = COALESCE(EXCLUDED.activity_id, l2_shares.activity_id),
last_synced_at = NOW()
RETURNING l2_server, asset_name, content_type, published_at, last_synced_at
RETURNING l2_server, asset_name, activity_id, content_type, published_at, last_synced_at
""",
content_hash, actor_id, l2_server, asset_name, content_type
content_hash, actor_id, l2_server, asset_name, activity_id, content_type
)
return {
"l2_server": row["l2_server"],
"asset_name": row["asset_name"],
"activity_id": row["activity_id"],
"content_type": row["content_type"],
"published_at": row["published_at"].isoformat() if row["published_at"] else None,
"last_synced_at": row["last_synced_at"].isoformat() if row["last_synced_at"] else None,

View File

@@ -1126,13 +1126,16 @@ async def run_detail(run_id: str, request: Request):
l2_server = share.get("l2_server", "")
l2_https = l2_server.replace("http://", "https://")
asset_name = share.get("asset_name", "")
activity_id = share.get("activity_id")
# Link to activity if available, otherwise fall back to asset
l2_link = f"{l2_https}/activities/{activity_id}" if activity_id else f"{l2_https}/assets/{asset_name}"
publish_html = f'''
<div class="border-t border-dark-500 pt-6 mt-6">
<h2 class="text-lg font-semibold text-white mb-3">Published to L2</h2>
<div class="bg-green-900/30 border border-green-700 rounded-lg p-4">
<p class="text-green-300">
Published as <strong>{asset_name}</strong>
<a href="{l2_https}/asset/{asset_name}" target="_blank" class="underline ml-2">View on L2</a>
Published as <strong>{asset_name[:16]}...</strong>
<a href="{l2_link}" target="_blank" class="underline ml-2">View on L2</a>
</p>
</div>
</div>
@@ -4035,12 +4038,16 @@ async def ui_publish_run(run_id: str, request: Request):
cache_path = get_cache_path(run.output_hash)
media_type = detect_media_type(cache_path) if cache_path else "image"
content_type = "video" if media_type == "video" else "image"
# Get activity_id for linking to the published run
activity = result.get("activity")
activity_id = activity.get("activity_id") if activity else None
await database.save_l2_share(
content_hash=run.output_hash,
actor_id=ctx.actor_id,
l2_server=l2_server,
asset_name=result["asset"]["name"],
content_type=content_type
content_type=content_type,
activity_id=activity_id
)
# Pin the inputs (for provenance)