Files
agent-radar 2ed3415fb7
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
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

362 lines
11 KiB
JavaScript

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 };
}