@bastani/atomic 0.9.11-alpha.6 → 0.9.11-alpha.7
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/CHANGELOG.md +14 -0
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/skills/impeccable/SKILL.md +33 -129
- package/dist/builtin/workflows/skills/impeccable/agents/impeccable_asset_producer.toml +11 -3
- package/dist/builtin/workflows/skills/impeccable/agents/impeccable_documenter.toml +26 -0
- package/dist/builtin/workflows/skills/impeccable/agents/impeccable_finish_reviewer.toml +35 -0
- package/dist/builtin/workflows/skills/impeccable/reference/android.md +1 -1
- package/dist/builtin/workflows/skills/impeccable/reference/animate.md +72 -189
- package/dist/builtin/workflows/skills/impeccable/reference/audit.md +10 -9
- package/dist/builtin/workflows/skills/impeccable/reference/audit.native.md +2 -2
- package/dist/builtin/workflows/skills/impeccable/reference/bolder.md +19 -108
- package/dist/builtin/workflows/skills/impeccable/reference/clarify.md +59 -253
- package/dist/builtin/workflows/skills/impeccable/reference/colorize.md +51 -222
- package/dist/builtin/workflows/skills/impeccable/reference/craft-floor.md +45 -0
- package/dist/builtin/workflows/skills/impeccable/reference/craft.md +3 -121
- package/dist/builtin/workflows/skills/impeccable/reference/critique.md +28 -20
- package/dist/builtin/workflows/skills/impeccable/reference/degraded/asset-producer.md +97 -0
- package/dist/builtin/workflows/skills/impeccable/reference/degraded/documenter.md +23 -0
- package/dist/builtin/workflows/skills/impeccable/reference/degraded/finish-reviewer.md +32 -0
- package/dist/builtin/workflows/skills/impeccable/reference/degraded/manual-edit-applier.md +92 -0
- package/dist/builtin/workflows/skills/impeccable/reference/delight.md +47 -279
- package/dist/builtin/workflows/skills/impeccable/reference/distill.md +2 -2
- package/dist/builtin/workflows/skills/impeccable/reference/doctor.md +53 -0
- package/dist/builtin/workflows/skills/impeccable/reference/document.md +60 -73
- package/dist/builtin/workflows/skills/impeccable/reference/harden.md +1 -12
- package/dist/builtin/workflows/skills/impeccable/reference/hooks.md +17 -4
- package/dist/builtin/workflows/skills/impeccable/reference/init.md +64 -163
- package/dist/builtin/workflows/skills/impeccable/reference/ios.md +1 -1
- package/dist/builtin/workflows/skills/impeccable/reference/layout.md +52 -153
- package/dist/builtin/workflows/skills/impeccable/reference/live.md +44 -32
- package/dist/builtin/workflows/skills/impeccable/reference/new-work.md +105 -0
- package/dist/builtin/workflows/skills/impeccable/reference/{product.md → operate.md} +6 -5
- package/dist/builtin/workflows/skills/impeccable/reference/optimize.md +4 -4
- package/dist/builtin/workflows/skills/impeccable/reference/overdrive.md +1 -4
- package/dist/builtin/workflows/skills/impeccable/reference/polish.md +68 -212
- package/dist/builtin/workflows/skills/impeccable/reference/quieter.md +3 -3
- package/dist/builtin/workflows/skills/impeccable/reference/routing.md +18 -0
- package/dist/builtin/workflows/skills/impeccable/reference/shape.md +38 -144
- package/dist/builtin/workflows/skills/impeccable/reference/typeset.md +48 -269
- package/dist/builtin/workflows/skills/impeccable/reference/visualize.md +38 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/command-metadata.json +1 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/concept-seed.mjs +584 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/context-signals.mjs +117 -9
- package/dist/builtin/workflows/skills/impeccable/scripts/context.mjs +486 -59
- package/dist/builtin/workflows/skills/impeccable/scripts/critique-storage.mjs +16 -45
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/browser/injected/index.mjs +96 -10
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/cli/main.mjs +143 -26
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/design-system.mjs +181 -12
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +3187 -182
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/browser/detect-url.mjs +102 -7
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +282 -70
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +183 -15
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +22 -7
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/findings.mjs +7 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/node/file-system.mjs +16 -2
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/registry/antipatterns.mjs +155 -42
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/rules/checks.mjs +2988 -155
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/constants.mjs +11 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/doctor.mjs +336 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/generate-image.mjs +232 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/hook-admin.mjs +90 -10
- package/dist/builtin/workflows/skills/impeccable/scripts/hook-lib.mjs +410 -78
- package/dist/builtin/workflows/skills/impeccable/scripts/hook.mjs +25 -8
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/artifact-schema.mjs +93 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/composition-catalog.mjs +165 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/concept-catalog.mjs +329 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/impeccable-config.mjs +20 -5
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/impeccable-paths.mjs +16 -8
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/provider.mjs +1 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/staleness-deep.mjs +455 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/staleness-notice.mjs +169 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/staleness.mjs +457 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/surface-briefs.mjs +151 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/target-slug.mjs +33 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/template-extensions.mjs +146 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/completion.mjs +10 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/live/event-validation.mjs +15 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/generation-preflight.mjs +149 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/poll-lanes.mjs +14 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/session-store.mjs +109 -32
- package/dist/builtin/workflows/skills/impeccable/scripts/live/source-lock.mjs +105 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/source-search.mjs +105 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/sveltekit-adapter.mjs +8 -6
- package/dist/builtin/workflows/skills/impeccable/scripts/live/tanstack-adapter.mjs +280 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-accept.mjs +208 -68
- package/dist/builtin/workflows/skills/impeccable/scripts/live-browser.js +563 -112
- package/dist/builtin/workflows/skills/impeccable/scripts/live-commit-manual-edits.mjs +3 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-inject.mjs +198 -13
- package/dist/builtin/workflows/skills/impeccable/scripts/live-insert.mjs +24 -6
- package/dist/builtin/workflows/skills/impeccable/scripts/live-manual-edit-evidence.mjs +6 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/live-poll.mjs +44 -14
- package/dist/builtin/workflows/skills/impeccable/scripts/live-server.mjs +398 -33
- package/dist/builtin/workflows/skills/impeccable/scripts/live-status.mjs +9 -5
- package/dist/builtin/workflows/skills/impeccable/scripts/live-wrap.mjs +81 -67
- package/dist/builtin/workflows/skills/impeccable/scripts/live.mjs +7 -2
- package/dist/builtin/workflows/skills/impeccable/scripts/palette.mjs +76 -81
- package/dist/builtin/workflows/skills/impeccable/scripts/pin.mjs +2 -2
- package/dist/builtin/workflows/skills/impeccable/scripts/serve-question.mjs +890 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/surface-brief.mjs +74 -0
- package/dist/builtin/workflows/src/extension/index.bundle.mjs +201 -22
- package/dist/builtin/workflows/src/runs/foreground/executor-stage-control.ts +23 -9
- package/dist/builtin/workflows/src/runs/foreground/executor-stage-replay.ts +1 -0
- package/dist/builtin/workflows/src/runs/foreground/postmortem-stage-chat.ts +20 -5
- package/dist/builtin/workflows/src/runs/foreground/stage-control-registry.ts +10 -0
- package/dist/builtin/workflows/src/runs/foreground/stage-delivery-activity.ts +86 -0
- package/dist/builtin/workflows/src/runs/foreground/stage-runner-context.ts +6 -2
- package/dist/builtin/workflows/src/runs/foreground/stage-runner-controller.ts +19 -13
- package/dist/builtin/workflows/src/runs/foreground/stage-runner-send-user-message.ts +8 -0
- package/dist/builtin/workflows/src/runs/foreground/stage-runner-types.ts +16 -0
- package/dist/builtin/workflows/src/tui/stage-chat-view-delivery-activity.ts +121 -0
- package/dist/builtin/workflows/src/tui/stage-chat-view-live-events.ts +42 -0
- package/dist/builtin/workflows/src/tui/stage-chat-view-state.ts +16 -0
- package/dist/builtin/workflows/src/tui/stage-chat-view-types.ts +4 -0
- package/dist/modes/interactive/components/chat-session-host-actions.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-actions.js +2 -14
- package/dist/modes/interactive/components/chat-session-host-actions.js.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-events.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-events.js +9 -0
- package/dist/modes/interactive/components/chat-session-host-events.js.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-runtime.d.ts +25 -0
- package/dist/modes/interactive/components/chat-session-host-runtime.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-runtime.js +53 -0
- package/dist/modes/interactive/components/chat-session-host-runtime.js.map +1 -1
- package/dist/modes/interactive/components/chat-session-host.d.ts +13 -0
- package/dist/modes/interactive/components/chat-session-host.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-session-host.js +22 -1
- package/dist/modes/interactive/components/chat-session-host.js.map +1 -1
- package/dist/modes/interactive/interactive-agent-events.js +9 -3
- package/dist/modes/interactive/interactive-agent-events.js.map +1 -1
- package/docs/tui.md +7 -2
- package/docs/workflows.md +1 -1
- package/npm-shrinkwrap.json +23 -23
- package/package.json +2 -2
- package/dist/builtin/workflows/skills/impeccable/reference/brand.md +0 -108
- package/dist/builtin/workflows/skills/impeccable/reference/codex.md +0 -105
- package/dist/builtin/workflows/skills/impeccable/reference/interaction-design.md +0 -189
|
@@ -28,8 +28,10 @@ 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
36
|
import { LIVE_COMMANDS } from './live/vocabulary.mjs';
|
|
35
37
|
import {
|
|
@@ -63,6 +65,10 @@ const DESIGN_MD_PATH = PROJECT_CONTEXT.designPath
|
|
|
63
65
|
: null;
|
|
64
66
|
const DEFAULT_POLL_TIMEOUT = 600_000; // 10 min — agent re-polls on timeout anyway
|
|
65
67
|
const SSE_HEARTBEAT_INTERVAL = 30_000; // keepalive ping every 30s
|
|
68
|
+
// The browser checkpoints for several unrelated reasons (see checkpointPayload
|
|
69
|
+
// in live-browser.js). Only these two report that variant availability changed,
|
|
70
|
+
// and only they may drive variant_progress / the *_reviewable phases.
|
|
71
|
+
const VARIANT_PROGRESS_CHECKPOINT_REASONS = new Set(['variants_progress', 'variants_ready']);
|
|
66
72
|
|
|
67
73
|
// ---------------------------------------------------------------------------
|
|
68
74
|
// Port detection
|
|
@@ -156,29 +162,207 @@ function restorePendingEventsFromStore() {
|
|
|
156
162
|
}
|
|
157
163
|
}
|
|
158
164
|
|
|
159
|
-
function findAvailablePendingEvent(now = Date.now()) {
|
|
160
|
-
|
|
161
|
-
if (entry.leaseUntil && entry.leaseUntil > now) continue;
|
|
162
|
-
return entry;
|
|
163
|
-
}
|
|
164
|
-
return null;
|
|
165
|
+
function findAvailablePendingEvent(now = Date.now(), types = null) {
|
|
166
|
+
return selectAvailablePendingEvent(state.pendingEvents, { now, types });
|
|
165
167
|
}
|
|
166
168
|
|
|
167
|
-
function leaseEvent(entry, leaseMs) {
|
|
169
|
+
async function leaseEvent(entry, leaseMs) {
|
|
170
|
+
// Claim the entry before awaiting anything. prepareGenerateEventForLease
|
|
171
|
+
// yields to the event loop, and selectAvailablePendingEvent only skips
|
|
172
|
+
// entries whose lease is in the future — an unclaimed entry would be handed
|
|
173
|
+
// to a second poll in that window and generated twice.
|
|
174
|
+
entry.leaseUntil = Date.now() + leaseMs;
|
|
175
|
+
await prepareGenerateEventForLease(entry);
|
|
168
176
|
if (!entry.event?.id) {
|
|
169
177
|
const idx = state.pendingEvents.indexOf(entry);
|
|
170
178
|
if (idx !== -1) state.pendingEvents.splice(idx, 1);
|
|
171
179
|
return entry.event;
|
|
172
180
|
}
|
|
181
|
+
// Re-stamp so the lease window starts when the agent actually receives the
|
|
182
|
+
// work, not when scaffolding began.
|
|
173
183
|
entry.leaseUntil = Date.now() + leaseMs;
|
|
184
|
+
recordGenerateDelivery(entry);
|
|
174
185
|
scheduleLeaseFlush();
|
|
175
186
|
broadcastAgentPollingIfChanged();
|
|
176
187
|
return entry.event;
|
|
177
188
|
}
|
|
178
189
|
|
|
179
|
-
function
|
|
190
|
+
function recordGenerateDelivery(entry) {
|
|
191
|
+
const event = entry?.event;
|
|
192
|
+
if (!event || event.type !== 'generate' || event.generationReadyAt) return;
|
|
193
|
+
const at = Date.now();
|
|
194
|
+
entry.event = { ...event, generationReadyAt: at };
|
|
195
|
+
state.sessionStore?.appendEvent(entry.event);
|
|
196
|
+
recordAgentPhase(event.id, 'generation_ready', { at });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function prepareGenerateEventForLease(entry) {
|
|
200
|
+
const event = entry?.event;
|
|
201
|
+
if (!event || event.type !== 'generate' || event.scaffoldAttempted) return;
|
|
202
|
+
|
|
203
|
+
recordAgentPhase(event.id, 'picked_up');
|
|
204
|
+
recordAgentPhase(event.id, 'scaffolding');
|
|
205
|
+
const result = await runGenerationPreflight(event, {
|
|
206
|
+
cwd: process.cwd(),
|
|
207
|
+
scriptsDir: __dirname,
|
|
208
|
+
});
|
|
209
|
+
entry.event = {
|
|
210
|
+
...event,
|
|
211
|
+
scaffoldAttempted: true,
|
|
212
|
+
scaffoldDurationMs: result.durationMs ?? null,
|
|
213
|
+
...(result.ok ? { scaffold: result.scaffold } : { scaffoldError: result.error || result.reason }),
|
|
214
|
+
};
|
|
215
|
+
state.sessionStore?.appendEvent(entry.event);
|
|
216
|
+
recordAgentPhase(event.id, result.ok ? 'source_ready' : 'scaffold_fallback', {
|
|
217
|
+
durationMs: result.durationMs ?? null,
|
|
218
|
+
previewMode: result.scaffold?.previewMode || 'source',
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function recordAgentPhase(id, phase, details = {}) {
|
|
223
|
+
if (!id) return;
|
|
224
|
+
const event = {
|
|
225
|
+
type: 'agent_phase',
|
|
226
|
+
id,
|
|
227
|
+
phase,
|
|
228
|
+
at: Date.now(),
|
|
229
|
+
...details,
|
|
230
|
+
};
|
|
231
|
+
state.sessionStore?.appendEvent(event);
|
|
232
|
+
broadcast(event);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Detect a browser that missed the generation `done` broadcast.
|
|
237
|
+
*
|
|
238
|
+
* The preflight no longer writes the scaffold into source for source-preview
|
|
239
|
+
* targets (the agent writes wrapper + variants in one atomic edit), so the old
|
|
240
|
+
* scaffold-write full-reload that opened the "stranded at 0/N" race is gone.
|
|
241
|
+
* This recovery stays as defense in depth: any framework reload that drops the
|
|
242
|
+
* agent's variant write + `done` while the browser is mid-reload leaves the new
|
|
243
|
+
* page in GENERATING at 0/N. That resumed page always checkpoints
|
|
244
|
+
* (`browser_resumed`), so a checkpoint claiming "still generating, variants
|
|
245
|
+
* missing" for a session whose generation already completed is direct
|
|
246
|
+
* evidence of the miss. Rebuild the `done` payload from the snapshot so the
|
|
247
|
+
* caller can re-broadcast it; the browser's done handler is idempotent and
|
|
248
|
+
* falls back to injecting variants from source.
|
|
249
|
+
*
|
|
250
|
+
* Keys on the store's monotone `generationCompletedAt`, not `phase` — the
|
|
251
|
+
* behind checkpoint itself regresses `phase` to `generating`, and a browser
|
|
252
|
+
* that misses the redelivered `done` too (another reload) must still trigger
|
|
253
|
+
* redelivery from its next checkpoint.
|
|
254
|
+
*/
|
|
255
|
+
function detectMissedGenerationCompletion(event) {
|
|
256
|
+
if (!event?.id || event.type !== 'checkpoint') return null;
|
|
257
|
+
if (event.phase !== 'generating') return null;
|
|
258
|
+
if (!variantCountLooksBehind(event.arrivedVariants, event.expectedVariants)) return null;
|
|
259
|
+
if (!state.sessionStore) return null;
|
|
260
|
+
let snapshot = null;
|
|
261
|
+
try {
|
|
262
|
+
snapshot = state.sessionStore.getSnapshot(event.id);
|
|
263
|
+
} catch {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
return missedCompletionFromSnapshot(snapshot);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function variantCountLooksBehind(arrivedValue, expectedValue) {
|
|
270
|
+
const arrived = Number(arrivedValue) || 0;
|
|
271
|
+
const expected = Number(expectedValue) || 0;
|
|
272
|
+
return arrived <= 0 || (expected > 0 && arrived < expected);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function missedCompletionFromSnapshot(snapshot) {
|
|
276
|
+
if (!snapshot?.id || !snapshot.generationCompletedAt) return null;
|
|
277
|
+
if (snapshot.generationCanceled) return null;
|
|
278
|
+
// Accept/discard already underway: the browser is no longer waiting on
|
|
279
|
+
// generation, and a late `done` there would collide with teardown.
|
|
280
|
+
if (GENERATION_FENCED_PHASES.has(snapshot.phase)) return null;
|
|
281
|
+
const file = snapshot.sourceFile || snapshot.previewFile;
|
|
282
|
+
if (!file) return null;
|
|
283
|
+
return {
|
|
284
|
+
type: 'done',
|
|
285
|
+
id: snapshot.id,
|
|
286
|
+
file,
|
|
287
|
+
sourceFile: snapshot.sourceFile || undefined,
|
|
288
|
+
previewFile: snapshot.previewFile || undefined,
|
|
289
|
+
previewMode: snapshot.previewMode || undefined,
|
|
290
|
+
redelivered: true,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function recordGenerationCheckpoint(event) {
|
|
295
|
+
if (!event?.id || event.type !== 'checkpoint') return;
|
|
296
|
+
if (generationIsFenced(event.id)) return;
|
|
297
|
+
// Only checkpoints that report a change in variant availability are
|
|
298
|
+
// generation progress. The browser also checkpoints for durability on Tune
|
|
299
|
+
// slider drags, resumes, and anchor recovery; treating those as progress
|
|
300
|
+
// echoed `variant_progress` straight back to the browser that sent it, which
|
|
301
|
+
// remounts the component preview mid-drag (reverting the user's live param
|
|
302
|
+
// edit and detaching the popover's element), and permanently latched the
|
|
303
|
+
// *_reviewable phases from the wrong trigger, corrupting generation timings.
|
|
304
|
+
if (!VARIANT_PROGRESS_CHECKPOINT_REASONS.has(event.reason)) return;
|
|
305
|
+
const arrived = Number(event.arrivedVariants) || 0;
|
|
306
|
+
const expected = Number(event.expectedVariants) || 0;
|
|
307
|
+
if (arrived <= 0 || expected <= 0) return;
|
|
308
|
+
const previewMode = event.previewMode || 'source';
|
|
309
|
+
const previewFile = event.previewFile || event.file;
|
|
310
|
+
if (previewFile) {
|
|
311
|
+
broadcast({
|
|
312
|
+
type: 'variant_progress',
|
|
313
|
+
id: event.id,
|
|
314
|
+
file: previewFile,
|
|
315
|
+
sourceFile: event.sourceFile || (previewMode === 'source' ? previewFile : undefined),
|
|
316
|
+
previewFile,
|
|
317
|
+
previewMode,
|
|
318
|
+
arrivedVariants: arrived,
|
|
319
|
+
expectedVariants: expected,
|
|
320
|
+
publicationKind: event.publicationKind || 'variants',
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
const details = {
|
|
324
|
+
arrivedVariants: arrived,
|
|
325
|
+
expectedVariants: expected,
|
|
326
|
+
checkpointReason: event.reason || null,
|
|
327
|
+
};
|
|
328
|
+
const at = Date.now();
|
|
329
|
+
if (!generationPhaseAlreadyRecorded(event.id, 'first_reviewable')) {
|
|
330
|
+
recordAgentPhase(event.id, 'first_reviewable', { ...details, at });
|
|
331
|
+
}
|
|
332
|
+
if (arrived >= 2 && expected >= 3 && !generationPhaseAlreadyRecorded(event.id, 'second_reviewable')) {
|
|
333
|
+
recordAgentPhase(event.id, 'second_reviewable', { ...details, at });
|
|
334
|
+
}
|
|
335
|
+
if (arrived >= expected && !generationPhaseAlreadyRecorded(event.id, 'all_variants_ready')) {
|
|
336
|
+
recordAgentPhase(event.id, 'all_variants_ready', { ...details, at });
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function generationIsFenced(id) {
|
|
341
|
+
if (!state.sessionStore || !id) return false;
|
|
342
|
+
try {
|
|
343
|
+
const snapshot = state.sessionStore.getSnapshot(id, { includeCompleted: true });
|
|
344
|
+
return snapshot?.generationCanceled === true;
|
|
345
|
+
} catch {
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function generationPhaseAlreadyRecorded(id, phase) {
|
|
351
|
+
if (!state.sessionStore) return false;
|
|
352
|
+
try {
|
|
353
|
+
const snapshot = state.sessionStore.getSnapshot(id, { includeCompleted: true });
|
|
354
|
+
return !!snapshot?.generationTimings?.[phase];
|
|
355
|
+
} catch {
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function acknowledgePendingEvent(id, sourceEventType) {
|
|
180
361
|
if (!id) return false;
|
|
181
|
-
const idx = state.pendingEvents.findIndex((entry) =>
|
|
362
|
+
const idx = state.pendingEvents.findIndex((entry) => (
|
|
363
|
+
entry.event?.id === id
|
|
364
|
+
&& (!sourceEventType || entry.event?.type === sourceEventType)
|
|
365
|
+
));
|
|
182
366
|
if (idx === -1) return false;
|
|
183
367
|
const acknowledged = state.pendingEvents[idx].event;
|
|
184
368
|
state.pendingEvents.splice(idx, 1);
|
|
@@ -187,9 +371,39 @@ function acknowledgePendingEvent(id) {
|
|
|
187
371
|
return acknowledged;
|
|
188
372
|
}
|
|
189
373
|
|
|
190
|
-
function
|
|
374
|
+
function releasePendingEvent(id, sourceEventType) {
|
|
375
|
+
const entry = state.pendingEvents.find((item) => (
|
|
376
|
+
item.event?.id === id
|
|
377
|
+
&& (!sourceEventType || item.event?.type === sourceEventType)
|
|
378
|
+
));
|
|
379
|
+
if (!entry) return null;
|
|
380
|
+
entry.leaseUntil = 0;
|
|
381
|
+
scheduleLeaseFlush();
|
|
382
|
+
return entry.event;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function retirePendingGeneration(id) {
|
|
386
|
+
if (!id) return 0;
|
|
387
|
+
let retired = 0;
|
|
388
|
+
for (let index = state.pendingEvents.length - 1; index >= 0; index -= 1) {
|
|
389
|
+
const event = state.pendingEvents[index]?.event;
|
|
390
|
+
if (event?.id !== id || event.type !== 'generate') continue;
|
|
391
|
+
state.pendingEvents.splice(index, 1);
|
|
392
|
+
retired += 1;
|
|
393
|
+
}
|
|
394
|
+
if (retired > 0) {
|
|
395
|
+
scheduleLeaseFlush();
|
|
396
|
+
broadcastAgentPollingIfChanged();
|
|
397
|
+
}
|
|
398
|
+
return retired;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function findPendingEventById(id, sourceEventType) {
|
|
191
402
|
if (!id) return null;
|
|
192
|
-
const entry = state.pendingEvents.find((item) =>
|
|
403
|
+
const entry = state.pendingEvents.find((item) => (
|
|
404
|
+
item.event?.id === id
|
|
405
|
+
&& (!sourceEventType || item.event?.type === sourceEventType)
|
|
406
|
+
));
|
|
193
407
|
return entry?.event || null;
|
|
194
408
|
}
|
|
195
409
|
|
|
@@ -198,7 +412,7 @@ function summarizePendingEventForStatus(entry) {
|
|
|
198
412
|
const summary = {
|
|
199
413
|
id: event.id,
|
|
200
414
|
type: event.type,
|
|
201
|
-
leased:
|
|
415
|
+
leased: isLeased(entry),
|
|
202
416
|
leaseUntil: entry.leaseUntil || null,
|
|
203
417
|
};
|
|
204
418
|
if (event.type === 'manual_edit_apply') {
|
|
@@ -224,7 +438,13 @@ function summarizeActiveSessionForClient(snapshot = {}) {
|
|
|
224
438
|
arrivedVariants: snapshot.arrivedVariants ?? 0,
|
|
225
439
|
visibleVariant: snapshot.visibleVariant ?? null,
|
|
226
440
|
checkpointRevision: snapshot.checkpointRevision ?? 0,
|
|
441
|
+
browserCheckpointRevision: snapshot.browserCheckpointRevision ?? snapshot.checkpointRevision ?? 0,
|
|
442
|
+
publicationCheckpointRevision: snapshot.publicationCheckpointRevision ?? 0,
|
|
227
443
|
paramValues: snapshot.paramValues || {},
|
|
444
|
+
generationPhase: snapshot.generationPhase ?? null,
|
|
445
|
+
generationCompletedAt: snapshot.generationCompletedAt ?? null,
|
|
446
|
+
generationCanceled: snapshot.generationCanceled === true,
|
|
447
|
+
cancelReason: snapshot.cancelReason ?? null,
|
|
228
448
|
};
|
|
229
449
|
}
|
|
230
450
|
|
|
@@ -269,24 +489,46 @@ function scheduleLeaseFlush() {
|
|
|
269
489
|
function flushPendingPolls() {
|
|
270
490
|
let changed = false;
|
|
271
491
|
while (state.pendingPolls.length > 0) {
|
|
272
|
-
|
|
492
|
+
let pollIndex = -1;
|
|
493
|
+
let entry = null;
|
|
494
|
+
for (let index = 0; index < state.pendingPolls.length; index += 1) {
|
|
495
|
+
const candidate = findAvailablePendingEvent(Date.now(), state.pendingPolls[index].types);
|
|
496
|
+
if (!candidate) continue;
|
|
497
|
+
pollIndex = index;
|
|
498
|
+
entry = candidate;
|
|
499
|
+
break;
|
|
500
|
+
}
|
|
273
501
|
if (!entry) {
|
|
274
502
|
scheduleLeaseFlush();
|
|
275
503
|
broadcastAgentPollingIfChanged();
|
|
276
504
|
return;
|
|
277
505
|
}
|
|
278
|
-
const poll = state.pendingPolls.
|
|
279
|
-
|
|
506
|
+
const [poll] = state.pendingPolls.splice(pollIndex, 1);
|
|
507
|
+
// leaseEvent is async (it may scaffold source), but it claims the entry
|
|
508
|
+
// synchronously, so the next loop iteration will not re-select it. Resolve
|
|
509
|
+
// the poll when the lease settles rather than awaiting here, so one slow
|
|
510
|
+
// scaffold never delays the other parked polls. On the exceptional failure
|
|
511
|
+
// path, answer `timeout` so the agent re-polls; the claim stays until the
|
|
512
|
+
// lease expires, which keeps a deterministic failure from hot-looping.
|
|
513
|
+
leaseEvent(entry, poll.leaseMs).then(poll.resolve, (error) => {
|
|
514
|
+
console.error('[live] lease failed for ' + (entry.event?.id || 'unknown') + ': ' + (error?.message || error));
|
|
515
|
+
poll.resolve({ type: 'timeout' });
|
|
516
|
+
});
|
|
280
517
|
changed = true;
|
|
281
518
|
}
|
|
282
519
|
scheduleLeaseFlush();
|
|
283
520
|
if (changed) broadcastAgentPollingIfChanged();
|
|
284
521
|
}
|
|
285
522
|
|
|
523
|
+
function isLeased(entry) {
|
|
524
|
+
return !!(entry?.leaseUntil && entry.leaseUntil > Date.now());
|
|
525
|
+
}
|
|
526
|
+
|
|
286
527
|
function agentPollingConnected() {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
528
|
+
// A leased event only proves that a poll returned once. The foreground task
|
|
529
|
+
// may have ended immediately afterward, so only an actively waiting poll is
|
|
530
|
+
// evidence that steering can wake the task right now.
|
|
531
|
+
return state.pendingPolls.length > 0;
|
|
290
532
|
}
|
|
291
533
|
|
|
292
534
|
function broadcastAgentPollingIfChanged() {
|
|
@@ -383,13 +625,37 @@ function statOrNull(filePath) {
|
|
|
383
625
|
try { return fs.statSync(filePath); } catch { return null; }
|
|
384
626
|
}
|
|
385
627
|
|
|
628
|
+
// Strict loopback-origin test for CORS. Parses the Origin as a URL (never a
|
|
629
|
+
// substring match, so `http://localhost.evil.com` and `http://127.0.0.1.evil.com`
|
|
630
|
+
// fail) and accepts only http/https on localhost, 127.0.0.1, or the IPv6 loopback.
|
|
631
|
+
function isLoopbackOrigin(origin) {
|
|
632
|
+
if (typeof origin !== 'string' || origin.length === 0) return false;
|
|
633
|
+
let parsed;
|
|
634
|
+
try { parsed = new URL(origin); } catch { return false; }
|
|
635
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return false;
|
|
636
|
+
const host = parsed.hostname.toLowerCase();
|
|
637
|
+
return host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '[::1]';
|
|
638
|
+
}
|
|
639
|
+
|
|
386
640
|
// HTTP request handler
|
|
387
641
|
// ---------------------------------------------------------------------------
|
|
388
642
|
|
|
389
643
|
function createRequestHandler({ detectScript, liveScriptParts }) {
|
|
390
644
|
return (req, res) => {
|
|
391
645
|
const url = new URL(req.url, `http://localhost:${state.port}`);
|
|
392
|
-
|
|
646
|
+
// Loopback-restricted CORS. Reflect the caller's Origin only when it is a
|
|
647
|
+
// loopback origin, always paired with `Vary: Origin` so an intermediary
|
|
648
|
+
// cache never serves a response authorized for one origin to another. A
|
|
649
|
+
// remote page (e.g. https://evil.example probing the port from a tab open
|
|
650
|
+
// on the same machine) gets no Access-Control-Allow-Origin, so its
|
|
651
|
+
// JS-initiated fetch cannot read any response. Requests with no Origin
|
|
652
|
+
// header (script tags, curl, the agent's own fetches) are not subject to
|
|
653
|
+
// CORS and keep working; no ACAO header is needed for them.
|
|
654
|
+
const origin = req.headers.origin;
|
|
655
|
+
if (origin && isLoopbackOrigin(origin)) {
|
|
656
|
+
res.setHeader('Access-Control-Allow-Origin', origin);
|
|
657
|
+
res.setHeader('Vary', 'Origin');
|
|
658
|
+
}
|
|
393
659
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
394
660
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
395
661
|
if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; }
|
|
@@ -398,6 +664,15 @@ function createRequestHandler({ detectScript, liveScriptParts }) {
|
|
|
398
664
|
|
|
399
665
|
// --- Scripts ---
|
|
400
666
|
if (p === '/live.js') {
|
|
667
|
+
// Token-gated: the script body embeds state.token, which unlocks every
|
|
668
|
+
// token-guarded route. Serving it unauthenticated let any local page read
|
|
669
|
+
// the token and drive the session. The injected <script src> carries
|
|
670
|
+
// `?token=...` (see live-inject.mjs). A missing/wrong token → 401.
|
|
671
|
+
if (url.searchParams.get('token') !== state.token) {
|
|
672
|
+
res.writeHead(401, { 'Content-Type': 'text/plain' });
|
|
673
|
+
res.end('Unauthorized');
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
401
676
|
// Re-read from disk each request so edits to live-browser.js land on
|
|
402
677
|
// the next tab reload. No-store headers prevent browser caching across
|
|
403
678
|
// sessions — during iteration, a cached old script silently breaks
|
|
@@ -605,7 +880,13 @@ function createRequestHandler({ detectScript, liveScriptParts }) {
|
|
|
605
880
|
const filePath = url.searchParams.get('path');
|
|
606
881
|
if (!filePath || filePath.includes('..')) { res.writeHead(400); res.end('Bad path'); return; }
|
|
607
882
|
const absPath = path.resolve(process.cwd(), filePath);
|
|
608
|
-
|
|
883
|
+
// Confine to the project root. A bare `startsWith(cwd)` string check lets a
|
|
884
|
+
// sibling dir whose name extends the root name (projeto -> projeto-backup)
|
|
885
|
+
// slip through; compare on the relative path instead (same pattern as
|
|
886
|
+
// sessionFileMetadataFromPollReply below). An empty rel means the request
|
|
887
|
+
// resolved to the root directory itself, which this file route never serves.
|
|
888
|
+
const rel = path.relative(process.cwd(), absPath);
|
|
889
|
+
if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) { res.writeHead(403); res.end('Forbidden'); return; }
|
|
609
890
|
let content;
|
|
610
891
|
try { content = fs.readFileSync(absPath, 'utf-8'); }
|
|
611
892
|
catch { res.writeHead(404); res.end('File not found'); return; }
|
|
@@ -689,6 +970,16 @@ function createRequestHandler({ detectScript, liveScriptParts }) {
|
|
|
689
970
|
res.end(JSON.stringify({ error }));
|
|
690
971
|
return;
|
|
691
972
|
}
|
|
973
|
+
if (msg.type === 'agent_phase') {
|
|
974
|
+
recordAgentPhase(msg.id, msg.phase, {
|
|
975
|
+
...(Number.isFinite(msg.durationMs) ? { durationMs: msg.durationMs } : {}),
|
|
976
|
+
owner: typeof msg.owner === 'string' ? msg.owner : undefined,
|
|
977
|
+
});
|
|
978
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
979
|
+
res.end(JSON.stringify({ ok: true }));
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
const missedCompletion = detectMissedGenerationCompletion(msg);
|
|
692
983
|
if (state.sessionStore && msg.id) {
|
|
693
984
|
try {
|
|
694
985
|
state.sessionStore.appendEvent(msg);
|
|
@@ -698,6 +989,11 @@ function createRequestHandler({ detectScript, liveScriptParts }) {
|
|
|
698
989
|
return;
|
|
699
990
|
}
|
|
700
991
|
}
|
|
992
|
+
if (msg.type === 'accept' || msg.type === 'discard') {
|
|
993
|
+
retirePendingGeneration(msg.id);
|
|
994
|
+
}
|
|
995
|
+
recordGenerationCheckpoint(msg);
|
|
996
|
+
if (missedCompletion) broadcast(missedCompletion);
|
|
701
997
|
if (msg.type === 'exit') {
|
|
702
998
|
cleanupSvelteComponentSessionsBeforeExit();
|
|
703
999
|
}
|
|
@@ -738,6 +1034,12 @@ function createRequestHandler({ detectScript, liveScriptParts }) {
|
|
|
738
1034
|
// Agent poll endpoints (unchanged from WS version)
|
|
739
1035
|
// ---------------------------------------------------------------------------
|
|
740
1036
|
|
|
1037
|
+
function parsePollTypes(value) {
|
|
1038
|
+
if (!value) return null;
|
|
1039
|
+
const types = String(value).split(',').map((type) => type.trim()).filter(Boolean);
|
|
1040
|
+
return types.length > 0 ? new Set(types) : null;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
741
1043
|
function handlePollGet(req, res, url) {
|
|
742
1044
|
const token = url.searchParams.get('token');
|
|
743
1045
|
if (token !== state.token) {
|
|
@@ -748,13 +1050,25 @@ function handlePollGet(req, res, url) {
|
|
|
748
1050
|
state.lastPollAt = Date.now();
|
|
749
1051
|
const timeout = parseInt(url.searchParams.get('timeout') || DEFAULT_POLL_TIMEOUT, 10);
|
|
750
1052
|
const leaseMs = parseInt(url.searchParams.get('leaseMs') || '30000', 10);
|
|
751
|
-
const
|
|
1053
|
+
const types = parsePollTypes(url.searchParams.get('types'));
|
|
1054
|
+
const available = findAvailablePendingEvent(Date.now(), types);
|
|
752
1055
|
if (available) {
|
|
753
|
-
|
|
754
|
-
|
|
1056
|
+
// Do not await inline: leaseEvent may scaffold source, and this handler runs
|
|
1057
|
+
// on the server's only thread. The client can disconnect during that window,
|
|
1058
|
+
// so check the socket before replying.
|
|
1059
|
+
leaseEvent(available, leaseMs).then((event) => {
|
|
1060
|
+
if (res.writableEnded || res.destroyed) return;
|
|
1061
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1062
|
+
res.end(JSON.stringify(event));
|
|
1063
|
+
}, (error) => {
|
|
1064
|
+
console.error('[live] lease failed for ' + (available.event?.id || 'unknown') + ': ' + (error?.message || error));
|
|
1065
|
+
if (res.writableEnded || res.destroyed) return;
|
|
1066
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1067
|
+
res.end(JSON.stringify({ type: 'timeout' }));
|
|
1068
|
+
});
|
|
755
1069
|
return;
|
|
756
1070
|
}
|
|
757
|
-
const poll = { resolve, leaseMs };
|
|
1071
|
+
const poll = { resolve, leaseMs, types };
|
|
758
1072
|
const timer = setTimeout(() => {
|
|
759
1073
|
const idx = state.pendingPolls.indexOf(poll);
|
|
760
1074
|
if (idx !== -1) state.pendingPolls.splice(idx, 1);
|
|
@@ -783,12 +1097,15 @@ function sessionFileMetadataFromPollReply(file) {
|
|
|
783
1097
|
if (!file || typeof file !== 'string') return { file };
|
|
784
1098
|
const normalized = file.split(path.sep).join('/');
|
|
785
1099
|
const base = { file: normalized };
|
|
786
|
-
|
|
787
|
-
if (!
|
|
1100
|
+
const metadataFile = normalized;
|
|
1101
|
+
if (!metadataFile.endsWith('/manifest.json') && metadataFile !== 'manifest.json') return base;
|
|
1102
|
+
if (!metadataFile.includes('node_modules/.impeccable-live/')
|
|
1103
|
+
&& !metadataFile.includes('src/lib/impeccable/')
|
|
1104
|
+
&& !metadataFile.includes('/.impeccable-live/')) return base;
|
|
788
1105
|
|
|
789
1106
|
let full;
|
|
790
1107
|
try {
|
|
791
|
-
full = path.resolve(process.cwd(),
|
|
1108
|
+
full = path.resolve(process.cwd(), metadataFile);
|
|
792
1109
|
const rel = path.relative(process.cwd(), full);
|
|
793
1110
|
if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) return base;
|
|
794
1111
|
} catch {
|
|
@@ -797,18 +1114,47 @@ function sessionFileMetadataFromPollReply(file) {
|
|
|
797
1114
|
|
|
798
1115
|
try {
|
|
799
1116
|
const manifest = JSON.parse(fs.readFileSync(full, 'utf-8'));
|
|
800
|
-
if (manifest?.previewMode !== 'svelte-component'
|
|
1117
|
+
if (manifest?.previewMode !== 'svelte-component'
|
|
1118
|
+
|| !manifest.sourceFile) return base;
|
|
801
1119
|
return {
|
|
802
1120
|
file: String(manifest.sourceFile).split(path.sep).join('/'),
|
|
803
1121
|
sourceFile: String(manifest.sourceFile).split(path.sep).join('/'),
|
|
804
1122
|
previewFile: normalized,
|
|
805
|
-
previewMode:
|
|
1123
|
+
previewMode: manifest.previewMode,
|
|
806
1124
|
};
|
|
807
1125
|
} catch {
|
|
808
1126
|
return base;
|
|
809
1127
|
}
|
|
810
1128
|
}
|
|
811
1129
|
|
|
1130
|
+
function inferSourceEventType(msg = {}, pendingEvents = state.pendingEvents) {
|
|
1131
|
+
const entriesForId = pendingEvents.filter((entry) => entry.event?.id === msg.id);
|
|
1132
|
+
const pendingTypes = new Set(entriesForId.map((entry) => entry.event?.type));
|
|
1133
|
+
if (msg.type === 'discarded' || msg.type === 'discard') return 'discard';
|
|
1134
|
+
if (msg.type === 'complete') {
|
|
1135
|
+
if (pendingTypes.has('carbonize_cleanup')) return 'carbonize_cleanup';
|
|
1136
|
+
return pendingTypes.has('accept') ? 'accept' : (pendingTypes.has('generate') ? 'generate' : undefined);
|
|
1137
|
+
}
|
|
1138
|
+
if (msg.type === 'steer_done') return 'steer';
|
|
1139
|
+
// `agent_done` can be the automatic acknowledgement for a carbonize Accept.
|
|
1140
|
+
// New pollers send sourceEventType explicitly; default to generate only for
|
|
1141
|
+
// older callers so a late worker cannot acknowledge a queued Accept.
|
|
1142
|
+
if (msg.type === 'agent_done' || msg.type === 'done') return 'generate';
|
|
1143
|
+
// `error` is reference/live.md's documented failure reply, and parseReplyArgs
|
|
1144
|
+
// never sets sourceEventType on it (the poller is a fresh process that cannot
|
|
1145
|
+
// know what it leased). Returning undefined here makes acknowledgePendingEvent
|
|
1146
|
+
// match *any* event for this id: a stale generate worker's failure silently
|
|
1147
|
+
// consumed the user's queued Accept, which was then never delivered to any
|
|
1148
|
+
// agent and left the browser in SAVING forever. Attribute the failure to the
|
|
1149
|
+
// event this agent actually holds a lease on, and otherwise to `generate` —
|
|
1150
|
+
// never to a wildcard. If that generate was already retired by an Accept, the
|
|
1151
|
+
// ack simply finds no match, which is the correct outcome for a stale reply.
|
|
1152
|
+
if (msg.type === 'error') {
|
|
1153
|
+
return entriesForId.find(isLeased)?.event?.type || 'generate';
|
|
1154
|
+
}
|
|
1155
|
+
return undefined;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
812
1158
|
function handlePollPost(req, res) {
|
|
813
1159
|
let body = '';
|
|
814
1160
|
req.on('data', (c) => { body += c; });
|
|
@@ -869,7 +1215,23 @@ function handlePollPost(req, res) {
|
|
|
869
1215
|
res.end(JSON.stringify({ error: 'stale_manual_edit_apply_reply', ...rollback }));
|
|
870
1216
|
return;
|
|
871
1217
|
}
|
|
872
|
-
const
|
|
1218
|
+
const sourceEventType = msg.sourceEventType || inferSourceEventType(msg);
|
|
1219
|
+
if (msg.type === 'retry') {
|
|
1220
|
+
const releasedEvent = releasePendingEvent(msg.id, sourceEventType);
|
|
1221
|
+
if (!releasedEvent) {
|
|
1222
|
+
res.writeHead(msg.id ? 404 : 400, { 'Content-Type': 'application/json' });
|
|
1223
|
+
res.end(JSON.stringify({
|
|
1224
|
+
error: msg.id ? 'unknown_poll_retry_id' : 'missing_poll_retry_id',
|
|
1225
|
+
id: msg.id,
|
|
1226
|
+
}));
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
flushPendingPolls();
|
|
1230
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1231
|
+
res.end(JSON.stringify({ ok: true, released: true }));
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
const pendingEventBeforeAck = findPendingEventById(msg.id, sourceEventType);
|
|
873
1235
|
if (pendingEventBeforeAck?.type === 'steer' && msg.type === 'steer_done'
|
|
874
1236
|
&& !msg.file && !(typeof msg.message === 'string' && msg.message.trim())) {
|
|
875
1237
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
@@ -879,7 +1241,7 @@ function handlePollPost(req, res) {
|
|
|
879
1241
|
}));
|
|
880
1242
|
return;
|
|
881
1243
|
}
|
|
882
|
-
const acknowledgedEvent = acknowledgePendingEvent(msg.id);
|
|
1244
|
+
const acknowledgedEvent = acknowledgePendingEvent(msg.id, sourceEventType);
|
|
883
1245
|
let skipJournalReply = false;
|
|
884
1246
|
let existingSession = null;
|
|
885
1247
|
if (!acknowledgedEvent && state.sessionStore && msg.id) {
|
|
@@ -1083,7 +1445,10 @@ if (args.includes('--background')) {
|
|
|
1083
1445
|
process.exit(0);
|
|
1084
1446
|
}
|
|
1085
1447
|
} catch { /* not ready yet */ }
|
|
1086
|
-
|
|
1448
|
+
// The detached child is typically listening in 35-45ms. A 200ms polling
|
|
1449
|
+
// floor dominated configured cold Live startup; poll cheaply and return
|
|
1450
|
+
// as soon as the child has written its ready record.
|
|
1451
|
+
await new Promise(r => setTimeout(r, 5));
|
|
1087
1452
|
}
|
|
1088
1453
|
console.error('Timed out waiting for live server to start.');
|
|
1089
1454
|
process.exit(1);
|
|
@@ -37,15 +37,19 @@ export async function statusCli() {
|
|
|
37
37
|
pendingEvents: server.pendingEvents,
|
|
38
38
|
} : null,
|
|
39
39
|
activeSessions: server?.activeSessions || activeSessions,
|
|
40
|
-
recoveryHint: manualApply
|
|
41
|
-
? manualApplyResumeHint(manualApply)
|
|
42
|
-
: server
|
|
43
|
-
? 'Run live-poll.mjs to continue pending work, or live-complete.mjs --id <session> after manual cleanup.'
|
|
44
|
-
: 'Start live-server.mjs to requeue pending durable events, then run live-poll.mjs.',
|
|
40
|
+
recoveryHint: recoveryHint({ server, manualApply }),
|
|
45
41
|
};
|
|
46
42
|
console.log(JSON.stringify(payload, null, 2));
|
|
47
43
|
}
|
|
48
44
|
|
|
45
|
+
function recoveryHint({ server, manualApply }) {
|
|
46
|
+
if (manualApply) return manualApplyResumeHint(manualApply);
|
|
47
|
+
if (server) {
|
|
48
|
+
return 'Run live-poll.mjs to continue pending work, or live-complete.mjs --id <session> after manual cleanup.';
|
|
49
|
+
}
|
|
50
|
+
return 'Start live-server.mjs to requeue pending durable events, then run live-poll.mjs.';
|
|
51
|
+
}
|
|
52
|
+
|
|
49
53
|
function findPendingManualApply(server, activeSessions) {
|
|
50
54
|
const fromServer = server?.pendingEvents?.find((event) => event?.type === 'manual_edit_apply');
|
|
51
55
|
if (fromServer) return fromServer;
|