@mindstudio-ai/remy 0.1.293 → 0.1.295
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/reviseFromAnnotatedImage.md +3 -1
- package/dist/headless.d.ts +29 -12
- package/dist/headless.js +232 -75
- package/dist/index.js +233 -76
- package/dist/prompt/compiled/sdk-actions.md +1 -1
- package/dist/prompt/skills/taskAgents.md +30 -3
- package/dist/prompt/static/coding.md +1 -1
- package/dist/prompt/static/instructions.md +2 -0
- package/dist/subagents/codeSanityCheck/prompt.md +2 -2
- package/package.json +1 -1
|
@@ -2,4 +2,6 @@
|
|
|
2
2
|
trigger: reviseFromAnnotatedImage
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
This is an automated message triggered by the user
|
|
5
|
+
This is an automated message triggered by the user annotating a screenshot of the app with revision notes. The attached image is a capture of the app's UI with the user's annotations drawn on top: pink (#DD2590) markers — a small pin dot or a dashed rectangle outlining an area — each with a pink speech bubble containing the note text in white. The image may be a vertical crop of a scrolled page, not necessarily the top of it.
|
|
6
|
+
|
|
7
|
+
The message params include a `notes` array with each annotation's exact text and position (pixel coordinates in the attached image; pins have `x`/`y`, areas add `w`/`h`). Treat the `notes` params as the authoritative note text and use the image to see what each note points at. Make the requested revisions to the web interface.
|
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 \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
|
|
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 reviews this in the editor area and can approve or dismiss. Call this BEFORE committing or pushing.",
|
|
1391
1396
|
inputSchema: {
|
|
1392
1397
|
type: "object",
|
|
1393
1398
|
properties: {
|
|
@@ -1522,7 +1527,7 @@ var markBuildCompleteTool = {
|
|
|
1522
1527
|
var promptUserTool = {
|
|
1523
1528
|
definition: {
|
|
1524
1529
|
name: "promptUser",
|
|
1525
|
-
description: 'Ask the user structured questions. Choose type first: "form" for structured intake (5+ questions,
|
|
1530
|
+
description: 'Ask the user structured questions. Choose type first: "form" for structured intake (5+ questions, opens as a full form in the editor area), "inline" for quick clarifications or confirmations. Blocks until the user responds. Result contains `_dismissed: true` if the user dismisses without answering.',
|
|
1526
1531
|
inputSchema: {
|
|
1527
1532
|
type: "object",
|
|
1528
1533
|
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
|
}
|