源码快照 @ 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
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:
@@ -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("'", "''");
|
||||
}
|
||||
Reference in New Issue
Block a user