@mindstudio-ai/remy 0.1.292 → 0.1.294
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/automatedActions/publish.md +1 -14
- package/dist/headless.d.ts +29 -12
- package/dist/headless.js +231 -74
- package/dist/index.js +232 -75
- package/dist/prompt/skills/publishing.md +36 -0
- package/dist/prompt/static/instructions.md +2 -1
- package/package.json +1 -1
|
@@ -4,17 +4,4 @@
|
|
|
4
4
|
|
|
5
5
|
This is an automated action triggered by the user pressing "Publish" in the editor.
|
|
6
6
|
|
|
7
|
-
Pressing Publish is the user's decision to ship
|
|
8
|
-
|
|
9
|
-
Read what's changed since the last push — the diffs and commits — and turn it into a user-friendly changelog with `presentPublishPlan`: a plain-language summary of what's new ("added vendor approval workflow", "fixed invoice totals", "updated the dashboard layout"). Reference specific code or file paths only when it helps clarity. This is what the user sees before deploying.
|
|
10
|
-
|
|
11
|
-
If approved:
|
|
12
|
-
- Stage and commit any uncommitted changes with a clean, descriptive commit message. If the committed work resolves any open issues (`mindstudio-prod 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.
|
|
13
|
-
- Push to main
|
|
14
|
-
- Use `mindstudio-prod releases wait` to poll the build until it completes. Let the user know it's deploying, then report back when it's live.
|
|
15
|
-
- Once the deploy is live, scale the close-out to what shipped — the changelog you just wrote is the measure. For a meaningful release (new features, interfaces, real behavior changes): dispatch `specSync` with a brief batching everything that changed this session and `refreshBuildOverview: true` (it reconciles the spec, then re-authors the Build Overview from it), and notify `productVision` about what shipped so the roadmap and pitch deck stay current — both run in the background, so hand off and move on without waiting. For a hotfix or small tweak, skip the ceremony: a plain `specSync` (no flag) only if documented behavior actually changed.
|
|
16
|
-
- Once deployed, offer to help with next steps. This includes technical steps likesetting up a custom domain (`mindstudio-prod domains`), checking for errors (`mindstudio-prod requests stats`), seeding production data (`mindstudio-prod db`), managing env vars/secrets, or anything else they need for launch. It also includes going above and beyond and helping holistically. If it's the initial deploy, offer to help create collateral to announce the launch (e.g., an image for sharing on social media, text copy for a post, etc); if it's a meaningful incremental update, an annoucement post or something similar - go above and beyond here to help the user see that you care about the product from end-to-end, not just writing code! They will be appreciative, grateful, and pleased with your creativity here. Refer to the design guidance in the spec for how to talk about the product, and consider consulting the design expert to generate images or other marketing collateral.
|
|
17
|
-
|
|
18
|
-
After everything is done, if this was a meaningful release, call `compactConversation` to summarize the session and free up context for the next phase of work. After a hotfix, don't bother.
|
|
19
|
-
|
|
20
|
-
If dismissed, acknowledge and do nothing.
|
|
7
|
+
Pressing Publish is the user's decision to ship what they've watched come together in the preview. Load the `publishing` skill with loadSkill and run the release it describes — changelog approval first, then ship.
|
package/dist/headless.d.ts
CHANGED
|
@@ -173,8 +173,9 @@ declare class HeadlessSession {
|
|
|
173
173
|
*/
|
|
174
174
|
private isDrainBarrier;
|
|
175
175
|
/**
|
|
176
|
-
* Drain the queue in
|
|
177
|
-
* User messages arriving during the drain will be enqueued
|
|
176
|
+
* Drain the queue in FIFO order over its deliverable items. Caller must hold
|
|
177
|
+
* `running = true`. User messages arriving during the drain will be enqueued
|
|
178
|
+
* behind current items.
|
|
178
179
|
*
|
|
179
180
|
* The queue serves two purposes with opposite delivery semantics:
|
|
180
181
|
* - Sequencer: chain steps and sentinel-bearing user items are pipeline
|
|
@@ -184,6 +185,11 @@ declare class HeadlessSession {
|
|
|
184
185
|
* merged turn, so the model reconciles all of it at once instead of
|
|
185
186
|
* burning a full turn per item (and possibly executing instructions a
|
|
186
187
|
* later queued message already amended).
|
|
188
|
+
*
|
|
189
|
+
* Held items are not in the delivery sequence at all: the drain starts at the
|
|
190
|
+
* first deliverable item and never merges across a held one. It skips rather
|
|
191
|
+
* than stops because a held message sits at the head of the array — stopping
|
|
192
|
+
* there would strand the chain steps and background results behind it.
|
|
187
193
|
*/
|
|
188
194
|
private drainQueueLoop;
|
|
189
195
|
/**
|
|
@@ -205,17 +211,28 @@ declare class HeadlessSession {
|
|
|
205
211
|
* every agent to "use server defaults". */
|
|
206
212
|
private handleChangeModels;
|
|
207
213
|
/**
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
+
* Stop everything the user can see running: the turn, an in-flight
|
|
215
|
+
* compaction, and any external tool waiting on a result. Flushes the
|
|
216
|
+
* follow-ups that belonged to the turn (`chain`/`background`) and HOLDS the
|
|
217
|
+
* `source: 'user'` items — those are independent user intent, so they're
|
|
218
|
+
* kept, but they no longer run on their own.
|
|
219
|
+
*
|
|
220
|
+
* Holding is the difference between Stop working and Stop looking broken.
|
|
221
|
+
* These items used to drain immediately: `executeTurn` swallows the abort,
|
|
222
|
+
* so `handleMessage` fell through to `drainQueueLoop` with `running` still
|
|
223
|
+
* held and the next turn began in the same tick — the spinner never stopped,
|
|
224
|
+
* and every additional press hit a turn that had just started. They now wait
|
|
225
|
+
* in the queue card until the user sends again or promotes one.
|
|
226
|
+
*
|
|
227
|
+
* A compaction is cancelled here too, unconditionally. It gates every queued
|
|
228
|
+
* message and outlives the turn that started it, so leaving it running means
|
|
229
|
+
* Stop can't reach idle. The cost is the summary work in flight; the forced
|
|
230
|
+
* gate re-compacts on the next turn if the context is still too big.
|
|
214
231
|
*
|
|
215
|
-
* Messages already absorbed into the in-flight merged turn are NOT
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
232
|
+
* Messages already absorbed into the in-flight merged turn are NOT held —
|
|
233
|
+
* they were delivered into the turn that's being cancelled and each gets a
|
|
234
|
+
* `{cancelled, absorbed:true}` terminal. Only items still sitting in the
|
|
235
|
+
* queue survive.
|
|
219
236
|
*/
|
|
220
237
|
private handleCancel;
|
|
221
238
|
/**
|
package/dist/headless.js
CHANGED
|
@@ -212,12 +212,17 @@ async function* streamChat(params) {
|
|
|
212
212
|
}
|
|
213
213
|
const isStall = err?.message === "stream_stall";
|
|
214
214
|
const errorMessage = isStall ? "Stream stalled \u2014 no data received for 5 minutes" : `Network error: stream interrupted \u2014 ${err?.message ?? "unknown"}`;
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
215
|
+
const wasAborted = !isStall && !!signal?.aborted;
|
|
216
|
+
const logAt = wasAborted ? log2.warn : log2.error;
|
|
217
|
+
logAt(
|
|
218
|
+
wasAborted ? "Request aborted mid-stream" : isStall ? "Stream stalled" : "Stream interrupted",
|
|
219
|
+
{
|
|
220
|
+
requestId,
|
|
221
|
+
...subAgentId && { subAgentId },
|
|
222
|
+
durationMs: Date.now() - startTime,
|
|
223
|
+
error: errorMessage
|
|
224
|
+
}
|
|
225
|
+
);
|
|
221
226
|
yield { type: "error", error: errorMessage };
|
|
222
227
|
return;
|
|
223
228
|
}
|
|
@@ -1387,7 +1392,7 @@ async function listRecursive(dir) {
|
|
|
1387
1392
|
var presentPublishPlanTool = {
|
|
1388
1393
|
definition: {
|
|
1389
1394
|
name: "presentPublishPlan",
|
|
1390
|
-
description: "Present a publish changelog to the user for approval. Write a clear markdown summary of what changed since the last deploy. The user will see this in a full-screen view and can approve or dismiss. Call this BEFORE committing or pushing.",
|
|
1395
|
+
description: "Present a publish changelog to the user for approval \u2014 the consent gate of the release flow, used when the user has asked to publish (the Publish button or an explicit chat request; the `publishing` skill covers the full sequence). Write a clear markdown summary of what changed since the last deploy. The user will see this in a full-screen view and can approve or dismiss. Call this BEFORE committing or pushing.",
|
|
1391
1396
|
inputSchema: {
|
|
1392
1397
|
type: "object",
|
|
1393
1398
|
properties: {
|
|
@@ -2075,7 +2080,9 @@ var compactConversationTool = {
|
|
|
2075
2080
|
onBackgroundComplete?.(
|
|
2076
2081
|
toolCallId,
|
|
2077
2082
|
"compactConversation",
|
|
2078
|
-
|
|
2083
|
+
// A cancel is the user's own doing — report it as an outcome, not as
|
|
2084
|
+
// an error the block renders as a failure.
|
|
2085
|
+
err instanceof CompactionCancelledError ? err.message : `Error: ${err.message || "Compaction failed"}`
|
|
2079
2086
|
);
|
|
2080
2087
|
}).finally(() => {
|
|
2081
2088
|
toolRegistry?.unregister(toolCallId);
|
|
@@ -6856,7 +6863,7 @@ var log10 = createLogger("compaction");
|
|
|
6856
6863
|
var CONVERSATION_SUMMARY_PROMPT = readAsset("compaction", "conversation.md");
|
|
6857
6864
|
var SUBAGENT_SUMMARY_PROMPT = readAsset("compaction", "subagent.md");
|
|
6858
6865
|
var SUMMARIZABLE_SUBAGENTS = ["visualDesignExpert", "productVision"];
|
|
6859
|
-
async function compactConversation(messages, apiConfig, model) {
|
|
6866
|
+
async function compactConversation(messages, apiConfig, model, signal) {
|
|
6860
6867
|
const endIndex = findSafeInsertionPoint(messages);
|
|
6861
6868
|
const boundary = endIndex > 0 ? messages[endIndex - 1] : null;
|
|
6862
6869
|
const summaries = [];
|
|
@@ -6873,7 +6880,8 @@ async function compactConversation(messages, apiConfig, model) {
|
|
|
6873
6880
|
"conversation",
|
|
6874
6881
|
CONVERSATION_SUMMARY_PROMPT,
|
|
6875
6882
|
conversationMessages,
|
|
6876
|
-
model
|
|
6883
|
+
model,
|
|
6884
|
+
{ signal }
|
|
6877
6885
|
).then((text) => {
|
|
6878
6886
|
if (text) {
|
|
6879
6887
|
summaries.push({ name: "conversation", text });
|
|
@@ -6896,7 +6904,8 @@ async function compactConversation(messages, apiConfig, model) {
|
|
|
6896
6904
|
name,
|
|
6897
6905
|
SUBAGENT_SUMMARY_PROMPT,
|
|
6898
6906
|
subagentMessages,
|
|
6899
|
-
model
|
|
6907
|
+
model,
|
|
6908
|
+
{ signal }
|
|
6900
6909
|
).then((text) => {
|
|
6901
6910
|
if (text) {
|
|
6902
6911
|
summaries.push({ name, text });
|
|
@@ -6912,7 +6921,7 @@ async function compactConversation(messages, apiConfig, model) {
|
|
|
6912
6921
|
await Promise.all(tasks);
|
|
6913
6922
|
if (conversationFailed) {
|
|
6914
6923
|
throw new Error(
|
|
6915
|
-
"Could not summarize the conversation \u2014 the model did not return a usable summary. History left intact."
|
|
6924
|
+
signal?.aborted ? "Compaction cancelled. History left intact." : "Could not summarize the conversation \u2014 the model did not return a usable summary. History left intact."
|
|
6916
6925
|
);
|
|
6917
6926
|
}
|
|
6918
6927
|
const recent = collectRecentNarrative(conversationMessages);
|
|
@@ -7152,7 +7161,10 @@ async function generateSummary(apiConfig, name, compactionPrompt, messagesToSumm
|
|
|
7152
7161
|
// A split driven by size gets its children a retry of their own. A
|
|
7153
7162
|
// split that IS the retry does not, or a model that will not
|
|
7154
7163
|
// summarize at any size fans out call after call before giving up.
|
|
7155
|
-
{
|
|
7164
|
+
{
|
|
7165
|
+
allowRetry: opts.forceChunk ? false : allowRetry,
|
|
7166
|
+
...opts.signal && { signal: opts.signal }
|
|
7167
|
+
}
|
|
7156
7168
|
)
|
|
7157
7169
|
)
|
|
7158
7170
|
);
|
|
@@ -7175,7 +7187,8 @@ async function generateSummary(apiConfig, name, compactionPrompt, messagesToSumm
|
|
|
7175
7187
|
name,
|
|
7176
7188
|
compactionPrompt,
|
|
7177
7189
|
serialized,
|
|
7178
|
-
model
|
|
7190
|
+
model,
|
|
7191
|
+
opts.signal
|
|
7179
7192
|
);
|
|
7180
7193
|
if (summaryText === null) {
|
|
7181
7194
|
return null;
|
|
@@ -7203,7 +7216,7 @@ async function generateSummary(apiConfig, name, compactionPrompt, messagesToSumm
|
|
|
7203
7216
|
{ forceChunk: true, allowRetry: false }
|
|
7204
7217
|
);
|
|
7205
7218
|
}
|
|
7206
|
-
async function runSummaryCall(apiConfig, name, compactionPrompt, serialized, model) {
|
|
7219
|
+
async function runSummaryCall(apiConfig, name, compactionPrompt, serialized, model, signal) {
|
|
7207
7220
|
const userContent = `Conversation to summarize:
|
|
7208
7221
|
|
|
7209
7222
|
${serialized}
|
|
@@ -7225,7 +7238,8 @@ Write the summary of the conversation above, following your instructions.`;
|
|
|
7225
7238
|
// Each summary call carries a unique 100-200KB chunk that is never
|
|
7226
7239
|
// re-read (parallel siblings can't read each other's in-flight writes
|
|
7227
7240
|
// either) — a cache write here is pure waste.
|
|
7228
|
-
cachePolicy: "oneshot"
|
|
7241
|
+
cachePolicy: "oneshot",
|
|
7242
|
+
...signal && { signal }
|
|
7229
7243
|
})) {
|
|
7230
7244
|
if (event.type === "text") {
|
|
7231
7245
|
summaryText += event.text;
|
|
@@ -7249,7 +7263,11 @@ Write the summary of the conversation above, following your instructions.`;
|
|
|
7249
7263
|
}
|
|
7250
7264
|
}
|
|
7251
7265
|
if (!summaryText.trim()) {
|
|
7252
|
-
|
|
7266
|
+
if (signal?.aborted) {
|
|
7267
|
+
log10.info("Summary cancelled", { name });
|
|
7268
|
+
} else {
|
|
7269
|
+
log10.warn("Empty summary generated", { name });
|
|
7270
|
+
}
|
|
7253
7271
|
return null;
|
|
7254
7272
|
}
|
|
7255
7273
|
return summaryText.trim();
|
|
@@ -7610,11 +7628,26 @@ function clearSession(state) {
|
|
|
7610
7628
|
|
|
7611
7629
|
// src/compaction/trigger.ts
|
|
7612
7630
|
var log12 = createLogger("compaction:trigger");
|
|
7631
|
+
var CompactionCancelledError = class extends Error {
|
|
7632
|
+
constructor() {
|
|
7633
|
+
super("Compaction cancelled \u2014 no checkpoint was created.");
|
|
7634
|
+
this.name = "CompactionCancelledError";
|
|
7635
|
+
}
|
|
7636
|
+
};
|
|
7613
7637
|
var pending = null;
|
|
7614
7638
|
var inflightCompaction = null;
|
|
7639
|
+
var inflightAbort = null;
|
|
7615
7640
|
function getInflightCompaction() {
|
|
7616
7641
|
return inflightCompaction;
|
|
7617
7642
|
}
|
|
7643
|
+
function cancelInflightCompaction() {
|
|
7644
|
+
if (!inflightCompaction || !inflightAbort) {
|
|
7645
|
+
return false;
|
|
7646
|
+
}
|
|
7647
|
+
log12.info("Cancelling in-flight compaction");
|
|
7648
|
+
inflightAbort.abort();
|
|
7649
|
+
return true;
|
|
7650
|
+
}
|
|
7618
7651
|
function summariesOf(result) {
|
|
7619
7652
|
const out = [];
|
|
7620
7653
|
for (const msg of result.checkpoints) {
|
|
@@ -7687,10 +7720,13 @@ function triggerCompaction(state, apiConfig, opts = {}) {
|
|
|
7687
7720
|
}
|
|
7688
7721
|
const { blocking = false, requestId, model, origin, toolCallId } = opts;
|
|
7689
7722
|
listener?.({ type: "started", blocking, requestId, origin, toolCallId });
|
|
7723
|
+
const abort = new AbortController();
|
|
7724
|
+
inflightAbort = abort;
|
|
7690
7725
|
inflightCompaction = compactConversation(
|
|
7691
7726
|
state.messages,
|
|
7692
7727
|
apiConfig,
|
|
7693
|
-
resolveModel("conversationSummarizer", state.models, model)
|
|
7728
|
+
resolveModel("conversationSummarizer", state.models, model),
|
|
7729
|
+
abort.signal
|
|
7694
7730
|
).then((result) => {
|
|
7695
7731
|
pending = result;
|
|
7696
7732
|
const summaries = summariesOf(result);
|
|
@@ -7702,12 +7738,23 @@ function triggerCompaction(state, apiConfig, opts = {}) {
|
|
|
7702
7738
|
log12.info("Compaction complete");
|
|
7703
7739
|
return summaries;
|
|
7704
7740
|
}).catch((err) => {
|
|
7705
|
-
const
|
|
7706
|
-
|
|
7707
|
-
|
|
7708
|
-
|
|
7741
|
+
const cancelled = abort.signal.aborted;
|
|
7742
|
+
const message = cancelled ? "Compaction cancelled" : err.message || "Compaction failed";
|
|
7743
|
+
listener?.({
|
|
7744
|
+
type: "complete",
|
|
7745
|
+
error: message,
|
|
7746
|
+
...cancelled && { cancelled: true },
|
|
7747
|
+
requestId
|
|
7748
|
+
});
|
|
7749
|
+
if (cancelled) {
|
|
7750
|
+
log12.info("Compaction cancelled");
|
|
7751
|
+
} else {
|
|
7752
|
+
log12.error("Compaction failed", { error: message });
|
|
7753
|
+
}
|
|
7754
|
+
throw cancelled ? new CompactionCancelledError() : err;
|
|
7709
7755
|
}).finally(() => {
|
|
7710
7756
|
inflightCompaction = null;
|
|
7757
|
+
inflightAbort = null;
|
|
7711
7758
|
});
|
|
7712
7759
|
return inflightCompaction;
|
|
7713
7760
|
}
|
|
@@ -9197,6 +9244,11 @@ function writeStats(stats, queue, passiveResults) {
|
|
|
9197
9244
|
}
|
|
9198
9245
|
|
|
9199
9246
|
// src/headless/messageQueue.ts
|
|
9247
|
+
function holdRestoredUserItems(items) {
|
|
9248
|
+
return items.map(
|
|
9249
|
+
(item) => item.source === "user" ? { ...item, held: true } : item
|
|
9250
|
+
);
|
|
9251
|
+
}
|
|
9200
9252
|
var MessageQueue = class {
|
|
9201
9253
|
items = [];
|
|
9202
9254
|
onChange;
|
|
@@ -9208,33 +9260,36 @@ var MessageQueue = class {
|
|
|
9208
9260
|
this.items.push(item);
|
|
9209
9261
|
this.onChange?.();
|
|
9210
9262
|
}
|
|
9211
|
-
|
|
9212
|
-
|
|
9263
|
+
/**
|
|
9264
|
+
* Index of the first deliverable item, or -1 when there is none.
|
|
9265
|
+
*
|
|
9266
|
+
* Held items are out of the delivery sequence, so the drain skips past them
|
|
9267
|
+
* rather than stopping at them: a message the user parked sits at the head
|
|
9268
|
+
* of the array, and stopping there would strand Remy's own chain steps and
|
|
9269
|
+
* background results queued behind it.
|
|
9270
|
+
*/
|
|
9271
|
+
firstDeliverableIndex() {
|
|
9272
|
+
return this.items.findIndex((item) => !item.held);
|
|
9273
|
+
}
|
|
9274
|
+
/** Remove and return the item at `i`; fires onChange. */
|
|
9275
|
+
takeAt(i) {
|
|
9276
|
+
const [item] = this.items.splice(i, 1);
|
|
9213
9277
|
if (item) {
|
|
9214
9278
|
this.onChange?.();
|
|
9215
9279
|
}
|
|
9216
9280
|
return item;
|
|
9217
9281
|
}
|
|
9218
|
-
/** Remove and return
|
|
9219
|
-
|
|
9220
|
-
if (
|
|
9282
|
+
/** Remove and return `count` items starting at `start`; fires onChange once. */
|
|
9283
|
+
takeRange(start, count) {
|
|
9284
|
+
if (count <= 0) {
|
|
9221
9285
|
return [];
|
|
9222
9286
|
}
|
|
9223
|
-
const items = this.items.splice(
|
|
9287
|
+
const items = this.items.splice(start, count);
|
|
9224
9288
|
if (items.length > 0) {
|
|
9225
9289
|
this.onChange?.();
|
|
9226
9290
|
}
|
|
9227
9291
|
return items;
|
|
9228
9292
|
}
|
|
9229
|
-
/** Remove and return all queued items. */
|
|
9230
|
-
drain() {
|
|
9231
|
-
if (this.items.length === 0) {
|
|
9232
|
-
return [];
|
|
9233
|
-
}
|
|
9234
|
-
const all = this.items.splice(0);
|
|
9235
|
-
this.onChange?.();
|
|
9236
|
-
return all;
|
|
9237
|
-
}
|
|
9238
9293
|
/**
|
|
9239
9294
|
* Remove all items matching `predicate`. Fires onChange only if something
|
|
9240
9295
|
* was removed. Returns the removed items.
|
|
@@ -9253,6 +9308,49 @@ var MessageQueue = class {
|
|
|
9253
9308
|
}
|
|
9254
9309
|
return removed;
|
|
9255
9310
|
}
|
|
9311
|
+
/**
|
|
9312
|
+
* Mark matching items `held` — waiting on the user rather than on the agent.
|
|
9313
|
+
* Fires onChange only if something changed. Returns the held items.
|
|
9314
|
+
*/
|
|
9315
|
+
holdWhere(predicate) {
|
|
9316
|
+
const held = [];
|
|
9317
|
+
let changed = false;
|
|
9318
|
+
for (const item of this.items) {
|
|
9319
|
+
if (!predicate(item)) {
|
|
9320
|
+
continue;
|
|
9321
|
+
}
|
|
9322
|
+
changed = changed || !item.held;
|
|
9323
|
+
item.held = true;
|
|
9324
|
+
held.push(item);
|
|
9325
|
+
}
|
|
9326
|
+
if (changed) {
|
|
9327
|
+
this.onChange?.();
|
|
9328
|
+
}
|
|
9329
|
+
return held;
|
|
9330
|
+
}
|
|
9331
|
+
/**
|
|
9332
|
+
* Release held items so the normal drain picks them up again — all of them,
|
|
9333
|
+
* or one by command requestId. Fires onChange only if something changed.
|
|
9334
|
+
* Returns the released items.
|
|
9335
|
+
*/
|
|
9336
|
+
releaseHeld(id) {
|
|
9337
|
+
const released = [];
|
|
9338
|
+
for (const item of this.items) {
|
|
9339
|
+
if (!item.held || id !== void 0 && item.command.requestId !== id) {
|
|
9340
|
+
continue;
|
|
9341
|
+
}
|
|
9342
|
+
delete item.held;
|
|
9343
|
+
released.push(item);
|
|
9344
|
+
}
|
|
9345
|
+
if (released.length > 0) {
|
|
9346
|
+
this.onChange?.();
|
|
9347
|
+
}
|
|
9348
|
+
return released;
|
|
9349
|
+
}
|
|
9350
|
+
/** Whether anything in the queue will drain on its own (i.e. isn't held). */
|
|
9351
|
+
hasDeliverable() {
|
|
9352
|
+
return this.items.some((item) => !item.held);
|
|
9353
|
+
}
|
|
9256
9354
|
/**
|
|
9257
9355
|
* Change a queued item's delivery semantics, keyed by its command
|
|
9258
9356
|
* requestId. Fires onChange (→ persist + queue_changed) on success.
|
|
@@ -9268,14 +9366,35 @@ var MessageQueue = class {
|
|
|
9268
9366
|
this.onChange?.();
|
|
9269
9367
|
return item;
|
|
9270
9368
|
}
|
|
9369
|
+
/**
|
|
9370
|
+
* Promote an item to ASAP: release any hold, and move it to the head so it
|
|
9371
|
+
* is the next thing delivered. One onChange for all of it.
|
|
9372
|
+
*
|
|
9373
|
+
* Position matters only when no turn is running — mid-turn, ASAP items are
|
|
9374
|
+
* pulled by predicate at the next tool boundary regardless of where they sit
|
|
9375
|
+
* (`takeSteering`), jumping whatever is queued ahead of them. Moving to the
|
|
9376
|
+
* head makes the idle case behave the same way, and matches the card, which
|
|
9377
|
+
* already renders promoted items above everything else.
|
|
9378
|
+
*
|
|
9379
|
+
* Returns the item, or undefined if nothing matches (e.g. it was already
|
|
9380
|
+
* consumed by the running turn).
|
|
9381
|
+
*/
|
|
9382
|
+
promoteToFront(id) {
|
|
9383
|
+
const idx = this.items.findIndex((it) => it.command.requestId === id);
|
|
9384
|
+
if (idx === -1) {
|
|
9385
|
+
return void 0;
|
|
9386
|
+
}
|
|
9387
|
+
const [item] = this.items.splice(idx, 1);
|
|
9388
|
+
item.delivery = "asap";
|
|
9389
|
+
delete item.held;
|
|
9390
|
+
this.items.unshift(item);
|
|
9391
|
+
this.onChange?.();
|
|
9392
|
+
return item;
|
|
9393
|
+
}
|
|
9271
9394
|
/** Copy of current queue contents (for surfacing on events). */
|
|
9272
9395
|
snapshot() {
|
|
9273
9396
|
return [...this.items];
|
|
9274
9397
|
}
|
|
9275
|
-
/** Return the next item without removing it. */
|
|
9276
|
-
peek() {
|
|
9277
|
-
return this.items[0];
|
|
9278
|
-
}
|
|
9279
9398
|
/** Return the item at index `i` without removing it. */
|
|
9280
9399
|
peekAt(i) {
|
|
9281
9400
|
return this.items[i];
|
|
@@ -9375,7 +9494,7 @@ var HeadlessSession = class {
|
|
|
9375
9494
|
});
|
|
9376
9495
|
await initOrgContext(this.config);
|
|
9377
9496
|
const resumed = loadSession(this.state);
|
|
9378
|
-
this.queue = new MessageQueue(loadQueue(), () => {
|
|
9497
|
+
this.queue = new MessageQueue(holdRestoredUserItems(loadQueue()), () => {
|
|
9379
9498
|
this.persistStats();
|
|
9380
9499
|
this.emit("queue_changed", { queuedMessages: this.queue.snapshot() });
|
|
9381
9500
|
});
|
|
@@ -9428,13 +9547,16 @@ var HeadlessSession = class {
|
|
|
9428
9547
|
});
|
|
9429
9548
|
}
|
|
9430
9549
|
} else {
|
|
9431
|
-
const data =
|
|
9550
|
+
const data = {
|
|
9551
|
+
...event.error && { error: event.error },
|
|
9552
|
+
...event.cancelled && { cancelled: true }
|
|
9553
|
+
};
|
|
9432
9554
|
this.emit("compaction_complete", data, event.requestId);
|
|
9433
9555
|
if (this.syntheticCompactionId) {
|
|
9434
9556
|
const id = this.syntheticCompactionId;
|
|
9435
9557
|
this.syntheticCompactionId = null;
|
|
9436
|
-
const result = event.error ? `Error: ${event.error}` : formatSummariesResult(event.summaries ?? []);
|
|
9437
|
-
const isError = !!event.error;
|
|
9558
|
+
const result = event.cancelled ? "Compaction cancelled \u2014 no checkpoint was created." : event.error ? `Error: ${event.error}` : formatSummariesResult(event.summaries ?? []);
|
|
9559
|
+
const isError = !!event.error && !event.cancelled;
|
|
9438
9560
|
for (let i = this.state.messages.length - 1; i >= 0; i--) {
|
|
9439
9561
|
const msg = this.state.messages[i];
|
|
9440
9562
|
if (msg.role !== "assistant" || !Array.isArray(msg.content)) {
|
|
@@ -10115,7 +10237,11 @@ var HeadlessSession = class {
|
|
|
10115
10237
|
this.applyPendingBlockUpdates();
|
|
10116
10238
|
}
|
|
10117
10239
|
async handleMessage(parsed, requestId) {
|
|
10118
|
-
|
|
10240
|
+
const foldIn = !this.running && !getInflightCompaction() && this.queue.length > 0 && !isAutomatedMessage(parsed.text ?? "");
|
|
10241
|
+
if (foldIn) {
|
|
10242
|
+
this.queue.releaseHeld();
|
|
10243
|
+
}
|
|
10244
|
+
if (this.running || getInflightCompaction() || foldIn) {
|
|
10119
10245
|
const command = { ...parsed };
|
|
10120
10246
|
if (requestId && command.requestId === void 0) {
|
|
10121
10247
|
command.requestId = requestId;
|
|
@@ -10152,8 +10278,9 @@ var HeadlessSession = class {
|
|
|
10152
10278
|
return item.source === "user" && isAutomatedMessage(item.command.text ?? "");
|
|
10153
10279
|
}
|
|
10154
10280
|
/**
|
|
10155
|
-
* Drain the queue in
|
|
10156
|
-
* User messages arriving during the drain will be enqueued
|
|
10281
|
+
* Drain the queue in FIFO order over its deliverable items. Caller must hold
|
|
10282
|
+
* `running = true`. User messages arriving during the drain will be enqueued
|
|
10283
|
+
* behind current items.
|
|
10157
10284
|
*
|
|
10158
10285
|
* The queue serves two purposes with opposite delivery semantics:
|
|
10159
10286
|
* - Sequencer: chain steps and sentinel-bearing user items are pipeline
|
|
@@ -10163,12 +10290,21 @@ var HeadlessSession = class {
|
|
|
10163
10290
|
* merged turn, so the model reconciles all of it at once instead of
|
|
10164
10291
|
* burning a full turn per item (and possibly executing instructions a
|
|
10165
10292
|
* later queued message already amended).
|
|
10293
|
+
*
|
|
10294
|
+
* Held items are not in the delivery sequence at all: the drain starts at the
|
|
10295
|
+
* first deliverable item and never merges across a held one. It skips rather
|
|
10296
|
+
* than stops because a held message sits at the head of the array — stopping
|
|
10297
|
+
* there would strand the chain steps and background results behind it.
|
|
10166
10298
|
*/
|
|
10167
10299
|
async drainQueueLoop() {
|
|
10168
|
-
|
|
10169
|
-
const
|
|
10300
|
+
for (; ; ) {
|
|
10301
|
+
const at = this.queue.firstDeliverableIndex();
|
|
10302
|
+
if (at === -1) {
|
|
10303
|
+
return;
|
|
10304
|
+
}
|
|
10305
|
+
const head = this.queue.peekAt(at);
|
|
10170
10306
|
if (head.command.action === "compact") {
|
|
10171
|
-
this.queue.
|
|
10307
|
+
this.queue.takeAt(at);
|
|
10172
10308
|
await triggerCompaction(this.state, this.config, {
|
|
10173
10309
|
blocking: true,
|
|
10174
10310
|
requestId: head.command.requestId,
|
|
@@ -10180,13 +10316,13 @@ var HeadlessSession = class {
|
|
|
10180
10316
|
continue;
|
|
10181
10317
|
}
|
|
10182
10318
|
if (head.source === "chain") {
|
|
10183
|
-
const item = this.queue.
|
|
10319
|
+
const item = this.queue.takeAt(at);
|
|
10184
10320
|
const rid = item.command.requestId ?? `chain-${Date.now()}`;
|
|
10185
10321
|
await this.runSingleTurn(item.command, rid, true);
|
|
10186
10322
|
continue;
|
|
10187
10323
|
}
|
|
10188
10324
|
if (this.isDrainBarrier(head)) {
|
|
10189
|
-
const item = this.queue.
|
|
10325
|
+
const item = this.queue.takeAt(at);
|
|
10190
10326
|
const rid = item.command.requestId ?? `user-${Date.now()}`;
|
|
10191
10327
|
await this.runSingleTurn(item.command, rid, false, true);
|
|
10192
10328
|
continue;
|
|
@@ -10194,8 +10330,8 @@ var HeadlessSession = class {
|
|
|
10194
10330
|
let n = 1;
|
|
10195
10331
|
let batchOb = head.command.onboardingState;
|
|
10196
10332
|
for (; ; n++) {
|
|
10197
|
-
const it = this.queue.peekAt(n);
|
|
10198
|
-
if (!it || it.source === "chain" || this.isDrainBarrier(it)) {
|
|
10333
|
+
const it = this.queue.peekAt(at + n);
|
|
10334
|
+
if (!it || it.held || it.source === "chain" || this.isDrainBarrier(it)) {
|
|
10199
10335
|
break;
|
|
10200
10336
|
}
|
|
10201
10337
|
const ob = it.command.onboardingState;
|
|
@@ -10206,7 +10342,7 @@ var HeadlessSession = class {
|
|
|
10206
10342
|
batchOb = ob;
|
|
10207
10343
|
}
|
|
10208
10344
|
}
|
|
10209
|
-
const batch = this.queue.
|
|
10345
|
+
const batch = this.queue.takeRange(at, n);
|
|
10210
10346
|
await this.runMergedTurn(batch);
|
|
10211
10347
|
}
|
|
10212
10348
|
}
|
|
@@ -10216,7 +10352,7 @@ var HeadlessSession = class {
|
|
|
10216
10352
|
* and by kickDrain (background-completion-initiated).
|
|
10217
10353
|
*/
|
|
10218
10354
|
async resumeQueue() {
|
|
10219
|
-
if (this.running || this.queue.
|
|
10355
|
+
if (this.running || !this.queue.hasDeliverable()) {
|
|
10220
10356
|
return;
|
|
10221
10357
|
}
|
|
10222
10358
|
this.running = true;
|
|
@@ -10235,7 +10371,7 @@ var HeadlessSession = class {
|
|
|
10235
10371
|
* racing any currently-synchronous path.
|
|
10236
10372
|
*/
|
|
10237
10373
|
kickDrain() {
|
|
10238
|
-
if (this.running || this.queue.
|
|
10374
|
+
if (this.running || !this.queue.hasDeliverable()) {
|
|
10239
10375
|
return;
|
|
10240
10376
|
}
|
|
10241
10377
|
setTimeout(() => this.resumeQueue(), 0);
|
|
@@ -10264,28 +10400,42 @@ var HeadlessSession = class {
|
|
|
10264
10400
|
};
|
|
10265
10401
|
}
|
|
10266
10402
|
/**
|
|
10267
|
-
*
|
|
10268
|
-
*
|
|
10269
|
-
*
|
|
10270
|
-
*
|
|
10271
|
-
*
|
|
10272
|
-
*
|
|
10403
|
+
* Stop everything the user can see running: the turn, an in-flight
|
|
10404
|
+
* compaction, and any external tool waiting on a result. Flushes the
|
|
10405
|
+
* follow-ups that belonged to the turn (`chain`/`background`) and HOLDS the
|
|
10406
|
+
* `source: 'user'` items — those are independent user intent, so they're
|
|
10407
|
+
* kept, but they no longer run on their own.
|
|
10408
|
+
*
|
|
10409
|
+
* Holding is the difference between Stop working and Stop looking broken.
|
|
10410
|
+
* These items used to drain immediately: `executeTurn` swallows the abort,
|
|
10411
|
+
* so `handleMessage` fell through to `drainQueueLoop` with `running` still
|
|
10412
|
+
* held and the next turn began in the same tick — the spinner never stopped,
|
|
10413
|
+
* and every additional press hit a turn that had just started. They now wait
|
|
10414
|
+
* in the queue card until the user sends again or promotes one.
|
|
10273
10415
|
*
|
|
10274
|
-
*
|
|
10275
|
-
*
|
|
10276
|
-
*
|
|
10277
|
-
*
|
|
10416
|
+
* A compaction is cancelled here too, unconditionally. It gates every queued
|
|
10417
|
+
* message and outlives the turn that started it, so leaving it running means
|
|
10418
|
+
* Stop can't reach idle. The cost is the summary work in flight; the forced
|
|
10419
|
+
* gate re-compacts on the next turn if the context is still too big.
|
|
10420
|
+
*
|
|
10421
|
+
* Messages already absorbed into the in-flight merged turn are NOT held —
|
|
10422
|
+
* they were delivered into the turn that's being cancelled and each gets a
|
|
10423
|
+
* `{cancelled, absorbed:true}` terminal. Only items still sitting in the
|
|
10424
|
+
* queue survive.
|
|
10278
10425
|
*/
|
|
10279
10426
|
handleCancel() {
|
|
10280
10427
|
if (this.currentAbort) {
|
|
10281
10428
|
this.currentAbort.abort();
|
|
10282
10429
|
}
|
|
10430
|
+
const cancelledCompaction = cancelInflightCompaction();
|
|
10283
10431
|
for (const [id, pending2] of this.pendingTools) {
|
|
10284
10432
|
clearTimeout(pending2.timeout);
|
|
10285
10433
|
pending2.resolve(USER_CANCELLED_RESULT);
|
|
10286
10434
|
this.pendingTools.delete(id);
|
|
10287
10435
|
}
|
|
10288
|
-
|
|
10436
|
+
const flushed = this.queue.removeWhere((item) => item.source !== "user");
|
|
10437
|
+
const held = this.queue.holdWhere((item) => item.source === "user");
|
|
10438
|
+
return { flushed, held, cancelledCompaction };
|
|
10289
10439
|
}
|
|
10290
10440
|
/**
|
|
10291
10441
|
* Remove pending queued messages — all user messages, or one by id.
|
|
@@ -10394,12 +10544,14 @@ var HeadlessSession = class {
|
|
|
10394
10544
|
return;
|
|
10395
10545
|
}
|
|
10396
10546
|
if (action === "cancel") {
|
|
10397
|
-
const
|
|
10547
|
+
const { flushed, held, cancelledCompaction } = this.handleCancel();
|
|
10398
10548
|
this.emit(
|
|
10399
10549
|
"completed",
|
|
10400
10550
|
{
|
|
10401
10551
|
success: true,
|
|
10402
|
-
...
|
|
10552
|
+
...flushed.length > 0 && { cancelledMessages: flushed },
|
|
10553
|
+
...held.length > 0 && { heldMessages: held },
|
|
10554
|
+
...cancelledCompaction && { cancelledCompaction: true }
|
|
10403
10555
|
},
|
|
10404
10556
|
requestId
|
|
10405
10557
|
);
|
|
@@ -10438,7 +10590,12 @@ var HeadlessSession = class {
|
|
|
10438
10590
|
);
|
|
10439
10591
|
return;
|
|
10440
10592
|
}
|
|
10441
|
-
|
|
10593
|
+
if (delivery === "asap") {
|
|
10594
|
+
this.queue.promoteToFront(id);
|
|
10595
|
+
this.kickDrain();
|
|
10596
|
+
} else {
|
|
10597
|
+
this.queue.setDelivery(id, delivery);
|
|
10598
|
+
}
|
|
10442
10599
|
this.emit("completed", { success: true }, requestId);
|
|
10443
10600
|
return;
|
|
10444
10601
|
}
|
|
@@ -10504,7 +10661,7 @@ var HeadlessSession = class {
|
|
|
10504
10661
|
}
|
|
10505
10662
|
this.emit("completed", { success: true }, requestId);
|
|
10506
10663
|
} catch (err) {
|
|
10507
|
-
const error = err.message || "Compaction failed";
|
|
10664
|
+
const error = err instanceof CompactionCancelledError ? "cancelled" : err.message || "Compaction failed";
|
|
10508
10665
|
this.emit("completed", { success: false, error }, requestId);
|
|
10509
10666
|
}
|
|
10510
10667
|
return;
|
|
@@ -10522,7 +10679,7 @@ var HeadlessSession = class {
|
|
|
10522
10679
|
);
|
|
10523
10680
|
return;
|
|
10524
10681
|
}
|
|
10525
|
-
if (this.queue.
|
|
10682
|
+
if (!this.queue.hasDeliverable()) {
|
|
10526
10683
|
this.emit("completed", { success: true }, requestId);
|
|
10527
10684
|
return;
|
|
10528
10685
|
}
|
package/dist/index.js
CHANGED
|
@@ -195,12 +195,17 @@ async function* streamChat(params) {
|
|
|
195
195
|
}
|
|
196
196
|
const isStall = err?.message === "stream_stall";
|
|
197
197
|
const errorMessage = isStall ? "Stream stalled \u2014 no data received for 5 minutes" : `Network error: stream interrupted \u2014 ${err?.message ?? "unknown"}`;
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
198
|
+
const wasAborted = !isStall && !!signal?.aborted;
|
|
199
|
+
const logAt = wasAborted ? log.warn : log.error;
|
|
200
|
+
logAt(
|
|
201
|
+
wasAborted ? "Request aborted mid-stream" : isStall ? "Stream stalled" : "Stream interrupted",
|
|
202
|
+
{
|
|
203
|
+
requestId,
|
|
204
|
+
...subAgentId && { subAgentId },
|
|
205
|
+
durationMs: Date.now() - startTime,
|
|
206
|
+
error: errorMessage
|
|
207
|
+
}
|
|
208
|
+
);
|
|
204
209
|
yield { type: "error", error: errorMessage };
|
|
205
210
|
return;
|
|
206
211
|
}
|
|
@@ -867,7 +872,7 @@ var init_presentPublishPlan = __esm({
|
|
|
867
872
|
presentPublishPlanTool = {
|
|
868
873
|
definition: {
|
|
869
874
|
name: "presentPublishPlan",
|
|
870
|
-
description: "Present a publish changelog to the user for approval. Write a clear markdown summary of what changed since the last deploy. The user will see this in a full-screen view and can approve or dismiss. Call this BEFORE committing or pushing.",
|
|
875
|
+
description: "Present a publish changelog to the user for approval \u2014 the consent gate of the release flow, used when the user has asked to publish (the Publish button or an explicit chat request; the `publishing` skill covers the full sequence). Write a clear markdown summary of what changed since the last deploy. The user will see this in a full-screen view and can approve or dismiss. Call this BEFORE committing or pushing.",
|
|
871
876
|
inputSchema: {
|
|
872
877
|
type: "object",
|
|
873
878
|
properties: {
|
|
@@ -1677,7 +1682,7 @@ var init_sentinel = __esm({
|
|
|
1677
1682
|
});
|
|
1678
1683
|
|
|
1679
1684
|
// src/compaction/index.ts
|
|
1680
|
-
async function compactConversation(messages, apiConfig, model) {
|
|
1685
|
+
async function compactConversation(messages, apiConfig, model, signal) {
|
|
1681
1686
|
const endIndex = findSafeInsertionPoint(messages);
|
|
1682
1687
|
const boundary = endIndex > 0 ? messages[endIndex - 1] : null;
|
|
1683
1688
|
const summaries = [];
|
|
@@ -1694,7 +1699,8 @@ async function compactConversation(messages, apiConfig, model) {
|
|
|
1694
1699
|
"conversation",
|
|
1695
1700
|
CONVERSATION_SUMMARY_PROMPT,
|
|
1696
1701
|
conversationMessages,
|
|
1697
|
-
model
|
|
1702
|
+
model,
|
|
1703
|
+
{ signal }
|
|
1698
1704
|
).then((text) => {
|
|
1699
1705
|
if (text) {
|
|
1700
1706
|
summaries.push({ name: "conversation", text });
|
|
@@ -1717,7 +1723,8 @@ async function compactConversation(messages, apiConfig, model) {
|
|
|
1717
1723
|
name,
|
|
1718
1724
|
SUBAGENT_SUMMARY_PROMPT,
|
|
1719
1725
|
subagentMessages,
|
|
1720
|
-
model
|
|
1726
|
+
model,
|
|
1727
|
+
{ signal }
|
|
1721
1728
|
).then((text) => {
|
|
1722
1729
|
if (text) {
|
|
1723
1730
|
summaries.push({ name, text });
|
|
@@ -1733,7 +1740,7 @@ async function compactConversation(messages, apiConfig, model) {
|
|
|
1733
1740
|
await Promise.all(tasks);
|
|
1734
1741
|
if (conversationFailed) {
|
|
1735
1742
|
throw new Error(
|
|
1736
|
-
"Could not summarize the conversation \u2014 the model did not return a usable summary. History left intact."
|
|
1743
|
+
signal?.aborted ? "Compaction cancelled. History left intact." : "Could not summarize the conversation \u2014 the model did not return a usable summary. History left intact."
|
|
1737
1744
|
);
|
|
1738
1745
|
}
|
|
1739
1746
|
const recent = collectRecentNarrative(conversationMessages);
|
|
@@ -1969,7 +1976,10 @@ async function generateSummary(apiConfig, name, compactionPrompt, messagesToSumm
|
|
|
1969
1976
|
// A split driven by size gets its children a retry of their own. A
|
|
1970
1977
|
// split that IS the retry does not, or a model that will not
|
|
1971
1978
|
// summarize at any size fans out call after call before giving up.
|
|
1972
|
-
{
|
|
1979
|
+
{
|
|
1980
|
+
allowRetry: opts.forceChunk ? false : allowRetry,
|
|
1981
|
+
...opts.signal && { signal: opts.signal }
|
|
1982
|
+
}
|
|
1973
1983
|
)
|
|
1974
1984
|
)
|
|
1975
1985
|
);
|
|
@@ -1992,7 +2002,8 @@ async function generateSummary(apiConfig, name, compactionPrompt, messagesToSumm
|
|
|
1992
2002
|
name,
|
|
1993
2003
|
compactionPrompt,
|
|
1994
2004
|
serialized,
|
|
1995
|
-
model
|
|
2005
|
+
model,
|
|
2006
|
+
opts.signal
|
|
1996
2007
|
);
|
|
1997
2008
|
if (summaryText === null) {
|
|
1998
2009
|
return null;
|
|
@@ -2020,7 +2031,7 @@ async function generateSummary(apiConfig, name, compactionPrompt, messagesToSumm
|
|
|
2020
2031
|
{ forceChunk: true, allowRetry: false }
|
|
2021
2032
|
);
|
|
2022
2033
|
}
|
|
2023
|
-
async function runSummaryCall(apiConfig, name, compactionPrompt, serialized, model) {
|
|
2034
|
+
async function runSummaryCall(apiConfig, name, compactionPrompt, serialized, model, signal) {
|
|
2024
2035
|
const userContent = `Conversation to summarize:
|
|
2025
2036
|
|
|
2026
2037
|
${serialized}
|
|
@@ -2042,7 +2053,8 @@ Write the summary of the conversation above, following your instructions.`;
|
|
|
2042
2053
|
// Each summary call carries a unique 100-200KB chunk that is never
|
|
2043
2054
|
// re-read (parallel siblings can't read each other's in-flight writes
|
|
2044
2055
|
// either) — a cache write here is pure waste.
|
|
2045
|
-
cachePolicy: "oneshot"
|
|
2056
|
+
cachePolicy: "oneshot",
|
|
2057
|
+
...signal && { signal }
|
|
2046
2058
|
})) {
|
|
2047
2059
|
if (event.type === "text") {
|
|
2048
2060
|
summaryText += event.text;
|
|
@@ -2066,7 +2078,11 @@ Write the summary of the conversation above, following your instructions.`;
|
|
|
2066
2078
|
}
|
|
2067
2079
|
}
|
|
2068
2080
|
if (!summaryText.trim()) {
|
|
2069
|
-
|
|
2081
|
+
if (signal?.aborted) {
|
|
2082
|
+
log2.info("Summary cancelled", { name });
|
|
2083
|
+
} else {
|
|
2084
|
+
log2.warn("Empty summary generated", { name });
|
|
2085
|
+
}
|
|
2070
2086
|
return null;
|
|
2071
2087
|
}
|
|
2072
2088
|
return summaryText.trim();
|
|
@@ -2861,6 +2877,14 @@ var init_session = __esm({
|
|
|
2861
2877
|
function getInflightCompaction() {
|
|
2862
2878
|
return inflightCompaction;
|
|
2863
2879
|
}
|
|
2880
|
+
function cancelInflightCompaction() {
|
|
2881
|
+
if (!inflightCompaction || !inflightAbort) {
|
|
2882
|
+
return false;
|
|
2883
|
+
}
|
|
2884
|
+
log4.info("Cancelling in-flight compaction");
|
|
2885
|
+
inflightAbort.abort();
|
|
2886
|
+
return true;
|
|
2887
|
+
}
|
|
2864
2888
|
function summariesOf(result) {
|
|
2865
2889
|
const out = [];
|
|
2866
2890
|
for (const msg of result.checkpoints) {
|
|
@@ -2927,10 +2951,13 @@ function triggerCompaction(state, apiConfig, opts = {}) {
|
|
|
2927
2951
|
}
|
|
2928
2952
|
const { blocking = false, requestId, model, origin, toolCallId } = opts;
|
|
2929
2953
|
listener?.({ type: "started", blocking, requestId, origin, toolCallId });
|
|
2954
|
+
const abort = new AbortController();
|
|
2955
|
+
inflightAbort = abort;
|
|
2930
2956
|
inflightCompaction = compactConversation(
|
|
2931
2957
|
state.messages,
|
|
2932
2958
|
apiConfig,
|
|
2933
|
-
resolveModel("conversationSummarizer", state.models, model)
|
|
2959
|
+
resolveModel("conversationSummarizer", state.models, model),
|
|
2960
|
+
abort.signal
|
|
2934
2961
|
).then((result) => {
|
|
2935
2962
|
pending = result;
|
|
2936
2963
|
const summaries = summariesOf(result);
|
|
@@ -2942,16 +2969,27 @@ function triggerCompaction(state, apiConfig, opts = {}) {
|
|
|
2942
2969
|
log4.info("Compaction complete");
|
|
2943
2970
|
return summaries;
|
|
2944
2971
|
}).catch((err) => {
|
|
2945
|
-
const
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2972
|
+
const cancelled = abort.signal.aborted;
|
|
2973
|
+
const message = cancelled ? "Compaction cancelled" : err.message || "Compaction failed";
|
|
2974
|
+
listener?.({
|
|
2975
|
+
type: "complete",
|
|
2976
|
+
error: message,
|
|
2977
|
+
...cancelled && { cancelled: true },
|
|
2978
|
+
requestId
|
|
2979
|
+
});
|
|
2980
|
+
if (cancelled) {
|
|
2981
|
+
log4.info("Compaction cancelled");
|
|
2982
|
+
} else {
|
|
2983
|
+
log4.error("Compaction failed", { error: message });
|
|
2984
|
+
}
|
|
2985
|
+
throw cancelled ? new CompactionCancelledError() : err;
|
|
2949
2986
|
}).finally(() => {
|
|
2950
2987
|
inflightCompaction = null;
|
|
2988
|
+
inflightAbort = null;
|
|
2951
2989
|
});
|
|
2952
2990
|
return inflightCompaction;
|
|
2953
2991
|
}
|
|
2954
|
-
var log4, pending, inflightCompaction, SUMMARY_SECTION_LABELS, listener;
|
|
2992
|
+
var log4, CompactionCancelledError, pending, inflightCompaction, inflightAbort, SUMMARY_SECTION_LABELS, listener;
|
|
2955
2993
|
var init_trigger = __esm({
|
|
2956
2994
|
"src/compaction/trigger.ts"() {
|
|
2957
2995
|
"use strict";
|
|
@@ -2960,8 +2998,15 @@ var init_trigger = __esm({
|
|
|
2960
2998
|
init_surfaces();
|
|
2961
2999
|
init_session();
|
|
2962
3000
|
log4 = createLogger("compaction:trigger");
|
|
3001
|
+
CompactionCancelledError = class extends Error {
|
|
3002
|
+
constructor() {
|
|
3003
|
+
super("Compaction cancelled \u2014 no checkpoint was created.");
|
|
3004
|
+
this.name = "CompactionCancelledError";
|
|
3005
|
+
}
|
|
3006
|
+
};
|
|
2963
3007
|
pending = null;
|
|
2964
3008
|
inflightCompaction = null;
|
|
3009
|
+
inflightAbort = null;
|
|
2965
3010
|
SUMMARY_SECTION_LABELS = {
|
|
2966
3011
|
conversation: "Conversation",
|
|
2967
3012
|
visualDesignExpert: "Design Agent Thread",
|
|
@@ -3018,7 +3063,9 @@ var init_compactConversation = __esm({
|
|
|
3018
3063
|
onBackgroundComplete?.(
|
|
3019
3064
|
toolCallId,
|
|
3020
3065
|
"compactConversation",
|
|
3021
|
-
|
|
3066
|
+
// A cancel is the user's own doing — report it as an outcome, not as
|
|
3067
|
+
// an error the block renders as a failure.
|
|
3068
|
+
err instanceof CompactionCancelledError ? err.message : `Error: ${err.message || "Compaction failed"}`
|
|
3022
3069
|
);
|
|
3023
3070
|
}).finally(() => {
|
|
3024
3071
|
toolRegistry?.unregister(toolCallId);
|
|
@@ -10147,6 +10194,11 @@ var init_stats = __esm({
|
|
|
10147
10194
|
});
|
|
10148
10195
|
|
|
10149
10196
|
// src/headless/messageQueue.ts
|
|
10197
|
+
function holdRestoredUserItems(items) {
|
|
10198
|
+
return items.map(
|
|
10199
|
+
(item) => item.source === "user" ? { ...item, held: true } : item
|
|
10200
|
+
);
|
|
10201
|
+
}
|
|
10150
10202
|
var MessageQueue;
|
|
10151
10203
|
var init_messageQueue = __esm({
|
|
10152
10204
|
"src/headless/messageQueue.ts"() {
|
|
@@ -10162,33 +10214,36 @@ var init_messageQueue = __esm({
|
|
|
10162
10214
|
this.items.push(item);
|
|
10163
10215
|
this.onChange?.();
|
|
10164
10216
|
}
|
|
10165
|
-
|
|
10166
|
-
|
|
10217
|
+
/**
|
|
10218
|
+
* Index of the first deliverable item, or -1 when there is none.
|
|
10219
|
+
*
|
|
10220
|
+
* Held items are out of the delivery sequence, so the drain skips past them
|
|
10221
|
+
* rather than stopping at them: a message the user parked sits at the head
|
|
10222
|
+
* of the array, and stopping there would strand Remy's own chain steps and
|
|
10223
|
+
* background results queued behind it.
|
|
10224
|
+
*/
|
|
10225
|
+
firstDeliverableIndex() {
|
|
10226
|
+
return this.items.findIndex((item) => !item.held);
|
|
10227
|
+
}
|
|
10228
|
+
/** Remove and return the item at `i`; fires onChange. */
|
|
10229
|
+
takeAt(i) {
|
|
10230
|
+
const [item] = this.items.splice(i, 1);
|
|
10167
10231
|
if (item) {
|
|
10168
10232
|
this.onChange?.();
|
|
10169
10233
|
}
|
|
10170
10234
|
return item;
|
|
10171
10235
|
}
|
|
10172
|
-
/** Remove and return
|
|
10173
|
-
|
|
10174
|
-
if (
|
|
10236
|
+
/** Remove and return `count` items starting at `start`; fires onChange once. */
|
|
10237
|
+
takeRange(start, count) {
|
|
10238
|
+
if (count <= 0) {
|
|
10175
10239
|
return [];
|
|
10176
10240
|
}
|
|
10177
|
-
const items = this.items.splice(
|
|
10241
|
+
const items = this.items.splice(start, count);
|
|
10178
10242
|
if (items.length > 0) {
|
|
10179
10243
|
this.onChange?.();
|
|
10180
10244
|
}
|
|
10181
10245
|
return items;
|
|
10182
10246
|
}
|
|
10183
|
-
/** Remove and return all queued items. */
|
|
10184
|
-
drain() {
|
|
10185
|
-
if (this.items.length === 0) {
|
|
10186
|
-
return [];
|
|
10187
|
-
}
|
|
10188
|
-
const all = this.items.splice(0);
|
|
10189
|
-
this.onChange?.();
|
|
10190
|
-
return all;
|
|
10191
|
-
}
|
|
10192
10247
|
/**
|
|
10193
10248
|
* Remove all items matching `predicate`. Fires onChange only if something
|
|
10194
10249
|
* was removed. Returns the removed items.
|
|
@@ -10207,6 +10262,49 @@ var init_messageQueue = __esm({
|
|
|
10207
10262
|
}
|
|
10208
10263
|
return removed;
|
|
10209
10264
|
}
|
|
10265
|
+
/**
|
|
10266
|
+
* Mark matching items `held` — waiting on the user rather than on the agent.
|
|
10267
|
+
* Fires onChange only if something changed. Returns the held items.
|
|
10268
|
+
*/
|
|
10269
|
+
holdWhere(predicate) {
|
|
10270
|
+
const held = [];
|
|
10271
|
+
let changed = false;
|
|
10272
|
+
for (const item of this.items) {
|
|
10273
|
+
if (!predicate(item)) {
|
|
10274
|
+
continue;
|
|
10275
|
+
}
|
|
10276
|
+
changed = changed || !item.held;
|
|
10277
|
+
item.held = true;
|
|
10278
|
+
held.push(item);
|
|
10279
|
+
}
|
|
10280
|
+
if (changed) {
|
|
10281
|
+
this.onChange?.();
|
|
10282
|
+
}
|
|
10283
|
+
return held;
|
|
10284
|
+
}
|
|
10285
|
+
/**
|
|
10286
|
+
* Release held items so the normal drain picks them up again — all of them,
|
|
10287
|
+
* or one by command requestId. Fires onChange only if something changed.
|
|
10288
|
+
* Returns the released items.
|
|
10289
|
+
*/
|
|
10290
|
+
releaseHeld(id) {
|
|
10291
|
+
const released = [];
|
|
10292
|
+
for (const item of this.items) {
|
|
10293
|
+
if (!item.held || id !== void 0 && item.command.requestId !== id) {
|
|
10294
|
+
continue;
|
|
10295
|
+
}
|
|
10296
|
+
delete item.held;
|
|
10297
|
+
released.push(item);
|
|
10298
|
+
}
|
|
10299
|
+
if (released.length > 0) {
|
|
10300
|
+
this.onChange?.();
|
|
10301
|
+
}
|
|
10302
|
+
return released;
|
|
10303
|
+
}
|
|
10304
|
+
/** Whether anything in the queue will drain on its own (i.e. isn't held). */
|
|
10305
|
+
hasDeliverable() {
|
|
10306
|
+
return this.items.some((item) => !item.held);
|
|
10307
|
+
}
|
|
10210
10308
|
/**
|
|
10211
10309
|
* Change a queued item's delivery semantics, keyed by its command
|
|
10212
10310
|
* requestId. Fires onChange (→ persist + queue_changed) on success.
|
|
@@ -10222,14 +10320,35 @@ var init_messageQueue = __esm({
|
|
|
10222
10320
|
this.onChange?.();
|
|
10223
10321
|
return item;
|
|
10224
10322
|
}
|
|
10323
|
+
/**
|
|
10324
|
+
* Promote an item to ASAP: release any hold, and move it to the head so it
|
|
10325
|
+
* is the next thing delivered. One onChange for all of it.
|
|
10326
|
+
*
|
|
10327
|
+
* Position matters only when no turn is running — mid-turn, ASAP items are
|
|
10328
|
+
* pulled by predicate at the next tool boundary regardless of where they sit
|
|
10329
|
+
* (`takeSteering`), jumping whatever is queued ahead of them. Moving to the
|
|
10330
|
+
* head makes the idle case behave the same way, and matches the card, which
|
|
10331
|
+
* already renders promoted items above everything else.
|
|
10332
|
+
*
|
|
10333
|
+
* Returns the item, or undefined if nothing matches (e.g. it was already
|
|
10334
|
+
* consumed by the running turn).
|
|
10335
|
+
*/
|
|
10336
|
+
promoteToFront(id) {
|
|
10337
|
+
const idx = this.items.findIndex((it) => it.command.requestId === id);
|
|
10338
|
+
if (idx === -1) {
|
|
10339
|
+
return void 0;
|
|
10340
|
+
}
|
|
10341
|
+
const [item] = this.items.splice(idx, 1);
|
|
10342
|
+
item.delivery = "asap";
|
|
10343
|
+
delete item.held;
|
|
10344
|
+
this.items.unshift(item);
|
|
10345
|
+
this.onChange?.();
|
|
10346
|
+
return item;
|
|
10347
|
+
}
|
|
10225
10348
|
/** Copy of current queue contents (for surfacing on events). */
|
|
10226
10349
|
snapshot() {
|
|
10227
10350
|
return [...this.items];
|
|
10228
10351
|
}
|
|
10229
|
-
/** Return the next item without removing it. */
|
|
10230
|
-
peek() {
|
|
10231
|
-
return this.items[0];
|
|
10232
|
-
}
|
|
10233
10352
|
/** Return the item at index `i` without removing it. */
|
|
10234
10353
|
peekAt(i) {
|
|
10235
10354
|
return this.items[i];
|
|
@@ -10357,7 +10476,7 @@ var init_headless = __esm({
|
|
|
10357
10476
|
});
|
|
10358
10477
|
await initOrgContext(this.config);
|
|
10359
10478
|
const resumed = loadSession(this.state);
|
|
10360
|
-
this.queue = new MessageQueue(loadQueue(), () => {
|
|
10479
|
+
this.queue = new MessageQueue(holdRestoredUserItems(loadQueue()), () => {
|
|
10361
10480
|
this.persistStats();
|
|
10362
10481
|
this.emit("queue_changed", { queuedMessages: this.queue.snapshot() });
|
|
10363
10482
|
});
|
|
@@ -10410,13 +10529,16 @@ var init_headless = __esm({
|
|
|
10410
10529
|
});
|
|
10411
10530
|
}
|
|
10412
10531
|
} else {
|
|
10413
|
-
const data =
|
|
10532
|
+
const data = {
|
|
10533
|
+
...event.error && { error: event.error },
|
|
10534
|
+
...event.cancelled && { cancelled: true }
|
|
10535
|
+
};
|
|
10414
10536
|
this.emit("compaction_complete", data, event.requestId);
|
|
10415
10537
|
if (this.syntheticCompactionId) {
|
|
10416
10538
|
const id = this.syntheticCompactionId;
|
|
10417
10539
|
this.syntheticCompactionId = null;
|
|
10418
|
-
const result = event.error ? `Error: ${event.error}` : formatSummariesResult(event.summaries ?? []);
|
|
10419
|
-
const isError = !!event.error;
|
|
10540
|
+
const result = event.cancelled ? "Compaction cancelled \u2014 no checkpoint was created." : event.error ? `Error: ${event.error}` : formatSummariesResult(event.summaries ?? []);
|
|
10541
|
+
const isError = !!event.error && !event.cancelled;
|
|
10420
10542
|
for (let i = this.state.messages.length - 1; i >= 0; i--) {
|
|
10421
10543
|
const msg = this.state.messages[i];
|
|
10422
10544
|
if (msg.role !== "assistant" || !Array.isArray(msg.content)) {
|
|
@@ -11097,7 +11219,11 @@ var init_headless = __esm({
|
|
|
11097
11219
|
this.applyPendingBlockUpdates();
|
|
11098
11220
|
}
|
|
11099
11221
|
async handleMessage(parsed, requestId) {
|
|
11100
|
-
|
|
11222
|
+
const foldIn = !this.running && !getInflightCompaction() && this.queue.length > 0 && !isAutomatedMessage(parsed.text ?? "");
|
|
11223
|
+
if (foldIn) {
|
|
11224
|
+
this.queue.releaseHeld();
|
|
11225
|
+
}
|
|
11226
|
+
if (this.running || getInflightCompaction() || foldIn) {
|
|
11101
11227
|
const command = { ...parsed };
|
|
11102
11228
|
if (requestId && command.requestId === void 0) {
|
|
11103
11229
|
command.requestId = requestId;
|
|
@@ -11134,8 +11260,9 @@ var init_headless = __esm({
|
|
|
11134
11260
|
return item.source === "user" && isAutomatedMessage(item.command.text ?? "");
|
|
11135
11261
|
}
|
|
11136
11262
|
/**
|
|
11137
|
-
* Drain the queue in
|
|
11138
|
-
* User messages arriving during the drain will be enqueued
|
|
11263
|
+
* Drain the queue in FIFO order over its deliverable items. Caller must hold
|
|
11264
|
+
* `running = true`. User messages arriving during the drain will be enqueued
|
|
11265
|
+
* behind current items.
|
|
11139
11266
|
*
|
|
11140
11267
|
* The queue serves two purposes with opposite delivery semantics:
|
|
11141
11268
|
* - Sequencer: chain steps and sentinel-bearing user items are pipeline
|
|
@@ -11145,12 +11272,21 @@ var init_headless = __esm({
|
|
|
11145
11272
|
* merged turn, so the model reconciles all of it at once instead of
|
|
11146
11273
|
* burning a full turn per item (and possibly executing instructions a
|
|
11147
11274
|
* later queued message already amended).
|
|
11275
|
+
*
|
|
11276
|
+
* Held items are not in the delivery sequence at all: the drain starts at the
|
|
11277
|
+
* first deliverable item and never merges across a held one. It skips rather
|
|
11278
|
+
* than stops because a held message sits at the head of the array — stopping
|
|
11279
|
+
* there would strand the chain steps and background results behind it.
|
|
11148
11280
|
*/
|
|
11149
11281
|
async drainQueueLoop() {
|
|
11150
|
-
|
|
11151
|
-
const
|
|
11282
|
+
for (; ; ) {
|
|
11283
|
+
const at = this.queue.firstDeliverableIndex();
|
|
11284
|
+
if (at === -1) {
|
|
11285
|
+
return;
|
|
11286
|
+
}
|
|
11287
|
+
const head = this.queue.peekAt(at);
|
|
11152
11288
|
if (head.command.action === "compact") {
|
|
11153
|
-
this.queue.
|
|
11289
|
+
this.queue.takeAt(at);
|
|
11154
11290
|
await triggerCompaction(this.state, this.config, {
|
|
11155
11291
|
blocking: true,
|
|
11156
11292
|
requestId: head.command.requestId,
|
|
@@ -11162,13 +11298,13 @@ var init_headless = __esm({
|
|
|
11162
11298
|
continue;
|
|
11163
11299
|
}
|
|
11164
11300
|
if (head.source === "chain") {
|
|
11165
|
-
const item = this.queue.
|
|
11301
|
+
const item = this.queue.takeAt(at);
|
|
11166
11302
|
const rid = item.command.requestId ?? `chain-${Date.now()}`;
|
|
11167
11303
|
await this.runSingleTurn(item.command, rid, true);
|
|
11168
11304
|
continue;
|
|
11169
11305
|
}
|
|
11170
11306
|
if (this.isDrainBarrier(head)) {
|
|
11171
|
-
const item = this.queue.
|
|
11307
|
+
const item = this.queue.takeAt(at);
|
|
11172
11308
|
const rid = item.command.requestId ?? `user-${Date.now()}`;
|
|
11173
11309
|
await this.runSingleTurn(item.command, rid, false, true);
|
|
11174
11310
|
continue;
|
|
@@ -11176,8 +11312,8 @@ var init_headless = __esm({
|
|
|
11176
11312
|
let n = 1;
|
|
11177
11313
|
let batchOb = head.command.onboardingState;
|
|
11178
11314
|
for (; ; n++) {
|
|
11179
|
-
const it = this.queue.peekAt(n);
|
|
11180
|
-
if (!it || it.source === "chain" || this.isDrainBarrier(it)) {
|
|
11315
|
+
const it = this.queue.peekAt(at + n);
|
|
11316
|
+
if (!it || it.held || it.source === "chain" || this.isDrainBarrier(it)) {
|
|
11181
11317
|
break;
|
|
11182
11318
|
}
|
|
11183
11319
|
const ob = it.command.onboardingState;
|
|
@@ -11188,7 +11324,7 @@ var init_headless = __esm({
|
|
|
11188
11324
|
batchOb = ob;
|
|
11189
11325
|
}
|
|
11190
11326
|
}
|
|
11191
|
-
const batch = this.queue.
|
|
11327
|
+
const batch = this.queue.takeRange(at, n);
|
|
11192
11328
|
await this.runMergedTurn(batch);
|
|
11193
11329
|
}
|
|
11194
11330
|
}
|
|
@@ -11198,7 +11334,7 @@ var init_headless = __esm({
|
|
|
11198
11334
|
* and by kickDrain (background-completion-initiated).
|
|
11199
11335
|
*/
|
|
11200
11336
|
async resumeQueue() {
|
|
11201
|
-
if (this.running || this.queue.
|
|
11337
|
+
if (this.running || !this.queue.hasDeliverable()) {
|
|
11202
11338
|
return;
|
|
11203
11339
|
}
|
|
11204
11340
|
this.running = true;
|
|
@@ -11217,7 +11353,7 @@ var init_headless = __esm({
|
|
|
11217
11353
|
* racing any currently-synchronous path.
|
|
11218
11354
|
*/
|
|
11219
11355
|
kickDrain() {
|
|
11220
|
-
if (this.running || this.queue.
|
|
11356
|
+
if (this.running || !this.queue.hasDeliverable()) {
|
|
11221
11357
|
return;
|
|
11222
11358
|
}
|
|
11223
11359
|
setTimeout(() => this.resumeQueue(), 0);
|
|
@@ -11246,28 +11382,42 @@ var init_headless = __esm({
|
|
|
11246
11382
|
};
|
|
11247
11383
|
}
|
|
11248
11384
|
/**
|
|
11249
|
-
*
|
|
11250
|
-
*
|
|
11251
|
-
*
|
|
11252
|
-
*
|
|
11253
|
-
*
|
|
11254
|
-
*
|
|
11385
|
+
* Stop everything the user can see running: the turn, an in-flight
|
|
11386
|
+
* compaction, and any external tool waiting on a result. Flushes the
|
|
11387
|
+
* follow-ups that belonged to the turn (`chain`/`background`) and HOLDS the
|
|
11388
|
+
* `source: 'user'` items — those are independent user intent, so they're
|
|
11389
|
+
* kept, but they no longer run on their own.
|
|
11390
|
+
*
|
|
11391
|
+
* Holding is the difference between Stop working and Stop looking broken.
|
|
11392
|
+
* These items used to drain immediately: `executeTurn` swallows the abort,
|
|
11393
|
+
* so `handleMessage` fell through to `drainQueueLoop` with `running` still
|
|
11394
|
+
* held and the next turn began in the same tick — the spinner never stopped,
|
|
11395
|
+
* and every additional press hit a turn that had just started. They now wait
|
|
11396
|
+
* in the queue card until the user sends again or promotes one.
|
|
11255
11397
|
*
|
|
11256
|
-
*
|
|
11257
|
-
*
|
|
11258
|
-
*
|
|
11259
|
-
*
|
|
11398
|
+
* A compaction is cancelled here too, unconditionally. It gates every queued
|
|
11399
|
+
* message and outlives the turn that started it, so leaving it running means
|
|
11400
|
+
* Stop can't reach idle. The cost is the summary work in flight; the forced
|
|
11401
|
+
* gate re-compacts on the next turn if the context is still too big.
|
|
11402
|
+
*
|
|
11403
|
+
* Messages already absorbed into the in-flight merged turn are NOT held —
|
|
11404
|
+
* they were delivered into the turn that's being cancelled and each gets a
|
|
11405
|
+
* `{cancelled, absorbed:true}` terminal. Only items still sitting in the
|
|
11406
|
+
* queue survive.
|
|
11260
11407
|
*/
|
|
11261
11408
|
handleCancel() {
|
|
11262
11409
|
if (this.currentAbort) {
|
|
11263
11410
|
this.currentAbort.abort();
|
|
11264
11411
|
}
|
|
11412
|
+
const cancelledCompaction = cancelInflightCompaction();
|
|
11265
11413
|
for (const [id, pending2] of this.pendingTools) {
|
|
11266
11414
|
clearTimeout(pending2.timeout);
|
|
11267
11415
|
pending2.resolve(USER_CANCELLED_RESULT);
|
|
11268
11416
|
this.pendingTools.delete(id);
|
|
11269
11417
|
}
|
|
11270
|
-
|
|
11418
|
+
const flushed = this.queue.removeWhere((item) => item.source !== "user");
|
|
11419
|
+
const held = this.queue.holdWhere((item) => item.source === "user");
|
|
11420
|
+
return { flushed, held, cancelledCompaction };
|
|
11271
11421
|
}
|
|
11272
11422
|
/**
|
|
11273
11423
|
* Remove pending queued messages — all user messages, or one by id.
|
|
@@ -11376,12 +11526,14 @@ var init_headless = __esm({
|
|
|
11376
11526
|
return;
|
|
11377
11527
|
}
|
|
11378
11528
|
if (action === "cancel") {
|
|
11379
|
-
const
|
|
11529
|
+
const { flushed, held, cancelledCompaction } = this.handleCancel();
|
|
11380
11530
|
this.emit(
|
|
11381
11531
|
"completed",
|
|
11382
11532
|
{
|
|
11383
11533
|
success: true,
|
|
11384
|
-
...
|
|
11534
|
+
...flushed.length > 0 && { cancelledMessages: flushed },
|
|
11535
|
+
...held.length > 0 && { heldMessages: held },
|
|
11536
|
+
...cancelledCompaction && { cancelledCompaction: true }
|
|
11385
11537
|
},
|
|
11386
11538
|
requestId
|
|
11387
11539
|
);
|
|
@@ -11420,7 +11572,12 @@ var init_headless = __esm({
|
|
|
11420
11572
|
);
|
|
11421
11573
|
return;
|
|
11422
11574
|
}
|
|
11423
|
-
|
|
11575
|
+
if (delivery === "asap") {
|
|
11576
|
+
this.queue.promoteToFront(id);
|
|
11577
|
+
this.kickDrain();
|
|
11578
|
+
} else {
|
|
11579
|
+
this.queue.setDelivery(id, delivery);
|
|
11580
|
+
}
|
|
11424
11581
|
this.emit("completed", { success: true }, requestId);
|
|
11425
11582
|
return;
|
|
11426
11583
|
}
|
|
@@ -11486,7 +11643,7 @@ var init_headless = __esm({
|
|
|
11486
11643
|
}
|
|
11487
11644
|
this.emit("completed", { success: true }, requestId);
|
|
11488
11645
|
} catch (err) {
|
|
11489
|
-
const error = err.message || "Compaction failed";
|
|
11646
|
+
const error = err instanceof CompactionCancelledError ? "cancelled" : err.message || "Compaction failed";
|
|
11490
11647
|
this.emit("completed", { success: false, error }, requestId);
|
|
11491
11648
|
}
|
|
11492
11649
|
return;
|
|
@@ -11504,7 +11661,7 @@ var init_headless = __esm({
|
|
|
11504
11661
|
);
|
|
11505
11662
|
return;
|
|
11506
11663
|
}
|
|
11507
|
-
if (this.queue.
|
|
11664
|
+
if (!this.queue.hasDeliverable()) {
|
|
11508
11665
|
this.emit("completed", { success: true }, requestId);
|
|
11509
11666
|
return;
|
|
11510
11667
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Publishing & Releases
|
|
3
|
+
what: Shipping to production is a release moment, not just a git push — a user-approved changelog (`presentPublishPlan`) gates the deploy, then commit → push to main → watch the build go live, then a close-out scaled to what shipped (spec sync, roadmap/pitch updates, launch collateral, next steps). Publishing happens at the user's ask, either the Publish button or an explicit request in chat.
|
|
4
|
+
when: The user wants to ship — the Publish automated action fired, or they asked in chat ("publish", "deploy", "ship it", "push it live"). Load BEFORE presenting a changelog, committing, or pushing to main.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Publishing
|
|
8
|
+
|
|
9
|
+
Publishing deploys the app to `main`, which triggers a production build. It is the user's decision to ship — it happens when they press Publish in the editor (which arrives as an automated action) or ask you directly in chat. The steps are the same either way, and they run in order: the changelog is the consent moment, so nothing is committed or pushed until it's approved.
|
|
10
|
+
|
|
11
|
+
If the user wants to see the work before it goes live, push a feature branch instead: that builds a preview at its own URL (`previewUrl` in the `releases wait` result) against a copy of the data, and publishing later is the same flow from `main`.
|
|
12
|
+
|
|
13
|
+
## 1. Present the changelog
|
|
14
|
+
|
|
15
|
+
Read what's changed since the last push — the diffs and commits — and turn it into a user-friendly changelog with `presentPublishPlan`: a plain-language summary of what's new ("added vendor approval workflow", "fixed invoice totals", "updated the dashboard layout"). Reference specific code or file paths only when it helps clarity. This is what the user sees, full-screen, before anything deploys.
|
|
16
|
+
|
|
17
|
+
If dismissed, acknowledge and do nothing — no commit, no push.
|
|
18
|
+
|
|
19
|
+
## 2. Ship (on approval)
|
|
20
|
+
|
|
21
|
+
- Stage and commit any uncommitted changes with a clean, descriptive commit message. If the committed work resolves any open issues (`mindstudio-prod 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
|
+
- Push to main.
|
|
23
|
+
- Use `mindstudio-prod releases wait` to poll the build until it completes. Let the user know it's deploying, then report back when it's live.
|
|
24
|
+
|
|
25
|
+
## 3. Close out — scaled to what shipped
|
|
26
|
+
|
|
27
|
+
The changelog you just wrote is the measure.
|
|
28
|
+
|
|
29
|
+
- **Meaningful release** (new features, interfaces, real behavior changes): dispatch `specSync` with a brief batching everything that changed this session and `refreshBuildOverview: true` (it reconciles the spec, then re-authors the Build Overview from it), and notify `productVision` about what shipped so the roadmap and pitch deck stay current — both run in the background, so hand off and move on without waiting.
|
|
30
|
+
- **Hotfix or small tweak**: skip the ceremony — a plain `specSync` (no flag), and only if documented behavior actually changed.
|
|
31
|
+
|
|
32
|
+
## 4. Offer next steps
|
|
33
|
+
|
|
34
|
+
Once deployed, offer to help with what comes next. This includes technical steps like setting up a custom domain (`mindstudio-prod domains`), checking for errors (`mindstudio-prod requests stats`), seeding production data (`mindstudio-prod db`), managing env vars/secrets, or anything else they need for launch. It also includes going above and beyond and helping holistically: if it's the initial deploy, offer to help create collateral to announce the launch (e.g., an image for sharing on social media, text copy for a post); if it's a meaningful incremental update, an announcement post or something similar. Refer to the design guidance in the spec for how to talk about the product, and consider consulting the design expert to generate images or other marketing collateral — help the user see that you care about the product from end-to-end, not just the code.
|
|
35
|
+
|
|
36
|
+
After everything is done, if this was a meaningful release, call `compactConversation` to summarize the session and free up context for the next phase of work. After a hotfix, don't bother.
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
- When multiple tool calls are independent, make them all in a single turn. Reading three files, writing two methods, or running a scenario while taking a screenshot: batch them instead of doing one per turn.
|
|
15
15
|
- After two failed attempts at the same approach, tell the user what's going wrong.
|
|
16
16
|
- Never estimate how long something will take or how much it will cost. Just do it. If the user asks, politely refuse — any number would be a guess. Never quote concrete time units for work you're about to do. You can describe scope qualitatively (small change, large refactor, etc.), but never estimate the time it will take you to do work.
|
|
17
|
-
- Pushing to main branch will trigger a deploy.
|
|
17
|
+
- Pushing to main branch will trigger a production deploy. Publishing happens at the user's ask — the Publish button or an explicit request in chat — and the release has its own playbook: load the `publishing` skill before presenting a changelog or pushing to main.
|
|
18
|
+
- Pushing any OTHER branch builds a preview instead of deploying: a private copy of the app at its own URL, with its own copy of the data, that anyone who can open the app in Remy can visit. `mindstudio-prod releases wait` returns its `previewUrl`. Use this to show the user working software before it goes live — a branch push plus the link is often a better answer than describing what you built.
|
|
18
19
|
|
|
19
20
|
### Build Notes
|
|
20
21
|
For complex tasks — especially an initial buildout from a spec or making multiple changes in a single turn — write a `.remy-notes.md` scratchpad in the project root. Use it to track progress: a checklist of what's been built and what's remaining. Do not include implementation details or other decisions in the notes - it is solely for keeping track of tasks. Read the spec files directly when you need design details, implementation decisions, or other reference materials - never write them to the notes file. Delete the notes file when your work is done. When implementing an approved plan, `.remy-plan.md` serves as your reference. Delete it when all planned work is complete.
|