Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/agentsview/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -1336,6 +1336,7 @@ func openDB(cfg config.Config) (*db.DB, error) {
return nil, err
}
database.SetToolResultImages(cfg.ToolResultImages)
database.SetAssetsDir(filepath.Join(cfg.DataDir, "assets"))
if cfg.InstallationID != "" {
unowned, err := database.EnsureInstallationIdentity(context.Background(), cfg.InstallationID)
if err != nil {
Expand Down
2 changes: 2 additions & 0 deletions cmd/agentsview/sync_worker.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"io"
"os"
"path/filepath"

"github.com/spf13/cobra"
"go.kenn.io/agentsview/internal/config"
Expand Down Expand Up @@ -387,6 +388,7 @@ func workerEngineConfig(cfg config.Config) sync.EngineConfig {
Machine: cfg.InstallationID,
BlockedResultCategories: cfg.ResultContentBlockedCategories,
ToolResultImages: cfg.ToolResultImages,
AssetsDir: filepath.Join(cfg.DataDir, "assets"),
ArchiveContent: cfg.ArchiveContent,
}
}
2 changes: 1 addition & 1 deletion desktop/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
}
],
"security": {
"csp": "default-src 'self' http://127.0.0.1:* http://localhost:*; script-src 'self' http://127.0.0.1:* http://localhost:*; connect-src 'self' http: https: ws: wss:; img-src 'self' data: http://127.0.0.1:* http://localhost:*; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com http://127.0.0.1:* http://localhost:*; font-src 'self' data: https://fonts.gstatic.com http://127.0.0.1:* http://localhost:*; object-src 'none'; base-uri 'none'; frame-ancestors 'none'"
"csp": "default-src 'self' http://127.0.0.1:* http://localhost:*; script-src 'self' http://127.0.0.1:* http://localhost:*; connect-src 'self' http: https: ws: wss:; img-src 'self' data: blob: http://127.0.0.1:* http://localhost:*; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com http://127.0.0.1:* http://localhost:*; font-src 'self' data: https://fonts.gstatic.com http://127.0.0.1:* http://localhost:*; object-src 'none'; base-uri 'none'; frame-ancestors 'none'"
}
},
"bundle": {
Expand Down
4 changes: 4 additions & 0 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -1638,3 +1638,7 @@ profile or pass them inline:
```bash
AGENTSVIEW_DATA_DIR=/tmp/av-test agentsview serve
```

### Ingest-time image offload

Set `tool_result_images = "offload"` to offload supported tool-result images on future ingestion. Run `agentsview db migrate --images` to retry retained inline images after an asset-write failure. The raw `agentsview session export` command continues to stream provider source bytes. See [image storage](/docs/data/#ingest-time-image-offload) for backup and remote-backend limits.
6 changes: 5 additions & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ zoom_level = 120
| `cursor_admin_user_id` | Optional default Cursor Admin usage filter by member user ID |
| `github_token` | Optional saved GitHub token for Gist publishing |
| `result_content_blocked_categories` | Tool categories whose result content is not stored (default: `["Read", "Glob"]`). Changes apply to new ingestion and full rebuilds; see [storage maintenance](/docs/data/#storage-maintenance) for existing source-backed sessions. |
| `tool_result_images` | Retain supported inline tool-result image blocks with `"keep"` (default), or store readable `agentsview_image` placeholders with `"drop"`. The setting affects future ingestion and full resyncs; run `db strip --images` for existing rows; also configurable under **Settings > Archive content**, and changes require a daemon restart. |
| `tool_result_images` | Retain supported inline tool-result image blocks with `"keep"` (default), or store readable `agentsview_image` placeholders with `"drop"`, or move supported images to the local asset store with `"offload"`. The setting affects future ingestion and full resyncs; run `db migrate --images` for existing rows; also configurable under **Settings > Archive content**, and changes require a daemon restart. |
| `archive_content` | How much of each session the archive stores: `"full"` (default), `"transcripts"`, or `"usage"`; changes require a daemon restart — see [Archive content](#archive-content) |
| `host` | Interface the server binds to (default `127.0.0.1`); non-loopback values require `require_auth = true` |
| `require_auth` | Require bearer-token authentication for API access |
Expand Down Expand Up @@ -1660,3 +1660,7 @@ Disable the CLI/web UI update check with any of:

The desktop app's auto-updater is controlled separately via
`AGENTSVIEW_DESKTOP_AUTOUPDATE=0`.

### Ingest-time image offload

`tool_result_images = "offload"` stores supported PNG, JPEG, WebP, and GIF tool-result images in `{dataDir}/assets/<sha256hex><ext>` before SQLite publishes their references. Restart the daemon to apply the setting. Failed writes keep inline content, and archives that omit tool content write no assets. See [image storage](/docs/data/#ingest-time-image-offload) for retries, backups, and remote-backend limits.
8 changes: 8 additions & 0 deletions docs/data.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,11 @@ reclaimed bytes separately; its result must not be combined with savings from
filtering or a future compression implementation. See
[`agentsview db compact`](/docs/commands/#agentsview-db-compact) for the staging
space model and interrupted-compaction recovery.

### Ingest-time image offload

Set `tool_result_images = "offload"` to move supported inline tool-result PNG, JPEG, WebP, and GIF payloads into `{dataDir}/assets/<sha256hex><ext>` during ingestion. Restart the daemon after changing the setting. Each asset write completes before SQLite can commit its `agentsview_image` placeholder and `image_ref`. Unsupported media and malformed data remain inline. Archives that omit tool content write no image assets. `keep` retains inline content; `drop` retains the existing readable placeholder without an asset.

If an asset write fails, ingestion and copied-session resync keep the original inline content. Retry retained inline payloads with `agentsview db migrate --images` after restoring access to the asset directory. Complete unreferenced objects from a failed database transaction remain available for reuse; the store does not automatically remove them. Back up the asset directory with the archive and copy both to another local serving host.

PostgreSQL and CockroachDB preserve the placeholder and reference text but cannot resolve the local asset. DuckDB, artifact exports, and the normalized Markdown server session export at `/api/v1/sessions/{id}/md` carry the stored content. The HTML export keeps its existing contract. The raw `agentsview session export` command streams provider source bytes, so its output retains the original inline payloads.
4 changes: 4 additions & 0 deletions docs/session-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1105,3 +1105,7 @@ When [PostgreSQL sync](/docs/pg-sync/) is enabled, the `secret_findings` table,
the session-level `secret_leak_count`, and the `--has-secret` filter all mirror
to the shared database. Substring and regex content search work the same way
against `pg serve`, with the same masking and `--reveal` constraints.

### Ingest-time image offload

The normalized Markdown endpoint `/api/v1/sessions/{id}/md` carries stored tool-result content, including `agentsview_image` placeholders and `image_ref` asset references. The HTML export keeps its existing contract. A serving host needs the matching `{dataDir}/assets` directory. PostgreSQL and CockroachDB retain the reference text but cannot resolve local assets. The raw `agentsview session export` command streams provider source bytes and retains their original inline payloads.
83 changes: 49 additions & 34 deletions frontend/e2e/message-content.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,9 @@ test.describe("Mixed content rendering", () => {
test.describe("retained tool images", () => {
test.describe.configure({ timeout: 60_000 });

test("renders retained, image-only, migrated, and fallback tool images", async ({ page }, testInfo) => {
test("renders retained, image-only, migrated, and fallback tool images", async ({
page,
}, testInfo) => {
const fixturePath = fileURLToPath(
new URL("../src/lib/utils/__fixtures__/retained-tool-image-1735.json", import.meta.url),
);
Expand Down Expand Up @@ -365,12 +367,15 @@ test.describe("retained tool images", () => {
}
await route.abort();
});
await page.route("**/api/v1/assets/first", (route) =>
route.fulfill({ body: assetBytes, contentType: "image/png" }),
);
await page.route("**/api/v1/assets/nested/second", (route) =>
route.fulfill({ body: assetBytes, contentType: "image/png" }),
);
await page.addInitScript(() => {
localStorage.setItem("agentsview-auth-token", "test-asset-token");
});
for (const assetPath of ["first", "nested%2Fsecond"]) {
await page.route(`**/api/v1/assets/${assetPath}`, async (route) => {
expect(route.request().headers()["authorization"]).toBe("Bearer test-asset-token");
await route.fulfill({ body: assetBytes, contentType: "image/png" });
});
}

await page.setViewportSize({ width: 1280, height: 900 });
await page.goto(`/sessions/${sessionId}`);
Expand Down Expand Up @@ -421,11 +426,15 @@ test.describe("retained tool images", () => {
const retainedImage = retainedFormatted.locator("img");
await expect(retainedImage).toHaveCount(1);
await expect(retainedImage).toHaveAttribute("src", retainedImageURL!);
await expect.poll(() => retainedImage.evaluate((img: HTMLImageElement) => ({
complete: img.complete,
naturalWidth: img.naturalWidth,
naturalHeight: img.naturalHeight,
}))).toEqual({ complete: true, naturalWidth: 600, naturalHeight: 600 });
await expect
.poll(() =>
retainedImage.evaluate((img: HTMLImageElement) => ({
complete: img.complete,
naturalWidth: img.naturalWidth,
naturalHeight: img.naturalHeight,
})),
)
.toEqual({ complete: true, naturalWidth: 600, naturalHeight: 600 });
const retainedText = await retainedFormatted.textContent();
expect(retainedText).not.toContain("input_image");
expect(retainedText).not.toContain(retainedImageURL!);
Expand All @@ -447,7 +456,9 @@ test.describe("retained tool images", () => {
expect(measurement.imageWidth).toBeLessThanOrEqual(measurement.clientWidth + 2);
console.log(`retained layout width=${width}px ${JSON.stringify(measurement)}`);
if (width === 1280) {
await retainedFormatted.screenshot({ path: testInfo.outputPath("agentsview-1735-after.png") });
await retainedFormatted.screenshot({
path: testInfo.outputPath("agentsview-1735-after.png"),
});
} else {
await retainedFormatted.screenshot({
path: testInfo.outputPath(
Expand All @@ -467,32 +478,36 @@ test.describe("retained tool images", () => {
const imageOnlyImage = imageOnly.formatted.locator("img");
await expect(imageOnlyImage).toHaveCount(1);
await expect(imageOnlyImage).toHaveAttribute("src", smallPNG);
await expect.poll(() => imageOnlyImage.evaluate((img: HTMLImageElement) => ({
complete: img.complete,
naturalWidth: img.naturalWidth,
naturalHeight: img.naturalHeight,
}))).toEqual({ complete: true, naturalWidth: 1, naturalHeight: 1 });
await expect
.poll(() =>
imageOnlyImage.evaluate((img: HTMLImageElement) => ({
complete: img.complete,
naturalWidth: img.naturalWidth,
naturalHeight: img.naturalHeight,
})),
)
.toEqual({ complete: true, naturalWidth: 1, naturalHeight: 1 });
expect(await imageOnly.formatted.textContent()).not.toContain("input_image");

const migrated = await openFormatted(migratedBlock);
const migratedImages = migrated.formatted.locator("img");
await expect(migratedImages).toHaveCount(2);
const migratedAssetBase = "/api/v1/assets";
await expect(migratedImages.nth(0)).toHaveAttribute("src", `${migratedAssetBase}/first`);
await expect(migratedImages.nth(1)).toHaveAttribute(
"src",
`${migratedAssetBase}/nested/second`,
);
await expect.poll(() => migratedImages.evaluateAll((images) =>
images.map((image) => ({
complete: (image as HTMLImageElement).complete,
naturalWidth: (image as HTMLImageElement).naturalWidth,
naturalHeight: (image as HTMLImageElement).naturalHeight,
})),
)).toEqual([
{ complete: true, naturalWidth: 1, naturalHeight: 1 },
{ complete: true, naturalWidth: 1, naturalHeight: 1 },
]);
await expect(migratedImages.nth(0)).toHaveAttribute("src", /^blob:/);
await expect(migratedImages.nth(1)).toHaveAttribute("src", /^blob:/);
await expect
.poll(() =>
migratedImages.evaluateAll((images) =>
images.map((image) => ({
complete: (image as HTMLImageElement).complete,
naturalWidth: (image as HTMLImageElement).naturalWidth,
naturalHeight: (image as HTMLImageElement).naturalHeight,
})),
),
)
.toEqual([
{ complete: true, naturalWidth: 1, naturalHeight: 1 },
{ complete: true, naturalWidth: 1, naturalHeight: 1 },
]);
expect(await migrated.formatted.textContent()).toContain("Before");
expect(await migrated.formatted.textContent()).toContain("After");
expect(await migrated.formatted.textContent()).not.toContain("asset://");
Expand Down
18 changes: 11 additions & 7 deletions frontend/e2e/settings-archive-content.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ test.describe("Settings archive content", () => {
const dropRadio = page.getByRole("radio", { name: "Drop" });
await expect(keepRadio).toHaveAttribute("aria-checked", "true");
await expect(dropRadio).toHaveAttribute("aria-checked", "false");
await expect(page.getByRole("radio", { name: "Offload" })).toHaveAttribute(
"aria-checked",
"false",
);
await expect(page.locator(".archive-content-settings").getByRole("status")).toHaveCount(0);
});

Expand Down Expand Up @@ -96,15 +100,15 @@ test.describe("Settings archive content", () => {
headers: { Origin: baseURL! },
data: { tool_result_images: "keep" },
});
// A silent restore failure would leave the daemon on drop and turn the
// A silent restore failure would leave the daemon on offload and turn the
// retry into a pass that asserts nothing.
expect(restored.ok()).toBe(true);
});

test("Drop selection persists after page reload", async ({ page }) => {
test("Offload selection persists after page reload", async ({ page }) => {
await openArchiveContent(page);

// Fails loudly if a prior restore left the daemon on drop, because
// Fails loudly if a prior restore left the daemon on offload, because
// clicking an already-selected option issues no PUT at all.
await expect(page.getByRole("radio", { name: "Keep" })).toHaveAttribute(
"aria-checked",
Expand All @@ -116,9 +120,9 @@ test.describe("Settings archive content", () => {
response.request().method() === "PUT" &&
new URL(response.url()).pathname === "/api/v1/settings",
);
await page.getByRole("radio", { name: "Drop" }).click();
expect(await (await putEcho).json()).toMatchObject({ tool_result_images: "drop" });
await expect(page.getByRole("radio", { name: "Drop" })).toHaveAttribute(
await page.getByRole("radio", { name: "Offload" }).click();
expect(await (await putEcho).json()).toMatchObject({ tool_result_images: "offload" });
await expect(page.getByRole("radio", { name: "Offload" })).toHaveAttribute(
"aria-checked",
"true",
);
Expand All @@ -127,7 +131,7 @@ test.describe("Settings archive content", () => {
const nav = page.getByRole("navigation", { name: "Settings" });
await nav.locator("button", { hasText: "Archive content" }).click();

await expect(page.getByRole("radio", { name: "Drop" })).toHaveAttribute(
await expect(page.getByRole("radio", { name: "Offload" })).toHaveAttribute(
"aria-checked",
"true",
);
Expand Down
3 changes: 2 additions & 1 deletion frontend/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2060,7 +2060,8 @@
"settings_tool_result_images": "Tool-result images",
"settings_tool_result_images_keep": "Keep",
"settings_tool_result_images_drop": "Drop",
"settings_tool_result_images_hint": "Stored results keep the policy they were written under, and switching back to Keep cannot restore payloads already removed. Run agentsview db strip --images to project existing rows.",
"settings_tool_result_images_offload": "Offload",
"settings_tool_result_images_hint": "Stored results keep the policy they were written under, and switching back to Keep cannot restore payloads already removed. Run agentsview db migrate --images to project existing rows.",
"settings_tool_result_images_restart_notice": "Restart the AgentsView daemon to apply this change to newly ingested sessions.",
"settings_tool_images_title": "Tool-result images",
"settings_tool_images_description": "Inline images stored inside tool results.",
Expand Down
3 changes: 2 additions & 1 deletion frontend/messages/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -2059,7 +2059,8 @@
"settings_tool_result_images": "Images des résultats d'outils",
"settings_tool_result_images_keep": "Conserver",
"settings_tool_result_images_drop": "Supprimer",
"settings_tool_result_images_hint": "Les résultats déjà stockés gardent la règle appliquée lors de leur écriture, et revenir à Conserver ne restaure pas les données déjà supprimées. Lancez agentsview db strip --images pour traiter les lignes existantes.",
"settings_tool_result_images_offload": "Stocker séparément",
"settings_tool_result_images_hint": "Les résultats déjà stockés gardent la règle appliquée lors de leur écriture, et revenir à Conserver ne restaure pas les données déjà supprimées. Lancez agentsview db migrate --images pour traiter les lignes existantes.",
"settings_tool_result_images_restart_notice": "Redémarrez le démon AgentsView pour appliquer ce changement aux nouvelles sessions ingérées.",
"settings_tool_images_title": "Images des résultats d'outils",
"settings_tool_images_description": "Images intégrées stockées dans les résultats d'outils.",
Expand Down
3 changes: 2 additions & 1 deletion frontend/messages/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -2060,7 +2060,8 @@
"settings_tool_result_images": "ツール結果の画像",
"settings_tool_result_images_keep": "保持",
"settings_tool_result_images_drop": "破棄",
"settings_tool_result_images_hint": "保存済みの結果は書き込み時のポリシーを保ち、保持に戻しても削除済みのデータは復元できません。既存の行は agentsview db strip --images で処理してください。",
"settings_tool_result_images_offload": "外部保存",
"settings_tool_result_images_hint": "保存済みの結果は書き込み時のポリシーを保ち、保持に戻しても削除済みのデータは復元できません。既存の行は agentsview db migrate --images で処理してください。",
"settings_tool_result_images_restart_notice": "この変更を新しく取り込むセッションに適用するには、AgentsView デーモンを再起動してください。",
"settings_tool_images_title": "ツール結果の画像",
"settings_tool_images_description": "ツール結果内に保存されたインライン画像。",
Expand Down
Loading
Loading