@mindstudio-ai/remy 0.1.303 → 0.1.304
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/dist/headless.d.ts +41 -7
- package/dist/headless.js +144 -52
- package/dist/index.js +144 -52
- package/dist/prompt/compiled/interfaces.md +23 -2
- package/dist/prompt/skills/publishing.md +1 -0
- package/dist/subagents/codeSanityCheck/prompt.md +1 -1
- package/package.json +1 -1
package/dist/headless.d.ts
CHANGED
|
@@ -63,6 +63,20 @@ declare class HeadlessSession {
|
|
|
63
63
|
* right state (the triggering turn's state, not a stale one).
|
|
64
64
|
*/
|
|
65
65
|
private currentOnboardingState;
|
|
66
|
+
/**
|
|
67
|
+
* The in-flight turn's command, when that turn is part of a build pipeline
|
|
68
|
+
* (its action declares a `next`, or it is itself a chain step). Captured so a
|
|
69
|
+
* cancel can put the interrupted step back on the queue held, instead of
|
|
70
|
+
* leaving the pipeline to resume at the step *after* the one that was
|
|
71
|
+
* stopped — which would run finalize/polish over half-built code.
|
|
72
|
+
*
|
|
73
|
+
* Null for every other turn shape, which is what scopes the whole
|
|
74
|
+
* paused-pipeline affordance to chains: stopping a one-off automated action
|
|
75
|
+
* (approvePlan, publish, sync) leaves nothing behind.
|
|
76
|
+
*/
|
|
77
|
+
private currentChainStep;
|
|
78
|
+
/** Monotonic suffix for synthesized chain requestIds — see nextChainRequestId. */
|
|
79
|
+
private chainRequestSeq;
|
|
66
80
|
/**
|
|
67
81
|
* Unified message queue. Holds pending work to deliver after the current
|
|
68
82
|
* turn completes: chained automated actions, background sub-agent results,
|
|
@@ -134,6 +148,14 @@ declare class HeadlessSession {
|
|
|
134
148
|
* filename de-dup set is per-call, so parallel calls race on names.
|
|
135
149
|
*/
|
|
136
150
|
private persistEntryAttachments;
|
|
151
|
+
/**
|
|
152
|
+
* Stable id for a queued chain step, so the frontend can address one (to
|
|
153
|
+
* discard a paused pipeline) rather than waiting for drainQueueLoop to
|
|
154
|
+
* synthesize an id at dequeue. Deliberately not an `ac-` id — the frontend
|
|
155
|
+
* keys "system turn" off that prefix — and deliberately not a bare
|
|
156
|
+
* Date.now(), which collides for steps enqueued in the same millisecond.
|
|
157
|
+
*/
|
|
158
|
+
private nextChainRequestId;
|
|
137
159
|
/**
|
|
138
160
|
* Run one turn for a single command (without acquiring the `running` lock).
|
|
139
161
|
* Owns the per-command machinery: @@automated:: action resolution, plan-file
|
|
@@ -213,9 +235,9 @@ declare class HeadlessSession {
|
|
|
213
235
|
/**
|
|
214
236
|
* Stop everything the user can see running: the turn, an in-flight
|
|
215
237
|
* compaction, and any external tool waiting on a result. Flushes the
|
|
216
|
-
* follow-ups that belonged to the turn
|
|
217
|
-
* `source: 'user'` items
|
|
218
|
-
*
|
|
238
|
+
* `background` follow-ups that belonged to the turn, and HOLDS everything
|
|
239
|
+
* still queued — the `source: 'user'` items, which are independent user
|
|
240
|
+
* intent, plus the `source: 'chain'` steps of a build pipeline.
|
|
219
241
|
*
|
|
220
242
|
* Holding is the difference between Stop working and Stop looking broken.
|
|
221
243
|
* These items used to drain immediately: `executeTurn` swallows the abort,
|
|
@@ -224,6 +246,13 @@ declare class HeadlessSession {
|
|
|
224
246
|
* and every additional press hit a turn that had just started. They now wait
|
|
225
247
|
* in the queue card until the user sends again or promotes one.
|
|
226
248
|
*
|
|
249
|
+
* The chain steps used to be flushed with the background items, which threw
|
|
250
|
+
* away the rest of the build — including the step that finalizes it and
|
|
251
|
+
* unlocks the editor. They are now paused instead, and the interrupted step
|
|
252
|
+
* goes back at the head, so a Stop is a pause and the next send resumes the
|
|
253
|
+
* pipeline where it stopped. Holding only the remainder would resume one step
|
|
254
|
+
* PAST the interruption, polishing and finalizing half-built code.
|
|
255
|
+
*
|
|
227
256
|
* A compaction is cancelled here too, unconditionally. It gates every queued
|
|
228
257
|
* message and outlives the turn that started it, so leaving it running means
|
|
229
258
|
* Stop can't reach idle. The cost is the summary work in flight; the forced
|
|
@@ -236,10 +265,15 @@ declare class HeadlessSession {
|
|
|
236
265
|
*/
|
|
237
266
|
private handleCancel;
|
|
238
267
|
/**
|
|
239
|
-
* Remove pending queued messages
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
268
|
+
* Remove pending queued messages: all user messages (no id), or a single item
|
|
269
|
+
* by id. Does not affect the in-flight turn (use `cancel` for that).
|
|
270
|
+
*
|
|
271
|
+
* Held chain items — a paused pipeline — are removable only by explicit id.
|
|
272
|
+
* The id-less form is the queue card's "Clear" and the pre-destroy quiesce,
|
|
273
|
+
* neither of which should get to decide the pipeline's fate. A DELIVERABLE
|
|
274
|
+
* chain item is never removable: that's live pipeline work. (The step
|
|
275
|
+
* actually running isn't in the queue at all — drainQueueLoop takes it out
|
|
276
|
+
* before running it.)
|
|
243
277
|
*/
|
|
244
278
|
private handleCancelQueued;
|
|
245
279
|
private handleStdinLine;
|
package/dist/headless.js
CHANGED
|
@@ -9349,6 +9349,15 @@ var MessageQueue = class {
|
|
|
9349
9349
|
this.items.push(item);
|
|
9350
9350
|
this.onChange?.();
|
|
9351
9351
|
}
|
|
9352
|
+
/**
|
|
9353
|
+
* Add an item at the head. Used to put an interrupted chain step back in
|
|
9354
|
+
* front of the rest of its pipeline on cancel; `promoteToFront` can't serve
|
|
9355
|
+
* that case, since it addresses an item already in the queue by requestId.
|
|
9356
|
+
*/
|
|
9357
|
+
unshift(item) {
|
|
9358
|
+
this.items.unshift(item);
|
|
9359
|
+
this.onChange?.();
|
|
9360
|
+
}
|
|
9352
9361
|
/**
|
|
9353
9362
|
* Index of the first deliverable item, or -1 when there is none.
|
|
9354
9363
|
*
|
|
@@ -9417,25 +9426,6 @@ var MessageQueue = class {
|
|
|
9417
9426
|
}
|
|
9418
9427
|
return held;
|
|
9419
9428
|
}
|
|
9420
|
-
/**
|
|
9421
|
-
* Release held items so the normal drain picks them up again — all of them,
|
|
9422
|
-
* or one by command requestId. Fires onChange only if something changed.
|
|
9423
|
-
* Returns the released items.
|
|
9424
|
-
*/
|
|
9425
|
-
releaseHeld(id) {
|
|
9426
|
-
const released = [];
|
|
9427
|
-
for (const item of this.items) {
|
|
9428
|
-
if (!item.held || id !== void 0 && item.command.requestId !== id) {
|
|
9429
|
-
continue;
|
|
9430
|
-
}
|
|
9431
|
-
delete item.held;
|
|
9432
|
-
released.push(item);
|
|
9433
|
-
}
|
|
9434
|
-
if (released.length > 0) {
|
|
9435
|
-
this.onChange?.();
|
|
9436
|
-
}
|
|
9437
|
-
return released;
|
|
9438
|
-
}
|
|
9439
9429
|
/** Whether anything in the queue will drain on its own (i.e. isn't held). */
|
|
9440
9430
|
hasDeliverable() {
|
|
9441
9431
|
return this.items.some((item) => !item.held);
|
|
@@ -9480,6 +9470,38 @@ var MessageQueue = class {
|
|
|
9480
9470
|
this.onChange?.();
|
|
9481
9471
|
return item;
|
|
9482
9472
|
}
|
|
9473
|
+
/**
|
|
9474
|
+
* Fold-in release: un-hold everything, then move items matching `defer` to
|
|
9475
|
+
* the tail, relative order preserved. One onChange for the whole
|
|
9476
|
+
* rearrangement, so no observer ever sees it half-applied.
|
|
9477
|
+
*
|
|
9478
|
+
* Callers push the new user message FIRST and call this second — "behind" is
|
|
9479
|
+
* defined by the array at the moment this runs. Held user messages stay put
|
|
9480
|
+
* so they still merge with that message into one mailbox batch; a paused
|
|
9481
|
+
* pipeline goes to the back, so the person's words run before the build
|
|
9482
|
+
* continues. `defer` is applied to every item, not just the newly-released
|
|
9483
|
+
* ones, so a chain step that was already deliverable defers too.
|
|
9484
|
+
*/
|
|
9485
|
+
releaseHeldDeferring(defer) {
|
|
9486
|
+
const released = [];
|
|
9487
|
+
for (const item of this.items) {
|
|
9488
|
+
if (!item.held) {
|
|
9489
|
+
continue;
|
|
9490
|
+
}
|
|
9491
|
+
delete item.held;
|
|
9492
|
+
released.push(item);
|
|
9493
|
+
}
|
|
9494
|
+
const back = this.items.filter(defer);
|
|
9495
|
+
const front = this.items.filter((item) => !defer(item));
|
|
9496
|
+
const reordered = back.length > 0 && front.length > 0;
|
|
9497
|
+
if (reordered) {
|
|
9498
|
+
this.items = [...front, ...back];
|
|
9499
|
+
}
|
|
9500
|
+
if (released.length > 0 || reordered) {
|
|
9501
|
+
this.onChange?.();
|
|
9502
|
+
}
|
|
9503
|
+
return released;
|
|
9504
|
+
}
|
|
9483
9505
|
/** Copy of current queue contents (for surfacing on events). */
|
|
9484
9506
|
snapshot() {
|
|
9485
9507
|
return [...this.items];
|
|
@@ -9529,6 +9551,20 @@ var HeadlessSession = class {
|
|
|
9529
9551
|
* right state (the triggering turn's state, not a stale one).
|
|
9530
9552
|
*/
|
|
9531
9553
|
currentOnboardingState;
|
|
9554
|
+
/**
|
|
9555
|
+
* The in-flight turn's command, when that turn is part of a build pipeline
|
|
9556
|
+
* (its action declares a `next`, or it is itself a chain step). Captured so a
|
|
9557
|
+
* cancel can put the interrupted step back on the queue held, instead of
|
|
9558
|
+
* leaving the pipeline to resume at the step *after* the one that was
|
|
9559
|
+
* stopped — which would run finalize/polish over half-built code.
|
|
9560
|
+
*
|
|
9561
|
+
* Null for every other turn shape, which is what scopes the whole
|
|
9562
|
+
* paused-pipeline affordance to chains: stopping a one-off automated action
|
|
9563
|
+
* (approvePlan, publish, sync) leaves nothing behind.
|
|
9564
|
+
*/
|
|
9565
|
+
currentChainStep = null;
|
|
9566
|
+
/** Monotonic suffix for synthesized chain requestIds — see nextChainRequestId. */
|
|
9567
|
+
chainRequestSeq = 0;
|
|
9532
9568
|
/**
|
|
9533
9569
|
* Unified message queue. Holds pending work to deliver after the current
|
|
9534
9570
|
* turn completes: chained automated actions, background sub-agent results,
|
|
@@ -10094,6 +10130,16 @@ var HeadlessSession = class {
|
|
|
10094
10130
|
return void 0;
|
|
10095
10131
|
}
|
|
10096
10132
|
}
|
|
10133
|
+
/**
|
|
10134
|
+
* Stable id for a queued chain step, so the frontend can address one (to
|
|
10135
|
+
* discard a paused pipeline) rather than waiting for drainQueueLoop to
|
|
10136
|
+
* synthesize an id at dequeue. Deliberately not an `ac-` id — the frontend
|
|
10137
|
+
* keys "system turn" off that prefix — and deliberately not a bare
|
|
10138
|
+
* Date.now(), which collides for steps enqueued in the same millisecond.
|
|
10139
|
+
*/
|
|
10140
|
+
nextChainRequestId() {
|
|
10141
|
+
return `chain-${Date.now()}-${++this.chainRequestSeq}`;
|
|
10142
|
+
}
|
|
10097
10143
|
/**
|
|
10098
10144
|
* Run one turn for a single command (without acquiring the `running` lock).
|
|
10099
10145
|
* Owns the per-command machinery: @@automated:: action resolution, plan-file
|
|
@@ -10131,36 +10177,42 @@ var HeadlessSession = class {
|
|
|
10131
10177
|
const onboardingState = parsed.onboardingState ?? "onboardingFinished";
|
|
10132
10178
|
this.currentOnboardingState = onboardingState;
|
|
10133
10179
|
const system = buildSystemPrompt(onboardingState);
|
|
10180
|
+
this.currentChainStep = resolved && (resolved.next != null || fromChain) ? { text: rawText, onboardingState } : null;
|
|
10134
10181
|
if (resolved?.next && !fromChain) {
|
|
10135
10182
|
for (const step of getActionChain(resolved.next)) {
|
|
10136
10183
|
this.queue.push({
|
|
10137
10184
|
command: {
|
|
10138
10185
|
action: "message",
|
|
10139
10186
|
text: sentinel(step),
|
|
10140
|
-
onboardingState
|
|
10187
|
+
onboardingState,
|
|
10188
|
+
requestId: this.nextChainRequestId()
|
|
10141
10189
|
},
|
|
10142
10190
|
source: "chain",
|
|
10143
10191
|
enqueuedAt: Date.now()
|
|
10144
10192
|
});
|
|
10145
10193
|
}
|
|
10146
10194
|
}
|
|
10147
|
-
|
|
10148
|
-
|
|
10149
|
-
|
|
10150
|
-
|
|
10151
|
-
|
|
10152
|
-
|
|
10153
|
-
|
|
10154
|
-
|
|
10155
|
-
|
|
10156
|
-
|
|
10157
|
-
|
|
10158
|
-
|
|
10159
|
-
|
|
10160
|
-
|
|
10161
|
-
|
|
10162
|
-
|
|
10163
|
-
|
|
10195
|
+
try {
|
|
10196
|
+
await this.executeTurn({
|
|
10197
|
+
entries: [
|
|
10198
|
+
{
|
|
10199
|
+
text: userMessage,
|
|
10200
|
+
attachments,
|
|
10201
|
+
attachmentHeader,
|
|
10202
|
+
hidden: isHidden || void 0,
|
|
10203
|
+
requestId,
|
|
10204
|
+
queued: queued || void 0
|
|
10205
|
+
}
|
|
10206
|
+
],
|
|
10207
|
+
requestId,
|
|
10208
|
+
absorbedRids: [],
|
|
10209
|
+
onboardingState,
|
|
10210
|
+
system,
|
|
10211
|
+
buildModel
|
|
10212
|
+
});
|
|
10213
|
+
} finally {
|
|
10214
|
+
this.currentChainStep = null;
|
|
10215
|
+
}
|
|
10164
10216
|
}
|
|
10165
10217
|
/**
|
|
10166
10218
|
* Run a mailbox batch — contiguous queued user + background items — as one
|
|
@@ -10173,6 +10225,7 @@ var HeadlessSession = class {
|
|
|
10173
10225
|
* with no side effects).
|
|
10174
10226
|
*/
|
|
10175
10227
|
async runMergedTurn(batch) {
|
|
10228
|
+
this.currentChainStep = null;
|
|
10176
10229
|
const primaryRid = batch[0].command.requestId ?? (batch.every((b) => b.source === "background") ? `background-${Date.now()}` : `merged-${Date.now()}`);
|
|
10177
10230
|
const absorbedRids = batch.slice(1).map((b) => b.command.requestId).filter((rid) => typeof rid === "string");
|
|
10178
10231
|
const entryList = [];
|
|
@@ -10327,9 +10380,6 @@ var HeadlessSession = class {
|
|
|
10327
10380
|
}
|
|
10328
10381
|
async handleMessage(parsed, requestId) {
|
|
10329
10382
|
const foldIn = !this.running && !getInflightCompaction() && this.queue.length > 0 && !isAutomatedMessage(parsed.text ?? "");
|
|
10330
|
-
if (foldIn) {
|
|
10331
|
-
this.queue.releaseHeld();
|
|
10332
|
-
}
|
|
10333
10383
|
if (this.running || getInflightCompaction() || foldIn) {
|
|
10334
10384
|
const command = { ...parsed };
|
|
10335
10385
|
if (requestId && command.requestId === void 0) {
|
|
@@ -10340,6 +10390,9 @@ var HeadlessSession = class {
|
|
|
10340
10390
|
source: "user",
|
|
10341
10391
|
enqueuedAt: Date.now()
|
|
10342
10392
|
});
|
|
10393
|
+
if (foldIn) {
|
|
10394
|
+
this.queue.releaseHeldDeferring((item) => item.source === "chain");
|
|
10395
|
+
}
|
|
10343
10396
|
if (!this.running && !getInflightCompaction()) {
|
|
10344
10397
|
this.kickDrain();
|
|
10345
10398
|
}
|
|
@@ -10492,9 +10545,9 @@ var HeadlessSession = class {
|
|
|
10492
10545
|
/**
|
|
10493
10546
|
* Stop everything the user can see running: the turn, an in-flight
|
|
10494
10547
|
* compaction, and any external tool waiting on a result. Flushes the
|
|
10495
|
-
* follow-ups that belonged to the turn
|
|
10496
|
-
* `source: 'user'` items
|
|
10497
|
-
*
|
|
10548
|
+
* `background` follow-ups that belonged to the turn, and HOLDS everything
|
|
10549
|
+
* still queued — the `source: 'user'` items, which are independent user
|
|
10550
|
+
* intent, plus the `source: 'chain'` steps of a build pipeline.
|
|
10498
10551
|
*
|
|
10499
10552
|
* Holding is the difference between Stop working and Stop looking broken.
|
|
10500
10553
|
* These items used to drain immediately: `executeTurn` swallows the abort,
|
|
@@ -10503,6 +10556,13 @@ var HeadlessSession = class {
|
|
|
10503
10556
|
* and every additional press hit a turn that had just started. They now wait
|
|
10504
10557
|
* in the queue card until the user sends again or promotes one.
|
|
10505
10558
|
*
|
|
10559
|
+
* The chain steps used to be flushed with the background items, which threw
|
|
10560
|
+
* away the rest of the build — including the step that finalizes it and
|
|
10561
|
+
* unlocks the editor. They are now paused instead, and the interrupted step
|
|
10562
|
+
* goes back at the head, so a Stop is a pause and the next send resumes the
|
|
10563
|
+
* pipeline where it stopped. Holding only the remainder would resume one step
|
|
10564
|
+
* PAST the interruption, polishing and finalizing half-built code.
|
|
10565
|
+
*
|
|
10506
10566
|
* A compaction is cancelled here too, unconditionally. It gates every queued
|
|
10507
10567
|
* message and outlives the turn that started it, so leaving it running means
|
|
10508
10568
|
* Stop can't reach idle. The cost is the summary work in flight; the forced
|
|
@@ -10523,19 +10583,50 @@ var HeadlessSession = class {
|
|
|
10523
10583
|
pending2.resolve(USER_CANCELLED_RESULT);
|
|
10524
10584
|
this.pendingTools.delete(id);
|
|
10525
10585
|
}
|
|
10526
|
-
const flushed = this.queue.removeWhere(
|
|
10527
|
-
|
|
10528
|
-
|
|
10586
|
+
const flushed = this.queue.removeWhere(
|
|
10587
|
+
(item) => item.source === "background"
|
|
10588
|
+
);
|
|
10589
|
+
const step = this.currentChainStep;
|
|
10590
|
+
if (step) {
|
|
10591
|
+
this.queue.unshift({
|
|
10592
|
+
command: {
|
|
10593
|
+
action: "message",
|
|
10594
|
+
text: step.text,
|
|
10595
|
+
onboardingState: step.onboardingState,
|
|
10596
|
+
// Fresh id: the original command's terminal has already gone out as
|
|
10597
|
+
// cancelled, and one command gets exactly one `completed`.
|
|
10598
|
+
requestId: this.nextChainRequestId()
|
|
10599
|
+
},
|
|
10600
|
+
source: "chain",
|
|
10601
|
+
enqueuedAt: Date.now(),
|
|
10602
|
+
held: true
|
|
10603
|
+
});
|
|
10604
|
+
this.currentChainStep = null;
|
|
10605
|
+
}
|
|
10606
|
+
const held = this.queue.holdWhere(
|
|
10607
|
+
(item) => item.source === "user" || item.source === "chain"
|
|
10608
|
+
);
|
|
10609
|
+
return {
|
|
10610
|
+
flushed,
|
|
10611
|
+
held,
|
|
10612
|
+
pausedPipeline: held.some((item) => item.source === "chain"),
|
|
10613
|
+
cancelledCompaction
|
|
10614
|
+
};
|
|
10529
10615
|
}
|
|
10530
10616
|
/**
|
|
10531
|
-
* Remove pending queued messages
|
|
10532
|
-
*
|
|
10533
|
-
*
|
|
10534
|
-
*
|
|
10617
|
+
* Remove pending queued messages: all user messages (no id), or a single item
|
|
10618
|
+
* by id. Does not affect the in-flight turn (use `cancel` for that).
|
|
10619
|
+
*
|
|
10620
|
+
* Held chain items — a paused pipeline — are removable only by explicit id.
|
|
10621
|
+
* The id-less form is the queue card's "Clear" and the pre-destroy quiesce,
|
|
10622
|
+
* neither of which should get to decide the pipeline's fate. A DELIVERABLE
|
|
10623
|
+
* chain item is never removable: that's live pipeline work. (The step
|
|
10624
|
+
* actually running isn't in the queue at all — drainQueueLoop takes it out
|
|
10625
|
+
* before running it.)
|
|
10535
10626
|
*/
|
|
10536
10627
|
handleCancelQueued(id) {
|
|
10537
10628
|
return this.queue.removeWhere(
|
|
10538
|
-
(item) => item.source === "user" && (
|
|
10629
|
+
(item) => id === void 0 ? item.source === "user" : item.command.requestId === id && (item.source === "user" || item.source === "chain" && !!item.held)
|
|
10539
10630
|
);
|
|
10540
10631
|
}
|
|
10541
10632
|
//////////////////////////////////////////////////////////////////////////////
|
|
@@ -10634,13 +10725,14 @@ var HeadlessSession = class {
|
|
|
10634
10725
|
return;
|
|
10635
10726
|
}
|
|
10636
10727
|
if (action === "cancel") {
|
|
10637
|
-
const { flushed, held, cancelledCompaction } = this.handleCancel();
|
|
10728
|
+
const { flushed, held, pausedPipeline, cancelledCompaction } = this.handleCancel();
|
|
10638
10729
|
this.emit(
|
|
10639
10730
|
"completed",
|
|
10640
10731
|
{
|
|
10641
10732
|
success: true,
|
|
10642
10733
|
...flushed.length > 0 && { cancelledMessages: flushed },
|
|
10643
10734
|
...held.length > 0 && { heldMessages: held },
|
|
10735
|
+
...pausedPipeline && { pausedPipeline: true },
|
|
10644
10736
|
...cancelledCompaction && { cancelledCompaction: true }
|
|
10645
10737
|
},
|
|
10646
10738
|
requestId
|
package/dist/index.js
CHANGED
|
@@ -10311,6 +10311,15 @@ var init_messageQueue = __esm({
|
|
|
10311
10311
|
this.items.push(item);
|
|
10312
10312
|
this.onChange?.();
|
|
10313
10313
|
}
|
|
10314
|
+
/**
|
|
10315
|
+
* Add an item at the head. Used to put an interrupted chain step back in
|
|
10316
|
+
* front of the rest of its pipeline on cancel; `promoteToFront` can't serve
|
|
10317
|
+
* that case, since it addresses an item already in the queue by requestId.
|
|
10318
|
+
*/
|
|
10319
|
+
unshift(item) {
|
|
10320
|
+
this.items.unshift(item);
|
|
10321
|
+
this.onChange?.();
|
|
10322
|
+
}
|
|
10314
10323
|
/**
|
|
10315
10324
|
* Index of the first deliverable item, or -1 when there is none.
|
|
10316
10325
|
*
|
|
@@ -10379,25 +10388,6 @@ var init_messageQueue = __esm({
|
|
|
10379
10388
|
}
|
|
10380
10389
|
return held;
|
|
10381
10390
|
}
|
|
10382
|
-
/**
|
|
10383
|
-
* Release held items so the normal drain picks them up again — all of them,
|
|
10384
|
-
* or one by command requestId. Fires onChange only if something changed.
|
|
10385
|
-
* Returns the released items.
|
|
10386
|
-
*/
|
|
10387
|
-
releaseHeld(id) {
|
|
10388
|
-
const released = [];
|
|
10389
|
-
for (const item of this.items) {
|
|
10390
|
-
if (!item.held || id !== void 0 && item.command.requestId !== id) {
|
|
10391
|
-
continue;
|
|
10392
|
-
}
|
|
10393
|
-
delete item.held;
|
|
10394
|
-
released.push(item);
|
|
10395
|
-
}
|
|
10396
|
-
if (released.length > 0) {
|
|
10397
|
-
this.onChange?.();
|
|
10398
|
-
}
|
|
10399
|
-
return released;
|
|
10400
|
-
}
|
|
10401
10391
|
/** Whether anything in the queue will drain on its own (i.e. isn't held). */
|
|
10402
10392
|
hasDeliverable() {
|
|
10403
10393
|
return this.items.some((item) => !item.held);
|
|
@@ -10442,6 +10432,38 @@ var init_messageQueue = __esm({
|
|
|
10442
10432
|
this.onChange?.();
|
|
10443
10433
|
return item;
|
|
10444
10434
|
}
|
|
10435
|
+
/**
|
|
10436
|
+
* Fold-in release: un-hold everything, then move items matching `defer` to
|
|
10437
|
+
* the tail, relative order preserved. One onChange for the whole
|
|
10438
|
+
* rearrangement, so no observer ever sees it half-applied.
|
|
10439
|
+
*
|
|
10440
|
+
* Callers push the new user message FIRST and call this second — "behind" is
|
|
10441
|
+
* defined by the array at the moment this runs. Held user messages stay put
|
|
10442
|
+
* so they still merge with that message into one mailbox batch; a paused
|
|
10443
|
+
* pipeline goes to the back, so the person's words run before the build
|
|
10444
|
+
* continues. `defer` is applied to every item, not just the newly-released
|
|
10445
|
+
* ones, so a chain step that was already deliverable defers too.
|
|
10446
|
+
*/
|
|
10447
|
+
releaseHeldDeferring(defer) {
|
|
10448
|
+
const released = [];
|
|
10449
|
+
for (const item of this.items) {
|
|
10450
|
+
if (!item.held) {
|
|
10451
|
+
continue;
|
|
10452
|
+
}
|
|
10453
|
+
delete item.held;
|
|
10454
|
+
released.push(item);
|
|
10455
|
+
}
|
|
10456
|
+
const back = this.items.filter(defer);
|
|
10457
|
+
const front = this.items.filter((item) => !defer(item));
|
|
10458
|
+
const reordered = back.length > 0 && front.length > 0;
|
|
10459
|
+
if (reordered) {
|
|
10460
|
+
this.items = [...front, ...back];
|
|
10461
|
+
}
|
|
10462
|
+
if (released.length > 0 || reordered) {
|
|
10463
|
+
this.onChange?.();
|
|
10464
|
+
}
|
|
10465
|
+
return released;
|
|
10466
|
+
}
|
|
10445
10467
|
/** Copy of current queue contents (for surfacing on events). */
|
|
10446
10468
|
snapshot() {
|
|
10447
10469
|
return [...this.items];
|
|
@@ -10519,6 +10541,20 @@ var init_headless = __esm({
|
|
|
10519
10541
|
* right state (the triggering turn's state, not a stale one).
|
|
10520
10542
|
*/
|
|
10521
10543
|
currentOnboardingState;
|
|
10544
|
+
/**
|
|
10545
|
+
* The in-flight turn's command, when that turn is part of a build pipeline
|
|
10546
|
+
* (its action declares a `next`, or it is itself a chain step). Captured so a
|
|
10547
|
+
* cancel can put the interrupted step back on the queue held, instead of
|
|
10548
|
+
* leaving the pipeline to resume at the step *after* the one that was
|
|
10549
|
+
* stopped — which would run finalize/polish over half-built code.
|
|
10550
|
+
*
|
|
10551
|
+
* Null for every other turn shape, which is what scopes the whole
|
|
10552
|
+
* paused-pipeline affordance to chains: stopping a one-off automated action
|
|
10553
|
+
* (approvePlan, publish, sync) leaves nothing behind.
|
|
10554
|
+
*/
|
|
10555
|
+
currentChainStep = null;
|
|
10556
|
+
/** Monotonic suffix for synthesized chain requestIds — see nextChainRequestId. */
|
|
10557
|
+
chainRequestSeq = 0;
|
|
10522
10558
|
/**
|
|
10523
10559
|
* Unified message queue. Holds pending work to deliver after the current
|
|
10524
10560
|
* turn completes: chained automated actions, background sub-agent results,
|
|
@@ -11084,6 +11120,16 @@ var init_headless = __esm({
|
|
|
11084
11120
|
return void 0;
|
|
11085
11121
|
}
|
|
11086
11122
|
}
|
|
11123
|
+
/**
|
|
11124
|
+
* Stable id for a queued chain step, so the frontend can address one (to
|
|
11125
|
+
* discard a paused pipeline) rather than waiting for drainQueueLoop to
|
|
11126
|
+
* synthesize an id at dequeue. Deliberately not an `ac-` id — the frontend
|
|
11127
|
+
* keys "system turn" off that prefix — and deliberately not a bare
|
|
11128
|
+
* Date.now(), which collides for steps enqueued in the same millisecond.
|
|
11129
|
+
*/
|
|
11130
|
+
nextChainRequestId() {
|
|
11131
|
+
return `chain-${Date.now()}-${++this.chainRequestSeq}`;
|
|
11132
|
+
}
|
|
11087
11133
|
/**
|
|
11088
11134
|
* Run one turn for a single command (without acquiring the `running` lock).
|
|
11089
11135
|
* Owns the per-command machinery: @@automated:: action resolution, plan-file
|
|
@@ -11121,36 +11167,42 @@ var init_headless = __esm({
|
|
|
11121
11167
|
const onboardingState = parsed.onboardingState ?? "onboardingFinished";
|
|
11122
11168
|
this.currentOnboardingState = onboardingState;
|
|
11123
11169
|
const system = buildSystemPrompt(onboardingState);
|
|
11170
|
+
this.currentChainStep = resolved && (resolved.next != null || fromChain) ? { text: rawText, onboardingState } : null;
|
|
11124
11171
|
if (resolved?.next && !fromChain) {
|
|
11125
11172
|
for (const step of getActionChain(resolved.next)) {
|
|
11126
11173
|
this.queue.push({
|
|
11127
11174
|
command: {
|
|
11128
11175
|
action: "message",
|
|
11129
11176
|
text: sentinel(step),
|
|
11130
|
-
onboardingState
|
|
11177
|
+
onboardingState,
|
|
11178
|
+
requestId: this.nextChainRequestId()
|
|
11131
11179
|
},
|
|
11132
11180
|
source: "chain",
|
|
11133
11181
|
enqueuedAt: Date.now()
|
|
11134
11182
|
});
|
|
11135
11183
|
}
|
|
11136
11184
|
}
|
|
11137
|
-
|
|
11138
|
-
|
|
11139
|
-
|
|
11140
|
-
|
|
11141
|
-
|
|
11142
|
-
|
|
11143
|
-
|
|
11144
|
-
|
|
11145
|
-
|
|
11146
|
-
|
|
11147
|
-
|
|
11148
|
-
|
|
11149
|
-
|
|
11150
|
-
|
|
11151
|
-
|
|
11152
|
-
|
|
11153
|
-
|
|
11185
|
+
try {
|
|
11186
|
+
await this.executeTurn({
|
|
11187
|
+
entries: [
|
|
11188
|
+
{
|
|
11189
|
+
text: userMessage,
|
|
11190
|
+
attachments,
|
|
11191
|
+
attachmentHeader,
|
|
11192
|
+
hidden: isHidden || void 0,
|
|
11193
|
+
requestId,
|
|
11194
|
+
queued: queued || void 0
|
|
11195
|
+
}
|
|
11196
|
+
],
|
|
11197
|
+
requestId,
|
|
11198
|
+
absorbedRids: [],
|
|
11199
|
+
onboardingState,
|
|
11200
|
+
system,
|
|
11201
|
+
buildModel
|
|
11202
|
+
});
|
|
11203
|
+
} finally {
|
|
11204
|
+
this.currentChainStep = null;
|
|
11205
|
+
}
|
|
11154
11206
|
}
|
|
11155
11207
|
/**
|
|
11156
11208
|
* Run a mailbox batch — contiguous queued user + background items — as one
|
|
@@ -11163,6 +11215,7 @@ var init_headless = __esm({
|
|
|
11163
11215
|
* with no side effects).
|
|
11164
11216
|
*/
|
|
11165
11217
|
async runMergedTurn(batch) {
|
|
11218
|
+
this.currentChainStep = null;
|
|
11166
11219
|
const primaryRid = batch[0].command.requestId ?? (batch.every((b) => b.source === "background") ? `background-${Date.now()}` : `merged-${Date.now()}`);
|
|
11167
11220
|
const absorbedRids = batch.slice(1).map((b) => b.command.requestId).filter((rid) => typeof rid === "string");
|
|
11168
11221
|
const entryList = [];
|
|
@@ -11317,9 +11370,6 @@ var init_headless = __esm({
|
|
|
11317
11370
|
}
|
|
11318
11371
|
async handleMessage(parsed, requestId) {
|
|
11319
11372
|
const foldIn = !this.running && !getInflightCompaction() && this.queue.length > 0 && !isAutomatedMessage(parsed.text ?? "");
|
|
11320
|
-
if (foldIn) {
|
|
11321
|
-
this.queue.releaseHeld();
|
|
11322
|
-
}
|
|
11323
11373
|
if (this.running || getInflightCompaction() || foldIn) {
|
|
11324
11374
|
const command = { ...parsed };
|
|
11325
11375
|
if (requestId && command.requestId === void 0) {
|
|
@@ -11330,6 +11380,9 @@ var init_headless = __esm({
|
|
|
11330
11380
|
source: "user",
|
|
11331
11381
|
enqueuedAt: Date.now()
|
|
11332
11382
|
});
|
|
11383
|
+
if (foldIn) {
|
|
11384
|
+
this.queue.releaseHeldDeferring((item) => item.source === "chain");
|
|
11385
|
+
}
|
|
11333
11386
|
if (!this.running && !getInflightCompaction()) {
|
|
11334
11387
|
this.kickDrain();
|
|
11335
11388
|
}
|
|
@@ -11482,9 +11535,9 @@ var init_headless = __esm({
|
|
|
11482
11535
|
/**
|
|
11483
11536
|
* Stop everything the user can see running: the turn, an in-flight
|
|
11484
11537
|
* compaction, and any external tool waiting on a result. Flushes the
|
|
11485
|
-
* follow-ups that belonged to the turn
|
|
11486
|
-
* `source: 'user'` items
|
|
11487
|
-
*
|
|
11538
|
+
* `background` follow-ups that belonged to the turn, and HOLDS everything
|
|
11539
|
+
* still queued — the `source: 'user'` items, which are independent user
|
|
11540
|
+
* intent, plus the `source: 'chain'` steps of a build pipeline.
|
|
11488
11541
|
*
|
|
11489
11542
|
* Holding is the difference between Stop working and Stop looking broken.
|
|
11490
11543
|
* These items used to drain immediately: `executeTurn` swallows the abort,
|
|
@@ -11493,6 +11546,13 @@ var init_headless = __esm({
|
|
|
11493
11546
|
* and every additional press hit a turn that had just started. They now wait
|
|
11494
11547
|
* in the queue card until the user sends again or promotes one.
|
|
11495
11548
|
*
|
|
11549
|
+
* The chain steps used to be flushed with the background items, which threw
|
|
11550
|
+
* away the rest of the build — including the step that finalizes it and
|
|
11551
|
+
* unlocks the editor. They are now paused instead, and the interrupted step
|
|
11552
|
+
* goes back at the head, so a Stop is a pause and the next send resumes the
|
|
11553
|
+
* pipeline where it stopped. Holding only the remainder would resume one step
|
|
11554
|
+
* PAST the interruption, polishing and finalizing half-built code.
|
|
11555
|
+
*
|
|
11496
11556
|
* A compaction is cancelled here too, unconditionally. It gates every queued
|
|
11497
11557
|
* message and outlives the turn that started it, so leaving it running means
|
|
11498
11558
|
* Stop can't reach idle. The cost is the summary work in flight; the forced
|
|
@@ -11513,19 +11573,50 @@ var init_headless = __esm({
|
|
|
11513
11573
|
pending2.resolve(USER_CANCELLED_RESULT);
|
|
11514
11574
|
this.pendingTools.delete(id);
|
|
11515
11575
|
}
|
|
11516
|
-
const flushed = this.queue.removeWhere(
|
|
11517
|
-
|
|
11518
|
-
|
|
11576
|
+
const flushed = this.queue.removeWhere(
|
|
11577
|
+
(item) => item.source === "background"
|
|
11578
|
+
);
|
|
11579
|
+
const step = this.currentChainStep;
|
|
11580
|
+
if (step) {
|
|
11581
|
+
this.queue.unshift({
|
|
11582
|
+
command: {
|
|
11583
|
+
action: "message",
|
|
11584
|
+
text: step.text,
|
|
11585
|
+
onboardingState: step.onboardingState,
|
|
11586
|
+
// Fresh id: the original command's terminal has already gone out as
|
|
11587
|
+
// cancelled, and one command gets exactly one `completed`.
|
|
11588
|
+
requestId: this.nextChainRequestId()
|
|
11589
|
+
},
|
|
11590
|
+
source: "chain",
|
|
11591
|
+
enqueuedAt: Date.now(),
|
|
11592
|
+
held: true
|
|
11593
|
+
});
|
|
11594
|
+
this.currentChainStep = null;
|
|
11595
|
+
}
|
|
11596
|
+
const held = this.queue.holdWhere(
|
|
11597
|
+
(item) => item.source === "user" || item.source === "chain"
|
|
11598
|
+
);
|
|
11599
|
+
return {
|
|
11600
|
+
flushed,
|
|
11601
|
+
held,
|
|
11602
|
+
pausedPipeline: held.some((item) => item.source === "chain"),
|
|
11603
|
+
cancelledCompaction
|
|
11604
|
+
};
|
|
11519
11605
|
}
|
|
11520
11606
|
/**
|
|
11521
|
-
* Remove pending queued messages
|
|
11522
|
-
*
|
|
11523
|
-
*
|
|
11524
|
-
*
|
|
11607
|
+
* Remove pending queued messages: all user messages (no id), or a single item
|
|
11608
|
+
* by id. Does not affect the in-flight turn (use `cancel` for that).
|
|
11609
|
+
*
|
|
11610
|
+
* Held chain items — a paused pipeline — are removable only by explicit id.
|
|
11611
|
+
* The id-less form is the queue card's "Clear" and the pre-destroy quiesce,
|
|
11612
|
+
* neither of which should get to decide the pipeline's fate. A DELIVERABLE
|
|
11613
|
+
* chain item is never removable: that's live pipeline work. (The step
|
|
11614
|
+
* actually running isn't in the queue at all — drainQueueLoop takes it out
|
|
11615
|
+
* before running it.)
|
|
11525
11616
|
*/
|
|
11526
11617
|
handleCancelQueued(id) {
|
|
11527
11618
|
return this.queue.removeWhere(
|
|
11528
|
-
(item) => item.source === "user" && (
|
|
11619
|
+
(item) => id === void 0 ? item.source === "user" : item.command.requestId === id && (item.source === "user" || item.source === "chain" && !!item.held)
|
|
11529
11620
|
);
|
|
11530
11621
|
}
|
|
11531
11622
|
//////////////////////////////////////////////////////////////////////////////
|
|
@@ -11624,13 +11715,14 @@ var init_headless = __esm({
|
|
|
11624
11715
|
return;
|
|
11625
11716
|
}
|
|
11626
11717
|
if (action === "cancel") {
|
|
11627
|
-
const { flushed, held, cancelledCompaction } = this.handleCancel();
|
|
11718
|
+
const { flushed, held, pausedPipeline, cancelledCompaction } = this.handleCancel();
|
|
11628
11719
|
this.emit(
|
|
11629
11720
|
"completed",
|
|
11630
11721
|
{
|
|
11631
11722
|
success: true,
|
|
11632
11723
|
...flushed.length > 0 && { cancelledMessages: flushed },
|
|
11633
11724
|
...held.length > 0 && { heldMessages: held },
|
|
11725
|
+
...pausedPipeline && { pausedPipeline: true },
|
|
11634
11726
|
...cancelledCompaction && { cancelledCompaction: true }
|
|
11635
11727
|
},
|
|
11636
11728
|
requestId
|
|
@@ -45,6 +45,8 @@ All fields are nested under the `"web"` key.
|
|
|
45
45
|
| `defaultPreviewMode` | `"desktop"` \| `"mobile"` | `"desktop"` | Default preview viewport in the editor. Set to `"mobile"` for mobile-first apps. |
|
|
46
46
|
| `prerender` | `object` | — | Opt into prerendering the listed routes/patterns for crawlers/unfurlers. See "Prerendering" below. |
|
|
47
47
|
| `mounts` | `array` | — | Serve other same-workspace apps under path prefixes of this app's hosts. See "Mounting other apps" below. |
|
|
48
|
+
| `redirects` | `array` | — | Path-level redirects. See "Redirects" below. |
|
|
49
|
+
| `trailingSlash` | `"strip"` \| `"append"` | — | Enforce a canonical trailing-slash form with a 308. Off by default. |
|
|
48
50
|
|
|
49
51
|
### Frontend SDK
|
|
50
52
|
|
|
@@ -136,7 +138,7 @@ Opt in per route in `web.json`:
|
|
|
136
138
|
{ "web": { "prerender": { "paths": ["/u/*", "/blog/*"] } } }
|
|
137
139
|
```
|
|
138
140
|
|
|
139
|
-
`prerender` is an object with a single field, `paths`: an array of route globs (`*` = one segment, `**` = any). Only listed routes prerender.
|
|
141
|
+
`prerender` is an object with a single field, `paths`: an array of route globs (`*` = one segment, `**` = any). Only listed routes prerender, and only routes — a path with a file extension (`/robots.txt`, `/sitemap.xml`) never prerenders, even under `/**`.
|
|
140
142
|
|
|
141
143
|
The opt-in alone is not enough. For every route in `prerender.paths`, the SPA must set `document.documentElement.setAttribute('data-prerender-ready', 'true')` once the head is written and the route has resolved — this is required, not optional. The renderer waits for this marker, so a prerendered route that never sets it times out. Set it even on routes that render synchronously, as soon as they're ready.
|
|
142
144
|
|
|
@@ -149,9 +151,28 @@ await prerender.invalidate(['/u/abc']); // omit arg to purge all
|
|
|
149
151
|
|
|
150
152
|
`remy-admin prerender` can help you verify/manage snapshots during development.
|
|
151
153
|
|
|
154
|
+
### Redirects
|
|
155
|
+
|
|
156
|
+
Declare moved URLs in `web.json` — never ship a component that redirects client-side, since that resolves after JS loads and crawlers won't follow it:
|
|
157
|
+
|
|
158
|
+
```json
|
|
159
|
+
{ "web": { "trailingSlash": "strip", "redirects": [
|
|
160
|
+
{ "source": "/old-blog/*slug", "destination": "/blog/*slug", "permanent": true },
|
|
161
|
+
{ "source": "/launch", "destination": "https://example.com/launch", "statusCode": 302 }
|
|
162
|
+
] } }
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
`source` is a `/`-prefixed pattern; `destination` is a path or absolute http(s) URL. `permanent` (true → 308, false → 307) is required unless you set an explicit `statusCode` of 301/302/307/308; you can't set both. First match wins, and the request's query carries over unless the destination has its own.
|
|
166
|
+
|
|
167
|
+
**Pattern syntax is path-to-regexp v8, NOT the Next.js dialect** — the one thing to get right here. One segment is `:name`; a multi-segment wildcard is `*name` (Next's `:name*`); optional segments use braces (`/users{/:id}`); inline regex (`:id(\\d+)`) is unsupported. The v6/Next forms fail the build with the correct syntax in the error.
|
|
168
|
+
|
|
169
|
+
`trailingSlash` picks the canonical form (`"strip"`: `/about/` → `/about`; `"append"`: the reverse, skipping the root and paths with file extensions). Off by default. Write `source` patterns without trailing slashes either way — normalization and matching resolve in one hop. Sources under `/_/` are rejected, as are self-referential and two-rule loops. A redirecting path is never prerendered, so don't list one in `prerender.paths`.
|
|
170
|
+
|
|
152
171
|
### Mounting other apps
|
|
153
172
|
|
|
154
|
-
Rare: `mounts` serves another same-workspace app under a path prefix of this app's hosts — `{ "web": { "mounts": [{ "path": "/docs", "app": "docs-site" }] } }` (`app` = the target's `custom_subdomain` or appId). The child is served first-class (its own bundle, session, backend) and needs no mount-specific config; the two serving conventions above are what make an app mountable.
|
|
173
|
+
Rare: `mounts` serves another same-workspace app under a path prefix of this app's hosts — `{ "web": { "mounts": [{ "path": "/docs", "app": "docs-site" }] } }` (`app` = the target's `custom_subdomain` or appId). The child is served first-class (its own bundle, session, backend, prerendering) and needs no mount-specific config; the two serving conventions above are what make an app mountable.
|
|
174
|
+
|
|
175
|
+
Under a mount the child's own `web.json` governs its SEO and routing: its `prerender.paths` gates its mounted pages, its `prerender.invalidate` purges them, its `redirects`/`trailingSlash` apply within the prefix (written against its own paths, with the prefix added back to relative destinations), and its `sitemap.xml` is served with URLs rewritten to the mount and advertised in the parent's `robots.txt`. The child's `robots.txt` *rules* don't carry over (the build log lists them prefixed for the parent to adopt), and canonicals must be written from the page's own location — a hardcoded absolute canonical points crawlers back at the child's host and undoes the mount.
|
|
155
176
|
|
|
156
177
|
## API Interface
|
|
157
178
|
|
|
@@ -18,6 +18,7 @@ If dismissed, acknowledge and do nothing — no commit, no push.
|
|
|
18
18
|
|
|
19
19
|
## 2. Ship (on approval)
|
|
20
20
|
|
|
21
|
+
- On a meaningful release, glance at dependencies before committing — `npm outdated` in the methods package and in each interface's web directory. The first-party packages (`@mindstudio-ai/agent`, `@mindstudio-ai/interface`, `@madewithremy/admin`) are ours and versioned additively: a bump brings new capabilities and bug fixes, not a migration. Bring those current without asking, typecheck, and mention it in plain language when you report the deploy; if a bump does need a small code change, just make it. Third-party packages are the user's time to spend, so flag anything meaningfully behind and let them decide. Skip all of this on a hotfix — a quick fix going out doesn't need a dependency pass.
|
|
21
22
|
- Stage and commit any uncommitted changes with a clean, descriptive commit message. If the committed work resolves any open issues (`remy-admin issues`), reference them in the commit message with a closing keyword — `fixes #42`, `closes #7` — so the deploy closes them automatically once it goes live.
|
|
22
23
|
- Push to main.
|
|
23
24
|
- Use `remy-admin releases wait` to poll the build until it completes. Let the user know it's deploying, then report back when it's live.
|
|
@@ -39,7 +39,7 @@ These are things we already know about and have decided to accept:
|
|
|
39
39
|
- use [wouter](https://github.com/molefrog/wouter) for React routing instead of reaching for react-router
|
|
40
40
|
- uploading user files should always happen via `platform.uploadFile()` from `@mindstudio-ai/interface` — not custom S3 code, not FormData to a method endpoint
|
|
41
41
|
- for build-time prerendering of purely static sites (marketing pages with no dynamic content — distinct from the platform's crawler prerendering, below), roll your own with a post-build `renderToString` script — do not use `vite-prerender-plugin` (it bundles the prerender script as a client chunk, adding ~800KB to the user-facing bundle with no way to prevent it)
|
|
42
|
-
- **Prerendering for crawlers/unfurlers is a platform feature — don't design around it.** For SEO / link-unfurl / AI-crawler visibility on a non-static SPA, the platform already handles it: routes opt in via `web.json` (`{ "web": { "prerender": { "paths": ["/blog/*"] } } }`), the SPA signals readiness by setting `data-prerender-ready` on the html element, deploys invalidate the snapshot cache automatically, and content that changes outside deploys is invalidated at runtime with `await prerender.invalidate([...])` from `@mindstudio-ai/agent` (called from the mutating method). It serves cached headless snapshots of the live SPA to bots — it is NOT build-time rendering. Do not recommend post-build render scripts, rebuild-on-content-change, or third-party prerender services for this. The developer's main context carries the full interfaces reference with exact semantics — tell them to consult it rather than improvising.
|
|
42
|
+
- **Prerendering for crawlers/unfurlers is a platform feature — don't design around it.** For SEO / link-unfurl / AI-crawler visibility on a non-static SPA, the platform already handles it: routes opt in via `web.json` (`{ "web": { "prerender": { "paths": ["/blog/*"] } } }`), the SPA signals readiness by setting `data-prerender-ready` on the html element, deploys invalidate the snapshot cache automatically, and content that changes outside deploys is invalidated at runtime with `await prerender.invalidate([...])` from `@mindstudio-ai/agent` (called from the mutating method). It serves cached headless snapshots of the live SPA to bots — it is NOT build-time rendering. Do not recommend post-build render scripts, rebuild-on-content-change, or third-party prerender services for this. An app mounted under another app's path prefix keeps its own `prerender.paths` and its own `prerender.invalidate` — don't recommend moving prerender config to the parent, and don't hand-roll sitemap/robots rewriting for a mount (the platform rewrites the mounted sitemap and advertises it in the parent's robots.txt). The developer's main context carries the full interfaces reference with exact semantics — tell them to consult it rather than improvising.
|
|
43
43
|
|
|
44
44
|
### Common pitfalls (always flag these)
|
|
45
45
|
|