源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s

This commit is contained in:
agent-radar
2026-09-01 20:17:27 +08:00
commit 2ed3415fb7
8018 changed files with 1936476 additions and 0 deletions
@@ -0,0 +1,47 @@
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { createCassetteHelpers } from "../../../packages/agent/session-replay-runner/src/cassette.mjs";
import { loadCassettePolicy } from "../../../packages/agent/session-replay-runner/src/cassette-policy.mjs";
import { resolveAgentSessionReplayProjectRoot } from "./project-root.mjs";
export { resolveAgentSessionReplayProjectRoot } from "./project-root.mjs";
export { portableReplayCWDToken } from "../../../packages/agent/session-replay-runner/src/cassette.mjs";
const scriptDirectory = dirname(fileURLToPath(import.meta.url));
const workspaceRoot = resolve(scriptDirectory, "..", "..", "..");
export const cassettePolicy = await loadCassettePolicy(
join(
workspaceRoot,
"packages",
"agent",
"session-replay",
"cassette-policy.json"
)
);
const helpers = createCassetteHelpers(cassettePolicy, {
canonicalizeResolvedPaths: true
});
export const verifyCassette = helpers.verifyCassette;
export const parseActivityEvents = helpers.parseActivityEvents;
export const materializeReplayWorkspaceBlobs =
helpers.materializeReplayWorkspaceBlobs;
export const resolvePortableActivityEventPayload =
helpers.resolvePortableActivityEventPayload;
export const loadReplayTurnIdentityPlan = helpers.loadReplayTurnIdentityPlan;
export const replayTurnIdentityPlan = helpers.replayTurnIdentityPlan;
export function replayActionFromManifest(
manifest,
activityEvents,
workspaceId
) {
return helpers.replayActionFromManifest(
manifest,
activityEvents,
workspaceId,
resolveAgentSessionReplayProjectRoot()
);
}
@@ -0,0 +1,361 @@
import { readFile, readdir } from "node:fs/promises";
import { join } from "node:path";
const CATALOG_REVISION_RE = /sha256:[0-9a-f]{8,}/g;
const BUSINESS_CATALOG_ASSIGNMENT_RE =
/businessEventCatalogRevision\s*=\s*"(sha256:[0-9a-f]+)"/;
const GO_CATALOG_ASSIGNMENT_RE =
/BusinessEventCatalogRevision\s*=\s*"(sha256:[0-9a-f]+)"/;
const MISMATCH_LOG_RE =
/Event stream catalog revision mismatch\.\s*Expected\s+(sha256:[0-9a-f]+),\s*received\s+(sha256:[0-9a-f]+)/;
/**
* Extract a business-event catalog revision from source, bundle, or binary text.
* Prefers explicit assignment forms, then falls back to the first sha256 token.
*/
export function extractCatalogRevision(text) {
if (typeof text !== "string" || text.length === 0) return null;
const assigned =
text.match(BUSINESS_CATALOG_ASSIGNMENT_RE)?.[1] ??
text.match(GO_CATALOG_ASSIGNMENT_RE)?.[1] ??
null;
if (assigned) return assigned;
const matches = text.match(CATALOG_REVISION_RE);
return matches?.[0] ?? null;
}
export function formatCatalogMismatchError(input) {
const desktop = input.desktopRevision ?? "unknown";
const daemon = input.daemonRevision ?? "unknown";
const desktopSource = input.desktopSource ?? "desktop";
const daemonSource = input.daemonSource ?? "tuttid";
const guidance =
input.guidance ??
"Rebuild desktop (apps/desktop/out) and tuttids from the same commit, " +
"or clear stale prepared Electron/out artifacts before recording.";
return (
`desktop/tuttid event stream catalog mismatch (fail-fast): ` +
`${desktopSource}=${desktop}, ${daemonSource}=${daemon}. ` +
guidance
);
}
export async function readCatalogRevisionFromFile(path) {
try {
return extractCatalogRevision(await readFile(path, "utf8"));
} catch (error) {
if (error && error.code === "ENOENT") return null;
throw error;
}
}
export async function readCatalogRevisionFromBinary(path) {
try {
const buffer = await readFile(path);
// Binary may contain multiple sha256 tokens; prefer the business-event one
// by scanning latin1 and matching assignment-like nearby text first.
const latin1 = buffer.toString("latin1");
const assigned = extractCatalogRevisionFromBinaryAssignment(latin1);
if (assigned) return assigned;
// Fall back: tuttidd embeds the revision as a naked string constant.
const matches = latin1.match(CATALOG_REVISION_RE) ?? [];
// Prefer 16-hex-nibble revisions used by generate-event-protocol.
const short = matches.find((value) => /^sha256:[0-9a-f]{16}$/.test(value));
return short ?? matches[0] ?? null;
} catch (error) {
if (error && error.code === "ENOENT") return null;
throw error;
}
}
function extractCatalogRevisionFromBinaryAssignment(text) {
const marker = "BusinessEventCatalogRevision";
const markerIndex = text.indexOf(marker);
if (markerIndex < 0) return null;
const assignmentIndex = text.indexOf("=", markerIndex + marker.length);
if (assignmentIndex < 0 || assignmentIndex - markerIndex > 128) {
return null;
}
const quoteIndex = text.indexOf('"', assignmentIndex + 1);
if (quoteIndex < 0 || quoteIndex - assignmentIndex > 128) {
return null;
}
return text.slice(quoteIndex + 1).match(/^sha256:[0-9a-f]+/)?.[0] ?? null;
}
export async function readCatalogRevisionFromDesktopRendererOut(workspaceRoot) {
const assetsDirectory = join(
workspaceRoot,
"apps",
"desktop",
"out",
"renderer",
"assets"
);
let entries = [];
try {
entries = await readdir(assetsDirectory);
} catch (error) {
if (error && error.code === "ENOENT") return null;
throw error;
}
for (const name of entries) {
if (!name.endsWith(".js")) continue;
const revision = await readCatalogRevisionFromFile(
join(assetsDirectory, name)
);
if (revision) return revision;
}
return null;
}
export async function readCatalogRevisionFromEventProtocolSource(
workspaceRoot
) {
return readCatalogRevisionFromFile(
join(
workspaceRoot,
"packages",
"events",
"protocol",
"src",
"generated",
"registry.ts"
)
);
}
/**
* @param {'out' | 'source' | 'out-then-source' | 'source-then-out'} [prefer]
*/
export async function resolveDesktopEventStreamCatalogRevision(
workspaceRoot,
prefer = "out-then-source"
) {
const readOut = async () => {
const renderer =
await readCatalogRevisionFromDesktopRendererOut(workspaceRoot);
if (renderer) {
return { revision: renderer, source: "desktop-out-renderer" };
}
const main = await readCatalogRevisionFromFile(
join(workspaceRoot, "apps", "desktop", "out", "main", "index.js")
);
if (main) {
return { revision: main, source: "desktop-out-main" };
}
return null;
};
const readSource = async () => {
const protocol =
await readCatalogRevisionFromEventProtocolSource(workspaceRoot);
if (protocol) {
return { revision: protocol, source: "event-protocol-source" };
}
return null;
};
if (prefer === "out") return readOut();
if (prefer === "source") return readSource();
if (prefer === "source-then-out") {
return (await readSource()) ?? (await readOut());
}
return (await readOut()) ?? (await readSource());
}
export async function resolveDaemonEventStreamCatalogRevision(input) {
const daemonPath = input.daemonPath?.trim?.() || input.daemonPath || "";
if (daemonPath) {
const fromBinary = await readCatalogRevisionFromBinary(daemonPath);
if (fromBinary) {
return { revision: fromBinary, source: "daemon-binary" };
}
}
const fromGo = await readCatalogRevisionFromFile(
join(
input.workspaceRoot,
"services",
"tuttid",
"api",
"events",
"generated",
"protocol.gen.go"
)
);
if (fromGo) {
return { revision: fromGo, source: "tuttid-protocol-source" };
}
return null;
}
/**
* Fail when desktop/tuttid catalogs diverge.
* No-ops when either side cannot be resolved (e.g. cold pnpm-dev without out/).
*
* @param {{ daemonPath?: string, workspaceRoot: string, desktopPrefer?: string }} input
*/
export async function assertEventStreamCatalogAligned(input) {
const desktopPrefer = input.desktopPrefer ?? "out-then-source";
const desktop = await resolveDesktopEventStreamCatalogRevision(
input.workspaceRoot,
desktopPrefer
);
const daemon = await resolveDaemonEventStreamCatalogRevision({
daemonPath: input.daemonPath,
workspaceRoot: input.workspaceRoot
});
if (!desktop || !daemon) return { checked: false, desktop, daemon };
if (desktop.revision !== daemon.revision) {
throw new Error(
formatCatalogMismatchError({
daemonRevision: daemon.revision,
daemonSource: daemon.source,
desktopRevision: desktop.revision,
desktopSource: desktop.source
})
);
}
return { checked: true, desktop, daemon };
}
/**
* Align catalogs for a launch. Stale prepared `apps/desktop/out` automatically
* falls back to pnpm-dev-desktop when event-protocol source matches tuttidd.
*
* Managed launches (must use a fixed Electron binary) cannot fall back — they
* still fail fast so the operator rebuilds out/.
*/
export async function reconcileEventStreamCatalogForLaunch(input) {
const preparedElectron = Boolean(input.preparedElectron);
const managed = Boolean(input.managed);
const daemon = await resolveDaemonEventStreamCatalogRevision({
daemonPath: input.daemonPath,
workspaceRoot: input.workspaceRoot
});
if (!daemon) {
return {
checked: false,
fallbackToPnpmDev: false,
daemon: null,
desktop: null
};
}
if (!preparedElectron) {
// Vite/dev uses protocol source; ignore stale out/ so we do not false-fail.
const result = await assertEventStreamCatalogAligned({
daemonPath: input.daemonPath,
desktopPrefer: "source-then-out",
workspaceRoot: input.workspaceRoot
});
return { ...result, fallbackToPnpmDev: false };
}
const out = await resolveDesktopEventStreamCatalogRevision(
input.workspaceRoot,
"out"
);
if (out && out.revision === daemon.revision) {
return {
checked: true,
desktop: out,
daemon,
fallbackToPnpmDev: false
};
}
const source = await resolveDesktopEventStreamCatalogRevision(
input.workspaceRoot,
"source"
);
if (source && source.revision === daemon.revision) {
if (managed) {
throw new Error(
formatCatalogMismatchError({
daemonRevision: daemon.revision,
daemonSource: daemon.source,
desktopRevision: out?.revision ?? "missing",
desktopSource: out?.source ?? "desktop-out",
guidance:
"Managed replay cannot auto-fall back to pnpm-dev-desktop. " +
"Rebuild apps/desktop/out (or clear managed Electron env) so " +
"renderer matches tuttidd."
})
);
}
return {
checked: true,
desktop: source,
daemon,
fallbackToPnpmDev: true,
message:
`stale prepared desktop out catalog ` +
`(${out?.source ?? "desktop-out"}=${out?.revision ?? "missing"} vs ` +
`${daemon.source}=${daemon.revision}); ` +
`auto-falling back to pnpm-dev-desktop (event-protocol source matches)`
};
}
throw new Error(
formatCatalogMismatchError({
daemonRevision: daemon.revision,
daemonSource: daemon.source,
desktopRevision: out?.revision ?? source?.revision ?? "unknown",
desktopSource: out?.source ?? source?.source ?? "desktop",
guidance:
"Neither prepared out/ nor event-protocol source match tuttidd. " +
"Run pnpm generate:event-protocol and rebuild desktop + tuttids " +
"from the same commit."
})
);
}
export function clearPreparedElectronEnv(env = process.env) {
delete env.TUTTI_AGENT_SESSION_REPLAY_ELECTRON_EXECUTABLE;
delete env.TUTTI_AGENT_SESSION_REPLAY_ELECTRON_ENTRY;
}
/**
* Check the live Desktop log once after CDP is ready. The source/artifact
* reconciliation above is the authoritative preflight; this snapshot only
* turns an already-emitted handshake failure into a useful error. Polling here
* has no success signal and used to delay every healthy replay by 8 seconds.
*/
export async function assertDesktopLogHasNoCatalogMismatch(logPath) {
let lastText;
try {
lastText = await readFile(logPath, "utf8");
} catch (error) {
if (error && error.code === "ENOENT") return { checked: false };
throw error;
}
const match = lastText.match(MISMATCH_LOG_RE);
if (match) {
throw new Error(
formatCatalogMismatchError({
daemonRevision: match[2],
daemonSource: "tuttid-ready-frame",
desktopRevision: match[1],
desktopSource: "desktop-event-stream-client"
})
);
}
if (
lastText.includes("catalog revision mismatch") ||
lastText.includes("event_stream.connect_failed")
) {
throw new Error(
formatCatalogMismatchError({
daemonRevision: "see desktop.log",
daemonSource: "tuttid",
desktopRevision: "see desktop.log",
desktopSource: "desktop"
})
);
}
return { checked: true };
}
@@ -0,0 +1,218 @@
import assert from "node:assert/strict";
import { mkdtemp, mkdir, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import test from "node:test";
import {
assertDesktopLogHasNoCatalogMismatch,
assertEventStreamCatalogAligned,
clearPreparedElectronEnv,
extractCatalogRevision,
formatCatalogMismatchError,
readCatalogRevisionFromBinary,
readCatalogRevisionFromDesktopRendererOut,
readCatalogRevisionFromFile,
reconcileEventStreamCatalogForLaunch
} from "./event-stream-catalog.mjs";
test("extractCatalogRevision prefers businessEventCatalogRevision assignment", () => {
const text = `
const other = "sha256:deadbeefdeadbeef";
const businessEventCatalogRevision = "sha256:0413e48c4012324e";
`;
assert.equal(extractCatalogRevision(text), "sha256:0413e48c4012324e");
});
test("extractCatalogRevision reads Go BusinessEventCatalogRevision", () => {
const text = `\tBusinessEventCatalogRevision = "sha256:0413e48c4012324e"\n`;
assert.equal(extractCatalogRevision(text), "sha256:0413e48c4012324e");
});
test("assertEventStreamCatalogAligned fails when renderer out and daemon diverge", async () => {
const root = await mkdtemp(join(tmpdir(), "catalog-align-"));
const assets = join(root, "apps", "desktop", "out", "renderer", "assets");
const daemonDir = join(root, "daemon");
await mkdir(assets, { recursive: true });
await mkdir(daemonDir, { recursive: true });
await writeFile(
join(assets, "browserNodeWebviewContext-test.js"),
`const businessEventCatalogRevision = "sha256:2e36dcc0d1c65637";\n`
);
const daemonPath = join(daemonDir, "tuttid");
await writeFile(
daemonPath,
Buffer.from(`xxBusinessEventCatalogRevision="sha256:0413e48c4012324e"yy`)
);
await assert.rejects(
() =>
assertEventStreamCatalogAligned({
daemonPath,
workspaceRoot: root
}),
/event stream catalog mismatch \(fail-fast\)/
);
});
test("assertEventStreamCatalogAligned passes when revisions match", async () => {
const root = await mkdtemp(join(tmpdir(), "catalog-align-ok-"));
const assets = join(root, "apps", "desktop", "out", "renderer", "assets");
const daemonDir = join(root, "daemon");
await mkdir(assets, { recursive: true });
await mkdir(daemonDir, { recursive: true });
await writeFile(
join(assets, "browserNodeWebviewContext-test.js"),
`const businessEventCatalogRevision = "sha256:0413e48c4012324e";\n`
);
const daemonPath = join(daemonDir, "tuttid");
await writeFile(
daemonPath,
Buffer.from(`BusinessEventCatalogRevision="sha256:0413e48c4012324e"`)
);
const result = await assertEventStreamCatalogAligned({
daemonPath,
workspaceRoot: root
});
assert.equal(result.checked, true);
assert.equal(result.desktop.revision, "sha256:0413e48c4012324e");
assert.equal(result.daemon.revision, "sha256:0413e48c4012324e");
});
test("reconcile falls back to pnpm-dev when out is stale but source matches", async () => {
const root = await mkdtemp(join(tmpdir(), "catalog-fallback-"));
const assets = join(root, "apps", "desktop", "out", "renderer", "assets");
const protocolDir = join(
root,
"packages",
"events",
"protocol",
"src",
"generated"
);
const daemonDir = join(root, "daemon");
await mkdir(assets, { recursive: true });
await mkdir(protocolDir, { recursive: true });
await mkdir(daemonDir, { recursive: true });
await writeFile(
join(assets, "browserNodeWebviewContext-test.js"),
`const businessEventCatalogRevision = "sha256:2e36dcc0d1c65637";\n`
);
await writeFile(
join(protocolDir, "registry.ts"),
`export const businessEventCatalogRevision = "sha256:0413e48c4012324e";\n`
);
const daemonPath = join(daemonDir, "tuttid");
await writeFile(
daemonPath,
Buffer.from(`BusinessEventCatalogRevision="sha256:0413e48c4012324e"`)
);
const result = await reconcileEventStreamCatalogForLaunch({
daemonPath,
preparedElectron: true,
workspaceRoot: root
});
assert.equal(result.fallbackToPnpmDev, true);
assert.equal(result.desktop.revision, "sha256:0413e48c4012324e");
assert.match(result.message, /auto-falling back to pnpm-dev-desktop/);
});
test("reconcile does not fall back for managed launches", async () => {
const root = await mkdtemp(join(tmpdir(), "catalog-managed-"));
const assets = join(root, "apps", "desktop", "out", "renderer", "assets");
const protocolDir = join(
root,
"packages",
"events",
"protocol",
"src",
"generated"
);
const daemonDir = join(root, "daemon");
await mkdir(assets, { recursive: true });
await mkdir(protocolDir, { recursive: true });
await mkdir(daemonDir, { recursive: true });
await writeFile(
join(assets, "browserNodeWebviewContext-test.js"),
`const businessEventCatalogRevision = "sha256:2e36dcc0d1c65637";\n`
);
await writeFile(
join(protocolDir, "registry.ts"),
`export const businessEventCatalogRevision = "sha256:0413e48c4012324e";\n`
);
const daemonPath = join(daemonDir, "tuttid");
await writeFile(
daemonPath,
Buffer.from(`BusinessEventCatalogRevision="sha256:0413e48c4012324e"`)
);
await assert.rejects(
() =>
reconcileEventStreamCatalogForLaunch({
daemonPath,
managed: true,
preparedElectron: true,
workspaceRoot: root
}),
/Managed replay cannot auto-fall back/
);
});
test("clearPreparedElectronEnv removes launch env", () => {
const env = {
TUTTI_AGENT_SESSION_REPLAY_ELECTRON_EXECUTABLE: "/bin/Electron",
TUTTI_AGENT_SESSION_REPLAY_ELECTRON_ENTRY: "/apps/desktop",
KEEP: "1"
};
clearPreparedElectronEnv(env);
assert.equal(env.TUTTI_AGENT_SESSION_REPLAY_ELECTRON_EXECUTABLE, undefined);
assert.equal(env.TUTTI_AGENT_SESSION_REPLAY_ELECTRON_ENTRY, undefined);
assert.equal(env.KEEP, "1");
});
test("assertDesktopLogHasNoCatalogMismatch fails fast on handshake error", async () => {
const root = await mkdtemp(join(tmpdir(), "catalog-log-"));
const logPath = join(root, "desktop.log");
await writeFile(
logPath,
`time=... msg="terminal diagnostic" details={"error":"Event stream catalog revision mismatch. Expected sha256:2e36dcc0d1c65637, received sha256:0413e48c4012324e."}\n`
);
await assert.rejects(
() => assertDesktopLogHasNoCatalogMismatch(logPath),
/fail-fast/
);
});
test("assertDesktopLogHasNoCatalogMismatch accepts a clean desktop snapshot", async () => {
const root = await mkdtemp(join(tmpdir(), "catalog-log-clean-"));
const logPath = join(root, "desktop.log");
await writeFile(logPath, 'time=... msg="desktop app ready"\n');
assert.deepEqual(await assertDesktopLogHasNoCatalogMismatch(logPath), {
checked: true
});
});
test("read helpers tolerate missing artifacts", async () => {
const root = await mkdtemp(join(tmpdir(), "catalog-missing-"));
assert.equal(await readCatalogRevisionFromFile(join(root, "nope.ts")), null);
assert.equal(await readCatalogRevisionFromBinary(join(root, "nope")), null);
assert.equal(await readCatalogRevisionFromDesktopRendererOut(root), null);
const result = await assertEventStreamCatalogAligned({
daemonPath: join(root, "missing-daemon"),
workspaceRoot: root
});
assert.equal(result.checked, false);
});
test("formatCatalogMismatchError mentions rebuild guidance", () => {
const message = formatCatalogMismatchError({
daemonRevision: "sha256:aaaa",
daemonSource: "daemon-binary",
desktopRevision: "sha256:bbbb",
desktopSource: "desktop-out-renderer"
});
assert.match(message, /desktop-out-renderer=sha256:bbbb/);
assert.match(message, /daemon-binary=sha256:aaaa/);
assert.match(message, /Rebuild desktop/);
});
@@ -0,0 +1,114 @@
import { spawn } from "node:child_process";
import { access, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { isAbsolute, join, resolve } from "node:path";
const projectRootEnvironment = "TUTTI_AGENT_SESSION_REPLAY_PROJECT_ROOT";
const temporaryProjectPrefix = "tutti-agent-session-rec-";
export function resolveAgentSessionReplayProjectRoot() {
const fromEnvironment = process.env[projectRootEnvironment]?.trim();
if (fromEnvironment) {
if (!isAbsolute(fromEnvironment)) {
throw new Error(`${projectRootEnvironment} must be an absolute path`);
}
return resolve(fromEnvironment);
}
// Pure helpers may resolve portable paths before the CLI acquires its
// run-scoped project. Keep that fallback outside the product checkout.
return join(tmpdir(), "tutti-agent-session-rec", `unbound-${process.pid}`);
}
export async function acquireAgentSessionReplayProjectRoot({
keepRuntime = false
} = {}) {
const previous = process.env[projectRootEnvironment];
if (previous?.trim()) {
const root = resolveAgentSessionReplayProjectRoot();
await mkdir(root, { recursive: true });
return {
owned: false,
root,
async dispose() {}
};
}
const root = await mkdtemp(join(tmpdir(), temporaryProjectPrefix));
try {
await initializeTemporaryProject(root);
} catch (error) {
await rm(root, { force: true, recursive: true });
throw error;
}
process.env[projectRootEnvironment] = root;
let disposed = false;
return {
owned: true,
root,
async dispose() {
if (disposed) return;
disposed = true;
if (previous === undefined) {
delete process.env[projectRootEnvironment];
} else {
process.env[projectRootEnvironment] = previous;
}
if (!keepRuntime) {
await rm(root, { force: true, recursive: true });
}
}
};
}
async function initializeTemporaryProject(root) {
await runGit(root, ["init", "--quiet"]);
await writeFile(
join(root, "README.md"),
[
"# Tutti session-replay temporary project",
"",
"This run-scoped project is intentionally outside the product checkout.",
""
].join("\n")
);
await runGit(root, ["add", "README.md"]);
await runGit(root, [
"-c",
"user.name=Tutti Session Replay",
"-c",
"user.email=session-replay@tutti.invalid",
"commit",
"--quiet",
"-m",
"chore: initialize session-replay project"
]);
await access(join(root, ".git"));
}
function runGit(cwd, args) {
return new Promise((resolveRun, rejectRun) => {
const child = spawn("git", args, {
cwd,
env: process.env,
stdio: ["ignore", "ignore", "pipe"]
});
let stderr = "";
child.stderr.on("data", (chunk) => {
stderr += chunk.toString();
});
child.once("error", rejectRun);
child.once("exit", (code, signal) => {
if (code === 0) {
resolveRun();
return;
}
rejectRun(
new Error(
`git ${args.join(" ")} failed (${code ?? signal ?? "unknown"})${
stderr.trim() ? `: ${stderr.trim()}` : ""
}`
)
);
});
});
}
@@ -0,0 +1,84 @@
import assert from "node:assert/strict";
import { access, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import test from "node:test";
import { acquireAgentSessionReplayProjectRoot } from "./project-root.mjs";
const projectRootEnvironment = "TUTTI_AGENT_SESSION_REPLAY_PROJECT_ROOT";
test("acquires and removes a run-scoped temporary Git project", async () => {
const previous = process.env[projectRootEnvironment];
delete process.env[projectRootEnvironment];
let project;
try {
project = await acquireAgentSessionReplayProjectRoot();
const root = project.root;
assert.equal(project.owned, true);
assert.equal(process.env[projectRootEnvironment], root);
assert.equal(
root.startsWith(join(tmpdir(), "tutti-agent-session-rec-")),
true
);
await access(join(root, ".git"));
assert.match(
await readFile(join(root, "README.md"), "utf8"),
/outside the product checkout/u
);
await project.dispose();
project = undefined;
await assert.rejects(access(root));
} finally {
await project?.dispose();
restoreProjectRootEnvironment(previous);
}
});
test("keep-runtime retains an owned temporary project", async () => {
const previous = process.env[projectRootEnvironment];
delete process.env[projectRootEnvironment];
let project;
try {
project = await acquireAgentSessionReplayProjectRoot({ keepRuntime: true });
await project.dispose();
await access(join(project.root, ".git"));
} finally {
if (project?.root) {
await rm(project.root, { force: true, recursive: true });
}
restoreProjectRootEnvironment(previous);
}
});
test("preserves an externally supplied project root", async () => {
const previous = process.env[projectRootEnvironment];
const parent = await mkdtemp(
join(tmpdir(), "tutti-session-replay-external-")
);
const externalRoot = join(parent, "project");
process.env[projectRootEnvironment] = externalRoot;
try {
const project = await acquireAgentSessionReplayProjectRoot();
assert.equal(project.owned, false);
assert.equal(project.root, externalRoot);
await writeFile(join(project.root, "marker.txt"), "preserve\n");
await project.dispose();
assert.equal(
await readFile(join(project.root, "marker.txt"), "utf8"),
"preserve\n"
);
assert.equal(process.env[projectRootEnvironment], externalRoot);
} finally {
restoreProjectRootEnvironment(previous);
await rm(parent, { force: true, recursive: true });
}
});
function restoreProjectRootEnvironment(previous) {
if (previous === undefined) {
delete process.env[projectRootEnvironment];
} else {
process.env[projectRootEnvironment] = previous;
}
}
@@ -0,0 +1,65 @@
import { realpathSync } from "node:fs";
import { resolve } from "node:path";
import {
assertForbiddenPathAbsent as assertForbiddenPathAbsentCore,
resolveRecordScenarioProject as resolveRecordScenarioProjectCore,
seedRecordingUserProject as seedRecordingUserProjectCore,
verifyRecordedProjectBindingArtifacts as verifyRecordedProjectBindingArtifactsCore
} from "../../../packages/agent/session-replay-runner/src/recording.mjs";
import {
cassettePolicy,
parseActivityEvents,
portableReplayCWDToken,
resolveAgentSessionReplayProjectRoot
} from "./cassette.mjs";
export { resolveAgentSessionReplayProjectRoot };
const activityEventsName = cassettePolicy.files.activityEvents.path;
const checkpointPlanName = cassettePolicy.files.checkpointPlan.path;
const expectedStateName = cassettePolicy.files.expectedState.path;
const initialStateName = cassettePolicy.files.initialState.path;
function canonicalizeProjectPath(path) {
const absolute = resolve(String(path ?? "").trim());
if (!absolute) return absolute;
try {
return realpathSync(absolute);
} catch {
return absolute;
}
}
export function resolveRecordScenarioProject(project, replayCWD) {
return resolveRecordScenarioProjectCore(project, replayCWD, {
canonicalizePath: canonicalizeProjectPath,
portableReplayCWDToken
});
}
export async function seedRecordingUserProject(databasePath, project) {
return seedRecordingUserProjectCore(databasePath, project, {
canonicalizePath: canonicalizeProjectPath
});
}
export async function verifyRecordedProjectBindingArtifacts(
recordingDirectory,
portableProjectPath
) {
return verifyRecordedProjectBindingArtifactsCore(
recordingDirectory,
portableProjectPath,
{
activityEventsPath: activityEventsName,
checkpointPlanPath: checkpointPlanName,
expectedStatePath: expectedStateName,
initialStatePath: initialStateName,
parseActivityEvents
}
);
}
export async function assertForbiddenPathAbsent(path, scenarioId) {
return assertForbiddenPathAbsentCore(path, scenarioId);
}
@@ -0,0 +1,560 @@
import { spawn } from "node:child_process";
import { createHash, randomUUID } from "node:crypto";
import {
access,
cp,
mkdir,
mkdtemp,
readFile,
rm,
stat,
writeFile
} from "node:fs/promises";
import { homedir } from "node:os";
import { join } from "node:path";
import { setTimeout as delay } from "node:timers/promises";
import { buildDaemon, stopProcessTree } from "../run-agent-gui-performance.mjs";
const hostAccountAuthEnvironment =
"TUTTI_AGENT_SESSION_REPLAY_HOST_ACCOUNT_AUTH";
const skipHostAccountAuthEnvironment =
"TUTTI_AGENT_SESSION_REPLAY_SKIP_HOST_ACCOUNT_AUTH";
export const replayListenerInfoPath = (stateDirectory) =>
join(stateDirectory, "run", "tuttid.listener.json");
const timingPrefix = "[tutti-agent-session-replay-timing] ";
export function logTiming(phase, durationMs, detail = undefined) {
const payload = {
detail: detail ?? null,
durationMs: Math.max(0, Math.round(durationMs)),
phase,
source: "tutti",
startedAt: new Date(
Date.now() - Math.max(0, Math.round(durationMs))
).toISOString()
};
process.stderr.write(`${timingPrefix}${JSON.stringify(payload)}\n`);
}
export async function measureTiming(phase, run, detail = undefined) {
const started = performance.now();
try {
return await run();
} finally {
logTiming(phase, performance.now() - started, detail);
}
}
export async function createRuntime(workspaceRoot, mode) {
return measureTiming(
"create-runtime",
async () => {
const runtimeParent =
process.env.TUTTI_AGENT_SESSION_REPLAY_RUNTIME_PARENT?.trim() ||
join(workspaceRoot, ".tmp");
await mkdir(runtimeParent, { recursive: true });
const directory = await mkdtemp(
join(runtimeParent, `agent-session-${mode}-`)
);
const stateDirectory = join(directory, "state");
const userDataDirectory = join(directory, "electron-user-data");
const daemonPath = join(directory, "tuttid");
await mkdir(stateDirectory, { recursive: true });
const preparedDaemonPath =
process.env.TUTTI_AGENT_SESSION_REPLAY_DAEMON_EXECUTABLE?.trim();
if (preparedDaemonPath) {
await access(preparedDaemonPath);
logTiming("create-runtime.reuse-prepared-daemon", 0, {
daemonPath: preparedDaemonPath,
mode
});
} else {
await measureTiming("create-runtime.generate-builtin-apps", () =>
runCommand("pnpm", ["generate:builtin-apps"], workspaceRoot)
);
await measureTiming("create-runtime.build-daemon", () =>
buildDaemon(daemonPath)
);
}
return {
daemonPath: preparedDaemonPath || daemonPath,
directory,
stateDirectory,
userDataDirectory
};
},
{ mode }
);
}
export function managedDesktopLaunch() {
const command =
process.env.TUTTI_AGENT_SESSION_REPLAY_ELECTRON_EXECUTABLE?.trim();
if (!command) {
throw new Error("managed replay Electron executable is unavailable");
}
const entry = process.env.TUTTI_AGENT_SESSION_REPLAY_ELECTRON_ENTRY?.trim();
return {
args: entry ? [entry] : [],
command
};
}
export function preparedDesktopLaunch() {
const command =
process.env.TUTTI_AGENT_SESSION_REPLAY_ELECTRON_EXECUTABLE?.trim();
if (!command) return undefined;
const entry = process.env.TUTTI_AGENT_SESSION_REPLAY_ELECTRON_ENTRY?.trim();
if (!entry) {
throw new Error("prepared replay Electron entry is unavailable");
}
return { args: [entry], command };
}
export async function initializeCleanDatabase(
workspaceRoot,
runtime,
workspaceId,
{ seedWorkspace = true } = {}
) {
return measureTiming(
"initialize-clean-database",
async () => {
const databasePath = join(runtime.stateDirectory, "tuttid.db");
const templatePath = await ensureMigratedDatabaseTemplate(
workspaceRoot,
runtime.daemonPath
);
await measureTiming("initialize-clean-database.copy-template", () =>
cp(templatePath, databasePath)
);
await measureTiming(
"initialize-clean-database.seed",
() =>
seedCleanDatabase(
workspaceRoot,
databasePath,
workspaceId,
seedWorkspace
),
{ seedWorkspace }
);
// Isolated runtimes start without Tutti account session; reuse the local
// desktop login when present so connector-market / host-auth paths work.
const hostAccountStarted = performance.now();
const hostAccount = await seedHostAccountSession(runtime.stateDirectory);
logTiming(
"initialize-clean-database.seed-host-account",
performance.now() - hostAccountStarted,
hostAccount
);
},
{ seedWorkspace, workspaceId }
);
}
/**
* Copy the developer machine Tutti account session into an isolated replay
* state dir when available. Missing/invalid host auth is a soft skip so CI
* and machines without a login still boot.
*/
export async function seedHostAccountSession(
stateDirectory,
{ env = process.env, homeDirectory = homedir() } = {}
) {
if (isTruthyEnv(env[skipHostAccountAuthEnvironment])) {
return { action: "skipped-disabled" };
}
const sourcePath = resolveHostAccountAuthPath(env, homeDirectory);
let raw;
try {
raw = await readFile(sourcePath, "utf8");
} catch (error) {
if (error?.code === "ENOENT") {
return { action: "skipped-missing", sourcePath };
}
throw error;
}
let session;
try {
session = JSON.parse(raw);
} catch {
return { action: "skipped-invalid", sourcePath };
}
if (
typeof session?.session_id !== "string" ||
!session.session_id.trim() ||
typeof session?.cookie !== "string" ||
!session.cookie.trim()
) {
return { action: "skipped-invalid", sourcePath };
}
const accountDirectory = join(stateDirectory, "account");
const destinationPath = join(accountDirectory, "auth.json");
await mkdir(accountDirectory, { recursive: true });
await writeFile(destinationPath, `${JSON.stringify(session, null, 2)}\n`, {
mode: 0o600
});
return { action: "copied", sourcePath, destinationPath };
}
export function resolveHostAccountAuthPath(
env = process.env,
homeDirectory = homedir()
) {
const override = env[hostAccountAuthEnvironment]?.trim();
if (override) {
return override;
}
// Replay Desktop always sets TUTTI_ENV=development, whose DefaultStateDir is
// ~/.tutti-dev — mirror that when picking the host session to reuse.
return join(homeDirectory, ".tutti-dev", "account", "auth.json");
}
function isTruthyEnv(value) {
if (value === undefined || value === null) {
return false;
}
const normalized = String(value).trim().toLowerCase();
return normalized !== "" && normalized !== "0" && normalized !== "false";
}
async function ensureMigratedDatabaseTemplate(workspaceRoot, daemonPath) {
const daemonStat = await stat(daemonPath);
// Prepared daemon binaries can change mtime when first executed (or when a
// dirty warmup slot is rewritten). Prefer an explicit stamp / path+size so
// consecutive ui-drive cases reuse the migrated template.
const preparedStamp =
process.env.TUTTI_AGENT_SESSION_REPLAY_DB_TEMPLATE_STAMP?.trim();
const stampHasher = createHash("sha1")
.update(daemonPath)
.update(String(daemonStat.size));
if (preparedStamp) {
stampHasher.update(preparedStamp);
} else if (
!process.env.TUTTI_AGENT_SESSION_REPLAY_DAEMON_EXECUTABLE?.trim()
) {
stampHasher.update(String(Math.trunc(daemonStat.mtimeMs)));
}
const stamp = stampHasher.digest("hex").slice(0, 16);
const templateRoot =
process.env.TUTTI_AGENT_SESSION_REPLAY_DB_TEMPLATE_ROOT?.trim() ||
join(workspaceRoot, ".tmp", "agent-session-replay-db-templates");
const templateDirectory = join(templateRoot, stamp);
const templatePath = join(templateDirectory, "tuttid.db");
try {
await access(templatePath);
logTiming("initialize-clean-database.reuse-template", 0, {
stamp,
templatePath
});
return templatePath;
} catch {
// build below
}
await mkdir(templateRoot, { recursive: true });
let ownsBuild = false;
try {
await mkdir(templateDirectory);
ownsBuild = true;
} catch (error) {
if (error?.code !== "EEXIST") throw error;
await waitForDatabaseTemplate(templatePath);
logTiming("initialize-clean-database.reuse-template", 0, {
stamp,
templatePath,
waited: true
});
return templatePath;
}
return measureTiming(
"initialize-clean-database.build-template",
async () => {
const stagingParent = join(templateRoot, ".staging");
await mkdir(stagingParent, { recursive: true });
const stagingDirectory = await mkdtemp(
join(stagingParent, `migrate-${stamp}-`)
);
const stagingStateDirectory = join(stagingDirectory, "state");
await mkdir(stagingStateDirectory, { recursive: true });
try {
await migrateEmptyDatabase(
workspaceRoot,
daemonPath,
stagingStateDirectory
);
const stagingDb = join(stagingStateDirectory, "tuttid.db");
await access(stagingDb);
await cp(stagingDb, templatePath);
} catch (error) {
if (ownsBuild) {
await rm(templateDirectory, {
force: true,
recursive: true,
maxRetries: 10,
retryDelay: 200
});
}
throw error;
} finally {
await rm(stagingDirectory, {
force: true,
recursive: true,
maxRetries: 10,
retryDelay: 200
});
}
return templatePath;
},
{ stamp }
);
}
async function waitForDatabaseTemplate(templatePath, timeoutMs = 120_000) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
try {
await access(templatePath);
return;
} catch {
await delay(100);
}
}
throw new Error(
`timed out waiting for migrated database template: ${templatePath}`
);
}
async function migrateEmptyDatabase(workspaceRoot, daemonPath, stateDirectory) {
const listenerInfoPath = replayListenerInfoPath(stateDirectory);
const daemon = spawn(daemonPath, [], {
cwd: workspaceRoot,
detached: process.platform !== "win32",
env: {
...process.env,
TUTTI_ANALYTICS_DISABLED: "1",
TUTTI_ENV: "development",
TUTTI_STATE_DIR: stateDirectory,
TUTTID_ADDR: "127.0.0.1:0",
TUTTID_ACCESS_TOKEN: randomUUID()
},
stdio: ["ignore", "pipe", "pipe"]
});
let daemonStdout = "";
let daemonStderr = "";
daemon.stdout.on("data", (chunk) => {
daemonStdout += chunk.toString();
});
daemon.stderr.on("data", (chunk) => {
daemonStderr += chunk.toString();
});
try {
const deadline = Date.now() + 60_000;
while (Date.now() < deadline) {
if (daemon.exitCode !== null || daemon.signalCode !== null) {
throw new Error(
`tuttid exited while initializing a clean database: ${(
daemonStderr.trim() ||
daemonStdout.trim() ||
daemon.signalCode ||
daemon.exitCode
)
.toString()
.slice(-4_000)}`
);
}
try {
await access(listenerInfoPath);
break;
} catch {
await delay(50);
}
}
await access(listenerInfoPath);
} finally {
await stopProcessTree(daemon);
}
}
async function seedCleanDatabase(
workspaceRoot,
databasePath,
workspaceId,
seedWorkspace
) {
const now = Date.now();
const workbenchSnapshot = replayWorkbenchSnapshot(
new Date(now).toISOString()
);
await runCommand(
"sqlite3",
[
databasePath,
`
PRAGMA foreign_keys = ON;
${
seedWorkspace
? `INSERT INTO workspaces (
id, name, created_at_unix_ms, updated_at_unix_ms, last_opened_at_unix_ms
) VALUES (
'${sqlString(workspaceId)}', 'Replay Scenario', ${now}, ${now}, ${now}
);`
: ""
}
INSERT INTO desktop_preferences (
id, locale, theme_source, updated_at_unix_ms
) VALUES (
'desktop', 'en', 'system', ${now}
)
ON CONFLICT(id) DO NOTHING;
${
seedWorkspace
? `INSERT INTO workspace_workbench_snapshots (
workspace_id,
schema_version,
snapshot_json,
created_at_unix_ms,
updated_at_unix_ms
) VALUES (
'${sqlString(workspaceId)}',
${workbenchSnapshot.schemaVersion},
'${sqlString(JSON.stringify(workbenchSnapshot))}',
${now},
${now}
);`
: ""
}
`
],
workspaceRoot
);
}
export function replayWorkbenchSnapshot(autoOpenedAt) {
return {
schemaVersion: 1,
nodes: [],
nodeStack: [],
activeNodeId: null,
metadata: {
workspaceOnboarding: {
autoOpened: true,
autoOpenedAt,
schemaVersion: 1
}
}
};
}
export async function enableAgentSessionRecordingFeature(
databasePath,
workspaceRoot
) {
await runCommand(
"sqlite3",
[
databasePath,
`
UPDATE desktop_preferences
SET feature_flags_json = json_set(
COALESCE(NULLIF(feature_flags_json, ''), '{}'),
'$."agent.sessionRecording"',
json('true')
)
WHERE id = 'desktop';
`
],
workspaceRoot
);
}
export async function enableAgentSessionRecordingTarget(
databasePath,
agentTargetId,
workspaceRoot
) {
await runCommand(
"sqlite3",
[
databasePath,
`
UPDATE agent_targets
SET enabled = 1, updated_at_ms = ${Date.now()}
WHERE id = '${sqlString(agentTargetId)}';
`
],
workspaceRoot
);
}
export async function setAgentComposerDefaults(
databasePath,
agentTargetId,
{ model, permissionModeId, reasoningEffort, speed },
workspaceRoot
) {
await runCommand(
"sqlite3",
[
databasePath,
`
UPDATE desktop_preferences
SET agent_composer_defaults_by_agent_target_json = json_set(
COALESCE(NULLIF(agent_composer_defaults_by_agent_target_json, ''), '{}'),
'$."${sqlString(agentTargetId)}".permissionModeId',
'${sqlString(permissionModeId)}',
'$."${sqlString(agentTargetId)}".model',
'${sqlString(model)}',
'$."${sqlString(agentTargetId)}".reasoningEffort',
'${sqlString(reasoningEffort)}',
'$."${sqlString(agentTargetId)}".speed',
'${sqlString(speed)}'
)
WHERE id = 'desktop';
`
],
workspaceRoot
);
}
export async function removeRuntime(directory) {
await rm(directory, {
recursive: true,
force: true,
maxRetries: 10,
retryDelay: 200
});
}
async function runCommand(command, args, workspaceRoot) {
await new Promise((resolveCommand, rejectCommand) => {
const child = spawn(command, args, {
cwd: workspaceRoot,
env: process.env,
stdio: "inherit"
});
child.once("error", rejectCommand);
child.once("exit", (code, signal) => {
if (code === 0) {
resolveCommand();
return;
}
rejectCommand(
new Error(
`${command} ${args.join(" ")} failed with ${signal ?? `exit ${code}`}`
)
);
});
});
}
function sqlString(value) {
return String(value).replaceAll("'", "''");
}
@@ -0,0 +1,126 @@
import assert from "node:assert/strict";
import {
access,
mkdir,
mkdtemp,
readFile,
rm,
writeFile
} from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import test from "node:test";
import {
resolveHostAccountAuthPath,
seedHostAccountSession
} from "./runtime.mjs";
test("resolveHostAccountAuthPath defaults to ~/.tutti-dev account session", () => {
assert.equal(
resolveHostAccountAuthPath({}, "/Users/ryan"),
join("/Users/ryan", ".tutti-dev", "account", "auth.json")
);
});
test("resolveHostAccountAuthPath honors explicit override", () => {
assert.equal(
resolveHostAccountAuthPath(
{
TUTTI_AGENT_SESSION_REPLAY_HOST_ACCOUNT_AUTH: " /tmp/custom-auth.json "
},
"/Users/ryan"
),
"/tmp/custom-auth.json"
);
});
test("seedHostAccountSession copies a valid host login into isolated state", async () => {
const root = await mkdtemp(join(tmpdir(), "replay-host-account-"));
try {
const home = join(root, "home");
const sourceDir = join(home, ".tutti-dev", "account");
await mkdir(sourceDir, { recursive: true });
const session = {
session_id: "session-1",
cookie: "session_id=session-1",
email: "ryan@example.com",
name: "ryan"
};
await writeFile(join(sourceDir, "auth.json"), JSON.stringify(session));
const stateDirectory = join(root, "state");
const result = await seedHostAccountSession(stateDirectory, {
env: {},
homeDirectory: home
});
assert.equal(result.action, "copied");
const copied = JSON.parse(
await readFile(join(stateDirectory, "account", "auth.json"), "utf8")
);
assert.equal(copied.session_id, "session-1");
assert.equal(copied.cookie, "session_id=session-1");
assert.equal(copied.email, "ryan@example.com");
} finally {
await rm(root, { force: true, recursive: true });
}
});
test("seedHostAccountSession skips when host login is missing", async () => {
const root = await mkdtemp(join(tmpdir(), "replay-host-account-missing-"));
try {
const result = await seedHostAccountSession(join(root, "state"), {
env: {},
homeDirectory: join(root, "empty-home")
});
assert.equal(result.action, "skipped-missing");
await assert.rejects(access(join(root, "state", "account", "auth.json")));
} finally {
await rm(root, { force: true, recursive: true });
}
});
test("seedHostAccountSession skips invalid host login payloads", async () => {
const root = await mkdtemp(join(tmpdir(), "replay-host-account-invalid-"));
try {
const home = join(root, "home");
const sourceDir = join(home, ".tutti-dev", "account");
await mkdir(sourceDir, { recursive: true });
await writeFile(
join(sourceDir, "auth.json"),
JSON.stringify({ name: "x" })
);
const result = await seedHostAccountSession(join(root, "state"), {
env: {},
homeDirectory: home
});
assert.equal(result.action, "skipped-invalid");
} finally {
await rm(root, { force: true, recursive: true });
}
});
test("seedHostAccountSession can be disabled via env", async () => {
const root = await mkdtemp(join(tmpdir(), "replay-host-account-skip-"));
try {
const home = join(root, "home");
const sourceDir = join(home, ".tutti-dev", "account");
await mkdir(sourceDir, { recursive: true });
await writeFile(
join(sourceDir, "auth.json"),
JSON.stringify({
session_id: "session-1",
cookie: "session_id=session-1"
})
);
const result = await seedHostAccountSession(join(root, "state"), {
env: { TUTTI_AGENT_SESSION_REPLAY_SKIP_HOST_ACCOUNT_AUTH: "1" },
homeDirectory: home
});
assert.equal(result.action, "skipped-disabled");
} finally {
await rm(root, { force: true, recursive: true });
}
});
@@ -0,0 +1,283 @@
import { mkdir, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { setTimeout as delay } from "node:timers/promises";
import { CdpClient } from "../capture-electron-trace.mjs";
import {
configureWaitDiagnostics,
evaluate,
selectProvider,
waitForEvaluation
} from "../agent-gui-performance-helpers.mjs";
import {
reservePort,
startDesktop,
stopProcessTree,
waitForPageWebSocket
} from "../run-agent-gui-performance.mjs";
import {
resolveAgentSessionReplayProjectRoot,
resolveRecordScenarioProject,
seedRecordingUserProject
} from "./recording.mjs";
import {
createRuntime,
enableAgentSessionRecordingFeature,
enableAgentSessionRecordingTarget,
initializeCleanDatabase,
preparedDesktopLaunch,
removeRuntime,
setAgentComposerDefaults
} from "./runtime.mjs";
import {
bindManagedReplayShutdown as bindManagedReplayShutdownCore,
loadUiScenario,
managedReplayCheckpointPrefix,
managedReplayCompletePrefix,
managedReplayFailedPrefix,
runUiDriveScenario
} from "../../../packages/agent/session-replay-runner/src/index.mjs";
function bindManagedReplayShutdown(desktop, options = {}) {
const { stopDesktop = stopProcessTree, ...rest } = options;
return bindManagedReplayShutdownCore(desktop, {
...rest,
exitOnSignal: rest.exitOnSignal ?? true,
stopDesktop
});
}
export const uiDriveCompletePrefix = managedReplayCompletePrefix;
export const uiDriveFailedPrefix = managedReplayFailedPrefix;
export const uiDriveCheckpointPrefix = managedReplayCheckpointPrefix;
export { loadUiScenario };
function log(message) {
process.stderr.write(`[agent-session-replay] ${message}\n`);
}
async function captureScreenshot(client, outputPath) {
const result = await client.send("Page.captureScreenshot", {
format: "png",
fromSurface: true
});
await writeFile(outputPath, Buffer.from(result.data, "base64"));
}
async function prepareAgentGuiSurface(client, agentTargetId, timeoutMs) {
await waitForEvaluation(
client,
`(() => {
const target = [...document.querySelectorAll('[data-provider-target-id]')]
.find((element) => element.dataset.providerTargetId === ${JSON.stringify(agentTargetId)});
const dock = document.querySelector(
'[data-desktop-dock-anchor-key="agent-gui:unified"] button'
);
return { ready: Boolean(target) || dock instanceof HTMLButtonElement };
})()`,
timeoutMs,
"AgentGUI surface or dock launcher"
);
const deadline = Date.now() + timeoutMs;
let latest;
while (Date.now() < deadline) {
latest = await evaluate(
client,
`(() => {
const target = [...document.querySelectorAll('[data-provider-target-id]')]
.find((element) => element.dataset.providerTargetId === ${JSON.stringify(agentTargetId)});
if (
target instanceof HTMLButtonElement &&
!target.disabled &&
target.dataset.disabled !== 'true'
) {
return { ready: true };
}
const dock = document.querySelector(
'[data-desktop-dock-anchor-key="agent-gui:unified"] button'
);
if (!(dock instanceof HTMLButtonElement)) {
return { ready: false, reason: 'dock-unavailable' };
}
dock.click();
return { ready: false, reason: 'dock-clicked' };
})()`
);
if (latest?.ready) break;
await delay(1_000);
}
if (!latest?.ready) {
throw new Error(
`timed out waiting for enabled Agent target ${agentTargetId}: ${JSON.stringify(latest)}`
);
}
await selectProvider(client, agentTargetId, timeoutMs);
await waitForEvaluation(
client,
`(() => {
const editor = document.querySelector('[data-testid="agent-gui-composer-editor"]');
const stop = document.querySelector('[data-testid="agent-gui-composer-stop-active-turn"]');
return { ready: editor instanceof HTMLElement && !stop };
})()`,
timeoutMs,
"idle Agent composer"
);
}
/**
* Run a pure-UI scenario with step checkpoints (screenshot + programmatic assert).
* Does not create Session Replay cassettes.
*/
export async function uiDriveScenario(options) {
const workspaceRoot = options.workspaceRoot;
const projectRoot = resolveAgentSessionReplayProjectRoot();
const agentTargetId = options.agentTargetId ?? "local:codex";
const cassetteId = options.cassetteId ?? `${options.scenario}_ui`;
return runUiDriveScenario(
{
...options,
agentTargetId,
cassetteId
},
{
log,
prefixes: {
// Tutti historically omitted a ready line for ui-drive.
ready: "",
complete: uiDriveCompletePrefix,
failed: uiDriveFailedPrefix,
checkpoint: uiDriveCheckpointPrefix
},
emitReady() {},
captureScreenshot,
async prepare({ options: runOptions, scenario }) {
const runtime = await createRuntime(workspaceRoot, "ui-drive");
const databasePath = join(runtime.stateDirectory, "tuttid.db");
const scenarioStallTimeoutMs = runOptions.stallTimeoutMs ?? 60_000;
configureWaitDiagnostics({
log,
stallTimeoutMs: scenarioStallTimeoutMs
});
const workspaceId = "11111111-1111-4111-8111-111111111111";
await initializeCleanDatabase(workspaceRoot, runtime, workspaceId);
await enableAgentSessionRecordingFeature(databasePath, workspaceRoot);
await enableAgentSessionRecordingTarget(
databasePath,
agentTargetId,
workspaceRoot
);
let replayComposerDefaults = null;
const scenarioState = await scenario.prepare({
agentTargetId,
workspaceRoot: projectRoot,
async selectProject(project) {
const projectSelection = resolveRecordScenarioProject(
project,
projectRoot
);
await seedRecordingUserProject(databasePath, projectSelection);
return projectSelection;
},
async setComposerDefaults(defaults) {
replayComposerDefaults = defaults;
await setAgentComposerDefaults(
databasePath,
agentTargetId,
defaults,
workspaceRoot
);
}
});
return {
composerDefaultsMissing: !replayComposerDefaults,
databasePath,
runtime,
scenarioState,
surface: "agent-gui"
};
},
async launch({ options: runOptions, prepared }) {
const runtime = prepared.runtime;
const cdpPort = await reservePort();
const logPath = join(runtime.directory, "logs", "desktop.log");
await mkdir(join(runtime.directory, "logs"), { recursive: true });
const desktopLaunch = preparedDesktopLaunch();
const desktop = startDesktop({
args: desktopLaunch?.args,
cdpPort,
command: desktopLaunch?.command,
daemonPath: runtime.daemonPath,
desktopLogPath: logPath,
environment: {},
headless: runOptions.headless === true,
stateDirectory: runtime.stateDirectory,
userDataDirectory: runtime.userDataDirectory
});
const disposeDesktopShutdown = bindManagedReplayShutdown(desktop);
const pageWebSocket = await waitForPageWebSocket(
cdpPort,
desktop,
runOptions.timeoutMs
);
const pageClient = await CdpClient.connect(pageWebSocket);
await pageClient.send("Runtime.enable");
await pageClient.send("Page.enable");
await prepareAgentGuiSurface(
pageClient,
agentTargetId,
runOptions.timeoutMs
);
return {
desktop,
disposeDesktopShutdown,
getClient: () => pageClient,
pageClient,
surface: "agent-gui"
};
},
async buildDriveContext({
options: runOptions,
prepared,
launched,
checkpoint
}) {
const pageClient = launched.getClient();
return {
checkpoint,
client: pageClient,
evaluate: (expression, awaitPromise) =>
evaluate(pageClient, expression, awaitPromise),
scenarioState: prepared.scenarioState,
timeoutMs: runOptions.timeoutMs,
waitForEvaluation: (expression, label, intervalMs) =>
waitForEvaluation(
pageClient,
expression,
runOptions.timeoutMs,
label,
intervalMs
)
};
},
async cleanup({ options: runOptions, prepared, launched }) {
launched?.disposeDesktopShutdown?.();
if (launched?.pageClient) {
try {
launched.pageClient.close?.();
} catch {
// ignore
}
}
if (launched?.desktop) {
await stopProcessTree(launched.desktop).catch(() => {});
}
if (prepared?.runtime && !runOptions.keepRuntime) {
await removeRuntime(prepared.runtime.directory).catch(() => {});
} else if (prepared?.runtime) {
log(`runtime kept: ${prepared.runtime.directory}`);
}
}
}
);
}