mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
feat(webworker): add selectable preview fixtures
This commit is contained in:
@@ -1,12 +1,14 @@
|
||||
/**
|
||||
* Worker-preview bootstrap: the one module preview.html adds ahead of the
|
||||
* stock entry tag. Connecting the worker host installs the boot globals and
|
||||
* settles `__DSH_BOOT_READY__`, where the stock entry's pre-boot await holds,
|
||||
* so everything after this module is the served startup chain verbatim. A
|
||||
* failed handshake rejects the deferred into the boot page's failure
|
||||
* rendering; this module owns no page painting.
|
||||
* stock entry tag. The runtime's optional source stage owns the pre-Cordis
|
||||
* chooser; the unchanged Host connector then owns the Worker handshake.
|
||||
* Everything after those calls is the served startup chain verbatim.
|
||||
*/
|
||||
import DshWorker from '@deepseek-ai/dsh-experimental-webworker-runtime/worker?worker'
|
||||
import { connectWorkerHost, IMAGE_FILE_NAME } from '@deepseek-ai/dsh-experimental-webworker-runtime/client'
|
||||
import {
|
||||
chooseWorkerHostSource, connectWorkerHost, IMAGE_FILE_NAME,
|
||||
} from '@deepseek-ai/dsh-experimental-webworker-runtime/client'
|
||||
|
||||
await connectWorkerHost(new DshWorker({ name: 'dsh-host' }), { image: `preview/${IMAGE_FILE_NAME}` })
|
||||
const image = `preview/${IMAGE_FILE_NAME}`
|
||||
const source = await chooseWorkerHostSource({ image })
|
||||
await connectWorkerHost(new DshWorker({ name: 'dsh-host' }), { image, overlays: source.overlays })
|
||||
|
||||
@@ -8,27 +8,33 @@
|
||||
* Two milestones prove that happened — the host's `tree active` boot line,
|
||||
* whose lowering contract must be the one this checkout's packer emits, and the
|
||||
* workspace hero, which paints only after the client tree comes up over the
|
||||
* tunnel. The same page then creates a Workspace and Session, lists skills,
|
||||
* and writes through the settings and credentials providers, exercising the
|
||||
* upstream Chokidar instances over the Worker filesystem implementation.
|
||||
* tunnel. The same page opens the seeded Workspace and showcase Session,
|
||||
* verifies its tool/subagent/history examples, then writes through the
|
||||
* settings and credentials providers. That keeps the upstream Chokidar
|
||||
* instances exercised over the Worker filesystem implementation.
|
||||
*
|
||||
* The site is served the way a static host serves it: bytes from `dist/` with
|
||||
* no rewrite rules, so a missing file is a 404 rather than the index page.
|
||||
*/
|
||||
import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { createServer } from 'node:http'
|
||||
import type { IncomingMessage, ServerResponse } from 'node:http'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { extname, join, normalize } from 'node:path'
|
||||
import { dirname, extname, join, normalize } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { chromium } from 'playwright'
|
||||
import type { Browser } from 'playwright'
|
||||
import { expect, it } from 'vitest'
|
||||
import {
|
||||
composeProfile, configTrees, indexWorkspacePackages, packVfsImage, WRAPPER_CONTRACT,
|
||||
composeProfile, configTrees, indexWorkspacePackages, packVfsImage, packVfsOverlay,
|
||||
previewFixtures, WRAPPER_CONTRACT,
|
||||
} from '@deepseek-ai/dsh-experimental-webworker-packer'
|
||||
import { IMAGE_FILE_NAME } from '@deepseek-ai/dsh-experimental-webworker-runtime'
|
||||
import {
|
||||
IMAGE_FILE_NAME, PREVIEW_FIXTURE_MANIFEST_FILE, PREVIEW_FIXTURE_MANIFEST_VERSION,
|
||||
type PreviewFixtureManifest,
|
||||
} from '@deepseek-ai/dsh-experimental-webworker-runtime'
|
||||
import { captureStableAria, compareOrRefreshGolden, webSnapshotMode } from './scaffold.ts'
|
||||
import { newEnglishPage, REPO_ROOT, saveFailureShot } from './support.ts'
|
||||
|
||||
const DIST_ROOT = fileURLToPath(new URL('../dist', import.meta.url))
|
||||
@@ -36,9 +42,22 @@ const DIST_ROOT = fileURLToPath(new URL('../dist', import.meta.url))
|
||||
/** Where the client looks for the image: the runtime's own name, beside the page. */
|
||||
const IMAGE_FILE = join(DIST_ROOT, 'preview', IMAGE_FILE_NAME)
|
||||
|
||||
/** Built-in source catalog read by the pre-boot chooser. */
|
||||
const FIXTURE_MANIFEST_FILE = join(DIST_ROOT, 'preview', PREVIEW_FIXTURE_MANIFEST_FILE)
|
||||
|
||||
/** Keyless browser golden for the pre-Worker source chooser. */
|
||||
const SOURCE_CHOOSER_EXPECTED = fileURLToPath(new URL('./snapshots/preview-boot/source-chooser.expected.md', import.meta.url))
|
||||
|
||||
const SNAPSHOT_MODE = webSnapshotMode()
|
||||
|
||||
/** Profile the preview deployment composes; `build:preview` packs the same one. */
|
||||
const PROFILE = 'web'
|
||||
|
||||
/** Stable labels authored by the deterministic VFS example fixture. */
|
||||
const SHOWCASE_TITLE = 'WebWorker Preview Showcase'
|
||||
const SHOWCASE_TAIL = 'Preview tour complete'
|
||||
const SHOWCASE_OLDEST = 'History checkpoint 01: verify deterministic preview state.'
|
||||
|
||||
/** Pages the preview needs; the Vite build emits both. */
|
||||
const PAGES = ['index.html', 'preview.html']
|
||||
|
||||
@@ -77,6 +96,12 @@ interface Site {
|
||||
close(): Promise<void>
|
||||
}
|
||||
|
||||
interface PreviewAssets {
|
||||
/** Static-host-relative path to a generated file outside `dist/`. */
|
||||
readonly overrides: ReadonlyMap<string, string>
|
||||
cleanup(): void
|
||||
}
|
||||
|
||||
/**
|
||||
* Fail before the browser opens a page the build never produced.
|
||||
* @throws When either preview page is missing from `dist/`.
|
||||
@@ -89,20 +114,25 @@ function requirePreviewPages(): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* The image file to serve, packed here when `dist/` carries none: `pnpm run
|
||||
* build` emits the pages but only `build:preview` packs, so this lane packs
|
||||
* for itself rather than skipping the deployment it is here to accept. An
|
||||
* image already in place is used as it stands — the worker refuses one lowered
|
||||
* against another wrapper contract, and that refusal names the rebuild. A
|
||||
* self-packed image lands in a temp directory, never in `dist/`: the
|
||||
* The base image, fixture manifest, and overlays to serve, packed here when
|
||||
* `dist/` does not carry the complete set: `pnpm run build` emits the pages but
|
||||
* only `build:preview` packs these files, so this lane packs for itself rather
|
||||
* than skipping the deployment it accepts. A complete built set is used as it
|
||||
* stands — the worker refuses a base lowered against another wrapper contract.
|
||||
* Self-packed files land in a temp directory, never in `dist/`: the
|
||||
* client-artifact digest record treats `dist/` as build-owned, so a test write
|
||||
* there fails the record check for every later consumer.
|
||||
* @returns The file to answer `preview/<image>` with, and its teardown.
|
||||
* @returns Static-path overrides and their teardown.
|
||||
* @throws When the closure leaves dependencies unresolved, which would pack an
|
||||
* incomplete image the tree fails on later and further from the cause.
|
||||
*/
|
||||
function requireVfsImage(): { path: string; cleanup(): void } {
|
||||
if (existsSync(IMAGE_FILE)) return { path: IMAGE_FILE, cleanup: () => {} }
|
||||
function requireVfsAssets(): PreviewAssets {
|
||||
const fixtureDefinitions = previewFixtures(REPO_ROOT)
|
||||
const fixtureFiles = fixtureDefinitions.map(fixture =>
|
||||
join(DIST_ROOT, 'preview', 'fixtures', `${fixture.id}.tar.gz`))
|
||||
if ([IMAGE_FILE, FIXTURE_MANIFEST_FILE, ...fixtureFiles].every(existsSync)) {
|
||||
return { overrides: new Map(), cleanup: () => {} }
|
||||
}
|
||||
const packed = packVfsImage({
|
||||
config: composeProfile(REPO_ROOT, PROFILE),
|
||||
profile: PROFILE,
|
||||
@@ -114,23 +144,48 @@ function requireVfsImage(): { path: string; cleanup(): void } {
|
||||
throw new Error(`preview boot: ${String(packed.missing.length)} dependencies did not resolve: ${packed.missing.join(', ')}`)
|
||||
}
|
||||
const directory = mkdtempSync(join(tmpdir(), 'dsh-preview-boot-'))
|
||||
const path = join(directory, IMAGE_FILE_NAME)
|
||||
writeFileSync(path, packed.image)
|
||||
return { path, cleanup: () => { rmSync(directory, { recursive: true, force: true }) } }
|
||||
const overrides = new Map<string, string>()
|
||||
const writeAsset = (relativePath: string, bytes: Uint8Array | string): void => {
|
||||
const path = join(directory, relativePath)
|
||||
mkdirSync(dirname(path), { recursive: true })
|
||||
writeFileSync(path, bytes)
|
||||
overrides.set(relativePath, path)
|
||||
}
|
||||
writeAsset(`preview/${IMAGE_FILE_NAME}`, packed.image)
|
||||
const fixtures = fixtureDefinitions.map((fixture) => {
|
||||
const relativePath = `preview/fixtures/${fixture.id}.tar.gz`
|
||||
writeAsset(relativePath, packVfsOverlay(fixture.trees).image)
|
||||
return {
|
||||
id: fixture.id,
|
||||
label: fixture.label,
|
||||
description: fixture.description,
|
||||
overlays: [`fixtures/${fixture.id}.tar.gz`],
|
||||
}
|
||||
})
|
||||
const manifest: PreviewFixtureManifest = {
|
||||
version: PREVIEW_FIXTURE_MANIFEST_VERSION,
|
||||
defaultFixture: fixtures[0]?.id ?? null,
|
||||
fixtures,
|
||||
}
|
||||
writeAsset(`preview/${PREVIEW_FIXTURE_MANIFEST_FILE}`, `${JSON.stringify(manifest, null, 2)}\n`)
|
||||
return { overrides, cleanup: () => { rmSync(directory, { recursive: true, force: true }) } }
|
||||
}
|
||||
|
||||
/**
|
||||
* Answer one request with the file it names under `dist/`; the image path
|
||||
* answers from wherever {@link requireVfsImage} put the file.
|
||||
* Answer one request with its generated override or the file under `dist/`.
|
||||
* @param request - Incoming request; only its path is read.
|
||||
* @param response - Response to write the bytes or the 404 to.
|
||||
* @param imagePath - File behind `preview/<image>`.
|
||||
* @param overrides - Generated deployment files used when `dist/` has none.
|
||||
*/
|
||||
async function respond(request: IncomingMessage, response: ServerResponse, imagePath: string): Promise<void> {
|
||||
async function respond(
|
||||
request: IncomingMessage,
|
||||
response: ServerResponse,
|
||||
overrides: ReadonlyMap<string, string>,
|
||||
): Promise<void> {
|
||||
const path = new URL(request.url ?? '/', 'http://127.0.0.1').pathname
|
||||
const relative = normalize(decodeURIComponent(path)).replace(/^\/+/, '')
|
||||
try {
|
||||
const body = await readFile(relative === `preview/${IMAGE_FILE_NAME}` ? imagePath : join(DIST_ROOT, relative))
|
||||
const body = await readFile(overrides.get(relative) ?? join(DIST_ROOT, relative))
|
||||
response.writeHead(200, { 'content-type': MIME[extname(relative)] ?? 'application/octet-stream' })
|
||||
response.end(body)
|
||||
} catch {
|
||||
@@ -144,11 +199,11 @@ async function respond(request: IncomingMessage, response: ServerResponse, image
|
||||
|
||||
/**
|
||||
* Serve `dist/` over loopback with static-host semantics.
|
||||
* @param imagePath - File behind `preview/<image>`.
|
||||
* @param overrides - Generated deployment files used when `dist/` has none.
|
||||
* @returns The origin to navigate, and its teardown.
|
||||
*/
|
||||
async function serveDist(imagePath: string): Promise<Site> {
|
||||
const server = createServer((request, response) => { void respond(request, response, imagePath) })
|
||||
async function serveDist(overrides: ReadonlyMap<string, string>): Promise<Site> {
|
||||
const server = createServer((request, response) => { void respond(request, response, overrides) })
|
||||
await new Promise<void>((listening) => { server.listen(0, '127.0.0.1', listening) })
|
||||
const address = server.address()
|
||||
if (address === null || typeof address === 'string') throw new Error('preview boot: the static server bound no port')
|
||||
@@ -188,12 +243,13 @@ async function within<T>(work: Promise<T>, ms: number, stalled: string): Promise
|
||||
|
||||
it('boots the packed worker deployment to an interactive page', async () => {
|
||||
requirePreviewPages()
|
||||
const image = requireVfsImage()
|
||||
const assets = requireVfsAssets()
|
||||
try {
|
||||
const site = await serveDist(image.path)
|
||||
const site = await serveDist(assets.overrides)
|
||||
try {
|
||||
const browser = await chromium.launch({ headless: true, args: ['--no-sandbox', '--disable-dev-shm-usage'] })
|
||||
try {
|
||||
await bootEmptyPreview(site.origin, browser)
|
||||
await bootPreview(site.origin, browser)
|
||||
} finally {
|
||||
await browser.close()
|
||||
@@ -202,7 +258,7 @@ it('boots the packed worker deployment to an interactive page', async () => {
|
||||
await site.close()
|
||||
}
|
||||
} finally {
|
||||
image.cleanup()
|
||||
assets.cleanup()
|
||||
}
|
||||
}, 600_000)
|
||||
|
||||
@@ -227,26 +283,34 @@ async function bootPreview(origin: string, browser: Browser): Promise<void> {
|
||||
})
|
||||
try {
|
||||
await page.goto(`${origin}/preview.html`, { waitUntil: 'domcontentloaded' })
|
||||
await page.getByRole('heading', { name: '选择 Preview 数据源' }).waitFor()
|
||||
expect(await page.locator('input[name="preview-source"][value="vfs-example"]').isChecked()).toBe(true)
|
||||
expect(await page.getByText('空白环境', { exact: true }).count()).toBe(1)
|
||||
expect(await page.getByText('WebFS 目录', { exact: true }).count()).toBe(1)
|
||||
expect(await page.locator('input[name="preview-source"][value="webfs"]').isDisabled()).toBe(true)
|
||||
expect(await page.getByRole('textbox', { name: 'Choose workspace' }).count()).toBe(0)
|
||||
await compareOrRefreshGolden(
|
||||
SOURCE_CHOOSER_EXPECTED,
|
||||
await captureStableAria(page, '[data-preview-source-card]', '/__preview_no_workspace__'),
|
||||
SNAPSHOT_MODE,
|
||||
)
|
||||
await page.getByRole('button', { name: '启动 Preview' }).click()
|
||||
const bootLine = await within(treeActive, BOOT_TIMEOUT_MS, `preview boot: the worker never reported "${TREE_ACTIVE}"`)
|
||||
// The activated tree ran bodies lowered against the contract this
|
||||
// checkout's packer emits; a dist built before a contract change would
|
||||
// report the older one.
|
||||
expect(bootLine).toContain(`image lowering=${WRAPPER_CONTRACT}`)
|
||||
expect(bootLine).toContain('data overlays=1')
|
||||
// The hero's workspace picker is the client tree's first interactive
|
||||
// surface, so it appears only once the startup chain completed over the
|
||||
// tunnel.
|
||||
await page.getByRole('textbox', { name: 'Choose workspace' }).waitFor({ timeout: HERO_TIMEOUT_MS })
|
||||
const continueButton = page.getByRole('button', { name: 'Continue' })
|
||||
if (await continueButton.isVisible()) await continueButton.click()
|
||||
await page.getByRole('button', { name: 'Configure later' }).click()
|
||||
await page.getByRole('textbox', { name: 'Choose workspace' }).click()
|
||||
const dialog = page.getByRole('dialog', { name: 'Select Workspace Directory' })
|
||||
await dialog.waitFor({ timeout: 10_000 })
|
||||
await dialog.getByRole('button', { name: 'Edit path' }).click()
|
||||
const pathInput = dialog.getByRole('textbox', { name: 'Edit path' })
|
||||
await pathInput.fill('/dsh/workspace')
|
||||
await pathInput.press('Enter')
|
||||
await dialog.getByRole('button', { name: 'Open', exact: true }).click()
|
||||
await continueButton.waitFor({ timeout: 30_000 })
|
||||
await continueButton.click()
|
||||
const configureLater = page.getByRole('button', { name: 'Configure later' })
|
||||
await configureLater.waitFor({ timeout: 30_000 })
|
||||
await configureLater.click()
|
||||
await page.locator('textarea:enabled[placeholder="Describe what you want to build"]')
|
||||
.waitFor({ timeout: 30_000 })
|
||||
|
||||
@@ -296,9 +360,7 @@ async function bootPreview(origin: string, browser: Browser): Promise<void> {
|
||||
const refreshed = await api.skills.list({ sessionId })
|
||||
if (!refreshed.result.ok) throw new Error(`skill.list refresh failed: ${refreshed.result.error.message}`)
|
||||
}
|
||||
await createDirectory('/dsh/workspace', '.agents')
|
||||
await createDirectory('/dsh/workspace/.agents', 'skills')
|
||||
await createDirectory('/dsh/workspace/.agents/skills', 'placeholder')
|
||||
await createDirectory('/dsh/workspace/.agents/skills', 'runtime-created')
|
||||
const settings = await api.settings.describe({})
|
||||
if (!settings.result.ok) throw new Error(`settings.describe failed: ${settings.result.error.message}`)
|
||||
const shell = settings.result.value.namespaces.find(namespace => namespace.ns === 'shell')
|
||||
@@ -317,8 +379,31 @@ async function bootPreview(origin: string, browser: Browser): Promise<void> {
|
||||
credentialConfigured: credentials.result.value.credentials.PREVIEW_TEST_SECRET?.configured,
|
||||
}
|
||||
})
|
||||
expect(exercised.skillCount).toBeGreaterThanOrEqual(0)
|
||||
expect(exercised.skillCount).toBeGreaterThan(0)
|
||||
expect(exercised.credentialConfigured).toBe(true)
|
||||
|
||||
const sessions = page.getByRole('tree', { name: 'Sessions' })
|
||||
const showcase = sessions.getByRole('treeitem').filter({ hasText: SHOWCASE_TITLE })
|
||||
await expect.poll(() => showcase.count(), { timeout: 15_000 }).toBe(1)
|
||||
await showcase.click()
|
||||
await page.getByText(SHOWCASE_TAIL, { exact: true }).waitFor({ timeout: 30_000 })
|
||||
|
||||
expect(await page.getByText(SHOWCASE_OLDEST, { exact: true }).count()).toBe(0)
|
||||
await page.getByText('PREVIEW.md', { exact: true }).waitFor()
|
||||
await page.getByText('src/preview.ts', { exact: true }).waitFor()
|
||||
await page.getByText('Update to-do list', { exact: true }).waitFor()
|
||||
await page.getByText('Error: ENOENT: no such file, open missing.txt', { exact: true }).waitFor()
|
||||
|
||||
const subagents = page.getByRole('button', { name: '2 subagents' })
|
||||
await subagents.waitFor({ timeout: 15_000 })
|
||||
await subagents.hover()
|
||||
const catalog = page.getByRole('tree', { name: 'Subagent sessions' })
|
||||
await catalog.getByRole('treeitem', { name: /Review preview architecture/ }).waitFor()
|
||||
await catalog.getByRole('treeitem', { name: /Continue preview verification/ }).waitFor()
|
||||
await catalog.press('Escape')
|
||||
|
||||
await page.getByRole('button', { name: 'Load earlier', exact: true }).click()
|
||||
await page.getByText(SHOWCASE_OLDEST, { exact: true }).waitFor({ timeout: 15_000 })
|
||||
expect(pageErrors.map(error => error.message)).toEqual([])
|
||||
expect(consoleErrors.filter(line =>
|
||||
/watchFile|failed to watch|node-addon-landlock-run\.probe|sandbox backend is usable|SANDBOX_UNAVAILABLE/i.test(line))).toEqual([])
|
||||
@@ -329,3 +414,65 @@ async function bootPreview(origin: string, browser: Browser): Promise<void> {
|
||||
: new AggregateError([error, ...pageErrors], 'preview boot failed, with uncaught page errors')
|
||||
}
|
||||
}
|
||||
|
||||
/** Verify the chooser can boot the untouched base image and reach first-run UI. */
|
||||
async function bootEmptyPreview(origin: string, browser: Browser): Promise<void> {
|
||||
const page = await newEnglishPage(browser)
|
||||
const pageErrors: Error[] = []
|
||||
const consoleErrors: string[] = []
|
||||
const failedResponses: string[] = []
|
||||
page.on('pageerror', (error) => { pageErrors.push(error) })
|
||||
page.on('response', (response) => {
|
||||
if (response.status() >= 400) failedResponses.push(new URL(response.url()).pathname)
|
||||
})
|
||||
const treeActive = new Promise<string>((reported) => {
|
||||
page.on('console', (message) => {
|
||||
const text = message.text()
|
||||
if (text.includes(TREE_ACTIVE)) reported(text)
|
||||
if (message.type() === 'error' || message.type() === 'warning') consoleErrors.push(text)
|
||||
})
|
||||
})
|
||||
try {
|
||||
await page.goto(`${origin}/preview.html?preview-fixture=none`, { waitUntil: 'domcontentloaded' })
|
||||
expect(await page.getByRole('heading', { name: '选择 Preview 数据源' }).count()).toBe(0)
|
||||
const bootLine = await within(
|
||||
treeActive,
|
||||
BOOT_TIMEOUT_MS,
|
||||
`empty preview boot: the worker never reported "${TREE_ACTIVE}"`,
|
||||
)
|
||||
expect(bootLine).toContain(`image lowering=${WRAPPER_CONTRACT}`)
|
||||
expect(bootLine).toContain('data overlays=0')
|
||||
await page.getByRole('textbox', { name: 'Choose workspace' }).waitFor({ timeout: HERO_TIMEOUT_MS })
|
||||
const sessionCount = await page.evaluate(async () => {
|
||||
const transport = (globalThis as typeof globalThis & {
|
||||
__DSH_TRANSPORT__?: { fetch(input: string, init: RequestInit): Promise<Response> }
|
||||
}).__DSH_TRANSPORT__
|
||||
if (transport === undefined) throw new Error('empty preview transport is absent after boot')
|
||||
const response = await transport.fetch('/api/session/list', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
type: 'client-request', rpcId: 'empty-preview-session-list', method: 'session/list',
|
||||
payload: { args: { _request: {} } },
|
||||
}),
|
||||
})
|
||||
const body = await response.json() as {
|
||||
result: { ok: true; value: { items: unknown[] } } | { ok: false; error: { message: string } }
|
||||
}
|
||||
if (!body.result.ok) throw new Error(`empty session/list failed: ${body.result.error.message}`)
|
||||
return body.result.value.items.length
|
||||
})
|
||||
expect(sessionCount).toBe(0)
|
||||
expect(pageErrors.map(error => error.message)).toEqual([])
|
||||
expect(failedResponses).toEqual(['/plugins/events'])
|
||||
expect(consoleErrors.filter(line => !line.includes('Failed to load resource: the server responded with a status of 404')))
|
||||
.toEqual([])
|
||||
} catch (error) {
|
||||
await saveFailureShot(page, 'preview-boot-empty')
|
||||
throw pageErrors.length === 0
|
||||
? error
|
||||
: new AggregateError([error, ...pageErrors], 'empty preview boot failed, with uncaught page errors')
|
||||
} finally {
|
||||
await page.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
- form "选择 Preview 数据源":
|
||||
- heading "选择 Preview 数据源" [level=1]
|
||||
- paragraph: 数据会在 Worker 和应用启动前挂载;刷新页面可重新选择。
|
||||
- group "文件系统来源":
|
||||
- text: 文件系统来源
|
||||
- radio "空白环境 只加载基础运行时,用于验证首次启动与新建 Workspace。"
|
||||
- strong: 空白环境
|
||||
- text: 只加载基础运行时,用于验证首次启动与新建 Workspace。
|
||||
- radio "内置综合示例 示例 Workspace、工具卡、子代理与分页会话。" [checked]
|
||||
- strong: 内置综合示例
|
||||
- text: 示例 Workspace、工具卡、子代理与分页会话。
|
||||
- radio "WebFS 目录 需要用户授权的目录来源,将在 WebFS provider 接入后开放。" [disabled]
|
||||
- strong: WebFS 目录
|
||||
- text: 需要用户授权的目录来源,将在 WebFS provider 接入后开放。
|
||||
- button "启动 Preview"
|
||||
@@ -241,7 +241,8 @@
|
||||
"packages/experimental/webworker-runtime": {
|
||||
"entry": [
|
||||
"tests/**/*.spec.ts",
|
||||
"tests/compile/transform-corpus-check.ts"
|
||||
"tests/compile/transform-corpus-check.ts",
|
||||
"tests/fixtures/vfs-example/workspace/src/preview.ts"
|
||||
],
|
||||
"project": [
|
||||
"src/**/*.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-experimental-webworker-packer",
|
||||
"description": "Build-time packer for the browser runtime's VFS image: materializes a profile's package closure into one gzip-compressed tar the worker mounts, with every module body pre-transformed",
|
||||
"description": "Build-time packer for the browser runtime's base VFS image and ordered data-overlay archives",
|
||||
"version": "0.1.1-rc.2",
|
||||
"private": true,
|
||||
"repository": {
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Pack a VFS image from this repository: compose the profile, materialize the
|
||||
* closure, lower every module body, write the gzip-compressed tar.
|
||||
* Pack a Preview deployment from this repository: compose and lower the base
|
||||
* image, then write each named fixture overlay and their manifest.
|
||||
*
|
||||
* Usage: dsh-pack-vfs-image --out <file> [--profile web] [--root /dsh]
|
||||
* node --import tsx/esm src/bin.ts --out ../../apps/web/dist/preview/vfs-image.tar.gz
|
||||
* @module @deepseek-ai/dsh-experimental-webworker-packer/src/bin
|
||||
*/
|
||||
import { mkdirSync, writeFileSync } from 'node:fs'
|
||||
import { dirname, isAbsolute, resolve } from 'node:path'
|
||||
import { dirname, isAbsolute, join, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { packVfsImage } from './pack.ts'
|
||||
import { composeProfile, configTrees, describePack, indexWorkspacePackages } from './repository.ts'
|
||||
import {
|
||||
PREVIEW_FIXTURE_MANIFEST_FILE, PREVIEW_FIXTURE_MANIFEST_VERSION,
|
||||
type PreviewFixtureManifest,
|
||||
} from '@deepseek-ai/dsh-experimental-webworker-runtime'
|
||||
import { packVfsImage, packVfsOverlay } from './pack.ts'
|
||||
import {
|
||||
composeProfile, configTrees, describePack, indexWorkspacePackages, previewFixtures,
|
||||
} from './repository.ts'
|
||||
|
||||
/**
|
||||
* Read one `--flag value` pair.
|
||||
@@ -54,4 +60,30 @@ if (result.missing.length > 0) {
|
||||
|
||||
mkdirSync(dirname(outputFile), { recursive: true })
|
||||
writeFileSync(outputFile, result.image)
|
||||
process.stdout.write(describePack(result, repoRoot, outputFile).join('\n'))
|
||||
|
||||
const fixtureDefinitions = previewFixtures(repoRoot)
|
||||
const fixtureDirectory = join(dirname(outputFile), 'fixtures')
|
||||
mkdirSync(fixtureDirectory, { recursive: true })
|
||||
const fixtureLines: string[] = []
|
||||
const fixtures = fixtureDefinitions.map((fixture) => {
|
||||
const packed = packVfsOverlay(fixture.trees)
|
||||
const file = `fixtures/${fixture.id}.tar.gz`
|
||||
writeFileSync(join(dirname(outputFile), file), packed.image)
|
||||
fixtureLines.push(` fixture overlay ${fixture.id} (${String(packed.image.byteLength)} B compressed)`)
|
||||
return {
|
||||
id: fixture.id,
|
||||
label: fixture.label,
|
||||
description: fixture.description,
|
||||
overlays: [file],
|
||||
}
|
||||
})
|
||||
const manifest: PreviewFixtureManifest = {
|
||||
version: PREVIEW_FIXTURE_MANIFEST_VERSION,
|
||||
defaultFixture: fixtures[0]?.id ?? null,
|
||||
fixtures,
|
||||
}
|
||||
writeFileSync(
|
||||
join(dirname(outputFile), PREVIEW_FIXTURE_MANIFEST_FILE),
|
||||
`${JSON.stringify(manifest, null, 2)}\n`,
|
||||
)
|
||||
process.stdout.write([...describePack(result, repoRoot, outputFile), ...fixtureLines, ''].join('\n'))
|
||||
|
||||
@@ -7,9 +7,10 @@ export {
|
||||
type ImageFiles, type TransformOutcome,
|
||||
} from './transform-image.ts'
|
||||
export {
|
||||
CONFIG_PATH, DEFAULT_ROOT, MANIFEST_PATH, packVfsImage,
|
||||
type ConfigTree, type PackOptions, type PackResult,
|
||||
CONFIG_PATH, DEFAULT_ROOT, MANIFEST_PATH, packVfsImage, packVfsOverlay,
|
||||
type ConfigTree, type ImageTree, type PackOptions, type PackOverlayResult, type PackResult,
|
||||
} from './pack.ts'
|
||||
export {
|
||||
composeProfile, configTrees, describePack, indexWorkspacePackages,
|
||||
composeProfile, configTrees, describePack, indexWorkspacePackages, previewFixtures,
|
||||
type PreviewFixture,
|
||||
} from './repository.ts'
|
||||
|
||||
@@ -19,6 +19,7 @@ import { gzipSync } from 'node:zlib'
|
||||
import {
|
||||
lowerModuleSource, MemoryVfs, packTar, WorkerModuleLoader,
|
||||
DEFAULT_ROOT, IMAGE_CONFIG_PATH, IMAGE_EMPTY_DIRECTORIES, IMAGE_MANIFEST_PATH,
|
||||
IMAGE_OVERLAY_DIRECTORIES,
|
||||
} from '@deepseek-ai/dsh-experimental-webworker-runtime'
|
||||
import picomatch from 'picomatch'
|
||||
import yaml from 'js-yaml'
|
||||
@@ -52,12 +53,16 @@ const workspaceExcluded = picomatch([...EXCLUDE, ...EXCLUDE_WORKSPACE], { dot: t
|
||||
/** Page-asset matcher over image paths ({@link PAGE_ASSETS}). */
|
||||
const pageAsset = picomatch([...PAGE_ASSETS], { dot: true })
|
||||
|
||||
/** One directory tree to copy in verbatim beside the composition. */
|
||||
export interface ConfigTree {
|
||||
/** One directory tree to copy into the image at a caller-selected mount. */
|
||||
export interface ImageTree {
|
||||
/** Image path to mount it at, relative to the virtual root. */
|
||||
readonly mount: string
|
||||
/** Absolute source directory. */
|
||||
readonly directory: string
|
||||
}
|
||||
|
||||
/** One configuration tree whose plugin rows may extend the package roster. */
|
||||
export interface ConfigTree extends ImageTree {
|
||||
/**
|
||||
* Whether plugin names inside its `.yml` files join the materialization closure.
|
||||
* An agent preset mounts plugins the base composition never lists, and creating a
|
||||
@@ -119,6 +124,14 @@ export interface PackResult {
|
||||
readonly contract: string
|
||||
}
|
||||
|
||||
/** One deterministic data-overlay archive and its uncompressed entries. */
|
||||
export interface PackOverlayResult {
|
||||
/** Gzip-compressed ustar bytes consumed by the Worker host. */
|
||||
readonly image: Uint8Array
|
||||
/** Every path in the overlay before compression. */
|
||||
readonly files: ImageFiles
|
||||
}
|
||||
|
||||
const readJson = (file: string): Record<string, unknown> =>
|
||||
JSON.parse(readFileSync(file, 'utf8')) as Record<string, unknown>
|
||||
|
||||
@@ -204,20 +217,28 @@ function resolveDependency(fromDirectory: string, name: string): string | undefi
|
||||
|
||||
/**
|
||||
* Collect files under one directory. Traversal mechanics live here — nested
|
||||
* `node_modules` never mounts (the image is flat) and dot directories are
|
||||
* tooling residue at any depth — while every judgement call comes in through
|
||||
* `keep` (the {@link EXCLUDE} tables and the npm publish view).
|
||||
* package/config collection flattens nested `node_modules` and prunes dot
|
||||
* directories, while seed collection preserves every directory. Every file
|
||||
* judgement comes in through `keep` (the {@link EXCLUDE} tables and the npm
|
||||
* publish view, or an unconditional seed predicate).
|
||||
* @param root - Source directory.
|
||||
* @param into - Image entries to add to.
|
||||
* @param prefix - Image path prefix.
|
||||
* @param keep - Filter over root-relative paths.
|
||||
* @param preserveDirectories - Whether dot directories and nested `node_modules`
|
||||
* are ordinary fixture content rather than package-manager residue.
|
||||
*/
|
||||
function collectTree(root: string, into: ImageFiles, prefix: string, keep: (relativePath: string) => boolean): void {
|
||||
function collectTree(
|
||||
root: string,
|
||||
into: ImageFiles,
|
||||
prefix: string,
|
||||
keep: (relativePath: string) => boolean,
|
||||
preserveDirectories = false,
|
||||
): void {
|
||||
const walk = (directory: string): void => {
|
||||
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
||||
if (entry.isDirectory()) {
|
||||
if (entry.name === 'node_modules') continue
|
||||
if (entry.name.startsWith('.')) continue
|
||||
if (!preserveDirectories && (entry.name === 'node_modules' || entry.name.startsWith('.'))) continue
|
||||
walk(join(directory, entry.name))
|
||||
continue
|
||||
}
|
||||
@@ -621,3 +642,32 @@ export function packVfsImage(options: PackOptions): PackResult {
|
||||
contract: WRAPPER_CONTRACT,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pack opaque data trees into one ordered VFS overlay.
|
||||
*
|
||||
* Overlay mounts are restricted to the runtime-owned data directories, so an
|
||||
* overlay cannot replace configuration, the lowering manifest, or modules.
|
||||
* Files bypass package excludes and module reachability processing; later
|
||||
* trees replace earlier files at the same path.
|
||||
* @param trees - Absolute source directories and their data-directory mounts.
|
||||
* @returns Deterministic compressed archive plus its uncompressed entries.
|
||||
*/
|
||||
export function packVfsOverlay(trees: readonly ImageTree[]): PackOverlayResult {
|
||||
const files: ImageFiles = {}
|
||||
for (const tree of trees) {
|
||||
if (!existsSync(tree.directory)) {
|
||||
throw new Error(`vfs overlay: tree ${tree.mount} is missing at ${tree.directory}`)
|
||||
}
|
||||
const mount = tree.mount.replace(/^\.\//, '').replace(/\/$/, '')
|
||||
const first = mount.split('/')[0]
|
||||
if (mount === '' || first === undefined || !IMAGE_OVERLAY_DIRECTORIES.includes(first)
|
||||
|| mount.split('/').some(segment => segment === '' || segment === '.' || segment === '..')) {
|
||||
throw new Error(
|
||||
`vfs overlay: mount ${JSON.stringify(tree.mount)} must stay under ${IMAGE_OVERLAY_DIRECTORIES.join(' or ')}`,
|
||||
)
|
||||
}
|
||||
collectTree(tree.directory, files, mount, () => true, true)
|
||||
}
|
||||
return { image: compressImage(packTar(files)), files }
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync } from 'node
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join, relative } from 'node:path'
|
||||
import { DSH_HOME_ENV } from '@deepseek-ai/dsh-home-paths'
|
||||
import type { ConfigTree, PackResult } from './pack.ts'
|
||||
import type { ConfigTree, ImageTree, PackResult } from './pack.ts'
|
||||
|
||||
/**
|
||||
* Repository directories scanned for workspace and vendored packages. The
|
||||
@@ -28,6 +28,21 @@ const CLI_PACKAGE = 'apps/cli'
|
||||
/** Composition entry point: the `dsh` CLI, run from source. */
|
||||
const CLI_ENTRY = `${CLI_PACKAGE}/src/bin.ts`
|
||||
|
||||
/** Repository-owned deterministic filesystem content offered by the preview. */
|
||||
const PREVIEW_EXAMPLE_ROOT = 'packages/experimental/webworker-runtime/tests/fixtures/vfs-example'
|
||||
|
||||
/** One built-in Preview source and the trees packed into its overlay. */
|
||||
export interface PreviewFixture {
|
||||
/** URL/query-safe identifier. */
|
||||
readonly id: string
|
||||
/** User-facing chooser label. */
|
||||
readonly label: string
|
||||
/** User-facing chooser detail. */
|
||||
readonly description: string
|
||||
/** Opaque trees packed into this fixture's overlay archive. */
|
||||
readonly trees: readonly ImageTree[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Index every workspace and vendored package by name.
|
||||
* @param repoRoot - Absolute repository root.
|
||||
@@ -130,6 +145,23 @@ export function configTrees(repoRoot: string): ConfigTree[] {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Built-in filesystem fixtures offered by the repository preview.
|
||||
* Session and Workspace semantics remain opaque here; the owning runtime tests
|
||||
* validate those files through their production readers.
|
||||
* @param repoRoot - Absolute repository root.
|
||||
* @returns Named chooser entries and their overlay trees.
|
||||
*/
|
||||
export function previewFixtures(repoRoot: string): PreviewFixture[] {
|
||||
const root = join(repoRoot, PREVIEW_EXAMPLE_ROOT)
|
||||
return [{
|
||||
id: 'vfs-example',
|
||||
label: '内置综合示例',
|
||||
description: '示例 Workspace、工具卡、子代理与分页会话。',
|
||||
trees: ['home', 'workspace'].map(mount => ({ mount, directory: join(root, mount) })),
|
||||
}]
|
||||
}
|
||||
|
||||
/**
|
||||
* Render one pack as the lines a build log should carry.
|
||||
*
|
||||
|
||||
@@ -26,8 +26,8 @@ import {
|
||||
} from '@deepseek-ai/dsh-experimental-webworker-runtime/src/module-system/module-loader.ts'
|
||||
import { inflateImage } from '@deepseek-ai/dsh-experimental-webworker-runtime/src/storage/image-gzip.ts'
|
||||
import { loadVfsImage } from '@deepseek-ai/dsh-experimental-webworker-runtime/src/storage/memory.ts'
|
||||
import { indexWorkspacePackages } from '../src/repository.ts'
|
||||
import { DEFAULT_ROOT, MANIFEST_PATH, packVfsImage } from '../src/pack.ts'
|
||||
import { indexWorkspacePackages, previewFixtures } from '../src/repository.ts'
|
||||
import { DEFAULT_ROOT, MANIFEST_PATH, packVfsImage, packVfsOverlay } from '../src/pack.ts'
|
||||
|
||||
const repoRoot = fileURLToPath(new URL('../../../../', import.meta.url))
|
||||
|
||||
@@ -37,6 +37,32 @@ const LANDLOCK = '@deepseek-ai/node-addon-landlock-run'
|
||||
|
||||
const workspaces = indexWorkspacePackages(repoRoot)
|
||||
|
||||
describe('preview example overlays', () => {
|
||||
it('packs source-looking paths and dot directories into a separate overlay', () => {
|
||||
const fixture = previewFixtures(repoRoot)[0]
|
||||
expect(fixture?.id).toBe('vfs-example')
|
||||
const result = packVfsOverlay(fixture?.trees ?? [])
|
||||
expect(new TextDecoder().decode(result.files['workspace/src/preview.ts']))
|
||||
.toContain("previewStatus = 'ready'")
|
||||
expect(new TextDecoder().decode(result.files['workspace/.agents/skills/preview-tour/SKILL.md']))
|
||||
.toContain('name: preview-tour')
|
||||
expect(Object.keys(result.files).filter(path => path.endsWith('/session.jsonl'))).toHaveLength(3)
|
||||
})
|
||||
|
||||
it('fails loud when a declared seed tree is absent', () => {
|
||||
expect(() => packVfsOverlay([
|
||||
{ mount: 'workspace', directory: join(repoRoot, 'missing-preview-seed') },
|
||||
])).toThrow(/tree workspace is missing/)
|
||||
})
|
||||
|
||||
it('refuses overlays that could replace runtime files', () => {
|
||||
const fixture = previewFixtures(repoRoot)[0]
|
||||
expect(() => packVfsOverlay([
|
||||
{ mount: 'config', directory: fixture?.trees[0]?.directory ?? repoRoot },
|
||||
])).toThrow(/must stay under home or workspace/)
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* The pack consumes built `lib/` output. An unbuilt checkout (the unit
|
||||
* coverage lane runs before any build) self-skips; the built lanes and every
|
||||
|
||||
@@ -57,6 +57,9 @@
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-sandbox-local": "workspace:^",
|
||||
"@deepseek-ai/dsh-sandbox-policy": "workspace:^",
|
||||
"@deepseek-ai/dsh-session": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^",
|
||||
"@deepseek-ai/dsh-subagent": "workspace:^",
|
||||
"@deepseek-ai/dsh-subprocess-local": "workspace:^",
|
||||
"@deepseek-ai/node-addon-landlock-run": "workspace:^",
|
||||
"@types/picomatch": "^3.0.2",
|
||||
|
||||
@@ -156,9 +156,10 @@ export class WorkerTunnel {
|
||||
/**
|
||||
* Open the tunnel: the worker assembles its host from this frame.
|
||||
* @param image - VFS image URL the worker fetches.
|
||||
* @param overlays - Ordered data overlay URLs applied before boot.
|
||||
*/
|
||||
init(image: string): void {
|
||||
this.worker.postMessage({ t: 'init', image })
|
||||
init(image: string, overlays: readonly string[] = []): void {
|
||||
this.worker.postMessage({ t: 'init', image, overlays })
|
||||
}
|
||||
|
||||
/** Fetch-shaped entry: one request frame, one Response (streamed when the worker streams). */
|
||||
|
||||
@@ -9,14 +9,20 @@
|
||||
* @module @deepseek-ai/dsh-experimental-webworker-runtime/client
|
||||
*/
|
||||
import { IMAGE_FILE_NAME } from '../image-layout.ts'
|
||||
import { PREVIEW_FIXTURE_MANIFEST_FILE } from '../fixture-manifest.ts'
|
||||
import { WorkerApiClient } from './api-client.ts'
|
||||
import { WorkerTunnel, type TunnelFetch } from './client.ts'
|
||||
import { applyIndexInjections } from './apply-injections.ts'
|
||||
import { choosePreviewSource } from './source-chooser.ts'
|
||||
|
||||
export { WorkerApiClient } from './api-client.ts'
|
||||
export { WorkerTunnel, type TunnelFetch } from './client.ts'
|
||||
export { applyIndexInjections } from './apply-injections.ts'
|
||||
export { IMAGE_FILE_NAME } from '../image-layout.ts'
|
||||
export {
|
||||
parsePreviewFixtureManifest, PREVIEW_FIXTURE_MANIFEST_FILE, PREVIEW_FIXTURE_MANIFEST_VERSION,
|
||||
type PreviewFixtureManifest, type PreviewFixtureManifestEntry,
|
||||
} from '../fixture-manifest.ts'
|
||||
|
||||
/** Transport global the connection plugin reads instead of building an HTTP carrier. */
|
||||
interface ClientTransportGlobal {
|
||||
@@ -35,9 +41,25 @@ export interface WorkerHostConnectOptions {
|
||||
/**
|
||||
* VFS image URL, the one deployment-shaped input. Defaults to
|
||||
* {@link IMAGE_FILE_NAME} beside the page; a deployment that packs the
|
||||
* image elsewhere passes its own URL.
|
||||
* image elsewhere passes its own URL. Data overlays are independent.
|
||||
*/
|
||||
readonly image?: string | URL
|
||||
/** Ordered data overlay URLs, resolved against the page like the base image. */
|
||||
readonly overlays?: readonly (string | URL)[]
|
||||
}
|
||||
|
||||
/** Inputs for the optional pre-boot filesystem-source chooser. */
|
||||
export interface WorkerHostSourceOptions {
|
||||
/** Base VFS image URL; defaults to {@link IMAGE_FILE_NAME} beside the page. */
|
||||
readonly image?: string | URL
|
||||
/** Fixture catalog URL; defaults to {@link PREVIEW_FIXTURE_MANIFEST_FILE} beside the image. */
|
||||
readonly fixtureManifest?: string | URL
|
||||
}
|
||||
|
||||
/** Filesystem inputs selected before {@link connectWorkerHost}. */
|
||||
export interface WorkerHostSource {
|
||||
/** Ordered data overlays to pass through unchanged to the Host connection. */
|
||||
readonly overlays: readonly URL[]
|
||||
}
|
||||
|
||||
/** A page connected to a worker-hosted harness, ready to run a shell entry. */
|
||||
@@ -53,12 +75,51 @@ interface BootReadyGlobal {
|
||||
__DSH_BOOT_READY__?: PromiseWithResolvers<void>
|
||||
}
|
||||
|
||||
function bootReadyGate(): PromiseWithResolvers<void> {
|
||||
return (globalThis as BootReadyGlobal).__DSH_BOOT_READY__ ??= Promise.withResolvers<void>()
|
||||
}
|
||||
|
||||
/**
|
||||
* Install the page boot barrier before an asynchronous source chooser waits
|
||||
* for user input. The later {@link connectWorkerHost} call settles the same
|
||||
* barrier.
|
||||
*/
|
||||
function holdWorkerHostBoot(): void {
|
||||
const ready = bootReadyGate()
|
||||
// A chooser may remain open indefinitely; if a later connection fails before
|
||||
// the stock entry subscribes, retain the rejection without browser noise.
|
||||
void ready.promise.catch(() => {})
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the optional pre-boot source-selection stage. Calling this stage holds
|
||||
* the stock shell until the caller passes its result to {@link connectWorkerHost};
|
||||
* callers that need no chooser call `connectWorkerHost` directly and receive
|
||||
* the base image with an empty overlay list.
|
||||
* @param options - Base image and optional fixture-catalog locations.
|
||||
* @returns The ordered overlays selected by the user.
|
||||
*/
|
||||
export async function chooseWorkerHostSource(
|
||||
options: WorkerHostSourceOptions = {},
|
||||
): Promise<WorkerHostSource> {
|
||||
holdWorkerHostBoot()
|
||||
const image = new URL(options.image ?? IMAGE_FILE_NAME, document.baseURI)
|
||||
const manifest = new URL(options.fixtureManifest ?? PREVIEW_FIXTURE_MANIFEST_FILE, image)
|
||||
try {
|
||||
const overlays = await choosePreviewSource(manifest)
|
||||
return { overlays }
|
||||
} catch (reason) {
|
||||
bootReadyGate().reject(reason)
|
||||
throw reason
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect a spawned host worker and complete the pre-Cordis handshake.
|
||||
*
|
||||
* The caller constructs the Worker so its bundler resolves the bundle URL
|
||||
* statically; the opening `init` frame then carries the image location, the
|
||||
* only input the worker takes from outside.
|
||||
* statically; the opening `init` frame then carries the base image and ordered
|
||||
* overlay locations.
|
||||
*
|
||||
* Order is fixed by the web boot protocol: the transport global must exist
|
||||
* before any bundle executes; the injection table then reproduces the served
|
||||
@@ -70,17 +131,20 @@ interface BootReadyGlobal {
|
||||
* row has taken effect, and surfaces a failed handshake instead of
|
||||
* proceeding on missing globals.
|
||||
* @param worker - The host worker.
|
||||
* @param options - Image location override.
|
||||
* @param options - Base-image and overlay location overrides.
|
||||
* @returns The connection; hand `loadBundle` to the shell entry's boot seam.
|
||||
*/
|
||||
export async function connectWorkerHost(worker: Worker, options?: WorkerHostConnectOptions): Promise<WorkerHostConnection> {
|
||||
const ready = (globalThis as BootReadyGlobal).__DSH_BOOT_READY__ ??= Promise.withResolvers<void>()
|
||||
const ready = bootReadyGate()
|
||||
// The handshake may fail before any entry awaits the promise; this no-op
|
||||
// subscription keeps that from surfacing as an unhandled rejection.
|
||||
void ready.promise.catch(() => {})
|
||||
try {
|
||||
const tunnel = new WorkerTunnel(worker)
|
||||
tunnel.init(new URL(options?.image ?? IMAGE_FILE_NAME, document.baseURI).href)
|
||||
tunnel.init(
|
||||
new URL(options?.image ?? IMAGE_FILE_NAME, document.baseURI).href,
|
||||
(options?.overlays ?? []).map(overlay => new URL(overlay, document.baseURI).href),
|
||||
)
|
||||
const payload = await tunnel.bootPayload()
|
||||
;(globalThis as ClientTransportGlobal).__DSH_TRANSPORT__ = {
|
||||
createApiClient: () => new WorkerApiClient(tunnel),
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
/** Pre-boot filesystem-source chooser for static WebWorker previews. */
|
||||
|
||||
import {
|
||||
parsePreviewFixtureManifest, type PreviewFixtureManifestEntry,
|
||||
} from '../fixture-manifest.ts'
|
||||
|
||||
const EMPTY_SOURCE = 'none'
|
||||
const WEBFS_SOURCE = 'webfs'
|
||||
const PREVIEW_FIXTURE_QUERY = 'preview-fixture'
|
||||
|
||||
interface PreviewSourceChoice {
|
||||
readonly id: string
|
||||
readonly label: string
|
||||
readonly description: string
|
||||
readonly overlays: readonly URL[]
|
||||
readonly disabled?: boolean
|
||||
}
|
||||
|
||||
const CHOOSER_STYLE = `
|
||||
:root { color-scheme: light dark; }
|
||||
body { margin: 0; }
|
||||
[data-preview-source-chooser] {
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 24px;
|
||||
box-sizing: border-box;
|
||||
color: #171717;
|
||||
background: radial-gradient(circle at 50% 35%, #eef4ff 0, #f8fafc 42%, #f3f4f6 100%);
|
||||
font: 14px/1.5 ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
}
|
||||
[data-preview-source-card] {
|
||||
width: min(560px, 100%);
|
||||
box-sizing: border-box;
|
||||
padding: 28px;
|
||||
border: 1px solid #d8dee9;
|
||||
border-radius: 20px;
|
||||
background: rgba(255, 255, 255, 0.94);
|
||||
box-shadow: 0 20px 60px rgba(15, 23, 42, 0.12);
|
||||
}
|
||||
[data-preview-source-card] h1 { margin: 0 0 6px; font-size: 24px; line-height: 1.25; }
|
||||
[data-preview-source-card] > p { margin: 0 0 22px; color: #5b6472; }
|
||||
[data-preview-source-card] fieldset { display: grid; gap: 10px; margin: 0; padding: 0; border: 0; }
|
||||
[data-preview-source-card] legend { margin-bottom: 10px; font-weight: 650; }
|
||||
[data-preview-source-option] {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 2px 12px;
|
||||
padding: 14px;
|
||||
border: 1px solid #d8dee9;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
[data-preview-source-option]:has(input:checked) { border-color: #4777df; background: #edf3ff; }
|
||||
[data-preview-source-option]:has(input:disabled) { cursor: not-allowed; opacity: 0.55; }
|
||||
[data-preview-source-option] input { grid-row: 1 / span 2; margin: 4px 0 0; }
|
||||
[data-preview-source-option] strong { font-size: 15px; }
|
||||
[data-preview-source-option] span { color: #667085; }
|
||||
[data-preview-source-submit] {
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
padding: 11px 16px;
|
||||
border: 0;
|
||||
border-radius: 10px;
|
||||
color: white;
|
||||
background: #315fc7;
|
||||
font: inherit;
|
||||
font-weight: 650;
|
||||
cursor: pointer;
|
||||
}
|
||||
[data-preview-source-submit]:disabled { cursor: not-allowed; opacity: 0.5; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
[data-preview-source-chooser] { color: #f4f4f5; background: radial-gradient(circle at 50% 35%, #172554 0, #111827 45%, #09090b 100%); }
|
||||
[data-preview-source-card] { border-color: #374151; background: rgba(24, 24, 27, 0.96); box-shadow: 0 20px 60px rgba(0, 0, 0, 0.35); }
|
||||
[data-preview-source-card] > p, [data-preview-source-option] span { color: #a1a1aa; }
|
||||
[data-preview-source-option] { border-color: #3f3f46; }
|
||||
[data-preview-source-option]:has(input:checked) { border-color: #7aa2ff; background: #172554; }
|
||||
}
|
||||
`
|
||||
|
||||
const ENTITIES: Readonly<Record<string, string>> = {
|
||||
'&': '&', '<': '<', '>': '>', '"': '"', "'": ''',
|
||||
}
|
||||
|
||||
function escapeMarkup(value: string): string {
|
||||
return value.replace(/[&<>"']/g, character => ENTITIES[character] ?? character)
|
||||
}
|
||||
|
||||
function optionMarkup(choice: PreviewSourceChoice, selected: string): string {
|
||||
return `<label data-preview-source-option>
|
||||
<input type="radio" name="preview-source" value="${choice.id}"${choice.id === selected ? ' checked' : ''}${choice.disabled === true ? ' disabled' : ''}>
|
||||
<strong>${escapeMarkup(choice.label)}</strong>
|
||||
<span>${escapeMarkup(choice.description)}</span>
|
||||
</label>`
|
||||
}
|
||||
|
||||
function fixtureChoices(entries: readonly PreviewFixtureManifestEntry[], manifestUrl: URL): PreviewSourceChoice[] {
|
||||
return entries.map(entry => ({
|
||||
id: entry.id,
|
||||
label: entry.label,
|
||||
description: entry.description,
|
||||
overlays: entry.overlays.map(overlay => new URL(overlay, manifestUrl)),
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the source chooser and wait for an enabled selection.
|
||||
* @param manifestUrl - Built-in fixture catalog URL.
|
||||
* @returns Ordered overlay URLs selected for the Worker mount.
|
||||
*/
|
||||
export async function choosePreviewSource(manifestUrl: URL): Promise<readonly URL[]> {
|
||||
const requested = new URL(location.href).searchParams.get(PREVIEW_FIXTURE_QUERY)
|
||||
if (requested === EMPTY_SOURCE) return []
|
||||
|
||||
const response = await fetch(manifestUrl)
|
||||
if (!response.ok) {
|
||||
throw new Error(`preview source chooser: fixture manifest returned ${String(response.status)}`)
|
||||
}
|
||||
const manifest = parsePreviewFixtureManifest(await response.json())
|
||||
const choices: PreviewSourceChoice[] = [
|
||||
{
|
||||
id: EMPTY_SOURCE,
|
||||
label: '空白环境',
|
||||
description: '只加载基础运行时,用于验证首次启动与新建 Workspace。',
|
||||
overlays: [],
|
||||
},
|
||||
...fixtureChoices(manifest.fixtures, manifestUrl),
|
||||
{
|
||||
id: WEBFS_SOURCE,
|
||||
label: 'WebFS 目录',
|
||||
description: '需要用户授权的目录来源,将在 WebFS provider 接入后开放。',
|
||||
overlays: [],
|
||||
disabled: true,
|
||||
},
|
||||
]
|
||||
if (requested !== null) {
|
||||
const requestedChoice = choices.find(choice => choice.id === requested && choice.disabled !== true)
|
||||
if (requestedChoice === undefined) {
|
||||
throw new Error(`preview source chooser: unknown or interactive source "${requested}"`)
|
||||
}
|
||||
return requestedChoice.overlays
|
||||
}
|
||||
|
||||
const root = document.getElementById('root')
|
||||
if (root === null) throw new Error('preview source chooser: missing #root')
|
||||
const selected = manifest.defaultFixture ?? EMPTY_SOURCE
|
||||
const style = document.createElement('style')
|
||||
style.dataset.previewSourceStyle = ''
|
||||
style.textContent = CHOOSER_STYLE
|
||||
document.head.append(style)
|
||||
|
||||
root.innerHTML = `<main data-preview-source-chooser>
|
||||
<form data-preview-source-card aria-labelledby="preview-source-title">
|
||||
<h1 id="preview-source-title">选择 Preview 数据源</h1>
|
||||
<p>数据会在 Worker 和应用启动前挂载;刷新页面可重新选择。</p>
|
||||
<fieldset>
|
||||
<legend>文件系统来源</legend>
|
||||
${choices.map(choice => optionMarkup(choice, selected)).join('')}
|
||||
</fieldset>
|
||||
<button data-preview-source-submit type="submit">启动 Preview</button>
|
||||
</form>
|
||||
</main>`
|
||||
const form = root.querySelector<HTMLFormElement>('[data-preview-source-card]')
|
||||
if (form === null) throw new Error('preview source chooser: form was not rendered')
|
||||
const sourceId = await new Promise<string>((resolve, reject) => {
|
||||
form.addEventListener('submit', (event) => {
|
||||
event.preventDefault()
|
||||
const value = new FormData(form).get('preview-source')
|
||||
if (typeof value === 'string') resolve(value)
|
||||
else reject(new Error('preview source chooser: no source selected'))
|
||||
}, { once: true })
|
||||
})
|
||||
const choice = choices.find(candidate => candidate.id === sourceId && candidate.disabled !== true)
|
||||
if (choice === undefined) throw new Error(`preview source chooser: unavailable source "${sourceId}"`)
|
||||
root.replaceChildren()
|
||||
style.remove()
|
||||
return choice.overlays
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/** Browser-readable catalog of built-in Preview filesystem overlays. */
|
||||
|
||||
/** Manifest format version emitted beside the base VFS image. */
|
||||
export const PREVIEW_FIXTURE_MANIFEST_VERSION = 1
|
||||
|
||||
/** Leaf name resolved beside the base image. */
|
||||
export const PREVIEW_FIXTURE_MANIFEST_FILE = 'fixtures.json'
|
||||
|
||||
/** One selectable built-in fixture and its ordered overlay archives. */
|
||||
export interface PreviewFixtureManifestEntry {
|
||||
readonly id: string
|
||||
readonly label: string
|
||||
readonly description: string
|
||||
readonly overlays: readonly string[]
|
||||
}
|
||||
|
||||
/** Complete built-in fixture catalog consumed before Worker startup. */
|
||||
export interface PreviewFixtureManifest {
|
||||
readonly version: number
|
||||
readonly defaultFixture: string | null
|
||||
readonly fixtures: readonly PreviewFixtureManifestEntry[]
|
||||
}
|
||||
|
||||
function recordOf(value: unknown): Record<string, unknown> | undefined {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
? value as Record<string, unknown>
|
||||
: undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the static fixture catalog before it controls Worker fetches.
|
||||
* @param value - Parsed JSON response.
|
||||
* @returns A detached manifest with unique ids and non-empty overlay lists.
|
||||
*/
|
||||
export function parsePreviewFixtureManifest(value: unknown): PreviewFixtureManifest {
|
||||
const record = recordOf(value)
|
||||
if (record?.version !== PREVIEW_FIXTURE_MANIFEST_VERSION || !Array.isArray(record.fixtures)) {
|
||||
throw new Error(`preview fixture manifest must use version ${String(PREVIEW_FIXTURE_MANIFEST_VERSION)}`)
|
||||
}
|
||||
const fixtures: PreviewFixtureManifestEntry[] = []
|
||||
const ids = new Set<string>()
|
||||
for (const value of record.fixtures) {
|
||||
const fixture = recordOf(value)
|
||||
const id = fixture?.id
|
||||
const label = fixture?.label
|
||||
const description = fixture?.description
|
||||
const overlays = fixture?.overlays
|
||||
const overlayUrls = Array.isArray(overlays)
|
||||
? overlays.filter((overlay): overlay is string => typeof overlay === 'string' && overlay.length > 0)
|
||||
: []
|
||||
if (typeof id !== 'string' || !/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(id)
|
||||
|| id === 'none' || id === 'webfs'
|
||||
|| typeof label !== 'string' || label.length === 0
|
||||
|| typeof description !== 'string' || description.length === 0
|
||||
|| !Array.isArray(overlays) || overlays.length === 0 || overlayUrls.length !== overlays.length) {
|
||||
throw new Error('preview fixture manifest contains an invalid fixture entry')
|
||||
}
|
||||
if (ids.has(id)) throw new Error(`preview fixture manifest repeats id "${id}"`)
|
||||
ids.add(id)
|
||||
fixtures.push({ id, label, description, overlays: overlayUrls })
|
||||
}
|
||||
const defaultFixture = record.defaultFixture
|
||||
if (defaultFixture !== null && (typeof defaultFixture !== 'string' || !ids.has(defaultFixture))) {
|
||||
throw new Error('preview fixture manifest defaultFixture does not name a fixture')
|
||||
}
|
||||
return { version: PREVIEW_FIXTURE_MANIFEST_VERSION, defaultFixture, fixtures }
|
||||
}
|
||||
@@ -9,9 +9,9 @@
|
||||
export const DEFAULT_ROOT = '/dsh'
|
||||
|
||||
/**
|
||||
* Leaf name of the packed image: one gzip member holding the ustar archive. The
|
||||
* app build writes it beside the page and the page's boot fetches it from there,
|
||||
* so the extension is part of what a deployment serves.
|
||||
* Leaf name of the packed base image: one gzip member holding the ustar archive.
|
||||
* The app build writes it beside the page and the page's boot fetches it from
|
||||
* there, so the extension is part of what a deployment serves.
|
||||
*/
|
||||
export const IMAGE_FILE_NAME = 'vfs-image.tar.gz'
|
||||
|
||||
@@ -27,6 +27,12 @@ export const IMAGE_HOME_DIRECTORY = 'home'
|
||||
/** Working directories the host tree expects to exist, empty. */
|
||||
export const IMAGE_EMPTY_DIRECTORIES: readonly string[] = ['home/', 'workspace/', 'tmp/']
|
||||
|
||||
/**
|
||||
* Top-level directories an overlay archive may populate. Runtime code,
|
||||
* configuration, and the lowering manifest remain owned by the base image.
|
||||
*/
|
||||
export const IMAGE_OVERLAY_DIRECTORIES: readonly string[] = ['home', 'workspace']
|
||||
|
||||
/**
|
||||
* Identity of the lowered code shape, recorded in the image manifest by the
|
||||
* packer and required by the worker host: an image lowered by an older transform
|
||||
|
||||
@@ -34,9 +34,13 @@ export {
|
||||
} from './worker-host.ts'
|
||||
export {
|
||||
DEFAULT_ROOT, IMAGE_CONFIG_PATH, IMAGE_EMPTY_DIRECTORIES, IMAGE_FILE_NAME, IMAGE_HOME_DIRECTORY,
|
||||
IMAGE_MANIFEST_PATH, LOWERING_VERSION, WRAPPER_PARAMS,
|
||||
IMAGE_MANIFEST_PATH, IMAGE_OVERLAY_DIRECTORIES, LOWERING_VERSION, WRAPPER_PARAMS,
|
||||
} from './image-layout.ts'
|
||||
export { loadVfsImage, MemoryVfs } from './storage/memory.ts'
|
||||
export {
|
||||
parsePreviewFixtureManifest, PREVIEW_FIXTURE_MANIFEST_FILE, PREVIEW_FIXTURE_MANIFEST_VERSION,
|
||||
type PreviewFixtureManifest, type PreviewFixtureManifestEntry,
|
||||
} from './fixture-manifest.ts'
|
||||
export { loadVfsImage, loadVfsOverlay, MemoryVfs } from './storage/memory.ts'
|
||||
export { inflateImage, inflateImageStream } from './storage/image-gzip.ts'
|
||||
export { packTar, parseTar, type TarEntry } from './storage/tar.ts'
|
||||
export { requireActiveVfs, setActiveVfs } from './storage/active.ts'
|
||||
|
||||
+1
@@ -413,6 +413,7 @@ export function watchAsync(
|
||||
throw(reason?: unknown): Promise<IteratorResult<WatchEvent>> {
|
||||
close()
|
||||
// AsyncIterator.throw forwards the caller's exact reason, including non-Error values.
|
||||
// oxlint-disable-next-line typescript/prefer-promise-reject-errors
|
||||
return Promise.reject(reason)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ type StreamStatics = typeof import('node:stream').Stream & {
|
||||
|
||||
const nodeStream = Stream as unknown as StreamRuntime
|
||||
|
||||
/* oxlint-disable typescript/unbound-method -- readable-stream's namespace statics do not read `this`. */
|
||||
const {
|
||||
Duplex, PassThrough, Readable, Stream: StreamBase, Transform, Writable,
|
||||
addAbortSignal, compose, destroy, finished, isDisturbed, isErrored, isReadable, pipeline, promises,
|
||||
@@ -31,6 +32,7 @@ const streamStatics = StreamBase as unknown as StreamStatics
|
||||
const {
|
||||
getDefaultHighWaterMark, isDestroyed, isWritable, setDefaultHighWaterMark,
|
||||
} = streamStatics
|
||||
/* oxlint-enable typescript/unbound-method */
|
||||
|
||||
// readable-stream tracks Node 18's 16 KiB byte default; this repository runs
|
||||
// Node 22+, whose generic and file streams use 64 KiB.
|
||||
|
||||
@@ -76,13 +76,13 @@ function statsOf(stats: VfsStats): ShellStats {
|
||||
*/
|
||||
export function hostFileSystem(): ShellFileSystem {
|
||||
const vfs = (): ReturnType<typeof requireActiveVfs> => requireActiveVfs()
|
||||
const stat = async (path: string): Promise<ShellStats | undefined> => {
|
||||
const stat = (path: string): Promise<ShellStats | undefined> => {
|
||||
try {
|
||||
return statsOf(vfs().statSync(path) as VfsStats)
|
||||
return Promise.resolve(statsOf(vfs().statSync(path) as VfsStats))
|
||||
} catch {
|
||||
// Absence is the answer callers branch on; every other failure mode of
|
||||
// the in-memory backend is also "this path holds nothing readable".
|
||||
return undefined
|
||||
return Promise.resolve(undefined)
|
||||
}
|
||||
}
|
||||
// Several members take no await: the face is asynchronous because a process
|
||||
@@ -101,18 +101,22 @@ export function hostFileSystem(): ShellFileSystem {
|
||||
if ((await stat(path))?.directory === true) throw filesystemError('EISDIR', 'read', path)
|
||||
return vfs().readFileSync(path, 'utf8') as string
|
||||
},
|
||||
writeText: async (path: string, text: string, append = false): Promise<void> => {
|
||||
writeText: (path: string, text: string, append = false): Promise<void> => {
|
||||
if (append) vfs().appendFileSync(path, text)
|
||||
else vfs().writeFileSync(path, text)
|
||||
return Promise.resolve()
|
||||
},
|
||||
mkdir: async (path: string, recursive: boolean): Promise<void> => {
|
||||
mkdir: (path: string, recursive: boolean): Promise<void> => {
|
||||
vfs().mkdirSync(path, { recursive })
|
||||
return Promise.resolve()
|
||||
},
|
||||
remove: async (path: string, options: { recursive: boolean; force: boolean }): Promise<void> => {
|
||||
remove: (path: string, options: { recursive: boolean; force: boolean }): Promise<void> => {
|
||||
vfs().rmSync(path, options)
|
||||
return Promise.resolve()
|
||||
},
|
||||
rename: async (from: string, to: string): Promise<void> => {
|
||||
rename: (from: string, to: string): Promise<void> => {
|
||||
vfs().renameSync(from, to)
|
||||
return Promise.resolve()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
* @module @deepseek-ai/dsh-experimental-webworker-runtime/src/storage/memory
|
||||
*/
|
||||
import { dirname, join, normalize, resolve, SEP } from '../module-system/posix-path.ts'
|
||||
import { IMAGE_OVERLAY_DIRECTORIES } from '../image-layout.ts'
|
||||
import { parseTar } from './tar.ts'
|
||||
import type {
|
||||
Vfs, VfsBigIntStats, VfsDir, VfsDirent, VfsEncoding, VfsError, VfsFileHandle, VfsMutation,
|
||||
@@ -773,3 +774,38 @@ export function loadVfsImage(image: Uint8Array, root = '/dsh', vfs = new MemoryV
|
||||
}
|
||||
return vfs
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply one ordered data overlay to an already mounted base image.
|
||||
*
|
||||
* Overlay entries may replace files only under the layout's data directories;
|
||||
* module code, configuration, and the lowering manifest cannot be shadowed.
|
||||
* Paths containing traversal segments are refused before normalization. Later
|
||||
* overlays win for files, while file/directory type conflicts fail loud.
|
||||
* @param image - Uncompressed ustar overlay archive.
|
||||
* @param root - Virtual root shared with the base image.
|
||||
* @param vfs - Mounted filesystem to update.
|
||||
* @returns The same filesystem after applying the overlay.
|
||||
*/
|
||||
export function loadVfsOverlay(image: Uint8Array, root: string, vfs: MemoryVfs): MemoryVfs {
|
||||
for (const entry of parseTar(image)) {
|
||||
const relativeName = entry.name.startsWith('./') ? entry.name.slice(2) : entry.name
|
||||
const path = relativeName.endsWith('/') ? relativeName.slice(0, -1) : relativeName
|
||||
const segments = path.split('/')
|
||||
if (path === '' || relativeName.startsWith(SEP)
|
||||
|| segments.some(segment => segment === '' || segment === '.' || segment === '..')
|
||||
|| !IMAGE_OVERLAY_DIRECTORIES.includes(segments[0] ?? '')) {
|
||||
throw new Error(`webworker vfs: overlay entry must stay under ${IMAGE_OVERLAY_DIRECTORIES.join('/ or ')}, received "${entry.name}"`)
|
||||
}
|
||||
const target = join(root, path)
|
||||
if (entry.directory) {
|
||||
vfs.seedDirectory(target, { mode: entry.mode })
|
||||
continue
|
||||
}
|
||||
if (vfs.existsSync(target) && vfs.statSync(target).isDirectory()) {
|
||||
throw new Error(`webworker vfs: overlay file cannot replace directory "${target}"`)
|
||||
}
|
||||
vfs.seed(target, entry.bytes, { mode: entry.mode })
|
||||
}
|
||||
return vfs
|
||||
}
|
||||
|
||||
@@ -33,12 +33,13 @@ export interface TunnelAbortFrame {
|
||||
|
||||
/** Frames the worker accepts. */
|
||||
/**
|
||||
* First inbound frame: the image URL, the one input the worker assembly
|
||||
* takes from outside.
|
||||
* First inbound frame: the base image URL and ordered data overlays selected
|
||||
* before the worker assembly starts.
|
||||
*/
|
||||
export interface TunnelInitFrame {
|
||||
readonly t: 'init'
|
||||
readonly image: string
|
||||
readonly overlays: readonly string[]
|
||||
}
|
||||
|
||||
/** Every frame the page sends the worker. */
|
||||
@@ -142,7 +143,10 @@ export function parseInboundFrame(data: unknown): TunnelInboundFrame {
|
||||
if (typeof frame.image !== 'string') {
|
||||
throw new Error('webworker tunnel: init frame needs a string image url')
|
||||
}
|
||||
return { t: 'init', image: frame.image }
|
||||
if (!Array.isArray(frame.overlays) || frame.overlays.some(overlay => typeof overlay !== 'string')) {
|
||||
throw new Error('webworker tunnel: init frame needs an array of string overlay urls')
|
||||
}
|
||||
return { t: 'init', image: frame.image, overlays: frame.overlays as string[] }
|
||||
}
|
||||
const id = frame.id
|
||||
if (typeof id !== 'string' && typeof id !== 'number') {
|
||||
|
||||
@@ -29,7 +29,7 @@ import { installProcessGlobal } from './node/globals/process.ts'
|
||||
import type { RequestListener } from './transport/synthetic-http.ts'
|
||||
import { TunnelServer, type TunnelPort } from './transport/tunnel.ts'
|
||||
import { inflateImage, inflateImageStream } from './storage/image-gzip.ts'
|
||||
import { loadVfsImage, MemoryVfs } from './storage/memory.ts'
|
||||
import { loadVfsImage, loadVfsOverlay, MemoryVfs } from './storage/memory.ts'
|
||||
import { setActiveVfs } from './storage/active.ts'
|
||||
import {
|
||||
DEFAULT_ROOT, IMAGE_CONFIG_PATH, IMAGE_EMPTY_DIRECTORIES, IMAGE_HOME_DIRECTORY, IMAGE_MANIFEST_PATH,
|
||||
@@ -87,6 +87,8 @@ export interface WorkerHostOptions {
|
||||
readonly requestListener: () => Promise<RequestListener>
|
||||
/** Image bytes, or the URL the worker fetches them from. */
|
||||
readonly image: Uint8Array | string
|
||||
/** Ordered data overlays applied after the base image and before boot. */
|
||||
readonly overlays?: readonly (Uint8Array | string)[]
|
||||
/** Virtual root; defaults to {@link DEFAULT_ROOT}. */
|
||||
readonly root?: string
|
||||
/** Composed configuration inside the image; defaults to `<root>/config/cordis.yml`. */
|
||||
@@ -182,8 +184,12 @@ export function createWorkerHost(options: WorkerHostOptions): WorkerHost {
|
||||
const home = join(root, IMAGE_HOME_DIRECTORY)
|
||||
installProcessGlobal({ cwd: root, env: { DSH_HOME: home, HOME: home, ...options.env } })
|
||||
|
||||
const bytes = await readImage(options.image)
|
||||
const [bytes, overlays] = await Promise.all([
|
||||
readImage(options.image),
|
||||
Promise.all((options.overlays ?? []).map(readImage)),
|
||||
])
|
||||
const mounted = loadVfsImage(bytes, root)
|
||||
for (const overlay of overlays) loadVfsOverlay(overlay, root, mounted)
|
||||
// Belt and braces over the image's own empty-directory entries: a hand
|
||||
// -built image without them still boots.
|
||||
for (const directory of IMAGE_EMPTY_DIRECTORIES) {
|
||||
@@ -248,7 +254,7 @@ export function createWorkerHost(options: WorkerHostOptions): WorkerHost {
|
||||
const shared = ctx.get('connection') !== undefined
|
||||
const handler = directFetchHandler(ctx, toFetchHandler(apiProxy))
|
||||
const usage = loader.usage()
|
||||
console.info(`webworker host: tree active (modules=${String(usage.modules)}, preset root overlay=${presetOverlay ? 'applied' : 'already in roster'}, direct lane=${shared ? 'connection.createSharedFetchHandler (interceptors kept)' : 'api surface only'}, als causality=${options.alsCausality === undefined ? 'inert' : 'snapshot/restore'}, image lowering=${LOWERING_VERSION})`)
|
||||
console.info(`webworker host: tree active (modules=${String(usage.modules)}, data overlays=${String(overlays.length)}, preset root overlay=${presetOverlay ? 'applied' : 'already in roster'}, direct lane=${shared ? 'connection.createSharedFetchHandler (interceptors kept)' : 'api surface only'}, als causality=${options.alsCausality === undefined ? 'inert' : 'snapshot/restore'}, image lowering=${LOWERING_VERSION})`)
|
||||
|
||||
tunnel.serve({
|
||||
directFetch: (request: Request) => handler.fetch(request),
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
* listener; the assembly owns everything else (process global, VFS image,
|
||||
* Cordis tree, tunnel server).
|
||||
*
|
||||
* The assembly needs the image location before it can exist, and it arrives in
|
||||
* the tunnel's opening `init` frame — this bundle reads nothing from its own
|
||||
* URL, so the deployment decides where both the bundle and the image live.
|
||||
* The assembly needs the base image and selected overlays before it can exist;
|
||||
* they arrive in the tunnel's opening `init` frame. This bundle reads nothing
|
||||
* from its own URL, so the deployment decides where every archive lives.
|
||||
* Messages before `init` queue here; requests during boot queue inside the
|
||||
* host, which attaches its handler before its first await.
|
||||
*/
|
||||
@@ -52,12 +52,16 @@ self.addEventListener('message', (event: MessageEvent) => {
|
||||
if (typeof data.image !== 'string') {
|
||||
throw new Error('webworker: init frame needs a string image url')
|
||||
}
|
||||
if (!Array.isArray(data.overlays) || data.overlays.some(overlay => typeof overlay !== 'string')) {
|
||||
throw new Error('webworker: init frame needs an array of string overlay urls')
|
||||
}
|
||||
const created = createWorkerHost({
|
||||
staticModules: createNodeBuiltins(),
|
||||
staticModulePrefixes: REPLACED_PREFIXES,
|
||||
requestListener: whenRequestListener,
|
||||
alsCausality,
|
||||
image: data.image,
|
||||
overlays: data.overlays as string[],
|
||||
})
|
||||
host = created
|
||||
for (const queued of pending) {
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { PreviewFixtureManifest } from '../../src/fixture-manifest.ts'
|
||||
import { choosePreviewSource } from '../../src/client/source-chooser.ts'
|
||||
|
||||
const MANIFEST_URL = new URL('https://preview.test/preview/fixtures.json')
|
||||
|
||||
const MANIFEST: PreviewFixtureManifest = {
|
||||
version: 1,
|
||||
defaultFixture: 'example',
|
||||
fixtures: [{
|
||||
id: 'example',
|
||||
label: 'Example & <demo> "quoted" \'single\'',
|
||||
description: 'A deterministic example.',
|
||||
overlays: ['fixtures/base.tar.gz', 'fixtures/tail.tar.gz'],
|
||||
}],
|
||||
}
|
||||
|
||||
function setLocation(search = ''): void {
|
||||
history.replaceState({}, '', `/preview.html${search}`)
|
||||
}
|
||||
|
||||
function installManifest(manifest: PreviewFixtureManifest = MANIFEST): ReturnType<typeof vi.fn> {
|
||||
const fetch = vi.fn(async () => Response.json(manifest))
|
||||
vi.stubGlobal('fetch', fetch)
|
||||
return fetch
|
||||
}
|
||||
|
||||
function submitChooser(): void {
|
||||
const form = document.querySelector<HTMLFormElement>('[data-preview-source-card]')
|
||||
if (form === null) throw new Error('test chooser form was not rendered')
|
||||
form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }))
|
||||
}
|
||||
|
||||
describe('Preview source chooser', () => {
|
||||
beforeEach(() => {
|
||||
document.head.replaceChildren()
|
||||
document.body.innerHTML = '<div id="root"></div>'
|
||||
setLocation()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
it('bypasses the chooser and manifest for an explicit empty source', async () => {
|
||||
setLocation('?preview-fixture=none')
|
||||
const fetch = installManifest()
|
||||
|
||||
await expect(choosePreviewSource(MANIFEST_URL)).resolves.toEqual([])
|
||||
expect(fetch).not.toHaveBeenCalled()
|
||||
expect(document.querySelector('[data-preview-source-chooser]')).toBeNull()
|
||||
})
|
||||
|
||||
it('bypasses the chooser and resolves an explicit built-in fixture', async () => {
|
||||
document.body.replaceChildren()
|
||||
setLocation('?preview-fixture=example')
|
||||
const fetch = installManifest()
|
||||
|
||||
await expect(choosePreviewSource(MANIFEST_URL)).resolves.toEqual([
|
||||
new URL('https://preview.test/preview/fixtures/base.tar.gz'),
|
||||
new URL('https://preview.test/preview/fixtures/tail.tar.gz'),
|
||||
])
|
||||
expect(fetch).toHaveBeenCalledOnce()
|
||||
expect(document.querySelector('[data-preview-source-chooser]')).toBeNull()
|
||||
})
|
||||
|
||||
it.each(['', 'missing', 'webfs'])(
|
||||
'fails loud for the explicit unavailable source %j without opening the chooser',
|
||||
async (source) => {
|
||||
setLocation(`?preview-fixture=${source}`)
|
||||
installManifest()
|
||||
|
||||
await expect(choosePreviewSource(MANIFEST_URL)).rejects.toThrow(/unknown or interactive source/)
|
||||
expect(document.querySelector('[data-preview-source-chooser]')).toBeNull()
|
||||
},
|
||||
)
|
||||
|
||||
it('shows the chooser only when the query is absent and returns its default selection', async () => {
|
||||
installManifest()
|
||||
|
||||
const selected = choosePreviewSource(MANIFEST_URL)
|
||||
await vi.waitFor(() => {
|
||||
expect(document.querySelector('[data-preview-source-chooser]')).not.toBeNull()
|
||||
})
|
||||
expect(document.querySelector<HTMLInputElement>('input[value="example"]')?.checked).toBe(true)
|
||||
expect(document.querySelector<HTMLInputElement>('input[value="webfs"]')?.disabled).toBe(true)
|
||||
expect(document.querySelector('[data-preview-source-card]')?.textContent)
|
||||
.toContain(MANIFEST.fixtures[0]?.label)
|
||||
expect(document.querySelector('[data-preview-source-card] script')).toBeNull()
|
||||
|
||||
submitChooser()
|
||||
|
||||
await expect(selected).resolves.toEqual([
|
||||
new URL('https://preview.test/preview/fixtures/base.tar.gz'),
|
||||
new URL('https://preview.test/preview/fixtures/tail.tar.gz'),
|
||||
])
|
||||
expect(document.getElementById('root')?.childElementCount).toBe(0)
|
||||
expect(document.querySelector('[data-preview-source-style]')).toBeNull()
|
||||
})
|
||||
|
||||
it('selects the empty source when the manifest has no default', async () => {
|
||||
installManifest({ ...MANIFEST, defaultFixture: null })
|
||||
|
||||
const selected = choosePreviewSource(MANIFEST_URL)
|
||||
await vi.waitFor(() => {
|
||||
expect(document.querySelector<HTMLInputElement>('input[value="none"]')?.checked).toBe(true)
|
||||
})
|
||||
submitChooser()
|
||||
|
||||
await expect(selected).resolves.toEqual([])
|
||||
})
|
||||
|
||||
it('reports manifest, mount, form, selection, and catalog failures', async () => {
|
||||
vi.stubGlobal('fetch', vi.fn(async () => new Response('missing', { status: 404 })))
|
||||
await expect(choosePreviewSource(MANIFEST_URL)).rejects.toThrow(/returned 404/)
|
||||
|
||||
installManifest()
|
||||
document.body.replaceChildren()
|
||||
await expect(choosePreviewSource(MANIFEST_URL)).rejects.toThrow(/missing #root/)
|
||||
|
||||
document.body.innerHTML = '<div id="root"></div>'
|
||||
const root = document.getElementById('root')
|
||||
if (root === null) throw new Error('test root is missing')
|
||||
vi.spyOn(root, 'querySelector').mockReturnValueOnce(null)
|
||||
await expect(choosePreviewSource(MANIFEST_URL)).rejects.toThrow(/form was not rendered/)
|
||||
|
||||
document.head.replaceChildren()
|
||||
document.body.innerHTML = '<div id="root"></div>'
|
||||
const missingSelection = choosePreviewSource(MANIFEST_URL)
|
||||
await vi.waitFor(() => { expect(document.querySelector('form')).not.toBeNull() })
|
||||
document.querySelectorAll('input[name="preview-source"]').forEach((input) => {
|
||||
input.removeAttribute('name')
|
||||
})
|
||||
submitChooser()
|
||||
await expect(missingSelection).rejects.toThrow(/no source selected/)
|
||||
|
||||
document.head.replaceChildren()
|
||||
document.body.innerHTML = '<div id="root"></div>'
|
||||
const unavailableSelection = choosePreviewSource(MANIFEST_URL)
|
||||
await vi.waitFor(() => { expect(document.querySelector('form')).not.toBeNull() })
|
||||
const selected = document.querySelector<HTMLInputElement>('input:checked')
|
||||
if (selected === null) throw new Error('test selection is missing')
|
||||
selected.value = 'missing'
|
||||
submitChooser()
|
||||
await expect(unavailableSelection).rejects.toThrow(/unavailable source/)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,48 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
parsePreviewFixtureManifest, PREVIEW_FIXTURE_MANIFEST_VERSION,
|
||||
} from '../src/fixture-manifest.ts'
|
||||
|
||||
describe('Preview fixture manifest', () => {
|
||||
it('accepts a unique named fixture with ordered overlays', () => {
|
||||
expect(parsePreviewFixtureManifest({
|
||||
version: PREVIEW_FIXTURE_MANIFEST_VERSION,
|
||||
defaultFixture: 'example',
|
||||
fixtures: [{
|
||||
id: 'example',
|
||||
label: 'Example',
|
||||
description: 'A deterministic example.',
|
||||
overlays: ['fixtures/base.tar.gz', 'fixtures/tail.tar.gz'],
|
||||
}],
|
||||
})).toEqual({
|
||||
version: PREVIEW_FIXTURE_MANIFEST_VERSION,
|
||||
defaultFixture: 'example',
|
||||
fixtures: [{
|
||||
id: 'example',
|
||||
label: 'Example',
|
||||
description: 'A deterministic example.',
|
||||
overlays: ['fixtures/base.tar.gz', 'fixtures/tail.tar.gz'],
|
||||
}],
|
||||
})
|
||||
})
|
||||
|
||||
it.each([
|
||||
[{ version: 2, defaultFixture: null, fixtures: [] }, /must use version/],
|
||||
[{ version: 1, defaultFixture: 'missing', fixtures: [] }, /defaultFixture/],
|
||||
[{
|
||||
version: 1,
|
||||
defaultFixture: 'duplicate',
|
||||
fixtures: [
|
||||
{ id: 'duplicate', label: 'One', description: 'First.', overlays: ['one.tar.gz'] },
|
||||
{ id: 'duplicate', label: 'Two', description: 'Second.', overlays: ['two.tar.gz'] },
|
||||
],
|
||||
}, /repeats id/],
|
||||
[{
|
||||
version: 1,
|
||||
defaultFixture: 'none',
|
||||
fixtures: [{ id: 'none', label: 'None', description: 'Reserved.', overlays: ['none.tar.gz'] }],
|
||||
}, /invalid fixture entry/],
|
||||
])('rejects malformed catalogs', (value, error) => {
|
||||
expect(() => parsePreviewFixtureManifest(value)).toThrow(error)
|
||||
})
|
||||
})
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
{"type":"session","version":0,"id":"preview-architecture-review","createdAt":1787472100000,"cwd":"/dsh/workspace","parentSession":"preview-showcase","seedLength":169,"origin":"subagent","delegationDepth":1,"agentPreset":"standard"}
|
||||
{"type":"turn/start","data":{"turn":1},"seq":0,"time":1787472000000}
|
||||
{"type":"user/message","data":{"id":"preview-user-01","role":"user","content":[{"type":"text","text":"History checkpoint 01: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":1,"time":1787472000001}
|
||||
{"type":"session/title","data":{"title":"WebWorker Preview Showcase","messageSeqs":[],"source":{"kind":"user"}},"seq":2,"time":1787472000002}
|
||||
{"type":"step/start","data":{"turn":1,"step":1},"seq":3,"time":1787472000003}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"id":"preview-assistant-01","role":"assistant","content":[{"type":"text","text":"Checkpoint 01 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":4,"time":1787472000004}
|
||||
{"type":"step/end","data":{"turn":1,"step":1},"seq":5,"time":1787472000005}
|
||||
{"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}},"seq":6,"time":1787472000006}
|
||||
{"type":"turn/start","data":{"turn":2},"seq":7,"time":1787472000007}
|
||||
{"type":"user/message","data":{"id":"preview-user-02","role":"user","content":[{"type":"text","text":"History checkpoint 02: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":8,"time":1787472000008}
|
||||
{"type":"step/start","data":{"turn":2,"step":1},"seq":9,"time":1787472000009}
|
||||
{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"id":"preview-assistant-02","role":"assistant","content":[{"type":"text","text":"Checkpoint 02 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":10,"time":1787472000010}
|
||||
{"type":"step/end","data":{"turn":2,"step":1},"seq":11,"time":1787472000011}
|
||||
{"type":"turn/end","data":{"turn":2,"reason":{"kind":"completed"}},"seq":12,"time":1787472000012}
|
||||
{"type":"turn/start","data":{"turn":3},"seq":13,"time":1787472000013}
|
||||
{"type":"user/message","data":{"id":"preview-user-03","role":"user","content":[{"type":"text","text":"History checkpoint 03: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":14,"time":1787472000014}
|
||||
{"type":"step/start","data":{"turn":3,"step":1},"seq":15,"time":1787472000015}
|
||||
{"type":"assistant/message","data":{"turn":3,"step":1,"message":{"id":"preview-assistant-03","role":"assistant","content":[{"type":"text","text":"Checkpoint 03 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":16,"time":1787472000016}
|
||||
{"type":"step/end","data":{"turn":3,"step":1},"seq":17,"time":1787472000017}
|
||||
{"type":"turn/end","data":{"turn":3,"reason":{"kind":"completed"}},"seq":18,"time":1787472000018}
|
||||
{"type":"turn/start","data":{"turn":4},"seq":19,"time":1787472000019}
|
||||
{"type":"user/message","data":{"id":"preview-user-04","role":"user","content":[{"type":"text","text":"History checkpoint 04: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":20,"time":1787472000020}
|
||||
{"type":"step/start","data":{"turn":4,"step":1},"seq":21,"time":1787472000021}
|
||||
{"type":"assistant/message","data":{"turn":4,"step":1,"message":{"id":"preview-assistant-04","role":"assistant","content":[{"type":"text","text":"Checkpoint 04 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":22,"time":1787472000022}
|
||||
{"type":"step/end","data":{"turn":4,"step":1},"seq":23,"time":1787472000023}
|
||||
{"type":"turn/end","data":{"turn":4,"reason":{"kind":"completed"}},"seq":24,"time":1787472000024}
|
||||
{"type":"turn/start","data":{"turn":5},"seq":25,"time":1787472000025}
|
||||
{"type":"user/message","data":{"id":"preview-user-05","role":"user","content":[{"type":"text","text":"History checkpoint 05: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":26,"time":1787472000026}
|
||||
{"type":"step/start","data":{"turn":5,"step":1},"seq":27,"time":1787472000027}
|
||||
{"type":"assistant/message","data":{"turn":5,"step":1,"message":{"id":"preview-assistant-05","role":"assistant","content":[{"type":"text","text":"Checkpoint 05 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":28,"time":1787472000028}
|
||||
{"type":"step/end","data":{"turn":5,"step":1},"seq":29,"time":1787472000029}
|
||||
{"type":"turn/end","data":{"turn":5,"reason":{"kind":"completed"}},"seq":30,"time":1787472000030}
|
||||
{"type":"turn/start","data":{"turn":6},"seq":31,"time":1787472000031}
|
||||
{"type":"user/message","data":{"id":"preview-user-06","role":"user","content":[{"type":"text","text":"History checkpoint 06: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":32,"time":1787472000032}
|
||||
{"type":"step/start","data":{"turn":6,"step":1},"seq":33,"time":1787472000033}
|
||||
{"type":"assistant/message","data":{"turn":6,"step":1,"message":{"id":"preview-assistant-06","role":"assistant","content":[{"type":"text","text":"Checkpoint 06 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":34,"time":1787472000034}
|
||||
{"type":"step/end","data":{"turn":6,"step":1},"seq":35,"time":1787472000035}
|
||||
{"type":"turn/end","data":{"turn":6,"reason":{"kind":"completed"}},"seq":36,"time":1787472000036}
|
||||
{"type":"turn/start","data":{"turn":7},"seq":37,"time":1787472000037}
|
||||
{"type":"user/message","data":{"id":"preview-user-07","role":"user","content":[{"type":"text","text":"History checkpoint 07: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":38,"time":1787472000038}
|
||||
{"type":"step/start","data":{"turn":7,"step":1},"seq":39,"time":1787472000039}
|
||||
{"type":"assistant/message","data":{"turn":7,"step":1,"message":{"id":"preview-assistant-07","role":"assistant","content":[{"type":"text","text":"Checkpoint 07 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":40,"time":1787472000040}
|
||||
{"type":"step/end","data":{"turn":7,"step":1},"seq":41,"time":1787472000041}
|
||||
{"type":"turn/end","data":{"turn":7,"reason":{"kind":"completed"}},"seq":42,"time":1787472000042}
|
||||
{"type":"turn/start","data":{"turn":8},"seq":43,"time":1787472000043}
|
||||
{"type":"user/message","data":{"id":"preview-user-08","role":"user","content":[{"type":"text","text":"History checkpoint 08: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":44,"time":1787472000044}
|
||||
{"type":"step/start","data":{"turn":8,"step":1},"seq":45,"time":1787472000045}
|
||||
{"type":"assistant/message","data":{"turn":8,"step":1,"message":{"id":"preview-assistant-08","role":"assistant","content":[{"type":"text","text":"Checkpoint 08 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":46,"time":1787472000046}
|
||||
{"type":"step/end","data":{"turn":8,"step":1},"seq":47,"time":1787472000047}
|
||||
{"type":"turn/end","data":{"turn":8,"reason":{"kind":"completed"}},"seq":48,"time":1787472000048}
|
||||
{"type":"turn/start","data":{"turn":9},"seq":49,"time":1787472000049}
|
||||
{"type":"user/message","data":{"id":"preview-user-09","role":"user","content":[{"type":"text","text":"History checkpoint 09: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":50,"time":1787472000050}
|
||||
{"type":"step/start","data":{"turn":9,"step":1},"seq":51,"time":1787472000051}
|
||||
{"type":"assistant/message","data":{"turn":9,"step":1,"message":{"id":"preview-assistant-09","role":"assistant","content":[{"type":"text","text":"Checkpoint 09 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":52,"time":1787472000052}
|
||||
{"type":"step/end","data":{"turn":9,"step":1},"seq":53,"time":1787472000053}
|
||||
{"type":"turn/end","data":{"turn":9,"reason":{"kind":"completed"}},"seq":54,"time":1787472000054}
|
||||
{"type":"turn/start","data":{"turn":10},"seq":55,"time":1787472000055}
|
||||
{"type":"user/message","data":{"id":"preview-user-10","role":"user","content":[{"type":"text","text":"History checkpoint 10: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":56,"time":1787472000056}
|
||||
{"type":"step/start","data":{"turn":10,"step":1},"seq":57,"time":1787472000057}
|
||||
{"type":"assistant/message","data":{"turn":10,"step":1,"message":{"id":"preview-assistant-10","role":"assistant","content":[{"type":"text","text":"Checkpoint 10 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":58,"time":1787472000058}
|
||||
{"type":"step/end","data":{"turn":10,"step":1},"seq":59,"time":1787472000059}
|
||||
{"type":"turn/end","data":{"turn":10,"reason":{"kind":"completed"}},"seq":60,"time":1787472000060}
|
||||
{"type":"turn/start","data":{"turn":11},"seq":61,"time":1787472000061}
|
||||
{"type":"user/message","data":{"id":"preview-user-11","role":"user","content":[{"type":"text","text":"History checkpoint 11: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":62,"time":1787472000062}
|
||||
{"type":"step/start","data":{"turn":11,"step":1},"seq":63,"time":1787472000063}
|
||||
{"type":"assistant/message","data":{"turn":11,"step":1,"message":{"id":"preview-assistant-11","role":"assistant","content":[{"type":"text","text":"Checkpoint 11 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":64,"time":1787472000064}
|
||||
{"type":"step/end","data":{"turn":11,"step":1},"seq":65,"time":1787472000065}
|
||||
{"type":"turn/end","data":{"turn":11,"reason":{"kind":"completed"}},"seq":66,"time":1787472000066}
|
||||
{"type":"turn/start","data":{"turn":12},"seq":67,"time":1787472000067}
|
||||
{"type":"user/message","data":{"id":"preview-user-12","role":"user","content":[{"type":"text","text":"History checkpoint 12: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":68,"time":1787472000068}
|
||||
{"type":"step/start","data":{"turn":12,"step":1},"seq":69,"time":1787472000069}
|
||||
{"type":"assistant/message","data":{"turn":12,"step":1,"message":{"id":"preview-assistant-12","role":"assistant","content":[{"type":"text","text":"Checkpoint 12 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":70,"time":1787472000070}
|
||||
{"type":"step/end","data":{"turn":12,"step":1},"seq":71,"time":1787472000071}
|
||||
{"type":"turn/end","data":{"turn":12,"reason":{"kind":"completed"}},"seq":72,"time":1787472000072}
|
||||
{"type":"turn/start","data":{"turn":13},"seq":73,"time":1787472000073}
|
||||
{"type":"user/message","data":{"id":"preview-user-13","role":"user","content":[{"type":"text","text":"History checkpoint 13: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":74,"time":1787472000074}
|
||||
{"type":"step/start","data":{"turn":13,"step":1},"seq":75,"time":1787472000075}
|
||||
{"type":"assistant/message","data":{"turn":13,"step":1,"message":{"id":"preview-assistant-13","role":"assistant","content":[{"type":"text","text":"Checkpoint 13 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":76,"time":1787472000076}
|
||||
{"type":"step/end","data":{"turn":13,"step":1},"seq":77,"time":1787472000077}
|
||||
{"type":"turn/end","data":{"turn":13,"reason":{"kind":"completed"}},"seq":78,"time":1787472000078}
|
||||
{"type":"turn/start","data":{"turn":14},"seq":79,"time":1787472000079}
|
||||
{"type":"user/message","data":{"id":"preview-user-14","role":"user","content":[{"type":"text","text":"History checkpoint 14: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":80,"time":1787472000080}
|
||||
{"type":"step/start","data":{"turn":14,"step":1},"seq":81,"time":1787472000081}
|
||||
{"type":"assistant/message","data":{"turn":14,"step":1,"message":{"id":"preview-assistant-14","role":"assistant","content":[{"type":"text","text":"Checkpoint 14 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":82,"time":1787472000082}
|
||||
{"type":"step/end","data":{"turn":14,"step":1},"seq":83,"time":1787472000083}
|
||||
{"type":"turn/end","data":{"turn":14,"reason":{"kind":"completed"}},"seq":84,"time":1787472000084}
|
||||
{"type":"turn/start","data":{"turn":15},"seq":85,"time":1787472000085}
|
||||
{"type":"user/message","data":{"id":"preview-user-15","role":"user","content":[{"type":"text","text":"History checkpoint 15: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":86,"time":1787472000086}
|
||||
{"type":"step/start","data":{"turn":15,"step":1},"seq":87,"time":1787472000087}
|
||||
{"type":"assistant/message","data":{"turn":15,"step":1,"message":{"id":"preview-assistant-15","role":"assistant","content":[{"type":"text","text":"Checkpoint 15 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":88,"time":1787472000088}
|
||||
{"type":"step/end","data":{"turn":15,"step":1},"seq":89,"time":1787472000089}
|
||||
{"type":"turn/end","data":{"turn":15,"reason":{"kind":"completed"}},"seq":90,"time":1787472000090}
|
||||
{"type":"turn/start","data":{"turn":16},"seq":91,"time":1787472000091}
|
||||
{"type":"user/message","data":{"id":"preview-user-16","role":"user","content":[{"type":"text","text":"History checkpoint 16: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":92,"time":1787472000092}
|
||||
{"type":"step/start","data":{"turn":16,"step":1},"seq":93,"time":1787472000093}
|
||||
{"type":"assistant/message","data":{"turn":16,"step":1,"message":{"id":"preview-assistant-16","role":"assistant","content":[{"type":"text","text":"Checkpoint 16 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":94,"time":1787472000094}
|
||||
{"type":"step/end","data":{"turn":16,"step":1},"seq":95,"time":1787472000095}
|
||||
{"type":"turn/end","data":{"turn":16,"reason":{"kind":"completed"}},"seq":96,"time":1787472000096}
|
||||
{"type":"turn/start","data":{"turn":17},"seq":97,"time":1787472000097}
|
||||
{"type":"user/message","data":{"id":"preview-user-17","role":"user","content":[{"type":"text","text":"History checkpoint 17: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":98,"time":1787472000098}
|
||||
{"type":"step/start","data":{"turn":17,"step":1},"seq":99,"time":1787472000099}
|
||||
{"type":"assistant/message","data":{"turn":17,"step":1,"message":{"id":"preview-assistant-17","role":"assistant","content":[{"type":"text","text":"Checkpoint 17 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":100,"time":1787472000100}
|
||||
{"type":"step/end","data":{"turn":17,"step":1},"seq":101,"time":1787472000101}
|
||||
{"type":"turn/end","data":{"turn":17,"reason":{"kind":"completed"}},"seq":102,"time":1787472000102}
|
||||
{"type":"turn/start","data":{"turn":18},"seq":103,"time":1787472000103}
|
||||
{"type":"user/message","data":{"id":"preview-user-18","role":"user","content":[{"type":"text","text":"History checkpoint 18: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":104,"time":1787472000104}
|
||||
{"type":"step/start","data":{"turn":18,"step":1},"seq":105,"time":1787472000105}
|
||||
{"type":"assistant/message","data":{"turn":18,"step":1,"message":{"id":"preview-assistant-18","role":"assistant","content":[{"type":"text","text":"Checkpoint 18 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":106,"time":1787472000106}
|
||||
{"type":"step/end","data":{"turn":18,"step":1},"seq":107,"time":1787472000107}
|
||||
{"type":"turn/end","data":{"turn":18,"reason":{"kind":"completed"}},"seq":108,"time":1787472000108}
|
||||
{"type":"turn/start","data":{"turn":19},"seq":109,"time":1787472000109}
|
||||
{"type":"user/message","data":{"id":"preview-user-19","role":"user","content":[{"type":"text","text":"History checkpoint 19: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":110,"time":1787472000110}
|
||||
{"type":"step/start","data":{"turn":19,"step":1},"seq":111,"time":1787472000111}
|
||||
{"type":"assistant/message","data":{"turn":19,"step":1,"message":{"id":"preview-assistant-19","role":"assistant","content":[{"type":"text","text":"Checkpoint 19 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":112,"time":1787472000112}
|
||||
{"type":"step/end","data":{"turn":19,"step":1},"seq":113,"time":1787472000113}
|
||||
{"type":"turn/end","data":{"turn":19,"reason":{"kind":"completed"}},"seq":114,"time":1787472000114}
|
||||
{"type":"turn/start","data":{"turn":20},"seq":115,"time":1787472000115}
|
||||
{"type":"user/message","data":{"id":"preview-user-20","role":"user","content":[{"type":"text","text":"History checkpoint 20: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":116,"time":1787472000116}
|
||||
{"type":"step/start","data":{"turn":20,"step":1},"seq":117,"time":1787472000117}
|
||||
{"type":"assistant/message","data":{"turn":20,"step":1,"message":{"id":"preview-assistant-20","role":"assistant","content":[{"type":"text","text":"Checkpoint 20 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":118,"time":1787472000118}
|
||||
{"type":"step/end","data":{"turn":20,"step":1},"seq":119,"time":1787472000119}
|
||||
{"type":"turn/end","data":{"turn":20,"reason":{"kind":"completed"}},"seq":120,"time":1787472000120}
|
||||
{"type":"turn/start","data":{"turn":21},"seq":121,"time":1787472000121}
|
||||
{"type":"user/message","data":{"id":"preview-user-21","role":"user","content":[{"type":"text","text":"History checkpoint 21: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":122,"time":1787472000122}
|
||||
{"type":"step/start","data":{"turn":21,"step":1},"seq":123,"time":1787472000123}
|
||||
{"type":"assistant/message","data":{"turn":21,"step":1,"message":{"id":"preview-assistant-21","role":"assistant","content":[{"type":"text","text":"Checkpoint 21 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":124,"time":1787472000124}
|
||||
{"type":"step/end","data":{"turn":21,"step":1},"seq":125,"time":1787472000125}
|
||||
{"type":"turn/end","data":{"turn":21,"reason":{"kind":"completed"}},"seq":126,"time":1787472000126}
|
||||
{"type":"turn/start","data":{"turn":22},"seq":127,"time":1787472000127}
|
||||
{"type":"user/message","data":{"id":"preview-user-22","role":"user","content":[{"type":"text","text":"History checkpoint 22: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":128,"time":1787472000128}
|
||||
{"type":"step/start","data":{"turn":22,"step":1},"seq":129,"time":1787472000129}
|
||||
{"type":"assistant/message","data":{"turn":22,"step":1,"message":{"id":"preview-assistant-22","role":"assistant","content":[{"type":"text","text":"Checkpoint 22 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":130,"time":1787472000130}
|
||||
{"type":"step/end","data":{"turn":22,"step":1},"seq":131,"time":1787472000131}
|
||||
{"type":"turn/end","data":{"turn":22,"reason":{"kind":"completed"}},"seq":132,"time":1787472000132}
|
||||
{"type":"turn/start","data":{"turn":23},"seq":133,"time":1787472000133}
|
||||
{"type":"user/message","data":{"id":"preview-user-23","role":"user","content":[{"type":"text","text":"History checkpoint 23: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":134,"time":1787472000134}
|
||||
{"type":"step/start","data":{"turn":23,"step":1},"seq":135,"time":1787472000135}
|
||||
{"type":"assistant/message","data":{"turn":23,"step":1,"message":{"id":"preview-assistant-23","role":"assistant","content":[{"type":"text","text":"Checkpoint 23 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":136,"time":1787472000136}
|
||||
{"type":"step/end","data":{"turn":23,"step":1},"seq":137,"time":1787472000137}
|
||||
{"type":"turn/end","data":{"turn":23,"reason":{"kind":"completed"}},"seq":138,"time":1787472000138}
|
||||
{"type":"turn/start","data":{"turn":24},"seq":139,"time":1787472000139}
|
||||
{"type":"user/message","data":{"id":"preview-user-24","role":"user","content":[{"type":"text","text":"History checkpoint 24: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":140,"time":1787472000140}
|
||||
{"type":"step/start","data":{"turn":24,"step":1},"seq":141,"time":1787472000141}
|
||||
{"type":"assistant/message","data":{"turn":24,"step":1,"message":{"id":"preview-assistant-24","role":"assistant","content":[{"type":"text","text":"Checkpoint 24 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":142,"time":1787472000142}
|
||||
{"type":"step/end","data":{"turn":24,"step":1},"seq":143,"time":1787472000143}
|
||||
{"type":"turn/end","data":{"turn":24,"reason":{"kind":"completed"}},"seq":144,"time":1787472000144}
|
||||
{"type":"turn/start","data":{"turn":25},"seq":145,"time":1787472000145}
|
||||
{"type":"user/message","data":{"id":"preview-user-25","role":"user","content":[{"type":"text","text":"History checkpoint 25: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":146,"time":1787472000146}
|
||||
{"type":"step/start","data":{"turn":25,"step":1},"seq":147,"time":1787472000147}
|
||||
{"type":"assistant/message","data":{"turn":25,"step":1,"message":{"id":"preview-assistant-25","role":"assistant","content":[{"type":"text","text":"Checkpoint 25 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":148,"time":1787472000148}
|
||||
{"type":"step/end","data":{"turn":25,"step":1},"seq":149,"time":1787472000149}
|
||||
{"type":"turn/end","data":{"turn":25,"reason":{"kind":"completed"}},"seq":150,"time":1787472000150}
|
||||
{"type":"turn/start","data":{"turn":26},"seq":151,"time":1787472000151}
|
||||
{"type":"user/message","data":{"id":"preview-user-26","role":"user","content":[{"type":"text","text":"History checkpoint 26: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":152,"time":1787472000152}
|
||||
{"type":"step/start","data":{"turn":26,"step":1},"seq":153,"time":1787472000153}
|
||||
{"type":"assistant/message","data":{"turn":26,"step":1,"message":{"id":"preview-assistant-26","role":"assistant","content":[{"type":"text","text":"Checkpoint 26 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":154,"time":1787472000154}
|
||||
{"type":"step/end","data":{"turn":26,"step":1},"seq":155,"time":1787472000155}
|
||||
{"type":"turn/end","data":{"turn":26,"reason":{"kind":"completed"}},"seq":156,"time":1787472000156}
|
||||
{"type":"turn/start","data":{"turn":27},"seq":157,"time":1787472000157}
|
||||
{"type":"user/message","data":{"id":"preview-user-27","role":"user","content":[{"type":"text","text":"History checkpoint 27: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":158,"time":1787472000158}
|
||||
{"type":"step/start","data":{"turn":27,"step":1},"seq":159,"time":1787472000159}
|
||||
{"type":"assistant/message","data":{"turn":27,"step":1,"message":{"id":"preview-assistant-27","role":"assistant","content":[{"type":"text","text":"Checkpoint 27 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":160,"time":1787472000160}
|
||||
{"type":"step/end","data":{"turn":27,"step":1},"seq":161,"time":1787472000161}
|
||||
{"type":"turn/end","data":{"turn":27,"reason":{"kind":"completed"}},"seq":162,"time":1787472000162}
|
||||
{"type":"turn/start","data":{"turn":28},"seq":163,"time":1787472000163}
|
||||
{"type":"user/message","data":{"id":"preview-user-28","role":"user","content":[{"type":"text","text":"History checkpoint 28: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":164,"time":1787472000164}
|
||||
{"type":"step/start","data":{"turn":28,"step":1},"seq":165,"time":1787472000165}
|
||||
{"type":"assistant/message","data":{"turn":28,"step":1,"message":{"id":"preview-assistant-28","role":"assistant","content":[{"type":"text","text":"Checkpoint 28 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":166,"time":1787472000166}
|
||||
{"type":"step/end","data":{"turn":28,"step":1},"seq":167,"time":1787472000167}
|
||||
{"type":"turn/end","data":{"turn":28,"reason":{"kind":"completed"}},"seq":168,"time":1787472000168}
|
||||
{"type":"session/end-seed","data":{},"seq":169,"time":1787472100000}
|
||||
{"type":"turn/start","data":{"turn":29},"seq":170,"time":1787472100001}
|
||||
{"type":"user/message","data":{"id":"preview-review-user","role":"user","content":[{"type":"text","text":"Review whether the preview fixture is isolated from future WebFS data."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":171,"time":1787472100002}
|
||||
{"type":"subagent/descriptor","data":{"version":2,"mode":"one-shot","provider":"fork","label":"Review preview architecture"},"seq":172,"time":1787472100003}
|
||||
{"type":"step/start","data":{"turn":29,"step":1},"seq":173,"time":1787472100004}
|
||||
{"type":"assistant/message","data":{"turn":29,"step":1,"message":{"id":"preview-review-assistant","role":"assistant","content":[{"type":"text","text":"The bundled fixture is static image content; future WebFS state remains user-owned."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":174,"time":1787472100005}
|
||||
{"type":"step/end","data":{"turn":29,"step":1},"seq":175,"time":1787472100006}
|
||||
{"type":"turn/end","data":{"turn":29,"reason":{"kind":"completed"}},"seq":176,"time":1787472100007}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
{"type":"session","version":0,"id":"preview-follow-up-builder","createdAt":1787472200000,"cwd":"/dsh/workspace","parentSession":"preview-showcase","origin":"subagent","delegationDepth":1,"agentPreset":"standard"}
|
||||
{"type":"turn/start","data":{"turn":1},"seq":0,"time":1787472200000}
|
||||
{"type":"user/message","data":{"id":"preview-builder-user","role":"user","content":[{"type":"text","text":"Check that the Preview workspace can support follow-up tasks."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":1,"time":1787472200001}
|
||||
{"type":"subagent/descriptor","data":{"version":2,"mode":"continuable","provider":"spawn","label":"Continue preview verification"},"seq":2,"time":1787472200002}
|
||||
{"type":"step/start","data":{"turn":1,"step":1},"seq":3,"time":1787472200003}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"id":"preview-builder-assistant","role":"assistant","content":[{"type":"text","text":"This child is continuable and ready for another verification turn."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":4,"time":1787472200004}
|
||||
{"type":"step/end","data":{"turn":1,"step":1},"seq":5,"time":1787472200005}
|
||||
{"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}},"seq":6,"time":1787472200006}
|
||||
+200
@@ -0,0 +1,200 @@
|
||||
{"type":"session","version":0,"id":"preview-showcase","createdAt":1787472000000,"cwd":"/dsh/workspace","delegationDepth":0,"agentPreset":"standard"}
|
||||
{"type":"turn/start","data":{"turn":1},"seq":0,"time":1787472000000}
|
||||
{"type":"user/message","data":{"id":"preview-user-01","role":"user","content":[{"type":"text","text":"History checkpoint 01: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":1,"time":1787472000001}
|
||||
{"type":"session/title","data":{"title":"WebWorker Preview Showcase","messageSeqs":[],"source":{"kind":"user"}},"seq":2,"time":1787472000002}
|
||||
{"type":"step/start","data":{"turn":1,"step":1},"seq":3,"time":1787472000003}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"id":"preview-assistant-01","role":"assistant","content":[{"type":"text","text":"Checkpoint 01 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":4,"time":1787472000004}
|
||||
{"type":"step/end","data":{"turn":1,"step":1},"seq":5,"time":1787472000005}
|
||||
{"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}},"seq":6,"time":1787472000006}
|
||||
{"type":"turn/start","data":{"turn":2},"seq":7,"time":1787472000007}
|
||||
{"type":"user/message","data":{"id":"preview-user-02","role":"user","content":[{"type":"text","text":"History checkpoint 02: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":8,"time":1787472000008}
|
||||
{"type":"step/start","data":{"turn":2,"step":1},"seq":9,"time":1787472000009}
|
||||
{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"id":"preview-assistant-02","role":"assistant","content":[{"type":"text","text":"Checkpoint 02 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":10,"time":1787472000010}
|
||||
{"type":"step/end","data":{"turn":2,"step":1},"seq":11,"time":1787472000011}
|
||||
{"type":"turn/end","data":{"turn":2,"reason":{"kind":"completed"}},"seq":12,"time":1787472000012}
|
||||
{"type":"turn/start","data":{"turn":3},"seq":13,"time":1787472000013}
|
||||
{"type":"user/message","data":{"id":"preview-user-03","role":"user","content":[{"type":"text","text":"History checkpoint 03: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":14,"time":1787472000014}
|
||||
{"type":"step/start","data":{"turn":3,"step":1},"seq":15,"time":1787472000015}
|
||||
{"type":"assistant/message","data":{"turn":3,"step":1,"message":{"id":"preview-assistant-03","role":"assistant","content":[{"type":"text","text":"Checkpoint 03 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":16,"time":1787472000016}
|
||||
{"type":"step/end","data":{"turn":3,"step":1},"seq":17,"time":1787472000017}
|
||||
{"type":"turn/end","data":{"turn":3,"reason":{"kind":"completed"}},"seq":18,"time":1787472000018}
|
||||
{"type":"turn/start","data":{"turn":4},"seq":19,"time":1787472000019}
|
||||
{"type":"user/message","data":{"id":"preview-user-04","role":"user","content":[{"type":"text","text":"History checkpoint 04: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":20,"time":1787472000020}
|
||||
{"type":"step/start","data":{"turn":4,"step":1},"seq":21,"time":1787472000021}
|
||||
{"type":"assistant/message","data":{"turn":4,"step":1,"message":{"id":"preview-assistant-04","role":"assistant","content":[{"type":"text","text":"Checkpoint 04 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":22,"time":1787472000022}
|
||||
{"type":"step/end","data":{"turn":4,"step":1},"seq":23,"time":1787472000023}
|
||||
{"type":"turn/end","data":{"turn":4,"reason":{"kind":"completed"}},"seq":24,"time":1787472000024}
|
||||
{"type":"turn/start","data":{"turn":5},"seq":25,"time":1787472000025}
|
||||
{"type":"user/message","data":{"id":"preview-user-05","role":"user","content":[{"type":"text","text":"History checkpoint 05: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":26,"time":1787472000026}
|
||||
{"type":"step/start","data":{"turn":5,"step":1},"seq":27,"time":1787472000027}
|
||||
{"type":"assistant/message","data":{"turn":5,"step":1,"message":{"id":"preview-assistant-05","role":"assistant","content":[{"type":"text","text":"Checkpoint 05 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":28,"time":1787472000028}
|
||||
{"type":"step/end","data":{"turn":5,"step":1},"seq":29,"time":1787472000029}
|
||||
{"type":"turn/end","data":{"turn":5,"reason":{"kind":"completed"}},"seq":30,"time":1787472000030}
|
||||
{"type":"turn/start","data":{"turn":6},"seq":31,"time":1787472000031}
|
||||
{"type":"user/message","data":{"id":"preview-user-06","role":"user","content":[{"type":"text","text":"History checkpoint 06: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":32,"time":1787472000032}
|
||||
{"type":"step/start","data":{"turn":6,"step":1},"seq":33,"time":1787472000033}
|
||||
{"type":"assistant/message","data":{"turn":6,"step":1,"message":{"id":"preview-assistant-06","role":"assistant","content":[{"type":"text","text":"Checkpoint 06 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":34,"time":1787472000034}
|
||||
{"type":"step/end","data":{"turn":6,"step":1},"seq":35,"time":1787472000035}
|
||||
{"type":"turn/end","data":{"turn":6,"reason":{"kind":"completed"}},"seq":36,"time":1787472000036}
|
||||
{"type":"turn/start","data":{"turn":7},"seq":37,"time":1787472000037}
|
||||
{"type":"user/message","data":{"id":"preview-user-07","role":"user","content":[{"type":"text","text":"History checkpoint 07: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":38,"time":1787472000038}
|
||||
{"type":"step/start","data":{"turn":7,"step":1},"seq":39,"time":1787472000039}
|
||||
{"type":"assistant/message","data":{"turn":7,"step":1,"message":{"id":"preview-assistant-07","role":"assistant","content":[{"type":"text","text":"Checkpoint 07 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":40,"time":1787472000040}
|
||||
{"type":"step/end","data":{"turn":7,"step":1},"seq":41,"time":1787472000041}
|
||||
{"type":"turn/end","data":{"turn":7,"reason":{"kind":"completed"}},"seq":42,"time":1787472000042}
|
||||
{"type":"turn/start","data":{"turn":8},"seq":43,"time":1787472000043}
|
||||
{"type":"user/message","data":{"id":"preview-user-08","role":"user","content":[{"type":"text","text":"History checkpoint 08: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":44,"time":1787472000044}
|
||||
{"type":"step/start","data":{"turn":8,"step":1},"seq":45,"time":1787472000045}
|
||||
{"type":"assistant/message","data":{"turn":8,"step":1,"message":{"id":"preview-assistant-08","role":"assistant","content":[{"type":"text","text":"Checkpoint 08 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":46,"time":1787472000046}
|
||||
{"type":"step/end","data":{"turn":8,"step":1},"seq":47,"time":1787472000047}
|
||||
{"type":"turn/end","data":{"turn":8,"reason":{"kind":"completed"}},"seq":48,"time":1787472000048}
|
||||
{"type":"turn/start","data":{"turn":9},"seq":49,"time":1787472000049}
|
||||
{"type":"user/message","data":{"id":"preview-user-09","role":"user","content":[{"type":"text","text":"History checkpoint 09: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":50,"time":1787472000050}
|
||||
{"type":"step/start","data":{"turn":9,"step":1},"seq":51,"time":1787472000051}
|
||||
{"type":"assistant/message","data":{"turn":9,"step":1,"message":{"id":"preview-assistant-09","role":"assistant","content":[{"type":"text","text":"Checkpoint 09 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":52,"time":1787472000052}
|
||||
{"type":"step/end","data":{"turn":9,"step":1},"seq":53,"time":1787472000053}
|
||||
{"type":"turn/end","data":{"turn":9,"reason":{"kind":"completed"}},"seq":54,"time":1787472000054}
|
||||
{"type":"turn/start","data":{"turn":10},"seq":55,"time":1787472000055}
|
||||
{"type":"user/message","data":{"id":"preview-user-10","role":"user","content":[{"type":"text","text":"History checkpoint 10: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":56,"time":1787472000056}
|
||||
{"type":"step/start","data":{"turn":10,"step":1},"seq":57,"time":1787472000057}
|
||||
{"type":"assistant/message","data":{"turn":10,"step":1,"message":{"id":"preview-assistant-10","role":"assistant","content":[{"type":"text","text":"Checkpoint 10 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":58,"time":1787472000058}
|
||||
{"type":"step/end","data":{"turn":10,"step":1},"seq":59,"time":1787472000059}
|
||||
{"type":"turn/end","data":{"turn":10,"reason":{"kind":"completed"}},"seq":60,"time":1787472000060}
|
||||
{"type":"turn/start","data":{"turn":11},"seq":61,"time":1787472000061}
|
||||
{"type":"user/message","data":{"id":"preview-user-11","role":"user","content":[{"type":"text","text":"History checkpoint 11: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":62,"time":1787472000062}
|
||||
{"type":"step/start","data":{"turn":11,"step":1},"seq":63,"time":1787472000063}
|
||||
{"type":"assistant/message","data":{"turn":11,"step":1,"message":{"id":"preview-assistant-11","role":"assistant","content":[{"type":"text","text":"Checkpoint 11 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":64,"time":1787472000064}
|
||||
{"type":"step/end","data":{"turn":11,"step":1},"seq":65,"time":1787472000065}
|
||||
{"type":"turn/end","data":{"turn":11,"reason":{"kind":"completed"}},"seq":66,"time":1787472000066}
|
||||
{"type":"turn/start","data":{"turn":12},"seq":67,"time":1787472000067}
|
||||
{"type":"user/message","data":{"id":"preview-user-12","role":"user","content":[{"type":"text","text":"History checkpoint 12: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":68,"time":1787472000068}
|
||||
{"type":"step/start","data":{"turn":12,"step":1},"seq":69,"time":1787472000069}
|
||||
{"type":"assistant/message","data":{"turn":12,"step":1,"message":{"id":"preview-assistant-12","role":"assistant","content":[{"type":"text","text":"Checkpoint 12 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":70,"time":1787472000070}
|
||||
{"type":"step/end","data":{"turn":12,"step":1},"seq":71,"time":1787472000071}
|
||||
{"type":"turn/end","data":{"turn":12,"reason":{"kind":"completed"}},"seq":72,"time":1787472000072}
|
||||
{"type":"turn/start","data":{"turn":13},"seq":73,"time":1787472000073}
|
||||
{"type":"user/message","data":{"id":"preview-user-13","role":"user","content":[{"type":"text","text":"History checkpoint 13: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":74,"time":1787472000074}
|
||||
{"type":"step/start","data":{"turn":13,"step":1},"seq":75,"time":1787472000075}
|
||||
{"type":"assistant/message","data":{"turn":13,"step":1,"message":{"id":"preview-assistant-13","role":"assistant","content":[{"type":"text","text":"Checkpoint 13 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":76,"time":1787472000076}
|
||||
{"type":"step/end","data":{"turn":13,"step":1},"seq":77,"time":1787472000077}
|
||||
{"type":"turn/end","data":{"turn":13,"reason":{"kind":"completed"}},"seq":78,"time":1787472000078}
|
||||
{"type":"turn/start","data":{"turn":14},"seq":79,"time":1787472000079}
|
||||
{"type":"user/message","data":{"id":"preview-user-14","role":"user","content":[{"type":"text","text":"History checkpoint 14: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":80,"time":1787472000080}
|
||||
{"type":"step/start","data":{"turn":14,"step":1},"seq":81,"time":1787472000081}
|
||||
{"type":"assistant/message","data":{"turn":14,"step":1,"message":{"id":"preview-assistant-14","role":"assistant","content":[{"type":"text","text":"Checkpoint 14 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":82,"time":1787472000082}
|
||||
{"type":"step/end","data":{"turn":14,"step":1},"seq":83,"time":1787472000083}
|
||||
{"type":"turn/end","data":{"turn":14,"reason":{"kind":"completed"}},"seq":84,"time":1787472000084}
|
||||
{"type":"turn/start","data":{"turn":15},"seq":85,"time":1787472000085}
|
||||
{"type":"user/message","data":{"id":"preview-user-15","role":"user","content":[{"type":"text","text":"History checkpoint 15: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":86,"time":1787472000086}
|
||||
{"type":"step/start","data":{"turn":15,"step":1},"seq":87,"time":1787472000087}
|
||||
{"type":"assistant/message","data":{"turn":15,"step":1,"message":{"id":"preview-assistant-15","role":"assistant","content":[{"type":"text","text":"Checkpoint 15 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":88,"time":1787472000088}
|
||||
{"type":"step/end","data":{"turn":15,"step":1},"seq":89,"time":1787472000089}
|
||||
{"type":"turn/end","data":{"turn":15,"reason":{"kind":"completed"}},"seq":90,"time":1787472000090}
|
||||
{"type":"turn/start","data":{"turn":16},"seq":91,"time":1787472000091}
|
||||
{"type":"user/message","data":{"id":"preview-user-16","role":"user","content":[{"type":"text","text":"History checkpoint 16: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":92,"time":1787472000092}
|
||||
{"type":"step/start","data":{"turn":16,"step":1},"seq":93,"time":1787472000093}
|
||||
{"type":"assistant/message","data":{"turn":16,"step":1,"message":{"id":"preview-assistant-16","role":"assistant","content":[{"type":"text","text":"Checkpoint 16 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":94,"time":1787472000094}
|
||||
{"type":"step/end","data":{"turn":16,"step":1},"seq":95,"time":1787472000095}
|
||||
{"type":"turn/end","data":{"turn":16,"reason":{"kind":"completed"}},"seq":96,"time":1787472000096}
|
||||
{"type":"turn/start","data":{"turn":17},"seq":97,"time":1787472000097}
|
||||
{"type":"user/message","data":{"id":"preview-user-17","role":"user","content":[{"type":"text","text":"History checkpoint 17: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":98,"time":1787472000098}
|
||||
{"type":"step/start","data":{"turn":17,"step":1},"seq":99,"time":1787472000099}
|
||||
{"type":"assistant/message","data":{"turn":17,"step":1,"message":{"id":"preview-assistant-17","role":"assistant","content":[{"type":"text","text":"Checkpoint 17 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":100,"time":1787472000100}
|
||||
{"type":"step/end","data":{"turn":17,"step":1},"seq":101,"time":1787472000101}
|
||||
{"type":"turn/end","data":{"turn":17,"reason":{"kind":"completed"}},"seq":102,"time":1787472000102}
|
||||
{"type":"turn/start","data":{"turn":18},"seq":103,"time":1787472000103}
|
||||
{"type":"user/message","data":{"id":"preview-user-18","role":"user","content":[{"type":"text","text":"History checkpoint 18: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":104,"time":1787472000104}
|
||||
{"type":"step/start","data":{"turn":18,"step":1},"seq":105,"time":1787472000105}
|
||||
{"type":"assistant/message","data":{"turn":18,"step":1,"message":{"id":"preview-assistant-18","role":"assistant","content":[{"type":"text","text":"Checkpoint 18 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":106,"time":1787472000106}
|
||||
{"type":"step/end","data":{"turn":18,"step":1},"seq":107,"time":1787472000107}
|
||||
{"type":"turn/end","data":{"turn":18,"reason":{"kind":"completed"}},"seq":108,"time":1787472000108}
|
||||
{"type":"turn/start","data":{"turn":19},"seq":109,"time":1787472000109}
|
||||
{"type":"user/message","data":{"id":"preview-user-19","role":"user","content":[{"type":"text","text":"History checkpoint 19: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":110,"time":1787472000110}
|
||||
{"type":"step/start","data":{"turn":19,"step":1},"seq":111,"time":1787472000111}
|
||||
{"type":"assistant/message","data":{"turn":19,"step":1,"message":{"id":"preview-assistant-19","role":"assistant","content":[{"type":"text","text":"Checkpoint 19 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":112,"time":1787472000112}
|
||||
{"type":"step/end","data":{"turn":19,"step":1},"seq":113,"time":1787472000113}
|
||||
{"type":"turn/end","data":{"turn":19,"reason":{"kind":"completed"}},"seq":114,"time":1787472000114}
|
||||
{"type":"turn/start","data":{"turn":20},"seq":115,"time":1787472000115}
|
||||
{"type":"user/message","data":{"id":"preview-user-20","role":"user","content":[{"type":"text","text":"History checkpoint 20: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":116,"time":1787472000116}
|
||||
{"type":"step/start","data":{"turn":20,"step":1},"seq":117,"time":1787472000117}
|
||||
{"type":"assistant/message","data":{"turn":20,"step":1,"message":{"id":"preview-assistant-20","role":"assistant","content":[{"type":"text","text":"Checkpoint 20 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":118,"time":1787472000118}
|
||||
{"type":"step/end","data":{"turn":20,"step":1},"seq":119,"time":1787472000119}
|
||||
{"type":"turn/end","data":{"turn":20,"reason":{"kind":"completed"}},"seq":120,"time":1787472000120}
|
||||
{"type":"turn/start","data":{"turn":21},"seq":121,"time":1787472000121}
|
||||
{"type":"user/message","data":{"id":"preview-user-21","role":"user","content":[{"type":"text","text":"History checkpoint 21: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":122,"time":1787472000122}
|
||||
{"type":"step/start","data":{"turn":21,"step":1},"seq":123,"time":1787472000123}
|
||||
{"type":"assistant/message","data":{"turn":21,"step":1,"message":{"id":"preview-assistant-21","role":"assistant","content":[{"type":"text","text":"Checkpoint 21 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":124,"time":1787472000124}
|
||||
{"type":"step/end","data":{"turn":21,"step":1},"seq":125,"time":1787472000125}
|
||||
{"type":"turn/end","data":{"turn":21,"reason":{"kind":"completed"}},"seq":126,"time":1787472000126}
|
||||
{"type":"turn/start","data":{"turn":22},"seq":127,"time":1787472000127}
|
||||
{"type":"user/message","data":{"id":"preview-user-22","role":"user","content":[{"type":"text","text":"History checkpoint 22: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":128,"time":1787472000128}
|
||||
{"type":"step/start","data":{"turn":22,"step":1},"seq":129,"time":1787472000129}
|
||||
{"type":"assistant/message","data":{"turn":22,"step":1,"message":{"id":"preview-assistant-22","role":"assistant","content":[{"type":"text","text":"Checkpoint 22 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":130,"time":1787472000130}
|
||||
{"type":"step/end","data":{"turn":22,"step":1},"seq":131,"time":1787472000131}
|
||||
{"type":"turn/end","data":{"turn":22,"reason":{"kind":"completed"}},"seq":132,"time":1787472000132}
|
||||
{"type":"turn/start","data":{"turn":23},"seq":133,"time":1787472000133}
|
||||
{"type":"user/message","data":{"id":"preview-user-23","role":"user","content":[{"type":"text","text":"History checkpoint 23: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":134,"time":1787472000134}
|
||||
{"type":"step/start","data":{"turn":23,"step":1},"seq":135,"time":1787472000135}
|
||||
{"type":"assistant/message","data":{"turn":23,"step":1,"message":{"id":"preview-assistant-23","role":"assistant","content":[{"type":"text","text":"Checkpoint 23 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":136,"time":1787472000136}
|
||||
{"type":"step/end","data":{"turn":23,"step":1},"seq":137,"time":1787472000137}
|
||||
{"type":"turn/end","data":{"turn":23,"reason":{"kind":"completed"}},"seq":138,"time":1787472000138}
|
||||
{"type":"turn/start","data":{"turn":24},"seq":139,"time":1787472000139}
|
||||
{"type":"user/message","data":{"id":"preview-user-24","role":"user","content":[{"type":"text","text":"History checkpoint 24: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":140,"time":1787472000140}
|
||||
{"type":"step/start","data":{"turn":24,"step":1},"seq":141,"time":1787472000141}
|
||||
{"type":"assistant/message","data":{"turn":24,"step":1,"message":{"id":"preview-assistant-24","role":"assistant","content":[{"type":"text","text":"Checkpoint 24 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":142,"time":1787472000142}
|
||||
{"type":"step/end","data":{"turn":24,"step":1},"seq":143,"time":1787472000143}
|
||||
{"type":"turn/end","data":{"turn":24,"reason":{"kind":"completed"}},"seq":144,"time":1787472000144}
|
||||
{"type":"turn/start","data":{"turn":25},"seq":145,"time":1787472000145}
|
||||
{"type":"user/message","data":{"id":"preview-user-25","role":"user","content":[{"type":"text","text":"History checkpoint 25: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":146,"time":1787472000146}
|
||||
{"type":"step/start","data":{"turn":25,"step":1},"seq":147,"time":1787472000147}
|
||||
{"type":"assistant/message","data":{"turn":25,"step":1,"message":{"id":"preview-assistant-25","role":"assistant","content":[{"type":"text","text":"Checkpoint 25 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":148,"time":1787472000148}
|
||||
{"type":"step/end","data":{"turn":25,"step":1},"seq":149,"time":1787472000149}
|
||||
{"type":"turn/end","data":{"turn":25,"reason":{"kind":"completed"}},"seq":150,"time":1787472000150}
|
||||
{"type":"turn/start","data":{"turn":26},"seq":151,"time":1787472000151}
|
||||
{"type":"user/message","data":{"id":"preview-user-26","role":"user","content":[{"type":"text","text":"History checkpoint 26: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":152,"time":1787472000152}
|
||||
{"type":"step/start","data":{"turn":26,"step":1},"seq":153,"time":1787472000153}
|
||||
{"type":"assistant/message","data":{"turn":26,"step":1,"message":{"id":"preview-assistant-26","role":"assistant","content":[{"type":"text","text":"Checkpoint 26 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":154,"time":1787472000154}
|
||||
{"type":"step/end","data":{"turn":26,"step":1},"seq":155,"time":1787472000155}
|
||||
{"type":"turn/end","data":{"turn":26,"reason":{"kind":"completed"}},"seq":156,"time":1787472000156}
|
||||
{"type":"turn/start","data":{"turn":27},"seq":157,"time":1787472000157}
|
||||
{"type":"user/message","data":{"id":"preview-user-27","role":"user","content":[{"type":"text","text":"History checkpoint 27: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":158,"time":1787472000158}
|
||||
{"type":"step/start","data":{"turn":27,"step":1},"seq":159,"time":1787472000159}
|
||||
{"type":"assistant/message","data":{"turn":27,"step":1,"message":{"id":"preview-assistant-27","role":"assistant","content":[{"type":"text","text":"Checkpoint 27 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":160,"time":1787472000160}
|
||||
{"type":"step/end","data":{"turn":27,"step":1},"seq":161,"time":1787472000161}
|
||||
{"type":"turn/end","data":{"turn":27,"reason":{"kind":"completed"}},"seq":162,"time":1787472000162}
|
||||
{"type":"turn/start","data":{"turn":28},"seq":163,"time":1787472000163}
|
||||
{"type":"user/message","data":{"id":"preview-user-28","role":"user","content":[{"type":"text","text":"History checkpoint 28: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":164,"time":1787472000164}
|
||||
{"type":"step/start","data":{"turn":28,"step":1},"seq":165,"time":1787472000165}
|
||||
{"type":"assistant/message","data":{"turn":28,"step":1,"message":{"id":"preview-assistant-28","role":"assistant","content":[{"type":"text","text":"Checkpoint 28 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":166,"time":1787472000166}
|
||||
{"type":"step/end","data":{"turn":28,"step":1},"seq":167,"time":1787472000167}
|
||||
{"type":"turn/end","data":{"turn":28,"reason":{"kind":"completed"}},"seq":168,"time":1787472000168}
|
||||
{"type":"turn/start","data":{"turn":29},"seq":169,"time":1787472000169}
|
||||
{"type":"user/message","data":{"id":"preview-gallery-user","role":"user","content":[{"type":"text","text":"Show the seeded workspace, tool cards, subagents, and pagination in one tour."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":170,"time":1787472000170}
|
||||
{"type":"step/start","data":{"turn":29,"step":1},"seq":171,"time":1787472000171}
|
||||
{"type":"assistant/message","data":{"turn":29,"step":1,"message":{"id":"preview-gallery-tools","role":"assistant","content":[{"type":"reasoning","text":"I will inspect the deterministic workspace and collect each preview surface."},{"type":"tool-call","id":"preview-read","name":"read","arguments":"{\"file_path\":\"PREVIEW.md\"}"},{"type":"tool-call","id":"preview-write","name":"write","arguments":"{\"file_path\":\"src/preview.ts\",\"content\":\"export const previewStatus = 'ready'\\n\\nexport const previewFeatures = ['tools', 'subagents', 'pagination'] as const\\n\"}"},{"type":"tool-call","id":"preview-bash","name":"bash","arguments":"{\"command\":\"printf 'preview ready\\\\n'\",\"description\":\"Print the preview readiness marker\"}"},{"type":"tool-call","id":"preview-glob","name":"glob","arguments":"{\"pattern\":\"**/*\",\"path\":\".\"}"},{"type":"tool-call","id":"preview-grep","name":"grep","arguments":"{\"pattern\":\"preview\",\"path\":\".\",\"include\":\"*.{md,ts,json}\"}"},{"type":"tool-call","id":"preview-web-search","name":"web_search","arguments":"{\"queries\":[\"Web Worker filesystem compatibility\"]}"},{"type":"tool-call","id":"preview-todo","name":"todo_write","arguments":"{\"todos\":[{\"content\":\"Inspect tool cards\",\"status\":\"completed\"},{\"content\":\"Open both subagents\",\"status\":\"completed\"},{\"content\":\"Load earlier history\",\"status\":\"in_progress\"}]}"},{"type":"tool-call","id":"preview-subagent","name":"subagent","arguments":"{\"description\":\"Continue preview verification\",\"prompt\":\"Check the remaining preview cases.\",\"run_in_background\":true}"},{"type":"tool-call","id":"preview-subagent-fork","name":"subagent_fork","arguments":"{\"description\":\"Review preview architecture\",\"prompt\":\"Review the fixture architecture.\",\"run_in_background\":false}"},{"type":"tool-call","id":"preview-failure","name":"read","arguments":"{\"file_path\":\"missing.txt\"}"}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":172,"time":1787472000172}
|
||||
{"type":"tool/call","data":{"turn":29,"step":1,"callId":"preview-read","name":"read","arguments":"{\"file_path\":\"PREVIEW.md\"}"},"seq":173,"time":1787472000173}
|
||||
{"type":"tool/result","data":{"turn":29,"step":1,"message":{"id":"preview-read-result","role":"user","content":[{"type":"tool-result","toolCallId":"preview-read","content":[{"type":"text","text":"<path>PREVIEW.md</path>\n<type>file</type>\n<content>\n1: # Preview Workspace\n2: \n3: This deterministic workspace is bundled with the browser-only preview.\n4: \n5: - `src/preview.ts` is the file changed by the example write result.\n6: - `data/tasks.json` mirrors the completed preview checklist.\n7: - `.agents/skills/preview-tour/SKILL.md` proves dot directories survive image packing.\n8: \n9: Refresh the preview to restore these image bytes.\n\n(End of file - total 9 lines)\n</content>"}],"isError":false}],"source":{"kind":"tool","callId":"preview-read"}},"meta":{"path":"PREVIEW.md","offset":1,"lines":[{"number":1,"text":"# Preview Workspace"},{"number":2,"text":""},{"number":3,"text":"This deterministic workspace is bundled with the browser-only preview."},{"number":4,"text":""},{"number":5,"text":"- `src/preview.ts` is the file changed by the example write result."},{"number":6,"text":"- `data/tasks.json` mirrors the completed preview checklist."},{"number":7,"text":"- `.agents/skills/preview-tour/SKILL.md` proves dot directories survive image packing."},{"number":8,"text":""},{"number":9,"text":"Refresh the preview to restore these image bytes."}],"totalLines":9,"lang":"md"}},"surfaceOp":"append","seq":174,"time":1787472000174}
|
||||
{"type":"tool/call","data":{"turn":29,"step":1,"callId":"preview-write","name":"write","arguments":"{\"file_path\":\"src/preview.ts\",\"content\":\"export const previewStatus = 'ready'\\n\\nexport const previewFeatures = ['tools', 'subagents', 'pagination'] as const\\n\"}"},"seq":175,"time":1787472000175}
|
||||
{"type":"tool/result","data":{"turn":29,"step":1,"message":{"id":"preview-write-result","role":"user","content":[{"type":"tool-result","toolCallId":"preview-write","content":[{"type":"text","text":"<path>src/preview.ts</path>\n<type>file</type>\n<content>\nUpdated file\n</content>"}],"isError":false}],"source":{"kind":"tool","callId":"preview-write"}},"meta":{"diffs":[{"path":"src/preview.ts","oldText":"export const previewStatus = 'draft'\n","newText":"export const previewStatus = 'ready'\n\nexport const previewFeatures = ['tools', 'subagents', 'pagination'] as const\n"}]}},"surfaceOp":"append","seq":176,"time":1787472000176}
|
||||
{"type":"tool/call","data":{"turn":29,"step":1,"callId":"preview-bash","name":"bash","arguments":"{\"command\":\"printf 'preview ready\\\\n'\",\"description\":\"Print the preview readiness marker\"}"},"seq":177,"time":1787472000177}
|
||||
{"type":"tool/result","data":{"turn":29,"step":1,"message":{"id":"preview-bash-result","role":"user","content":[{"type":"tool-result","toolCallId":"preview-bash","content":[{"type":"text","text":"preview ready\n"}],"isError":false}],"source":{"kind":"tool","callId":"preview-bash"}}},"surfaceOp":"append","seq":178,"time":1787472000178}
|
||||
{"type":"tool/call","data":{"turn":29,"step":1,"callId":"preview-glob","name":"glob","arguments":"{\"pattern\":\"**/*\",\"path\":\".\"}"},"seq":179,"time":1787472000179}
|
||||
{"type":"tool/result","data":{"turn":29,"step":1,"message":{"id":"preview-glob-result","role":"user","content":[{"type":"tool-result","toolCallId":"preview-glob","content":[{"type":"text","text":"PREVIEW.md\ndata/tasks.json\nsrc/preview.ts"}],"isError":false}],"source":{"kind":"tool","callId":"preview-glob"}},"meta":{"shape":"paths","paths":["PREVIEW.md","data/tasks.json","src/preview.ts"],"truncated":false,"total":3}},"surfaceOp":"append","seq":180,"time":1787472000180}
|
||||
{"type":"tool/call","data":{"turn":29,"step":1,"callId":"preview-grep","name":"grep","arguments":"{\"pattern\":\"preview\",\"path\":\".\",\"include\":\"*.{md,ts,json}\"}"},"seq":181,"time":1787472000181}
|
||||
{"type":"tool/result","data":{"turn":29,"step":1,"message":{"id":"preview-grep-result","role":"user","content":[{"type":"tool-result","toolCallId":"preview-grep","content":[{"type":"text","text":"PREVIEW.md:3:This deterministic workspace is bundled with the browser-only preview.\nsrc/preview.ts:1:export const previewStatus = 'ready'"}],"isError":false}],"source":{"kind":"tool","callId":"preview-grep"}},"meta":{"shape":"matches","files":[{"path":"PREVIEW.md","matches":[{"lineNumber":3,"line":"This deterministic workspace is bundled with the browser-only preview."}]},{"path":"src/preview.ts","matches":[{"lineNumber":1,"line":"export const previewStatus = 'ready'"}]}],"truncated":false,"total":2}},"surfaceOp":"append","seq":182,"time":1787472000182}
|
||||
{"type":"tool/call","data":{"turn":29,"step":1,"callId":"preview-web-search","name":"web_search","arguments":"{\"queries\":[\"Web Worker filesystem compatibility\"]}"},"seq":183,"time":1787472000183}
|
||||
{"type":"tool/result","data":{"turn":29,"step":1,"message":{"id":"preview-web-search-result","role":"user","content":[{"type":"tool-result","toolCallId":"preview-web-search","content":[{"type":"text","text":"Browser workers can host deterministic in-memory filesystems.\n\nSources:\n1. MDN Web Workers API — https://developer.mozilla.org/docs/Web/API/Web_Workers_API"}],"isError":false}],"source":{"kind":"tool","callId":"preview-web-search"}},"meta":{"sources":[{"url":"https://developer.mozilla.org/docs/Web/API/Web_Workers_API","title":"Web Workers API","snippet":"Web Workers run scripts in background threads."}],"truncated":false,"answer":"Browser workers can host deterministic in-memory filesystems."}},"surfaceOp":"append","seq":184,"time":1787472000184}
|
||||
{"type":"tool/call","data":{"turn":29,"step":1,"callId":"preview-todo","name":"todo_write","arguments":"{\"todos\":[{\"content\":\"Inspect tool cards\",\"status\":\"completed\"},{\"content\":\"Open both subagents\",\"status\":\"completed\"},{\"content\":\"Load earlier history\",\"status\":\"in_progress\"}]}"},"seq":185,"time":1787472000185}
|
||||
{"type":"todo/write","data":{"todos":[{"content":"Inspect tool cards","status":"completed"},{"content":"Open both subagents","status":"completed"},{"content":"Load earlier history","status":"in_progress"}]},"seq":186,"time":1787472000186}
|
||||
{"type":"tool/result","data":{"turn":29,"step":1,"message":{"id":"preview-todo-result","role":"user","content":[{"type":"tool-result","toolCallId":"preview-todo","content":[{"type":"text","text":"Updated todo list: 0 pending, 1 in progress, 2 completed."}],"isError":false}],"source":{"kind":"tool","callId":"preview-todo"}}},"surfaceOp":"append","seq":187,"time":1787472000187}
|
||||
{"type":"tool/call","data":{"turn":29,"step":1,"callId":"preview-subagent","name":"subagent","arguments":"{\"description\":\"Continue preview verification\",\"prompt\":\"Check the remaining preview cases.\",\"run_in_background\":true}"},"seq":188,"time":1787472000188}
|
||||
{"type":"tool/result","data":{"turn":29,"step":1,"message":{"id":"preview-subagent-result","role":"user","content":[{"type":"tool-result","toolCallId":"preview-subagent","content":[{"type":"text","text":"started subagent preview-follow-up-builder"}],"isError":false}],"source":{"kind":"tool","callId":"preview-subagent"}}},"surfaceOp":"append","seq":189,"time":1787472000189}
|
||||
{"type":"tool/call","data":{"turn":29,"step":1,"callId":"preview-subagent-fork","name":"subagent_fork","arguments":"{\"description\":\"Review preview architecture\",\"prompt\":\"Review the fixture architecture.\",\"run_in_background\":false}"},"seq":190,"time":1787472000190}
|
||||
{"type":"tool/result","data":{"turn":29,"step":1,"message":{"id":"preview-subagent-fork-result","role":"user","content":[{"type":"tool-result","toolCallId":"preview-subagent-fork","content":[{"type":"text","text":"The preview fixture remains separate from user-owned WebFS data."}],"isError":false}],"source":{"kind":"tool","callId":"preview-subagent-fork"}}},"surfaceOp":"append","seq":191,"time":1787472000191}
|
||||
{"type":"tool/call","data":{"turn":29,"step":1,"callId":"preview-failure","name":"read","arguments":"{\"file_path\":\"missing.txt\"}"},"seq":192,"time":1787472000192}
|
||||
{"type":"tool/result","data":{"turn":29,"step":1,"message":{"id":"preview-failure-result","role":"user","content":[{"type":"tool-result","toolCallId":"preview-failure","content":[{"type":"text","text":"Error: ENOENT: no such file, open missing.txt"}],"isError":true}],"source":{"kind":"tool","callId":"preview-failure"}},"error":{"name":"FsError","code":"ENOENT"}},"surfaceOp":"append","seq":193,"time":1787472000193}
|
||||
{"type":"step/end","data":{"turn":29,"step":1},"seq":194,"time":1787472000194}
|
||||
{"type":"step/start","data":{"turn":29,"step":2},"seq":195,"time":1787472000195}
|
||||
{"type":"assistant/message","data":{"turn":29,"step":2,"message":{"id":"preview-gallery-final","role":"assistant","content":[{"type":"text","text":"## Preview tour complete\n\nThe workspace, specialized tool cards, two subagent histories, and an earlier history page are ready to inspect."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":196,"time":1787472000196}
|
||||
{"type":"step/end","data":{"turn":29,"step":2},"seq":197,"time":1787472000197}
|
||||
{"type":"turn/end","data":{"turn":29,"reason":{"kind":"completed"}},"seq":198,"time":1787472000198}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"unit": {
|
||||
"name": "session_projcache",
|
||||
"version": 3
|
||||
},
|
||||
"global": null,
|
||||
"tables": {
|
||||
"sessions": {
|
||||
"preview-showcase": {
|
||||
"identity": {
|
||||
"createdAt": 1787472000000,
|
||||
"cwd": "/dsh/workspace"
|
||||
},
|
||||
"rows": {
|
||||
"title": {
|
||||
"ver": 1,
|
||||
"seq": 198,
|
||||
"val": "WebWorker Preview Showcase"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
---
|
||||
name: preview-tour
|
||||
description: Inspect the bundled Preview workspace and its deterministic Session examples.
|
||||
---
|
||||
|
||||
# Preview tour
|
||||
|
||||
Read the workspace files, inspect the tool gallery, open both subagent histories, and load the earlier conversation page.
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
# Preview Workspace
|
||||
|
||||
This deterministic workspace is bundled with the browser-only preview.
|
||||
|
||||
- `src/preview.ts` is the file changed by the example write result.
|
||||
- `data/tasks.json` mirrors the completed preview checklist.
|
||||
- `.agents/skills/preview-tour/SKILL.md` proves dot directories survive image packing.
|
||||
|
||||
Refresh the preview to restore these image bytes.
|
||||
Vendored
+17
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"title": "Preview verification",
|
||||
"tasks": [
|
||||
{
|
||||
"name": "Inspect tool cards",
|
||||
"status": "completed"
|
||||
},
|
||||
{
|
||||
"name": "Open both subagents",
|
||||
"status": "completed"
|
||||
},
|
||||
{
|
||||
"name": "Load earlier history",
|
||||
"status": "completed"
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
export const previewStatus = 'ready'
|
||||
|
||||
export const previewFeatures = ['tools', 'subagents', 'pagination'] as const
|
||||
@@ -108,10 +108,9 @@ it('reports that a synchronous run cannot happen, without throwing at the probe'
|
||||
})
|
||||
expect(spawnSync(launcherPath(), ['--ro', '/', '--', 'echo', 'x']).error?.message)
|
||||
.toContain('commands run asynchronously')
|
||||
expect(spawnSync(launcherPath(), ['--probe', '--'])).toMatchObject({
|
||||
status: LAUNCHER_FAILURE_EXIT,
|
||||
stderr: expect.any(Buffer),
|
||||
})
|
||||
const failedProbe = spawnSync(launcherPath(), ['--probe', '--'])
|
||||
expect(failedProbe.status).toBe(LAUNCHER_FAILURE_EXIT)
|
||||
expect(Buffer.isBuffer(failedProbe.stderr)).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps the native Landlock package API and CLI failure contract', async () => {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { packTar, parseTar } from '../../src/storage/tar.ts'
|
||||
import { loadVfsImage } from '../../src/storage/memory.ts'
|
||||
import { loadVfsImage, loadVfsOverlay } from '../../src/storage/memory.ts'
|
||||
|
||||
const encoder = new TextEncoder()
|
||||
|
||||
@@ -38,4 +38,21 @@ describe('tar codec', () => {
|
||||
expect(vfs.existsSync('/dsh/workspace')).toBe(true)
|
||||
expect(vfs.existsSync('/dsh/absent')).toBe(false)
|
||||
})
|
||||
|
||||
it('applies ordered data overlays without exposing runtime paths', () => {
|
||||
const vfs = loadVfsImage(packTar({
|
||||
'config/cordis.yml': encoder.encode('- id: subject\n'),
|
||||
'workspace/status.txt': encoder.encode('base'),
|
||||
}), '/dsh')
|
||||
loadVfsOverlay(packTar({
|
||||
'workspace/status.txt': encoder.encode('fixture'),
|
||||
'home/sessions/example/session.jsonl': encoder.encode('{}\n'),
|
||||
}), '/dsh', vfs)
|
||||
expect(vfs.readFileSync('/dsh/workspace/status.txt', 'utf8')).toBe('fixture')
|
||||
expect(vfs.readFileSync('/dsh/home/sessions/example/session.jsonl', 'utf8')).toBe('{}\n')
|
||||
expect(() => loadVfsOverlay(packTar({
|
||||
'config/cordis.yml': encoder.encode('replaced'),
|
||||
}), '/dsh', vfs)).toThrow(/overlay entry must stay under home\/ or workspace/)
|
||||
expect(vfs.readFileSync('/dsh/config/cordis.yml', 'utf8')).toBe('- id: subject\n')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { parseInboundFrame } from '../../src/transport/frames.ts'
|
||||
|
||||
describe('tunnel init frame', () => {
|
||||
it('retains the selected overlay order', () => {
|
||||
expect(parseInboundFrame({
|
||||
t: 'init',
|
||||
image: 'base.tar.gz',
|
||||
overlays: ['workspace.tar.gz', 'session.tar.gz'],
|
||||
})).toEqual({
|
||||
t: 'init',
|
||||
image: 'base.tar.gz',
|
||||
overlays: ['workspace.tar.gz', 'session.tar.gz'],
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects a missing or non-string overlay list', () => {
|
||||
expect(() => parseInboundFrame({ t: 'init', image: 'base.tar.gz' })).toThrow(/array of string overlay urls/)
|
||||
expect(() => parseInboundFrame({ t: 'init', image: 'base.tar.gz', overlays: [1] }))
|
||||
.toThrow(/array of string overlay urls/)
|
||||
})
|
||||
})
|
||||
@@ -54,6 +54,27 @@ function stubWorker(): {
|
||||
}
|
||||
}
|
||||
|
||||
// The opening frame preserves overlay order for deterministic pre-boot mounts.
|
||||
{
|
||||
const { worker, sent } = stubWorker()
|
||||
const tunnel = new WorkerTunnel(worker)
|
||||
tunnel.init('https://preview.test/base.tar.gz', [
|
||||
'https://preview.test/first.tar.gz',
|
||||
'https://preview.test/second.tar.gz',
|
||||
])
|
||||
check('the init frame carries ordered overlays', sent[0], {
|
||||
t: 'init',
|
||||
image: 'https://preview.test/base.tar.gz',
|
||||
overlays: ['https://preview.test/first.tar.gz', 'https://preview.test/second.tar.gz'],
|
||||
})
|
||||
|
||||
const direct = stubWorker()
|
||||
new WorkerTunnel(direct.worker).init('https://preview.test/base.tar.gz')
|
||||
check('the direct init path defaults to no overlays', direct.sent[0], {
|
||||
t: 'init', image: 'https://preview.test/base.tar.gz', overlays: [],
|
||||
})
|
||||
}
|
||||
|
||||
// A normal reply resolves and says nothing on the console.
|
||||
{
|
||||
const { worker, sent, deliver } = stubWorker()
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import { readFileSync, readdirSync } from 'node:fs'
|
||||
import { join, relative } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Session, SessionId, type SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import { scanLog } from '@deepseek-ai/dsh-session-persistence-jsonl/src/format.ts'
|
||||
import { foldSubagentDescriptor } from '@deepseek-ai/dsh-subagent'
|
||||
import {
|
||||
buildVfsExampleFiles,
|
||||
VFS_EXAMPLE_OLDEST_MESSAGE,
|
||||
VFS_EXAMPLE_ROOT,
|
||||
VFS_EXAMPLE_SESSION_IDS,
|
||||
VFS_EXAMPLE_TAIL_MESSAGE,
|
||||
VFS_EXAMPLE_TITLE,
|
||||
} from './vfs-example-fixture.ts'
|
||||
|
||||
function filesUnder(root: string): string[] {
|
||||
const files: string[] = []
|
||||
const visit = (directory: string): void => {
|
||||
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
||||
const path = join(directory, entry.name)
|
||||
if (entry.isDirectory()) visit(path)
|
||||
else if (entry.isFile()) files.push(relative(root, path).replaceAll('\\', '/'))
|
||||
}
|
||||
}
|
||||
visit(root)
|
||||
return files.sort()
|
||||
}
|
||||
|
||||
function readSession(id: string): ReturnType<typeof scanLog> {
|
||||
return scanLog(readFileSync(
|
||||
join(VFS_EXAMPLE_ROOT, 'home/sessions/--dsh-workspace--', id, 'session.jsonl'),
|
||||
))
|
||||
}
|
||||
|
||||
function textOf(event: SessionEvent): string {
|
||||
if (event.type === 'user/message') {
|
||||
return event.data.content.flatMap(block => block.type === 'text' ? [block.text] : []).join('\n')
|
||||
}
|
||||
if (event.type === 'assistant/message') {
|
||||
return event.data.message.content.flatMap(block => block.type === 'text' ? [block.text] : []).join('\n')
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
describe('WebWorker preview VFS example', () => {
|
||||
it('matches its deterministic source byte for byte', () => {
|
||||
const expected = buildVfsExampleFiles()
|
||||
expect(filesUnder(VFS_EXAMPLE_ROOT)).toEqual([...expected.keys()].sort())
|
||||
for (const [path, content] of expected) {
|
||||
expect(readFileSync(join(VFS_EXAMPLE_ROOT, path), 'utf8'), path).toBe(content)
|
||||
}
|
||||
})
|
||||
|
||||
it('seeds the cold-list title cache against the main log identity', () => {
|
||||
const cache = JSON.parse(readFileSync(
|
||||
join(VFS_EXAMPLE_ROOT, 'home/storages/session_projcache.json'),
|
||||
'utf8',
|
||||
)) as {
|
||||
unit: { name: string; version: number }
|
||||
tables: { sessions: Record<string, { identity: { createdAt: number; cwd: string }; rows: { title: unknown } }> }
|
||||
}
|
||||
expect(cache.unit).toEqual({ name: 'session_projcache', version: 3 })
|
||||
expect(cache.tables.sessions[VFS_EXAMPLE_SESSION_IDS.main]).toMatchObject({
|
||||
identity: { createdAt: 1_787_472_000_000, cwd: '/dsh/workspace' },
|
||||
rows: { title: { ver: 1, val: VFS_EXAMPLE_TITLE } },
|
||||
})
|
||||
})
|
||||
|
||||
it('restores the main production log with paging and tool coverage', () => {
|
||||
const { meta, events } = readSession(VFS_EXAMPLE_SESSION_IDS.main)
|
||||
expect(meta).toMatchObject({
|
||||
id: VFS_EXAMPLE_SESSION_IDS.main,
|
||||
cwd: '/dsh/workspace',
|
||||
delegationDepth: 0,
|
||||
agentPreset: 'standard',
|
||||
})
|
||||
expect(events.map(event => event.seq)).toEqual(events.map((_, index) => index))
|
||||
expect(events.at(-1)).toMatchObject({ type: 'turn/end', data: { reason: { kind: 'completed' } } })
|
||||
expect(() => Session.fromRestore(SessionId(meta.id), events, meta)).not.toThrow()
|
||||
|
||||
const messages = events.filter(event =>
|
||||
(event.type === 'user/message' || event.type === 'assistant/message') && event.surfaceOp === 'append')
|
||||
expect(messages.length).toBeGreaterThan(50)
|
||||
expect(messages.some(event => textOf(event).includes(VFS_EXAMPLE_OLDEST_MESSAGE))).toBe(true)
|
||||
expect(messages.some(event => textOf(event).includes(VFS_EXAMPLE_TAIL_MESSAGE))).toBe(true)
|
||||
expect(events.some(event => event.type === 'session/title'
|
||||
&& (event.data as { title?: unknown }).title === VFS_EXAMPLE_TITLE)).toBe(true)
|
||||
|
||||
const tools = events.flatMap(event => event.type === 'tool/call' ? [event.data.name] : [])
|
||||
expect(new Set(tools)).toEqual(new Set([
|
||||
'read', 'write', 'bash', 'glob', 'grep', 'web_search', 'todo_write', 'subagent', 'subagent_fork',
|
||||
]))
|
||||
expect(events.some(event => event.type === 'todo/write')).toBe(true)
|
||||
expect(events.some(event => event.type === 'tool/result' && event.data.message.content[0].isError === true)).toBe(true)
|
||||
})
|
||||
|
||||
it('restores one-shot and continuable child Sessions with durable descriptors', () => {
|
||||
const expected = [
|
||||
[VFS_EXAMPLE_SESSION_IDS.oneShot, 'one-shot'],
|
||||
[VFS_EXAMPLE_SESSION_IDS.continuable, 'continuable'],
|
||||
] as const
|
||||
for (const [id, mode] of expected) {
|
||||
const { meta, events } = readSession(id)
|
||||
expect(meta).toMatchObject({
|
||||
id,
|
||||
cwd: '/dsh/workspace',
|
||||
parentSession: VFS_EXAMPLE_SESSION_IDS.main,
|
||||
origin: 'subagent',
|
||||
delegationDepth: 1,
|
||||
agentPreset: 'standard',
|
||||
})
|
||||
expect(events.map(event => event.seq)).toEqual(events.map((_, index) => index))
|
||||
expect(events.at(-1)).toMatchObject({ type: 'turn/end', data: { reason: { kind: 'completed' } } })
|
||||
expect(foldSubagentDescriptor(events.slice(meta.seedLength ?? 0))).toMatchObject({ mode })
|
||||
expect(() => Session.fromRestore(SessionId(meta.id), events, meta)).not.toThrow()
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,432 @@
|
||||
/** Deterministic source for the filesystem tree bundled into the WebWorker preview. */
|
||||
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { SessionId, type SessionEvent, type SessionHeader } from '@deepseek-ai/dsh-session'
|
||||
import {
|
||||
eventLines, projectKey, toHeaderLine,
|
||||
} from '@deepseek-ai/dsh-session-persistence-jsonl/src/format.ts'
|
||||
import { snapshotSubagentDescriptor } from '@deepseek-ai/dsh-subagent'
|
||||
|
||||
/** Root copied by the preview image's repository adapter. */
|
||||
export const VFS_EXAMPLE_ROOT = fileURLToPath(new URL('./fixtures/vfs-example', import.meta.url))
|
||||
|
||||
/** Durable ids used by browser assertions and subagent parent links. */
|
||||
export const VFS_EXAMPLE_SESSION_IDS = {
|
||||
main: SessionId('preview-showcase'),
|
||||
oneShot: SessionId('preview-architecture-review'),
|
||||
continuable: SessionId('preview-follow-up-builder'),
|
||||
} as const
|
||||
|
||||
/** Stable title rendered in the root Session list. */
|
||||
export const VFS_EXAMPLE_TITLE = 'WebWorker Preview Showcase'
|
||||
|
||||
/** Oldest prompt, intentionally outside the first 50-message history page. */
|
||||
export const VFS_EXAMPLE_OLDEST_MESSAGE = 'History checkpoint 01: verify deterministic preview state.'
|
||||
|
||||
/** Settled tail marker used by browser acceptance and the demonstration GIF. */
|
||||
export const VFS_EXAMPLE_TAIL_MESSAGE = 'Preview tour complete'
|
||||
|
||||
const WORKSPACE = '/dsh/workspace'
|
||||
const CREATED_AT = 1_787_472_000_000
|
||||
const HISTORICAL_TURNS = 28
|
||||
|
||||
const PREVIEW_GUIDE = `# Preview Workspace
|
||||
|
||||
This deterministic workspace is bundled with the browser-only preview.
|
||||
|
||||
- \`src/preview.ts\` is the file changed by the example write result.
|
||||
- \`data/tasks.json\` mirrors the completed preview checklist.
|
||||
- \`.agents/skills/preview-tour/SKILL.md\` proves dot directories survive image packing.
|
||||
|
||||
Refresh the preview to restore these image bytes.
|
||||
`
|
||||
|
||||
const PREVIEW_SOURCE_BEFORE = 'export const previewStatus = \'draft\'\n'
|
||||
|
||||
const PREVIEW_SOURCE = `export const previewStatus = 'ready'
|
||||
|
||||
export const previewFeatures = ['tools', 'subagents', 'pagination'] as const
|
||||
`
|
||||
|
||||
const TASKS = `${JSON.stringify({
|
||||
title: 'Preview verification',
|
||||
tasks: [
|
||||
{ name: 'Inspect tool cards', status: 'completed' },
|
||||
{ name: 'Open both subagents', status: 'completed' },
|
||||
{ name: 'Load earlier history', status: 'completed' },
|
||||
],
|
||||
}, null, 2)}\n`
|
||||
|
||||
const SKILL = `---
|
||||
name: preview-tour
|
||||
description: Inspect the bundled Preview workspace and its deterministic Session examples.
|
||||
---
|
||||
|
||||
# Preview tour
|
||||
|
||||
Read the workspace files, inspect the tool gallery, open both subagent histories, and load the earlier conversation page.
|
||||
`
|
||||
|
||||
interface EventDraft {
|
||||
readonly type: string
|
||||
readonly data: unknown
|
||||
readonly surfaceOp?: 'append'
|
||||
readonly sourceEventSeqs?: number[]
|
||||
readonly ignorable?: true
|
||||
}
|
||||
|
||||
class EventLog {
|
||||
readonly events: SessionEvent[]
|
||||
private nextTime: number
|
||||
|
||||
constructor(time: number, seed: readonly SessionEvent[] = []) {
|
||||
this.events = seed.map(event => structuredClone(event))
|
||||
this.nextTime = Math.max(time, (this.events.at(-1)?.time ?? time - 1) + 1)
|
||||
}
|
||||
|
||||
add(draft: EventDraft): number {
|
||||
const seq = this.events.length
|
||||
this.events.push({ ...draft, seq, time: this.nextTime++ } as unknown as SessionEvent)
|
||||
return seq
|
||||
}
|
||||
}
|
||||
|
||||
function userMessage(id: string, text: string): EventDraft {
|
||||
return {
|
||||
type: 'user/message',
|
||||
data: {
|
||||
id,
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text }],
|
||||
source: { kind: 'user' },
|
||||
},
|
||||
surfaceOp: 'append',
|
||||
}
|
||||
}
|
||||
|
||||
function assistantMessage(id: string, turn: number, step: number, content: unknown[]): EventDraft {
|
||||
return {
|
||||
type: 'assistant/message',
|
||||
data: {
|
||||
turn,
|
||||
step,
|
||||
message: {
|
||||
id,
|
||||
role: 'assistant',
|
||||
content,
|
||||
source: { kind: 'model', provider: 'preview-fixture', model: 'deterministic' },
|
||||
},
|
||||
},
|
||||
sourceEventSeqs: [],
|
||||
surfaceOp: 'append',
|
||||
}
|
||||
}
|
||||
|
||||
interface GalleryCall {
|
||||
readonly id: string
|
||||
readonly name: string
|
||||
readonly args: Record<string, unknown>
|
||||
readonly result: string
|
||||
readonly meta?: unknown
|
||||
readonly error?: { readonly name: string; readonly code: string }
|
||||
readonly todos?: Array<{ readonly content: string; readonly status: 'pending' | 'in_progress' | 'completed' }>
|
||||
}
|
||||
|
||||
function readResult(): { text: string; meta: unknown } {
|
||||
const lines = PREVIEW_GUIDE.trimEnd().split('\n').map((text, index) => ({ number: index + 1, text }))
|
||||
return {
|
||||
text: `<path>PREVIEW.md</path>\n<type>file</type>\n<content>\n${lines.map(line => `${String(line.number)}: ${line.text}`).join('\n')}\n\n(End of file - total ${String(lines.length)} lines)\n</content>`,
|
||||
meta: { path: 'PREVIEW.md', offset: 1, lines, totalLines: lines.length, lang: 'md' },
|
||||
}
|
||||
}
|
||||
|
||||
function galleryCalls(): GalleryCall[] {
|
||||
const read = readResult()
|
||||
return [
|
||||
{
|
||||
id: 'preview-read',
|
||||
name: 'read',
|
||||
args: { file_path: 'PREVIEW.md' },
|
||||
result: read.text,
|
||||
meta: read.meta,
|
||||
},
|
||||
{
|
||||
id: 'preview-write',
|
||||
name: 'write',
|
||||
args: { file_path: 'src/preview.ts', content: PREVIEW_SOURCE },
|
||||
result: '<path>src/preview.ts</path>\n<type>file</type>\n<content>\nUpdated file\n</content>',
|
||||
meta: { diffs: [{ path: 'src/preview.ts', oldText: PREVIEW_SOURCE_BEFORE, newText: PREVIEW_SOURCE }] },
|
||||
},
|
||||
{
|
||||
id: 'preview-bash',
|
||||
name: 'bash',
|
||||
args: { command: "printf 'preview ready\\n'", description: 'Print the preview readiness marker' },
|
||||
result: 'preview ready\n',
|
||||
},
|
||||
{
|
||||
id: 'preview-glob',
|
||||
name: 'glob',
|
||||
args: { pattern: '**/*', path: '.' },
|
||||
result: 'PREVIEW.md\ndata/tasks.json\nsrc/preview.ts',
|
||||
meta: {
|
||||
shape: 'paths',
|
||||
paths: ['PREVIEW.md', 'data/tasks.json', 'src/preview.ts'],
|
||||
truncated: false,
|
||||
total: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'preview-grep',
|
||||
name: 'grep',
|
||||
args: { pattern: 'preview', path: '.', include: '*.{md,ts,json}' },
|
||||
result: 'PREVIEW.md:3:This deterministic workspace is bundled with the browser-only preview.\nsrc/preview.ts:1:export const previewStatus = \'ready\'',
|
||||
meta: {
|
||||
shape: 'matches',
|
||||
files: [
|
||||
{ path: 'PREVIEW.md', matches: [{ lineNumber: 3, line: 'This deterministic workspace is bundled with the browser-only preview.' }] },
|
||||
{ path: 'src/preview.ts', matches: [{ lineNumber: 1, line: "export const previewStatus = 'ready'" }] },
|
||||
],
|
||||
truncated: false,
|
||||
total: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'preview-web-search',
|
||||
name: 'web_search',
|
||||
args: { queries: ['Web Worker filesystem compatibility'] },
|
||||
result: 'Browser workers can host deterministic in-memory filesystems.\n\nSources:\n1. MDN Web Workers API — https://developer.mozilla.org/docs/Web/API/Web_Workers_API',
|
||||
meta: {
|
||||
sources: [{
|
||||
url: 'https://developer.mozilla.org/docs/Web/API/Web_Workers_API',
|
||||
title: 'Web Workers API',
|
||||
snippet: 'Web Workers run scripts in background threads.',
|
||||
}],
|
||||
truncated: false,
|
||||
answer: 'Browser workers can host deterministic in-memory filesystems.',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'preview-todo',
|
||||
name: 'todo_write',
|
||||
args: {
|
||||
todos: [
|
||||
{ content: 'Inspect tool cards', status: 'completed' },
|
||||
{ content: 'Open both subagents', status: 'completed' },
|
||||
{ content: 'Load earlier history', status: 'in_progress' },
|
||||
],
|
||||
},
|
||||
result: 'Updated todo list: 0 pending, 1 in progress, 2 completed.',
|
||||
todos: [
|
||||
{ content: 'Inspect tool cards', status: 'completed' },
|
||||
{ content: 'Open both subagents', status: 'completed' },
|
||||
{ content: 'Load earlier history', status: 'in_progress' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'preview-subagent',
|
||||
name: 'subagent',
|
||||
args: { description: 'Continue preview verification', prompt: 'Check the remaining preview cases.', run_in_background: true },
|
||||
result: `started subagent ${VFS_EXAMPLE_SESSION_IDS.continuable}`,
|
||||
},
|
||||
{
|
||||
id: 'preview-subagent-fork',
|
||||
name: 'subagent_fork',
|
||||
args: { description: 'Review preview architecture', prompt: 'Review the fixture architecture.', run_in_background: false },
|
||||
result: 'The preview fixture remains separate from user-owned WebFS data.',
|
||||
},
|
||||
{
|
||||
id: 'preview-failure',
|
||||
name: 'read',
|
||||
args: { file_path: 'missing.txt' },
|
||||
result: 'Error: ENOENT: no such file, open missing.txt',
|
||||
error: { name: 'FsError', code: 'ENOENT' },
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
function addClosedTextTurn(log: EventLog, turn: number): void {
|
||||
const checkpoint = String(turn).padStart(2, '0')
|
||||
log.add({ type: 'turn/start', data: { turn } })
|
||||
log.add(userMessage(`preview-user-${checkpoint}`, `History checkpoint ${checkpoint}: verify deterministic preview state.`))
|
||||
if (turn === 1) {
|
||||
log.add({
|
||||
type: 'session/title',
|
||||
data: { title: VFS_EXAMPLE_TITLE, messageSeqs: [], source: { kind: 'user' } },
|
||||
})
|
||||
}
|
||||
log.add({ type: 'step/start', data: { turn, step: 1 } })
|
||||
log.add(assistantMessage(
|
||||
`preview-assistant-${checkpoint}`,
|
||||
turn,
|
||||
1,
|
||||
[{ type: 'text', text: `Checkpoint ${checkpoint} is recorded.` }],
|
||||
))
|
||||
log.add({ type: 'step/end', data: { turn, step: 1 } })
|
||||
log.add({ type: 'turn/end', data: { turn, reason: { kind: 'completed' } } })
|
||||
}
|
||||
|
||||
function mainLog(): { readonly events: SessionEvent[]; readonly forkSeedLength: number } {
|
||||
const log = new EventLog(CREATED_AT)
|
||||
for (let turn = 1; turn <= HISTORICAL_TURNS; turn++) addClosedTextTurn(log, turn)
|
||||
const forkSeedLength = log.events.length
|
||||
const turn = HISTORICAL_TURNS + 1
|
||||
const calls = galleryCalls()
|
||||
|
||||
log.add({ type: 'turn/start', data: { turn } })
|
||||
log.add(userMessage('preview-gallery-user', 'Show the seeded workspace, tool cards, subagents, and pagination in one tour.'))
|
||||
log.add({ type: 'step/start', data: { turn, step: 1 } })
|
||||
log.add(assistantMessage('preview-gallery-tools', turn, 1, [
|
||||
{ type: 'reasoning', text: 'I will inspect the deterministic workspace and collect each preview surface.' },
|
||||
...calls.map(call => ({ type: 'tool-call', id: call.id, name: call.name, arguments: JSON.stringify(call.args) })),
|
||||
]))
|
||||
for (const call of calls) {
|
||||
log.add({
|
||||
type: 'tool/call',
|
||||
data: { turn, step: 1, callId: call.id, name: call.name, arguments: JSON.stringify(call.args) },
|
||||
})
|
||||
if (call.todos !== undefined) log.add({ type: 'todo/write', data: { todos: call.todos } })
|
||||
log.add({
|
||||
type: 'tool/result',
|
||||
data: {
|
||||
turn,
|
||||
step: 1,
|
||||
message: {
|
||||
id: `${call.id}-result`,
|
||||
role: 'user',
|
||||
content: [{
|
||||
type: 'tool-result',
|
||||
toolCallId: call.id,
|
||||
content: [{ type: 'text', text: call.result }],
|
||||
isError: call.error !== undefined,
|
||||
}],
|
||||
source: { kind: 'tool', callId: call.id },
|
||||
},
|
||||
...call.meta === undefined ? {} : { meta: call.meta },
|
||||
...call.error === undefined ? {} : { error: call.error },
|
||||
},
|
||||
surfaceOp: 'append',
|
||||
})
|
||||
}
|
||||
log.add({ type: 'step/end', data: { turn, step: 1 } })
|
||||
log.add({ type: 'step/start', data: { turn, step: 2 } })
|
||||
log.add(assistantMessage('preview-gallery-final', turn, 2, [{
|
||||
type: 'text',
|
||||
text: `## ${VFS_EXAMPLE_TAIL_MESSAGE}\n\nThe workspace, specialized tool cards, two subagent histories, and an earlier history page are ready to inspect.`,
|
||||
}]))
|
||||
log.add({ type: 'step/end', data: { turn, step: 2 } })
|
||||
log.add({ type: 'turn/end', data: { turn, reason: { kind: 'completed' } } })
|
||||
return { events: log.events, forkSeedLength }
|
||||
}
|
||||
|
||||
function oneShotLog(seed: readonly SessionEvent[]): SessionEvent[] {
|
||||
const log = new EventLog(CREATED_AT + 100_000, seed)
|
||||
log.add({ type: 'session/end-seed', data: {} })
|
||||
const turn = HISTORICAL_TURNS + 1
|
||||
log.add({ type: 'turn/start', data: { turn } })
|
||||
log.add(userMessage('preview-review-user', 'Review whether the preview fixture is isolated from future WebFS data.'))
|
||||
log.add({
|
||||
type: 'subagent/descriptor',
|
||||
data: snapshotSubagentDescriptor({
|
||||
mode: 'one-shot', provider: 'fork', label: 'Review preview architecture',
|
||||
}),
|
||||
})
|
||||
log.add({ type: 'step/start', data: { turn, step: 1 } })
|
||||
log.add(assistantMessage('preview-review-assistant', turn, 1, [{
|
||||
type: 'text',
|
||||
text: 'The bundled fixture is static image content; future WebFS state remains user-owned.',
|
||||
}]))
|
||||
log.add({ type: 'step/end', data: { turn, step: 1 } })
|
||||
log.add({ type: 'turn/end', data: { turn, reason: { kind: 'completed' } } })
|
||||
return log.events
|
||||
}
|
||||
|
||||
function continuableLog(): SessionEvent[] {
|
||||
const log = new EventLog(CREATED_AT + 200_000)
|
||||
log.add({ type: 'turn/start', data: { turn: 1 } })
|
||||
log.add(userMessage('preview-builder-user', 'Check that the Preview workspace can support follow-up tasks.'))
|
||||
log.add({
|
||||
type: 'subagent/descriptor',
|
||||
data: snapshotSubagentDescriptor({
|
||||
mode: 'continuable', provider: 'spawn', label: 'Continue preview verification',
|
||||
}),
|
||||
})
|
||||
log.add({ type: 'step/start', data: { turn: 1, step: 1 } })
|
||||
log.add(assistantMessage('preview-builder-assistant', 1, 1, [{
|
||||
type: 'text',
|
||||
text: 'This child is continuable and ready for another verification turn.',
|
||||
}]))
|
||||
log.add({ type: 'step/end', data: { turn: 1, step: 1 } })
|
||||
log.add({ type: 'turn/end', data: { turn: 1, reason: { kind: 'completed' } } })
|
||||
return log.events
|
||||
}
|
||||
|
||||
function header(
|
||||
id: SessionHeader['id'],
|
||||
createdAt: number,
|
||||
child?: { readonly parentSession: SessionHeader['id']; readonly mode: 'one-shot' | 'continuable'; readonly seedLength?: number },
|
||||
): SessionHeader {
|
||||
return {
|
||||
version: 0,
|
||||
id,
|
||||
createdAt,
|
||||
cwd: WORKSPACE,
|
||||
delegationDepth: child === undefined ? 0 : 1,
|
||||
agentPreset: 'standard',
|
||||
...child === undefined ? {} : {
|
||||
parentSession: child.parentSession,
|
||||
origin: 'subagent' as const,
|
||||
...child.seedLength === undefined ? {} : { seedLength: child.seedLength },
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function renderLog(meta: SessionHeader, events: readonly SessionEvent[]): string {
|
||||
return `${JSON.stringify(toHeaderLine(meta))}\n${eventLines(events, true)}\n`
|
||||
}
|
||||
|
||||
/** Build every committed fixture file as repository-relative UTF-8 text. */
|
||||
export function buildVfsExampleFiles(): ReadonlyMap<string, string> {
|
||||
const main = mainLog()
|
||||
const project = projectKey(WORKSPACE)
|
||||
const sessionPath = (id: string): string => `home/sessions/${project}/${id}/session.jsonl`
|
||||
const projectionCache = `${JSON.stringify({
|
||||
unit: { name: 'session_projcache', version: 3 },
|
||||
global: null,
|
||||
tables: {
|
||||
sessions: {
|
||||
[VFS_EXAMPLE_SESSION_IDS.main]: {
|
||||
identity: { createdAt: CREATED_AT, cwd: WORKSPACE },
|
||||
rows: {
|
||||
title: { ver: 1, seq: main.events.at(-1)?.seq ?? -1, val: VFS_EXAMPLE_TITLE },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, null, 2)}\n`
|
||||
return new Map([
|
||||
['workspace/PREVIEW.md', PREVIEW_GUIDE],
|
||||
['workspace/src/preview.ts', PREVIEW_SOURCE],
|
||||
['workspace/data/tasks.json', TASKS],
|
||||
['workspace/.agents/skills/preview-tour/SKILL.md', SKILL],
|
||||
['home/storages/session_projcache.json', projectionCache],
|
||||
[sessionPath(VFS_EXAMPLE_SESSION_IDS.main), renderLog(
|
||||
header(VFS_EXAMPLE_SESSION_IDS.main, CREATED_AT),
|
||||
main.events,
|
||||
)],
|
||||
[sessionPath(VFS_EXAMPLE_SESSION_IDS.oneShot), renderLog(
|
||||
header(VFS_EXAMPLE_SESSION_IDS.oneShot, CREATED_AT + 100_000, {
|
||||
parentSession: VFS_EXAMPLE_SESSION_IDS.main,
|
||||
mode: 'one-shot',
|
||||
seedLength: main.forkSeedLength,
|
||||
}),
|
||||
oneShotLog(main.events.slice(0, main.forkSeedLength)),
|
||||
)],
|
||||
[sessionPath(VFS_EXAMPLE_SESSION_IDS.continuable), renderLog(
|
||||
header(VFS_EXAMPLE_SESSION_IDS.continuable, CREATED_AT + 200_000, {
|
||||
parentSession: VFS_EXAMPLE_SESSION_IDS.main,
|
||||
mode: 'continuable',
|
||||
}),
|
||||
continuableLog(),
|
||||
)],
|
||||
])
|
||||
}
|
||||
Generated
+9
@@ -4848,6 +4848,15 @@ importers:
|
||||
'@deepseek-ai/dsh-sandbox-policy':
|
||||
specifier: workspace:^
|
||||
version: link:../../sandbox/sandbox-policy
|
||||
'@deepseek-ai/dsh-session':
|
||||
specifier: workspace:^
|
||||
version: link:../../core/session
|
||||
'@deepseek-ai/dsh-session-persistence-jsonl':
|
||||
specifier: workspace:^
|
||||
version: link:../../session/session-persistence-jsonl
|
||||
'@deepseek-ai/dsh-subagent':
|
||||
specifier: workspace:^
|
||||
version: link:../../subagent/subagent
|
||||
'@deepseek-ai/dsh-subprocess-local':
|
||||
specifier: workspace:^
|
||||
version: link:../../subprocess/subprocess-local
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { type SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import { parseSessionLog } from '@deepseek-ai/dsh-llm-replay'
|
||||
import { canonicalSessionFixture } from './session-fixture-layout.ts'
|
||||
import { canonicalSessionFixture, isPhysicalSessionFixture } from './session-fixture-layout.ts'
|
||||
|
||||
const HEADER = ' {"type":"session","version":0,"id":"fixture","createdAt":1,"delegationDepth":0} '
|
||||
|
||||
@@ -68,3 +68,15 @@ describe('canonicalSessionFixture', () => {
|
||||
.toThrow(/broken\.jsonl: session snapshot line 2: malformed text-chunks storage row/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isPhysicalSessionFixture', () => {
|
||||
it('excludes only persisted logs under the WebWorker example root', () => {
|
||||
expect(isPhysicalSessionFixture(
|
||||
'packages/experimental/webworker-runtime/tests/fixtures/vfs-example/home/sessions/--dsh-workspace--/main/session.jsonl',
|
||||
)).toBe(true)
|
||||
expect(isPhysicalSessionFixture(
|
||||
'packages/experimental/webworker-runtime/tests/fixtures/vfs-example/home/sessions/README.jsonl',
|
||||
)).toBe(false)
|
||||
expect(isPhysicalSessionFixture('apps/web/tests/snapshots/example/session.jsonl')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -7,6 +7,10 @@ import { resolve } from 'node:path'
|
||||
import { packChunkRuns, type SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import { parseSessionLog } from '@deepseek-ai/dsh-llm-replay'
|
||||
|
||||
/** Physical persistence artifacts validated by the WebWorker runtime fixture spec. */
|
||||
const PHYSICAL_SESSION_FIXTURE_ROOT =
|
||||
'packages/experimental/webworker-runtime/tests/fixtures/vfs-example/home/sessions/'
|
||||
|
||||
/** One repository session fixture and its canonical projected representation. */
|
||||
export interface SessionFixtureLayout {
|
||||
/** Repository-relative path with `/` separators. */
|
||||
@@ -17,6 +21,16 @@ export interface SessionFixtureLayout {
|
||||
canonical: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a repository JSONL is a production-layout persistence artifact rather
|
||||
* than an envelope-free replay snapshot owned by this script.
|
||||
* @param path - Repository-relative path with `/` separators.
|
||||
* @returns True only for Session logs under the WebWorker VFS example root.
|
||||
*/
|
||||
export function isPhysicalSessionFixture(path: string): boolean {
|
||||
return path.startsWith(PHYSICAL_SESSION_FIXTURE_ROOT) && path.endsWith('/session.jsonl')
|
||||
}
|
||||
|
||||
function isSessionHeader(value: unknown): boolean {
|
||||
return value !== null && typeof value === 'object' && (value as { type?: unknown }).type === 'session'
|
||||
}
|
||||
@@ -109,6 +123,7 @@ function discoverJsonlFiles(root: string): string[] {
|
||||
*/
|
||||
export function inspectSessionFixtureLayouts(root: string): SessionFixtureLayout[] {
|
||||
return discoverJsonlFiles(root).flatMap((path) => {
|
||||
if (isPhysicalSessionFixture(path)) return []
|
||||
const source = readFileSync(resolve(root, path), 'utf8')
|
||||
const canonical = canonicalSessionFixture(source, path)
|
||||
return canonical === undefined ? [] : [{ path, source, canonical }]
|
||||
|
||||
Reference in New Issue
Block a user