quiver-cli 0.8.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -45
- package/bin/quiver-cli.mjs +3 -1
- package/dist/cli.js +546 -458
- package/package.json +2 -2
- package/template/.agents/AGENTS.md +4 -3
- package/template/.agents/config.json +7 -0
- package/template/.agents/plugins/opencode/rtk.ts +34 -0
- package/template/.agents/skills/agent-browser/SKILL.md +1 -0
- package/template/.agents/skills/apps/skybridge/SKILL.md +4 -0
- package/template/.agents/skills/design/impeccable/SKILL.md +36 -118
- package/template/.agents/skills/design/impeccable/reference/adapt.md +1 -0
- package/template/.agents/skills/design/impeccable/reference/adapt.native.md +58 -0
- package/template/.agents/skills/design/impeccable/reference/android.md +40 -0
- package/template/.agents/skills/design/impeccable/reference/animate.md +73 -188
- package/template/.agents/skills/design/impeccable/reference/audit.md +12 -9
- package/template/.agents/skills/design/impeccable/reference/audit.native.md +139 -0
- package/template/.agents/skills/design/impeccable/reference/bolder.md +19 -101
- package/template/.agents/skills/design/impeccable/reference/clarify.md +59 -253
- package/template/.agents/skills/design/impeccable/reference/colorize.md +51 -222
- package/template/.agents/skills/design/impeccable/reference/craft-floor.md +42 -0
- package/template/.agents/skills/design/impeccable/reference/craft.md +3 -121
- package/template/.agents/skills/design/impeccable/reference/critique.md +44 -23
- package/template/.agents/skills/design/impeccable/reference/degraded/asset-producer.md +90 -0
- package/template/.agents/skills/design/impeccable/reference/degraded/documenter.md +24 -0
- package/template/.agents/skills/design/impeccable/reference/degraded/finish-reviewer.md +37 -0
- package/template/.agents/skills/design/impeccable/reference/degraded/manual-edit-applier.md +92 -0
- package/template/.agents/skills/design/impeccable/reference/delight.md +47 -279
- package/template/.agents/skills/design/impeccable/reference/distill.md +2 -2
- package/template/.agents/skills/design/impeccable/reference/doctor.md +53 -0
- package/template/.agents/skills/design/impeccable/reference/document.md +60 -73
- package/template/.agents/skills/design/impeccable/reference/harden.md +1 -12
- package/template/.agents/skills/design/impeccable/reference/hooks.md +20 -5
- package/template/.agents/skills/design/impeccable/reference/init.md +72 -119
- package/template/.agents/skills/design/impeccable/reference/ios.md +45 -0
- package/template/.agents/skills/design/impeccable/reference/layout.md +54 -131
- package/template/.agents/skills/design/impeccable/reference/live-setup.md +102 -0
- package/template/.agents/skills/design/impeccable/reference/live.md +116 -511
- package/template/.agents/skills/design/impeccable/reference/new-work.md +105 -0
- package/template/.agents/skills/design/impeccable/reference/{product.md → operate.md} +6 -5
- package/template/.agents/skills/design/impeccable/reference/optimize.md +4 -4
- package/template/.agents/skills/design/impeccable/reference/overdrive.md +1 -4
- package/template/.agents/skills/design/impeccable/reference/polish.md +68 -212
- package/template/.agents/skills/design/impeccable/reference/quieter.md +3 -3
- package/template/.agents/skills/design/impeccable/reference/routing.md +18 -0
- package/template/.agents/skills/design/impeccable/reference/shape.md +38 -144
- package/template/.agents/skills/design/impeccable/reference/typeset.md +51 -250
- package/template/.agents/skills/design/impeccable/reference/visualize.md +47 -0
- package/template/.agents/skills/design/impeccable/scripts/command-metadata.json +1 -1
- package/template/.agents/skills/design/impeccable/scripts/concept-seed.mjs +558 -0
- package/template/.agents/skills/design/impeccable/scripts/context-signals.mjs +119 -10
- package/template/.agents/skills/design/impeccable/scripts/context.mjs +534 -45
- package/template/.agents/skills/design/impeccable/scripts/critique-storage.mjs +18 -47
- package/template/.agents/skills/design/impeccable/scripts/detector/browser/injected/index.mjs +96 -10
- package/template/.agents/skills/design/impeccable/scripts/detector/cli/main.mjs +174 -26
- package/template/.agents/skills/design/impeccable/scripts/detector/design-system.mjs +233 -0
- package/template/.agents/skills/design/impeccable/scripts/detector/detect-antipatterns-browser.js +3348 -203
- package/template/.agents/skills/design/impeccable/scripts/detector/engines/browser/detect-url.mjs +102 -7
- package/template/.agents/skills/design/impeccable/scripts/detector/engines/regex/detect-text.mjs +297 -97
- package/template/.agents/skills/design/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +187 -16
- package/template/.agents/skills/design/impeccable/scripts/detector/engines/static-html/detect-html.mjs +41 -11
- package/template/.agents/skills/design/impeccable/scripts/detector/findings.mjs +7 -1
- package/template/.agents/skills/design/impeccable/scripts/detector/node/file-system.mjs +16 -2
- package/template/.agents/skills/design/impeccable/scripts/detector/registry/antipatterns.mjs +207 -38
- package/template/.agents/skills/design/impeccable/scripts/detector/rules/checks.mjs +3082 -173
- package/template/.agents/skills/design/impeccable/scripts/detector/shared/constants.mjs +11 -0
- package/template/.agents/skills/design/impeccable/scripts/detector/shared/fonts.mjs +30 -0
- package/template/.agents/skills/design/impeccable/scripts/doctor.mjs +336 -0
- package/template/.agents/skills/design/impeccable/scripts/embed-prompt.mjs +133 -0
- package/template/.agents/skills/design/impeccable/scripts/generate-image.mjs +240 -0
- package/template/.agents/skills/design/impeccable/scripts/hook-admin.mjs +98 -18
- package/template/.agents/skills/design/impeccable/scripts/hook-before-edit.mjs +46 -6
- package/template/.agents/skills/design/impeccable/scripts/hook-lib.mjs +518 -50
- package/template/.agents/skills/design/impeccable/scripts/hook.mjs +25 -8
- package/template/.agents/skills/design/impeccable/scripts/lib/artifact-schema.mjs +93 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/composition-catalog.mjs +200 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/concept-catalog.mjs +357 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/impeccable-config.mjs +27 -7
- package/template/.agents/skills/design/impeccable/scripts/lib/impeccable-paths.mjs +17 -8
- package/template/.agents/skills/design/impeccable/scripts/lib/provider.mjs +5 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/roll-selection.mjs +362 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/staleness-deep.mjs +457 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/staleness-notice.mjs +169 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/staleness.mjs +457 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/surface-briefs.mjs +151 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/target-slug.mjs +33 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/template-extensions.mjs +146 -0
- package/template/.agents/skills/design/impeccable/scripts/live/accept-css.mjs +617 -0
- package/template/.agents/skills/design/impeccable/scripts/live/accept-verify.mjs +60 -0
- package/template/.agents/skills/design/impeccable/scripts/live/browser-script-parts.mjs +7 -1
- package/template/.agents/skills/design/impeccable/scripts/live/completion.mjs +10 -1
- package/template/.agents/skills/design/impeccable/scripts/live/event-validation.mjs +67 -5
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/astro.mjs +47 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/detect-utils.mjs +73 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/index.mjs +143 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/journal.mjs +197 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/nextjs.mjs +49 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/nuxt.mjs +161 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/script-src.mjs +17 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/static-html.mjs +26 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/sveltekit.mjs +71 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/tag-strategy.mjs +247 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/tanstack-start.mjs +70 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/vite-generic.mjs +42 -0
- package/template/.agents/skills/design/impeccable/scripts/live/generation-preflight.mjs +149 -0
- package/template/.agents/skills/design/impeccable/scripts/live/instructions.mjs +142 -0
- package/template/.agents/skills/design/impeccable/scripts/live/poll-lanes.mjs +14 -0
- package/template/.agents/skills/design/impeccable/scripts/live/roots.mjs +508 -0
- package/template/.agents/skills/design/impeccable/scripts/live/session-store.mjs +324 -50
- package/template/.agents/skills/design/impeccable/scripts/live/source-lock.mjs +105 -0
- package/template/.agents/skills/design/impeccable/scripts/live/source-search.mjs +105 -0
- package/template/.agents/skills/design/impeccable/scripts/live/svelte-ast.mjs +961 -0
- package/template/.agents/skills/design/impeccable/scripts/live/svelte-component.mjs +588 -72
- package/template/.agents/skills/design/impeccable/scripts/live/sveltekit-adapter.mjs +59 -17
- package/template/.agents/skills/design/impeccable/scripts/live/tanstack-adapter.mjs +280 -0
- package/template/.agents/skills/design/impeccable/scripts/live/vocabulary.mjs +135 -0
- package/template/.agents/skills/design/impeccable/scripts/live-accept.mjs +210 -68
- package/template/.agents/skills/design/impeccable/scripts/live-browser.js +1562 -223
- package/template/.agents/skills/design/impeccable/scripts/live-commit-manual-edits.mjs +3 -0
- package/template/.agents/skills/design/impeccable/scripts/live-complete.mjs +33 -1
- package/template/.agents/skills/design/impeccable/scripts/live-inject.mjs +175 -255
- package/template/.agents/skills/design/impeccable/scripts/live-insert.mjs +26 -6
- package/template/.agents/skills/design/impeccable/scripts/live-manual-edit-evidence.mjs +6 -1
- package/template/.agents/skills/design/impeccable/scripts/live-poll.mjs +61 -16
- package/template/.agents/skills/design/impeccable/scripts/live-resume.mjs +39 -10
- package/template/.agents/skills/design/impeccable/scripts/live-server.mjs +573 -47
- package/template/.agents/skills/design/impeccable/scripts/live-status.mjs +17 -7
- package/template/.agents/skills/design/impeccable/scripts/live-wrap.mjs +124 -91
- package/template/.agents/skills/design/impeccable/scripts/live.mjs +88 -26
- package/template/.agents/skills/design/impeccable/scripts/palette.mjs +76 -81
- package/template/.agents/skills/design/impeccable/scripts/pin.mjs +18 -11
- package/template/.agents/skills/design/impeccable/scripts/serve-question.mjs +932 -0
- package/template/.agents/skills/design/impeccable/scripts/surface-brief.mjs +74 -0
- package/template/.agents/skills/design/shadcn/SKILL.md +46 -11
- package/template/.agents/skills/design/shadcn/cli.md +49 -16
- package/template/.agents/skills/design/shadcn/customization.md +14 -7
- package/template/.agents/skills/design/shadcn/evals/evals.json +30 -0
- package/template/.agents/skills/design/shadcn/mcp.md +27 -16
- package/template/.agents/skills/design/shadcn/registry.md +277 -0
- package/template/.agents/skills/design/shadcn/rules/chat.md +224 -0
- package/template/.agents/skills/design/shadcn/rules/composition.md +20 -2
- package/template/.agents/skills/design/shadcn/rules/styling.md +23 -0
- package/template/.agents/skills/find-skills/SKILL.md +2 -3
- package/template/.agents/skills/hono/SKILL.md +579 -0
- package/template/.agents/skills/integrations/langfuse/SKILL.md +10 -6
- package/template/.agents/skills/integrations/langfuse/references/ci-cd.md +41 -0
- package/template/.agents/skills/integrations/langfuse/references/cli.md +8 -0
- package/template/.agents/skills/integrations/langfuse/references/error-analysis.md +15 -27
- package/template/.agents/skills/integrations/langfuse/references/instrumentation.md +39 -52
- package/template/.agents/skills/integrations/langfuse/references/judge-calibration.md +5 -2
- package/template/.agents/skills/integrations/langfuse/references/prompt-engineering.md +35 -0
- package/template/.agents/skills/integrations/langfuse/references/prompt-migration.md +41 -196
- package/template/.agents/skills/integrations/langfuse/references/skill-feedback.md +3 -0
- package/template/.agents/skills/integrations/langfuse/references/trace-evaluator-upgrade.md +76 -0
- package/template/.agents/skills/integrations/langfuse/references/user-feedback.md +4 -0
- package/template/.agents/skills/integrations/langfuse/references/v4-project-migration.md +73 -0
- package/template/.agents/skills/supabase/CHANGELOG.md +71 -0
- package/template/.agents/skills/supabase/SKILL.md +145 -0
- package/template/.agents/skills/supabase/assets/feedback-issue-template.md +17 -0
- package/template/.agents/skills/supabase/references/skill-feedback.md +17 -0
- package/template/.agents/skills/supabase-postgres-best-practices/CHANGELOG.md +73 -0
- package/template/.agents/skills/supabase-postgres-best-practices/SKILL.md +64 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/_contributing.md +170 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/_sections.md +39 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/_template.md +34 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/advanced-full-text-search.md +55 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/advanced-jsonb-indexing.md +49 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/conn-idle-timeout.md +46 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/conn-limits.md +44 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/conn-pooling.md +41 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/conn-prepared-statements.md +46 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/data-batch-inserts.md +54 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/data-n-plus-one.md +53 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/data-pagination.md +50 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/data-upsert.md +50 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/lock-advisory.md +56 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/lock-deadlock-prevention.md +68 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/lock-short-transactions.md +50 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/lock-skip-locked.md +54 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/monitor-explain-analyze.md +45 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/monitor-pg-stat-statements.md +55 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/monitor-vacuum-analyze.md +55 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/query-composite-indexes.md +44 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/query-covering-indexes.md +40 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/query-index-types.md +48 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/query-missing-indexes.md +43 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/query-partial-indexes.md +45 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/schema-constraints.md +80 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/schema-data-types.md +46 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/schema-foreign-key-indexes.md +59 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/schema-lowercase-identifiers.md +55 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/schema-partitioning.md +55 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/schema-primary-keys.md +61 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/security-privileges.md +54 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/security-rls-basics.md +50 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/security-rls-performance.md +63 -0
- package/template/.agents/upstreams.json +34 -12
- package/template/.agents/skills/design/impeccable/reference/brand.md +0 -108
- package/template/.agents/skills/design/impeccable/reference/codex.md +0 -105
- package/template/.agents/skills/design/impeccable/reference/interaction-design.md +0 -189
- package/template/.agents/skills/integrations/langfuse/references/sdk-upgrade.md +0 -175
|
@@ -28,14 +28,20 @@ import {
|
|
|
28
28
|
readLiveBrowserScriptParts,
|
|
29
29
|
resolveLiveBrowserScriptParts,
|
|
30
30
|
} from './live/browser-script-parts.mjs';
|
|
31
|
-
import { createLiveSessionStore } from './live/session-store.mjs';
|
|
31
|
+
import { createLiveSessionStore, GENERATION_FENCED_PHASES } from './live/session-store.mjs';
|
|
32
|
+
import { runGenerationPreflight } from './live/generation-preflight.mjs';
|
|
32
33
|
import { validateEvent } from './live/event-validation.mjs';
|
|
34
|
+
import { selectAvailablePendingEvent } from './live/poll-lanes.mjs';
|
|
33
35
|
import { createManualEditRoutes } from './live/manual-edit-routes.mjs';
|
|
34
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
LIVE_COMMANDS,
|
|
38
|
+
VARIANT_PROGRESS_CHECKPOINT_REASONS as VARIANT_PROGRESS_CHECKPOINT_REASON_LIST,
|
|
39
|
+
} from './live/vocabulary.mjs';
|
|
35
40
|
import {
|
|
36
41
|
getDesignSidecarPath,
|
|
37
42
|
getLiveDir,
|
|
38
43
|
getLiveAnnotationsDir,
|
|
44
|
+
IMPECCABLE_COMMAND_PREFIX,
|
|
39
45
|
readLiveServerInfo,
|
|
40
46
|
removeLiveServerInfo,
|
|
41
47
|
resolveDesignSidecarPath,
|
|
@@ -48,21 +54,54 @@ import {
|
|
|
48
54
|
} from './live/manual-apply.mjs';
|
|
49
55
|
import {
|
|
50
56
|
applyDeferredSvelteComponentAccepts,
|
|
57
|
+
bumpSvelteComponentPreviewRevision,
|
|
58
|
+
compileCheckVariants,
|
|
51
59
|
removeAllSvelteComponentSessions,
|
|
60
|
+
sweepInactiveSvelteComponentSessions,
|
|
52
61
|
} from './live/svelte-component.mjs';
|
|
62
|
+
import { enterLiveRoot } from './live/roots.mjs';
|
|
53
63
|
|
|
54
64
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
65
|
+
// Anchor the whole process on the live roots manifest before anything derives
|
|
66
|
+
// a path from cwd. A server started from the wrong directory re-roots itself
|
|
67
|
+
// onto the appRoot the boot decided on instead of minting a second project.
|
|
68
|
+
const LIVE_ROOTS = enterLiveRoot(process.cwd());
|
|
69
|
+
|
|
70
|
+
// PRODUCT.md / DESIGN.md context, resolved lazily and per request so a server
|
|
71
|
+
// that outlives an `impeccable document` run (or a context file created after
|
|
72
|
+
// boot) reports current truth instead of a boot-time snapshot. The roots
|
|
73
|
+
// manifest wins when the ambient resolution misses (nested app inheriting
|
|
74
|
+
// repo-level context files).
|
|
75
|
+
function resolveProjectContext() {
|
|
76
|
+
const ctx = loadContext(process.cwd());
|
|
77
|
+
const designPath = ctx.designPath
|
|
78
|
+
? path.resolve(process.cwd(), ctx.designPath)
|
|
79
|
+
: (LIVE_ROOTS?.designPath && fs.existsSync(LIVE_ROOTS.designPath) ? LIVE_ROOTS.designPath : null);
|
|
80
|
+
const hasProduct = ctx.hasProduct
|
|
81
|
+
|| !!(LIVE_ROOTS?.productPath && fs.existsSync(LIVE_ROOTS.productPath));
|
|
82
|
+
return {
|
|
83
|
+
...ctx,
|
|
84
|
+
hasProduct,
|
|
85
|
+
hasDesign: !!designPath,
|
|
86
|
+
resolvedDesignPath: designPath,
|
|
87
|
+
contextDir: ctx.contextDir || LIVE_ROOTS?.contextRoot || process.cwd(),
|
|
88
|
+
designContextDir: ctx.designContextDir
|
|
89
|
+
|| (designPath ? path.dirname(designPath) : null),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
63
92
|
const DEFAULT_POLL_TIMEOUT = 600_000; // 10 min — agent re-polls on timeout anyway
|
|
64
93
|
const SSE_HEARTBEAT_INTERVAL = 30_000; // keepalive ping every 30s
|
|
65
94
|
|
|
95
|
+
// The browser events allowed to mint a NEW session journal. `generate` starts
|
|
96
|
+
// a variant session at Go; `steer` mints its own request id. Every other
|
|
97
|
+
// id-carrying event must land on an existing session (see the unknown_session
|
|
98
|
+
// gate in the /events handler).
|
|
99
|
+
const SESSION_CREATING_EVENT_TYPES = new Set(['generate', 'steer']);
|
|
100
|
+
// The browser checkpoints for several unrelated reasons (see checkpointPayload
|
|
101
|
+
// in live-browser.js). Only these two report that variant availability changed,
|
|
102
|
+
// and only they may drive variant_progress / the *_reviewable phases.
|
|
103
|
+
const VARIANT_PROGRESS_CHECKPOINT_REASONS = new Set(VARIANT_PROGRESS_CHECKPOINT_REASON_LIST);
|
|
104
|
+
|
|
66
105
|
// ---------------------------------------------------------------------------
|
|
67
106
|
// Port detection
|
|
68
107
|
// ---------------------------------------------------------------------------
|
|
@@ -143,7 +182,16 @@ function chatAgentLikelyActive() {
|
|
|
143
182
|
const MAX_ANNOTATION_BYTES = 10 * 1024 * 1024;
|
|
144
183
|
|
|
145
184
|
function enqueueEvent(event) {
|
|
146
|
-
if (!event
|
|
185
|
+
if (!event) return;
|
|
186
|
+
// Dedupe by (session, type), except mount failures, which are per-variant:
|
|
187
|
+
// variant 2 failing must not be swallowed because variant 1's failure is
|
|
188
|
+
// still queued.
|
|
189
|
+
const duplicate = event.id && state.pendingEvents.some((entry) => (
|
|
190
|
+
entry.event?.id === event.id
|
|
191
|
+
&& entry.event?.type === event.type
|
|
192
|
+
&& (event.type !== 'variant_mount_failed' || entry.event?.variant === event.variant)
|
|
193
|
+
));
|
|
194
|
+
if (duplicate) return;
|
|
147
195
|
state.pendingEvents.push({ event, leaseUntil: 0, seq: state.nextEventSeq++ });
|
|
148
196
|
flushPendingPolls();
|
|
149
197
|
}
|
|
@@ -155,29 +203,207 @@ function restorePendingEventsFromStore() {
|
|
|
155
203
|
}
|
|
156
204
|
}
|
|
157
205
|
|
|
158
|
-
function findAvailablePendingEvent(now = Date.now()) {
|
|
159
|
-
|
|
160
|
-
if (entry.leaseUntil && entry.leaseUntil > now) continue;
|
|
161
|
-
return entry;
|
|
162
|
-
}
|
|
163
|
-
return null;
|
|
206
|
+
function findAvailablePendingEvent(now = Date.now(), types = null) {
|
|
207
|
+
return selectAvailablePendingEvent(state.pendingEvents, { now, types });
|
|
164
208
|
}
|
|
165
209
|
|
|
166
|
-
function leaseEvent(entry, leaseMs) {
|
|
210
|
+
async function leaseEvent(entry, leaseMs) {
|
|
211
|
+
// Claim the entry before awaiting anything. prepareGenerateEventForLease
|
|
212
|
+
// yields to the event loop, and selectAvailablePendingEvent only skips
|
|
213
|
+
// entries whose lease is in the future — an unclaimed entry would be handed
|
|
214
|
+
// to a second poll in that window and generated twice.
|
|
215
|
+
entry.leaseUntil = Date.now() + leaseMs;
|
|
216
|
+
await prepareGenerateEventForLease(entry);
|
|
167
217
|
if (!entry.event?.id) {
|
|
168
218
|
const idx = state.pendingEvents.indexOf(entry);
|
|
169
219
|
if (idx !== -1) state.pendingEvents.splice(idx, 1);
|
|
170
220
|
return entry.event;
|
|
171
221
|
}
|
|
222
|
+
// Re-stamp so the lease window starts when the agent actually receives the
|
|
223
|
+
// work, not when scaffolding began.
|
|
172
224
|
entry.leaseUntil = Date.now() + leaseMs;
|
|
225
|
+
recordGenerateDelivery(entry);
|
|
173
226
|
scheduleLeaseFlush();
|
|
174
227
|
broadcastAgentPollingIfChanged();
|
|
175
228
|
return entry.event;
|
|
176
229
|
}
|
|
177
230
|
|
|
178
|
-
function
|
|
231
|
+
function recordGenerateDelivery(entry) {
|
|
232
|
+
const event = entry?.event;
|
|
233
|
+
if (!event || event.type !== 'generate' || event.generationReadyAt) return;
|
|
234
|
+
const at = Date.now();
|
|
235
|
+
entry.event = { ...event, generationReadyAt: at };
|
|
236
|
+
state.sessionStore?.appendEvent(entry.event);
|
|
237
|
+
recordAgentPhase(event.id, 'generation_ready', { at });
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function prepareGenerateEventForLease(entry) {
|
|
241
|
+
const event = entry?.event;
|
|
242
|
+
if (!event || event.type !== 'generate' || event.scaffoldAttempted) return;
|
|
243
|
+
|
|
244
|
+
recordAgentPhase(event.id, 'picked_up');
|
|
245
|
+
recordAgentPhase(event.id, 'scaffolding');
|
|
246
|
+
const result = await runGenerationPreflight(event, {
|
|
247
|
+
cwd: process.cwd(),
|
|
248
|
+
scriptsDir: __dirname,
|
|
249
|
+
});
|
|
250
|
+
entry.event = {
|
|
251
|
+
...event,
|
|
252
|
+
scaffoldAttempted: true,
|
|
253
|
+
scaffoldDurationMs: result.durationMs ?? null,
|
|
254
|
+
...(result.ok ? { scaffold: result.scaffold } : { scaffoldError: result.error || result.reason }),
|
|
255
|
+
};
|
|
256
|
+
state.sessionStore?.appendEvent(entry.event);
|
|
257
|
+
recordAgentPhase(event.id, result.ok ? 'source_ready' : 'scaffold_fallback', {
|
|
258
|
+
durationMs: result.durationMs ?? null,
|
|
259
|
+
previewMode: result.scaffold?.previewMode || 'source',
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function recordAgentPhase(id, phase, details = {}) {
|
|
264
|
+
if (!id) return;
|
|
265
|
+
const event = {
|
|
266
|
+
type: 'agent_phase',
|
|
267
|
+
id,
|
|
268
|
+
phase,
|
|
269
|
+
at: Date.now(),
|
|
270
|
+
...details,
|
|
271
|
+
};
|
|
272
|
+
state.sessionStore?.appendEvent(event);
|
|
273
|
+
broadcast(event);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Detect a browser that missed the generation `done` broadcast.
|
|
278
|
+
*
|
|
279
|
+
* The preflight no longer writes the scaffold into source for source-preview
|
|
280
|
+
* targets (the agent writes wrapper + variants in one atomic edit), so the old
|
|
281
|
+
* scaffold-write full-reload that opened the "stranded at 0/N" race is gone.
|
|
282
|
+
* This recovery stays as defense in depth: any framework reload that drops the
|
|
283
|
+
* agent's variant write + `done` while the browser is mid-reload leaves the new
|
|
284
|
+
* page in GENERATING at 0/N. That resumed page always checkpoints
|
|
285
|
+
* (`browser_resumed`), so a checkpoint claiming "still generating, variants
|
|
286
|
+
* missing" for a session whose generation already completed is direct
|
|
287
|
+
* evidence of the miss. Rebuild the `done` payload from the snapshot so the
|
|
288
|
+
* caller can re-broadcast it; the browser's done handler is idempotent and
|
|
289
|
+
* falls back to injecting variants from source.
|
|
290
|
+
*
|
|
291
|
+
* Keys on the store's monotone `generationCompletedAt`, not `phase` — the
|
|
292
|
+
* behind checkpoint itself regresses `phase` to `generating`, and a browser
|
|
293
|
+
* that misses the redelivered `done` too (another reload) must still trigger
|
|
294
|
+
* redelivery from its next checkpoint.
|
|
295
|
+
*/
|
|
296
|
+
function detectMissedGenerationCompletion(event) {
|
|
297
|
+
if (!event?.id || event.type !== 'checkpoint') return null;
|
|
298
|
+
if (event.phase !== 'generating') return null;
|
|
299
|
+
if (!variantCountLooksBehind(event.arrivedVariants, event.expectedVariants)) return null;
|
|
300
|
+
if (!state.sessionStore) return null;
|
|
301
|
+
let snapshot = null;
|
|
302
|
+
try {
|
|
303
|
+
snapshot = state.sessionStore.getSnapshot(event.id);
|
|
304
|
+
} catch {
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
return missedCompletionFromSnapshot(snapshot);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function variantCountLooksBehind(arrivedValue, expectedValue) {
|
|
311
|
+
const arrived = Number(arrivedValue) || 0;
|
|
312
|
+
const expected = Number(expectedValue) || 0;
|
|
313
|
+
return arrived <= 0 || (expected > 0 && arrived < expected);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function missedCompletionFromSnapshot(snapshot) {
|
|
317
|
+
if (!snapshot?.id || !snapshot.generationCompletedAt) return null;
|
|
318
|
+
if (snapshot.generationCanceled) return null;
|
|
319
|
+
// Accept/discard already underway: the browser is no longer waiting on
|
|
320
|
+
// generation, and a late `done` there would collide with teardown.
|
|
321
|
+
if (GENERATION_FENCED_PHASES.has(snapshot.phase)) return null;
|
|
322
|
+
const file = snapshot.sourceFile || snapshot.previewFile;
|
|
323
|
+
if (!file) return null;
|
|
324
|
+
return {
|
|
325
|
+
type: 'done',
|
|
326
|
+
id: snapshot.id,
|
|
327
|
+
file,
|
|
328
|
+
sourceFile: snapshot.sourceFile || undefined,
|
|
329
|
+
previewFile: snapshot.previewFile || undefined,
|
|
330
|
+
previewMode: snapshot.previewMode || undefined,
|
|
331
|
+
redelivered: true,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function recordGenerationCheckpoint(event) {
|
|
336
|
+
if (!event?.id || event.type !== 'checkpoint') return;
|
|
337
|
+
if (generationIsFenced(event.id)) return;
|
|
338
|
+
// Only checkpoints that report a change in variant availability are
|
|
339
|
+
// generation progress. The browser also checkpoints for durability on Tune
|
|
340
|
+
// slider drags, resumes, and anchor recovery; treating those as progress
|
|
341
|
+
// echoed `variant_progress` straight back to the browser that sent it, which
|
|
342
|
+
// remounts the component preview mid-drag (reverting the user's live param
|
|
343
|
+
// edit and detaching the popover's element), and permanently latched the
|
|
344
|
+
// *_reviewable phases from the wrong trigger, corrupting generation timings.
|
|
345
|
+
if (!VARIANT_PROGRESS_CHECKPOINT_REASONS.has(event.reason)) return;
|
|
346
|
+
const arrived = Number(event.arrivedVariants) || 0;
|
|
347
|
+
const expected = Number(event.expectedVariants) || 0;
|
|
348
|
+
if (arrived <= 0 || expected <= 0) return;
|
|
349
|
+
const previewMode = event.previewMode || 'source';
|
|
350
|
+
const previewFile = event.previewFile || event.file;
|
|
351
|
+
if (previewFile) {
|
|
352
|
+
broadcast({
|
|
353
|
+
type: 'variant_progress',
|
|
354
|
+
id: event.id,
|
|
355
|
+
file: previewFile,
|
|
356
|
+
sourceFile: event.sourceFile || (previewMode === 'source' ? previewFile : undefined),
|
|
357
|
+
previewFile,
|
|
358
|
+
previewMode,
|
|
359
|
+
arrivedVariants: arrived,
|
|
360
|
+
expectedVariants: expected,
|
|
361
|
+
publicationKind: event.publicationKind || 'variants',
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
const details = {
|
|
365
|
+
arrivedVariants: arrived,
|
|
366
|
+
expectedVariants: expected,
|
|
367
|
+
checkpointReason: event.reason || null,
|
|
368
|
+
};
|
|
369
|
+
const at = Date.now();
|
|
370
|
+
if (!generationPhaseAlreadyRecorded(event.id, 'first_reviewable')) {
|
|
371
|
+
recordAgentPhase(event.id, 'first_reviewable', { ...details, at });
|
|
372
|
+
}
|
|
373
|
+
if (arrived >= 2 && expected >= 3 && !generationPhaseAlreadyRecorded(event.id, 'second_reviewable')) {
|
|
374
|
+
recordAgentPhase(event.id, 'second_reviewable', { ...details, at });
|
|
375
|
+
}
|
|
376
|
+
if (arrived >= expected && !generationPhaseAlreadyRecorded(event.id, 'all_variants_ready')) {
|
|
377
|
+
recordAgentPhase(event.id, 'all_variants_ready', { ...details, at });
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function generationIsFenced(id) {
|
|
382
|
+
if (!state.sessionStore || !id) return false;
|
|
383
|
+
try {
|
|
384
|
+
const snapshot = state.sessionStore.getSnapshot(id, { includeCompleted: true });
|
|
385
|
+
return snapshot?.generationCanceled === true;
|
|
386
|
+
} catch {
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function generationPhaseAlreadyRecorded(id, phase) {
|
|
392
|
+
if (!state.sessionStore) return false;
|
|
393
|
+
try {
|
|
394
|
+
const snapshot = state.sessionStore.getSnapshot(id, { includeCompleted: true });
|
|
395
|
+
return !!snapshot?.generationTimings?.[phase];
|
|
396
|
+
} catch {
|
|
397
|
+
return false;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function acknowledgePendingEvent(id, sourceEventType) {
|
|
179
402
|
if (!id) return false;
|
|
180
|
-
const idx = state.pendingEvents.findIndex((entry) =>
|
|
403
|
+
const idx = state.pendingEvents.findIndex((entry) => (
|
|
404
|
+
entry.event?.id === id
|
|
405
|
+
&& (!sourceEventType || entry.event?.type === sourceEventType)
|
|
406
|
+
));
|
|
181
407
|
if (idx === -1) return false;
|
|
182
408
|
const acknowledged = state.pendingEvents[idx].event;
|
|
183
409
|
state.pendingEvents.splice(idx, 1);
|
|
@@ -186,9 +412,39 @@ function acknowledgePendingEvent(id) {
|
|
|
186
412
|
return acknowledged;
|
|
187
413
|
}
|
|
188
414
|
|
|
189
|
-
function
|
|
415
|
+
function releasePendingEvent(id, sourceEventType) {
|
|
416
|
+
const entry = state.pendingEvents.find((item) => (
|
|
417
|
+
item.event?.id === id
|
|
418
|
+
&& (!sourceEventType || item.event?.type === sourceEventType)
|
|
419
|
+
));
|
|
420
|
+
if (!entry) return null;
|
|
421
|
+
entry.leaseUntil = 0;
|
|
422
|
+
scheduleLeaseFlush();
|
|
423
|
+
return entry.event;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function retirePendingGeneration(id) {
|
|
427
|
+
if (!id) return 0;
|
|
428
|
+
let retired = 0;
|
|
429
|
+
for (let index = state.pendingEvents.length - 1; index >= 0; index -= 1) {
|
|
430
|
+
const event = state.pendingEvents[index]?.event;
|
|
431
|
+
if (event?.id !== id || event.type !== 'generate') continue;
|
|
432
|
+
state.pendingEvents.splice(index, 1);
|
|
433
|
+
retired += 1;
|
|
434
|
+
}
|
|
435
|
+
if (retired > 0) {
|
|
436
|
+
scheduleLeaseFlush();
|
|
437
|
+
broadcastAgentPollingIfChanged();
|
|
438
|
+
}
|
|
439
|
+
return retired;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function findPendingEventById(id, sourceEventType) {
|
|
190
443
|
if (!id) return null;
|
|
191
|
-
const entry = state.pendingEvents.find((item) =>
|
|
444
|
+
const entry = state.pendingEvents.find((item) => (
|
|
445
|
+
item.event?.id === id
|
|
446
|
+
&& (!sourceEventType || item.event?.type === sourceEventType)
|
|
447
|
+
));
|
|
192
448
|
return entry?.event || null;
|
|
193
449
|
}
|
|
194
450
|
|
|
@@ -197,7 +453,7 @@ function summarizePendingEventForStatus(entry) {
|
|
|
197
453
|
const summary = {
|
|
198
454
|
id: event.id,
|
|
199
455
|
type: event.type,
|
|
200
|
-
leased:
|
|
456
|
+
leased: isLeased(entry),
|
|
201
457
|
leaseUntil: entry.leaseUntil || null,
|
|
202
458
|
};
|
|
203
459
|
if (event.type === 'manual_edit_apply') {
|
|
@@ -223,7 +479,18 @@ function summarizeActiveSessionForClient(snapshot = {}) {
|
|
|
223
479
|
arrivedVariants: snapshot.arrivedVariants ?? 0,
|
|
224
480
|
visibleVariant: snapshot.visibleVariant ?? null,
|
|
225
481
|
checkpointRevision: snapshot.checkpointRevision ?? 0,
|
|
482
|
+
browserCheckpointRevision: snapshot.browserCheckpointRevision ?? snapshot.checkpointRevision ?? 0,
|
|
483
|
+
publicationCheckpointRevision: snapshot.publicationCheckpointRevision ?? 0,
|
|
226
484
|
paramValues: snapshot.paramValues || {},
|
|
485
|
+
generationPhase: snapshot.generationPhase ?? null,
|
|
486
|
+
generationCompletedAt: snapshot.generationCompletedAt ?? null,
|
|
487
|
+
generationCanceled: snapshot.generationCanceled === true,
|
|
488
|
+
cancelReason: snapshot.cancelReason ?? null,
|
|
489
|
+
// Render truth, so a browser with no localStorage can rehydrate to the
|
|
490
|
+
// same comparison the server already knows about.
|
|
491
|
+
mountedVariants: Array.isArray(snapshot.mountedVariants) ? snapshot.mountedVariants : [],
|
|
492
|
+
mountFailures: Array.isArray(snapshot.mountFailures) ? snapshot.mountFailures : [],
|
|
493
|
+
renderState: snapshot.renderState ?? null,
|
|
227
494
|
};
|
|
228
495
|
}
|
|
229
496
|
|
|
@@ -268,24 +535,46 @@ function scheduleLeaseFlush() {
|
|
|
268
535
|
function flushPendingPolls() {
|
|
269
536
|
let changed = false;
|
|
270
537
|
while (state.pendingPolls.length > 0) {
|
|
271
|
-
|
|
538
|
+
let pollIndex = -1;
|
|
539
|
+
let entry = null;
|
|
540
|
+
for (let index = 0; index < state.pendingPolls.length; index += 1) {
|
|
541
|
+
const candidate = findAvailablePendingEvent(Date.now(), state.pendingPolls[index].types);
|
|
542
|
+
if (!candidate) continue;
|
|
543
|
+
pollIndex = index;
|
|
544
|
+
entry = candidate;
|
|
545
|
+
break;
|
|
546
|
+
}
|
|
272
547
|
if (!entry) {
|
|
273
548
|
scheduleLeaseFlush();
|
|
274
549
|
broadcastAgentPollingIfChanged();
|
|
275
550
|
return;
|
|
276
551
|
}
|
|
277
|
-
const poll = state.pendingPolls.
|
|
278
|
-
|
|
552
|
+
const [poll] = state.pendingPolls.splice(pollIndex, 1);
|
|
553
|
+
// leaseEvent is async (it may scaffold source), but it claims the entry
|
|
554
|
+
// synchronously, so the next loop iteration will not re-select it. Resolve
|
|
555
|
+
// the poll when the lease settles rather than awaiting here, so one slow
|
|
556
|
+
// scaffold never delays the other parked polls. On the exceptional failure
|
|
557
|
+
// path, answer `timeout` so the agent re-polls; the claim stays until the
|
|
558
|
+
// lease expires, which keeps a deterministic failure from hot-looping.
|
|
559
|
+
leaseEvent(entry, poll.leaseMs).then(poll.resolve, (error) => {
|
|
560
|
+
console.error('[live] lease failed for ' + (entry.event?.id || 'unknown') + ': ' + (error?.message || error));
|
|
561
|
+
poll.resolve({ type: 'timeout' });
|
|
562
|
+
});
|
|
279
563
|
changed = true;
|
|
280
564
|
}
|
|
281
565
|
scheduleLeaseFlush();
|
|
282
566
|
if (changed) broadcastAgentPollingIfChanged();
|
|
283
567
|
}
|
|
284
568
|
|
|
569
|
+
function isLeased(entry) {
|
|
570
|
+
return !!(entry?.leaseUntil && entry.leaseUntil > Date.now());
|
|
571
|
+
}
|
|
572
|
+
|
|
285
573
|
function agentPollingConnected() {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
574
|
+
// A leased event only proves that a poll returned once. The foreground task
|
|
575
|
+
// may have ended immediately afterward, so only an actively waiting poll is
|
|
576
|
+
// evidence that steering can wake the task right now.
|
|
577
|
+
return state.pendingPolls.length > 0;
|
|
289
578
|
}
|
|
290
579
|
|
|
291
580
|
function broadcastAgentPollingIfChanged() {
|
|
@@ -375,20 +664,44 @@ function hasProjectContext() {
|
|
|
375
664
|
// PRODUCT.md carries brand voice / anti-references — that's what determines
|
|
376
665
|
// whether variants are brand-aware. DESIGN.md (visual tokens) is a separate
|
|
377
666
|
// concern, surfaced by the design panel's own empty state.
|
|
378
|
-
return !!
|
|
667
|
+
return !!resolveProjectContext().hasProduct;
|
|
379
668
|
}
|
|
380
669
|
|
|
381
670
|
function statOrNull(filePath) {
|
|
382
671
|
try { return fs.statSync(filePath); } catch { return null; }
|
|
383
672
|
}
|
|
384
673
|
|
|
674
|
+
// Strict loopback-origin test for CORS. Parses the Origin as a URL (never a
|
|
675
|
+
// substring match, so `http://localhost.evil.com` and `http://127.0.0.1.evil.com`
|
|
676
|
+
// fail) and accepts only http/https on localhost, 127.0.0.1, or the IPv6 loopback.
|
|
677
|
+
function isLoopbackOrigin(origin) {
|
|
678
|
+
if (typeof origin !== 'string' || origin.length === 0) return false;
|
|
679
|
+
let parsed;
|
|
680
|
+
try { parsed = new URL(origin); } catch { return false; }
|
|
681
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return false;
|
|
682
|
+
const host = parsed.hostname.toLowerCase();
|
|
683
|
+
return host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '[::1]';
|
|
684
|
+
}
|
|
685
|
+
|
|
385
686
|
// HTTP request handler
|
|
386
687
|
// ---------------------------------------------------------------------------
|
|
387
688
|
|
|
388
689
|
function createRequestHandler({ detectScript, liveScriptParts }) {
|
|
389
690
|
return (req, res) => {
|
|
390
691
|
const url = new URL(req.url, `http://localhost:${state.port}`);
|
|
391
|
-
|
|
692
|
+
// Loopback-restricted CORS. Reflect the caller's Origin only when it is a
|
|
693
|
+
// loopback origin, always paired with `Vary: Origin` so an intermediary
|
|
694
|
+
// cache never serves a response authorized for one origin to another. A
|
|
695
|
+
// remote page (e.g. https://evil.example probing the port from a tab open
|
|
696
|
+
// on the same machine) gets no Access-Control-Allow-Origin, so its
|
|
697
|
+
// JS-initiated fetch cannot read any response. Requests with no Origin
|
|
698
|
+
// header (script tags, curl, the agent's own fetches) are not subject to
|
|
699
|
+
// CORS and keep working; no ACAO header is needed for them.
|
|
700
|
+
const origin = req.headers.origin;
|
|
701
|
+
if (origin && isLoopbackOrigin(origin)) {
|
|
702
|
+
res.setHeader('Access-Control-Allow-Origin', origin);
|
|
703
|
+
res.setHeader('Vary', 'Origin');
|
|
704
|
+
}
|
|
392
705
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
393
706
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
394
707
|
if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; }
|
|
@@ -397,6 +710,15 @@ function createRequestHandler({ detectScript, liveScriptParts }) {
|
|
|
397
710
|
|
|
398
711
|
// --- Scripts ---
|
|
399
712
|
if (p === '/live.js') {
|
|
713
|
+
// Token-gated: the script body embeds state.token, which unlocks every
|
|
714
|
+
// token-guarded route. Serving it unauthenticated let any local page read
|
|
715
|
+
// the token and drive the session. The injected <script src> carries
|
|
716
|
+
// `?token=...` (see live-inject.mjs). A missing/wrong token → 401.
|
|
717
|
+
if (url.searchParams.get('token') !== state.token) {
|
|
718
|
+
res.writeHead(401, { 'Content-Type': 'text/plain' });
|
|
719
|
+
res.end('Unauthorized');
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
400
722
|
// Re-read from disk each request so edits to live-browser.js land on
|
|
401
723
|
// the next tab reload. No-store headers prevent browser caching across
|
|
402
724
|
// sessions — during iteration, a cached old script silently breaks
|
|
@@ -413,6 +735,8 @@ function createRequestHandler({ detectScript, liveScriptParts }) {
|
|
|
413
735
|
token: state.token,
|
|
414
736
|
port: state.port,
|
|
415
737
|
vocabulary: LIVE_COMMANDS,
|
|
738
|
+
commandPrefix: IMPECCABLE_COMMAND_PREFIX,
|
|
739
|
+
appRoot: process.cwd(),
|
|
416
740
|
parts,
|
|
417
741
|
});
|
|
418
742
|
res.writeHead(200, {
|
|
@@ -550,8 +874,9 @@ function createRequestHandler({ detectScript, liveScriptParts }) {
|
|
|
550
874
|
const token = url.searchParams.get('token');
|
|
551
875
|
if (token !== state.token) { res.writeHead(401); res.end('Unauthorized'); return; }
|
|
552
876
|
|
|
553
|
-
const
|
|
554
|
-
const
|
|
877
|
+
const projectContext = resolveProjectContext();
|
|
878
|
+
const mdPath = projectContext.resolvedDesignPath;
|
|
879
|
+
const jsonPath = resolveDesignSidecarPath(process.cwd(), projectContext.designContextDir || projectContext.contextDir) || getDesignSidecarPath(process.cwd());
|
|
555
880
|
const mdStat = statOrNull(mdPath);
|
|
556
881
|
const jsonStat = statOrNull(jsonPath);
|
|
557
882
|
|
|
@@ -603,7 +928,13 @@ function createRequestHandler({ detectScript, liveScriptParts }) {
|
|
|
603
928
|
const filePath = url.searchParams.get('path');
|
|
604
929
|
if (!filePath || filePath.includes('..')) { res.writeHead(400); res.end('Bad path'); return; }
|
|
605
930
|
const absPath = path.resolve(process.cwd(), filePath);
|
|
606
|
-
|
|
931
|
+
// Confine to the project root. A bare `startsWith(cwd)` string check lets a
|
|
932
|
+
// sibling dir whose name extends the root name (projeto -> projeto-backup)
|
|
933
|
+
// slip through; compare on the relative path instead (same pattern as
|
|
934
|
+
// sessionFileMetadataFromPollReply below). An empty rel means the request
|
|
935
|
+
// resolved to the root directory itself, which this file route never serves.
|
|
936
|
+
const rel = path.relative(process.cwd(), absPath);
|
|
937
|
+
if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) { res.writeHead(403); res.end('Forbidden'); return; }
|
|
607
938
|
let content;
|
|
608
939
|
try { content = fs.readFileSync(absPath, 'utf-8'); }
|
|
609
940
|
catch { res.writeHead(404); res.end('File not found'); return; }
|
|
@@ -687,6 +1018,30 @@ function createRequestHandler({ detectScript, liveScriptParts }) {
|
|
|
687
1018
|
res.end(JSON.stringify({ error }));
|
|
688
1019
|
return;
|
|
689
1020
|
}
|
|
1021
|
+
if (msg.type === 'agent_phase') {
|
|
1022
|
+
recordAgentPhase(msg.id, msg.phase, {
|
|
1023
|
+
...(Number.isFinite(msg.durationMs) ? { durationMs: msg.durationMs } : {}),
|
|
1024
|
+
owner: typeof msg.owner === 'string' ? msg.owner : undefined,
|
|
1025
|
+
});
|
|
1026
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1027
|
+
res.end(JSON.stringify({ ok: true }));
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
// Only the events that START a session may create its journal.
|
|
1031
|
+
// Everything else (checkpoints, mount acks, accept/discard) must
|
|
1032
|
+
// reference a session THIS store already knows: appendEvent creates a
|
|
1033
|
+
// journal for any id it is handed, so without this gate a browser
|
|
1034
|
+
// resuming another project's session from per-origin storage (two
|
|
1035
|
+
// apps sharing a localhost port) materializes a ghost session here
|
|
1036
|
+
// that keeps reattaching after every discard.
|
|
1037
|
+
if (msg.id && state.sessionStore
|
|
1038
|
+
&& !SESSION_CREATING_EVENT_TYPES.has(msg.type)
|
|
1039
|
+
&& !state.sessionStore.has(msg.id)) {
|
|
1040
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
1041
|
+
res.end(JSON.stringify({ error: 'unknown_session', id: msg.id }));
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
const missedCompletion = detectMissedGenerationCompletion(msg);
|
|
690
1045
|
if (state.sessionStore && msg.id) {
|
|
691
1046
|
try {
|
|
692
1047
|
state.sessionStore.appendEvent(msg);
|
|
@@ -696,10 +1051,33 @@ function createRequestHandler({ detectScript, liveScriptParts }) {
|
|
|
696
1051
|
return;
|
|
697
1052
|
}
|
|
698
1053
|
}
|
|
1054
|
+
if (msg.type === 'accept' || msg.type === 'discard') {
|
|
1055
|
+
retirePendingGeneration(msg.id);
|
|
1056
|
+
}
|
|
1057
|
+
recordGenerationCheckpoint(msg);
|
|
1058
|
+
if (missedCompletion) broadcast(missedCompletion);
|
|
699
1059
|
if (msg.type === 'exit') {
|
|
700
1060
|
cleanupSvelteComponentSessionsBeforeExit();
|
|
701
1061
|
}
|
|
702
|
-
|
|
1062
|
+
// An ORPHANED discard is the browser reporting that the session's
|
|
1063
|
+
// wrapper no longer exists in source (edited or regenerated away).
|
|
1064
|
+
// There is no cleanup for an agent to perform, and asking one to run
|
|
1065
|
+
// the normal discard flow would just fail against the missing
|
|
1066
|
+
// scaffolding, so the server terminalizes the session itself and the
|
|
1067
|
+
// event stays out of the poll queue.
|
|
1068
|
+
const orphanedDiscard = msg.type === 'discard' && msg.orphaned === true;
|
|
1069
|
+
if (orphanedDiscard && state.sessionStore && msg.id) {
|
|
1070
|
+
try {
|
|
1071
|
+
state.sessionStore.appendEvent({ type: 'discarded', id: msg.id, orphaned: true });
|
|
1072
|
+
} catch { /* the discard_requested phase already left the resumable set */ }
|
|
1073
|
+
}
|
|
1074
|
+
// `variant_mounted` is the happy path: it is journaled above so the
|
|
1075
|
+
// snapshot carries render truth, but there is nothing for the agent to
|
|
1076
|
+
// do about it, so it stays out of the poll queue and off the SSE bus.
|
|
1077
|
+
// `variant_mount_failed` is the opposite: the agent published something
|
|
1078
|
+
// the browser could not render, and only the agent can fix it, so it
|
|
1079
|
+
// goes to the queue as a first-class event.
|
|
1080
|
+
if (msg.type !== 'checkpoint' && msg.type !== 'variant_mounted' && !orphanedDiscard) {
|
|
703
1081
|
enqueueEvent(msg);
|
|
704
1082
|
}
|
|
705
1083
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
@@ -736,6 +1114,12 @@ function createRequestHandler({ detectScript, liveScriptParts }) {
|
|
|
736
1114
|
// Agent poll endpoints (unchanged from WS version)
|
|
737
1115
|
// ---------------------------------------------------------------------------
|
|
738
1116
|
|
|
1117
|
+
function parsePollTypes(value) {
|
|
1118
|
+
if (!value) return null;
|
|
1119
|
+
const types = String(value).split(',').map((type) => type.trim()).filter(Boolean);
|
|
1120
|
+
return types.length > 0 ? new Set(types) : null;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
739
1123
|
function handlePollGet(req, res, url) {
|
|
740
1124
|
const token = url.searchParams.get('token');
|
|
741
1125
|
if (token !== state.token) {
|
|
@@ -746,13 +1130,25 @@ function handlePollGet(req, res, url) {
|
|
|
746
1130
|
state.lastPollAt = Date.now();
|
|
747
1131
|
const timeout = parseInt(url.searchParams.get('timeout') || DEFAULT_POLL_TIMEOUT, 10);
|
|
748
1132
|
const leaseMs = parseInt(url.searchParams.get('leaseMs') || '30000', 10);
|
|
749
|
-
const
|
|
1133
|
+
const types = parsePollTypes(url.searchParams.get('types'));
|
|
1134
|
+
const available = findAvailablePendingEvent(Date.now(), types);
|
|
750
1135
|
if (available) {
|
|
751
|
-
|
|
752
|
-
|
|
1136
|
+
// Do not await inline: leaseEvent may scaffold source, and this handler runs
|
|
1137
|
+
// on the server's only thread. The client can disconnect during that window,
|
|
1138
|
+
// so check the socket before replying.
|
|
1139
|
+
leaseEvent(available, leaseMs).then((event) => {
|
|
1140
|
+
if (res.writableEnded || res.destroyed) return;
|
|
1141
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1142
|
+
res.end(JSON.stringify(event));
|
|
1143
|
+
}, (error) => {
|
|
1144
|
+
console.error('[live] lease failed for ' + (available.event?.id || 'unknown') + ': ' + (error?.message || error));
|
|
1145
|
+
if (res.writableEnded || res.destroyed) return;
|
|
1146
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1147
|
+
res.end(JSON.stringify({ type: 'timeout' }));
|
|
1148
|
+
});
|
|
753
1149
|
return;
|
|
754
1150
|
}
|
|
755
|
-
const poll = { resolve, leaseMs };
|
|
1151
|
+
const poll = { resolve, leaseMs, types };
|
|
756
1152
|
const timer = setTimeout(() => {
|
|
757
1153
|
const idx = state.pendingPolls.indexOf(poll);
|
|
758
1154
|
if (idx !== -1) state.pendingPolls.splice(idx, 1);
|
|
@@ -781,12 +1177,16 @@ function sessionFileMetadataFromPollReply(file) {
|
|
|
781
1177
|
if (!file || typeof file !== 'string') return { file };
|
|
782
1178
|
const normalized = file.split(path.sep).join('/');
|
|
783
1179
|
const base = { file: normalized };
|
|
784
|
-
|
|
785
|
-
if (!
|
|
1180
|
+
const metadataFile = normalized;
|
|
1181
|
+
if (!metadataFile.endsWith('/manifest.json') && metadataFile !== 'manifest.json') return base;
|
|
1182
|
+
if (!metadataFile.includes('.impeccable/live/previews/')
|
|
1183
|
+
&& !metadataFile.includes('node_modules/.impeccable-live/')
|
|
1184
|
+
&& !metadataFile.includes('src/lib/impeccable/')
|
|
1185
|
+
&& !metadataFile.includes('/.impeccable-live/')) return base;
|
|
786
1186
|
|
|
787
1187
|
let full;
|
|
788
1188
|
try {
|
|
789
|
-
full = path.resolve(process.cwd(),
|
|
1189
|
+
full = path.resolve(process.cwd(), metadataFile);
|
|
790
1190
|
const rel = path.relative(process.cwd(), full);
|
|
791
1191
|
if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) return base;
|
|
792
1192
|
} catch {
|
|
@@ -795,18 +1195,54 @@ function sessionFileMetadataFromPollReply(file) {
|
|
|
795
1195
|
|
|
796
1196
|
try {
|
|
797
1197
|
const manifest = JSON.parse(fs.readFileSync(full, 'utf-8'));
|
|
798
|
-
if (manifest?.previewMode !== 'svelte-component'
|
|
1198
|
+
if (manifest?.previewMode !== 'svelte-component'
|
|
1199
|
+
|| !manifest.sourceFile) return base;
|
|
799
1200
|
return {
|
|
800
1201
|
file: String(manifest.sourceFile).split(path.sep).join('/'),
|
|
801
1202
|
sourceFile: String(manifest.sourceFile).split(path.sep).join('/'),
|
|
802
1203
|
previewFile: normalized,
|
|
803
|
-
previewMode:
|
|
1204
|
+
previewMode: manifest.previewMode,
|
|
804
1205
|
};
|
|
805
1206
|
} catch {
|
|
806
1207
|
return base;
|
|
807
1208
|
}
|
|
808
1209
|
}
|
|
809
1210
|
|
|
1211
|
+
function inferSourceEventType(msg = {}, pendingEvents = state.pendingEvents) {
|
|
1212
|
+
const entriesForId = pendingEvents.filter((entry) => entry.event?.id === msg.id);
|
|
1213
|
+
const pendingTypes = new Set(entriesForId.map((entry) => entry.event?.type));
|
|
1214
|
+
if (msg.type === 'discarded' || msg.type === 'discard') return 'discard';
|
|
1215
|
+
if (msg.type === 'complete') {
|
|
1216
|
+
if (pendingTypes.has('carbonize_cleanup')) return 'carbonize_cleanup';
|
|
1217
|
+
return pendingTypes.has('accept') ? 'accept' : (pendingTypes.has('generate') ? 'generate' : undefined);
|
|
1218
|
+
}
|
|
1219
|
+
if (msg.type === 'steer_done') return 'steer';
|
|
1220
|
+
// `agent_done` can be the automatic acknowledgement for a carbonize Accept.
|
|
1221
|
+
// New pollers send sourceEventType explicitly; default to generate only for
|
|
1222
|
+
// older callers so a late worker cannot acknowledge a queued Accept.
|
|
1223
|
+
if (msg.type === 'agent_done' || msg.type === 'done') {
|
|
1224
|
+
// A `done` reply to a mount failure is the republish that unblocks the
|
|
1225
|
+
// browser. Without this the ack would look for a `generate` that was
|
|
1226
|
+
// already retired, the mount-failure event would stay queued, and the next
|
|
1227
|
+
// poll would hand the same failure back to the agent forever.
|
|
1228
|
+
if (!pendingTypes.has('generate') && pendingTypes.has('variant_mount_failed')) return 'variant_mount_failed';
|
|
1229
|
+
return 'generate';
|
|
1230
|
+
}
|
|
1231
|
+
// `error` is reference/live.md's documented failure reply, and parseReplyArgs
|
|
1232
|
+
// never sets sourceEventType on it (the poller is a fresh process that cannot
|
|
1233
|
+
// know what it leased). Returning undefined here makes acknowledgePendingEvent
|
|
1234
|
+
// match *any* event for this id: a stale generate worker's failure silently
|
|
1235
|
+
// consumed the user's queued Accept, which was then never delivered to any
|
|
1236
|
+
// agent and left the browser in SAVING forever. Attribute the failure to the
|
|
1237
|
+
// event this agent actually holds a lease on, and otherwise to `generate` —
|
|
1238
|
+
// never to a wildcard. If that generate was already retired by an Accept, the
|
|
1239
|
+
// ack simply finds no match, which is the correct outcome for a stale reply.
|
|
1240
|
+
if (msg.type === 'error') {
|
|
1241
|
+
return entriesForId.find(isLeased)?.event?.type || 'generate';
|
|
1242
|
+
}
|
|
1243
|
+
return undefined;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
810
1246
|
function handlePollPost(req, res) {
|
|
811
1247
|
let body = '';
|
|
812
1248
|
req.on('data', (c) => { body += c; });
|
|
@@ -867,7 +1303,23 @@ function handlePollPost(req, res) {
|
|
|
867
1303
|
res.end(JSON.stringify({ error: 'stale_manual_edit_apply_reply', ...rollback }));
|
|
868
1304
|
return;
|
|
869
1305
|
}
|
|
870
|
-
const
|
|
1306
|
+
const sourceEventType = msg.sourceEventType || inferSourceEventType(msg);
|
|
1307
|
+
if (msg.type === 'retry') {
|
|
1308
|
+
const releasedEvent = releasePendingEvent(msg.id, sourceEventType);
|
|
1309
|
+
if (!releasedEvent) {
|
|
1310
|
+
res.writeHead(msg.id ? 404 : 400, { 'Content-Type': 'application/json' });
|
|
1311
|
+
res.end(JSON.stringify({
|
|
1312
|
+
error: msg.id ? 'unknown_poll_retry_id' : 'missing_poll_retry_id',
|
|
1313
|
+
id: msg.id,
|
|
1314
|
+
}));
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
flushPendingPolls();
|
|
1318
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1319
|
+
res.end(JSON.stringify({ ok: true, released: true }));
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
const pendingEventBeforeAck = findPendingEventById(msg.id, sourceEventType);
|
|
871
1323
|
if (pendingEventBeforeAck?.type === 'steer' && msg.type === 'steer_done'
|
|
872
1324
|
&& !msg.file && !(typeof msg.message === 'string' && msg.message.trim())) {
|
|
873
1325
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
@@ -877,7 +1329,7 @@ function handlePollPost(req, res) {
|
|
|
877
1329
|
}));
|
|
878
1330
|
return;
|
|
879
1331
|
}
|
|
880
|
-
const acknowledgedEvent = acknowledgePendingEvent(msg.id);
|
|
1332
|
+
const acknowledgedEvent = acknowledgePendingEvent(msg.id, sourceEventType);
|
|
881
1333
|
let skipJournalReply = false;
|
|
882
1334
|
let existingSession = null;
|
|
883
1335
|
if (!acknowledgedEvent && state.sessionStore && msg.id) {
|
|
@@ -900,6 +1352,30 @@ function handlePollPost(req, res) {
|
|
|
900
1352
|
return;
|
|
901
1353
|
}
|
|
902
1354
|
const replyFileMeta = sessionFileMetadataFromPollReply(msg.file);
|
|
1355
|
+
// A publish (done reply carrying a component manifest) snapshots the
|
|
1356
|
+
// variant files into a fresh revision dir before the browser is told:
|
|
1357
|
+
// the import path changes every publish, so no transform cache can pin a
|
|
1358
|
+
// stale compile of a republished module (node_modules is unwatched).
|
|
1359
|
+
// Broken variants are bounced HERE, before the browser imports anything:
|
|
1360
|
+
// a compile error that reaches the page is a red overlay in the user's
|
|
1361
|
+
// face; bounced at publish it is a private fix with file and line.
|
|
1362
|
+
if (replyFileMeta.previewMode === 'svelte-component'
|
|
1363
|
+
&& msg.id
|
|
1364
|
+
&& (msg.type === 'done' || !msg.type)) {
|
|
1365
|
+
let compileCheck = { ok: true, failures: [] };
|
|
1366
|
+
try { compileCheck = compileCheckVariants(msg.id, process.cwd()); } catch { /* best-effort */ }
|
|
1367
|
+
if (!compileCheck.ok) {
|
|
1368
|
+
res.writeHead(422, { 'Content-Type': 'application/json' });
|
|
1369
|
+
res.end(JSON.stringify({
|
|
1370
|
+
error: 'variant_compile_failed',
|
|
1371
|
+
id: msg.id,
|
|
1372
|
+
failures: compileCheck.failures,
|
|
1373
|
+
_instructions: 'The publish was NOT delivered: the listed variant file(s) do not compile, so the browser never saw them. Fix each failure at the given file and line (the most common cause is a second top-level <style> element; Svelte allows exactly one, so merge all rules into the existing block), then send the same --reply done again.',
|
|
1374
|
+
}));
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
try { bumpSvelteComponentPreviewRevision(msg.id, process.cwd()); } catch { /* best-effort */ }
|
|
1378
|
+
}
|
|
903
1379
|
if (state.sessionStore && msg.id && !skipJournalReply) {
|
|
904
1380
|
try {
|
|
905
1381
|
const eventType = msg.type === 'steer_done'
|
|
@@ -971,6 +1447,51 @@ function cleanupSvelteComponentSessionsBeforeExit() {
|
|
|
971
1447
|
}
|
|
972
1448
|
}
|
|
973
1449
|
|
|
1450
|
+
/**
|
|
1451
|
+
* A previous run that died without its shutdown hook leaves preview component
|
|
1452
|
+
* dirs behind. Drop the ones whose session the store no longer considers
|
|
1453
|
+
* active; anything still active is mid-generation and must survive a restart.
|
|
1454
|
+
*/
|
|
1455
|
+
function sweepOrphanSvelteComponentSessionsOnStartup() {
|
|
1456
|
+
try {
|
|
1457
|
+
const activeIds = (state.sessionStore?.listActiveSessions() || [])
|
|
1458
|
+
.map((snapshot) => snapshot?.id)
|
|
1459
|
+
.filter(Boolean);
|
|
1460
|
+
const result = sweepInactiveSvelteComponentSessions(activeIds, process.cwd());
|
|
1461
|
+
if (result.removed.length > 0 || result.removedRoot) {
|
|
1462
|
+
console.log('[impeccable] swept orphaned Svelte component sessions:', JSON.stringify(result));
|
|
1463
|
+
}
|
|
1464
|
+
} catch (err) {
|
|
1465
|
+
console.warn('[impeccable] Svelte component session sweep failed:', err.message);
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
// Accept receipts are a short-lived idempotency record for a single accept.
|
|
1470
|
+
// Nothing reads one after the session that wrote it is gone, so they only need
|
|
1471
|
+
// to outlive a crash-and-retry window.
|
|
1472
|
+
const ACCEPT_RECEIPT_MAX_AGE_MS = 14 * 24 * 60 * 60 * 1000;
|
|
1473
|
+
|
|
1474
|
+
function sweepStaleAcceptReceiptsOnStartup() {
|
|
1475
|
+
try {
|
|
1476
|
+
const dir = path.join(getLiveDir(process.cwd()), 'accept-receipts');
|
|
1477
|
+
if (!fs.existsSync(dir)) return;
|
|
1478
|
+
const cutoff = Date.now() - ACCEPT_RECEIPT_MAX_AGE_MS;
|
|
1479
|
+
let removed = 0;
|
|
1480
|
+
for (const name of fs.readdirSync(dir)) {
|
|
1481
|
+
if (!name.endsWith('.json') && !name.endsWith('.tmp')) continue;
|
|
1482
|
+
const file = path.join(dir, name);
|
|
1483
|
+
try {
|
|
1484
|
+
if (fs.statSync(file).mtimeMs >= cutoff) continue;
|
|
1485
|
+
fs.rmSync(file, { force: true });
|
|
1486
|
+
removed++;
|
|
1487
|
+
} catch { /* non-fatal */ }
|
|
1488
|
+
}
|
|
1489
|
+
if (removed > 0) console.log(`[impeccable] removed ${removed} accept receipt(s) older than 14 days`);
|
|
1490
|
+
} catch (err) {
|
|
1491
|
+
console.warn('[impeccable] accept receipt retention sweep failed:', err.message);
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
|
|
974
1495
|
function applyLegacyDeferredAcceptsOnStartup() {
|
|
975
1496
|
try {
|
|
976
1497
|
const result = applyDeferredSvelteComponentAccepts(process.cwd());
|
|
@@ -1081,7 +1602,10 @@ if (args.includes('--background')) {
|
|
|
1081
1602
|
process.exit(0);
|
|
1082
1603
|
}
|
|
1083
1604
|
} catch { /* not ready yet */ }
|
|
1084
|
-
|
|
1605
|
+
// The detached child is typically listening in 35-45ms. A 200ms polling
|
|
1606
|
+
// floor dominated configured cold Live startup; poll cheaply and return
|
|
1607
|
+
// as soon as the child has written its ready record.
|
|
1608
|
+
await new Promise(r => setTimeout(r, 5));
|
|
1085
1609
|
}
|
|
1086
1610
|
console.error('Timed out waiting for live server to start.');
|
|
1087
1611
|
process.exit(1);
|
|
@@ -1107,6 +1631,8 @@ manualApply.rollbackTransaction({
|
|
|
1107
1631
|
reason: 'manual_edit_server_start_recovered_abandoned_transaction',
|
|
1108
1632
|
});
|
|
1109
1633
|
applyLegacyDeferredAcceptsOnStartup();
|
|
1634
|
+
sweepOrphanSvelteComponentSessionsOnStartup();
|
|
1635
|
+
sweepStaleAcceptReceiptsOnStartup();
|
|
1110
1636
|
restorePendingEventsFromStore();
|
|
1111
1637
|
manualApply.pruneStaleEvidence();
|
|
1112
1638
|
const portArg = args.find(a => a.startsWith('--port='));
|