Live: polling rework, source locks, preflight scaffolding, Vue previews

Carved out of #371, minus progressive publication. Everything here works
against real project source the way main's Live already does: the agent
writes variants into the file the browser loaded, HMR fires, Accept
promotes and carbonizes. Nothing is staged anywhere.

Poll lanes. Events now carry an explicit priority: accept/discard/exit
ahead of manual_edit_apply/steer/carbonize_cleanup ahead of generate. A
long generate can no longer sit in front of the Accept the user just
clicked. leaseEvent claims its lease before awaiting, so a slow prepare
cannot hand the same event to two pollers.

Source locks. A per-file mutex around every accept and discard path, keyed
on a digest of the absolute path. Staleness is decided by owner-pid
liveness rather than mtime, so a wedged lock clears when its owner dies
instead of after an arbitrary timeout, and a slow-but-live accept is never
stolen from. Only the owning process can release a lock.

Preflight scaffolding. The server runs live-wrap (or live-insert) before
the poll returns and hands the result back as event.scaffold. That walk is
measured at ~7.6s on a large repo; moving it off the agent's critical path
removes a deterministic tool round trip without touching the generated
design. Falls back cleanly to the agent running the helper itself.

Vue previews. previewMode: "vue-component" for Nuxt/Vue targets, matching
the existing Svelte component path: variants compile as real SFCs from a
dev-only directory so the route is never rewritten during generation, and
Vite mounts them without invalidating page state. Accept is the only route
write. Includes a Vue attr tokenizer that normalizes shorthand bindings
(@x, :x, #x) to their canonical forms.

Accept hardening. Every thrown failure now returns mode: 'error' rather
than an ambiguous unhandled result, so a real failure is never classified
as a deliberate manual handoff and silently dropped. The marker search
skips node_modules/.git/dist/build/.impeccable.

Shared CLI arg parsing extracted to scripts/lib/cli-args.mjs.

Assisted-by: Claude Code
This commit is contained in:
Paul Bakaus
2026-07-18 14:11:07 -07:00
parent 97dbaad4a4
commit 3600edc5e9
33 changed files with 62 additions and 2530 deletions
-40
View File
@@ -121,11 +121,7 @@ function baseSnapshot(id) {
fallbackMode: null,
generationPhase: null,
generationTimings: {},
generationEpoch: 1,
publishedRevision: 0,
deliveredVariants: {},
variantPlan: null,
paramsPublished: false,
generationCanceled: false,
generationCanceledAt: null,
cancelReason: null,
@@ -169,7 +165,6 @@ function applyEvent(snapshot, entry, inheritedDiagnostics = []) {
paramValues: { ...(snapshot.paramValues || {}) },
sourceMarkers: { ...(snapshot.sourceMarkers || {}) },
generationTimings: { ...(snapshot.generationTimings || {}) },
deliveredVariants: { ...(snapshot.deliveredVariants || {}) },
variantPlan: snapshot.variantPlan || null,
annotationArtifacts: [...(snapshot.annotationArtifacts || [])],
diagnostics: [...(snapshot.diagnostics || [])],
@@ -183,7 +178,6 @@ function applyEvent(snapshot, entry, inheritedDiagnostics = []) {
switch (event.type) {
case 'generate':
next.phase = 'generate_requested';
next.generationEpoch = Number(event.generationEpoch || next.generationEpoch || 1);
next.pageUrl = event.pageUrl ?? next.pageUrl;
next.expectedVariants = event.count ?? next.expectedVariants;
next.pendingEventSeq = entry.seq ?? next.pendingEventSeq;
@@ -204,40 +198,6 @@ function applyEvent(snapshot, entry, inheritedDiagnostics = []) {
];
}
break;
case 'variant_published':
if (next.generationCanceled || GENERATION_FENCED_PHASES.has(next.phase)) {
next.diagnostics.push({
error: 'late_generation_event_ignored',
type: event.type,
phase: next.phase,
revision: event.revision ?? null,
});
break;
}
if (Number(event.generationEpoch || 0) !== Number(next.generationEpoch || 1)) {
next.diagnostics.push({
error: 'stale_generation_epoch_ignored',
epoch: event.generationEpoch ?? null,
expectedEpoch: next.generationEpoch || 1,
});
break;
}
next.phase = 'variants_progress';
next.publishedRevision = Math.max(next.publishedRevision || 0, Number(event.revision || 0));
next.arrivedVariants = Math.max(next.arrivedVariants || 0, Number(event.arrivedVariants || 0));
next.expectedVariants = Number(event.expectedVariants || next.expectedVariants || 0);
if (event.publicationKind === 'params') next.paramsPublished = true;
next.sourceFile = event.sourceFile ?? next.sourceFile;
next.previewFile = event.previewFile ?? next.previewFile;
next.previewMode = event.previewMode ?? next.previewMode;
if (event.revision) {
next.deliveredVariants[String(event.revision)] = {
digest: event.digest || null,
arrivedVariants: Number(event.arrivedVariants || 0),
publishedAt: event.at || null,
};
}
break;
case 'agent_phase':
next.generationPhase = event.phase ?? next.generationPhase;
if (event.phase) {