@xynogen/pix-core 0.5.0 → 0.5.2

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.
Files changed (2) hide show
  1. package/package.json +2 -2
  2. package/src/compaction.ts +44 -28
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xynogen/pix-core",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Pi extension bundle — installs and activates all core pix-* extensions",
5
5
  "type": "module",
6
6
  "main": "src/extension.ts",
@@ -59,7 +59,7 @@
59
59
  "@xynogen/pix-ls": "^0.1.20",
60
60
  "@xynogen/pix-bash": "^0.1.19",
61
61
  "@xynogen/pix-todo": "^0.1.21",
62
- "@xynogen/pix-ask": "^0.2.15",
62
+ "@xynogen/pix-ask": "^0.2.16",
63
63
  "@xynogen/pix-optimizer": "^1.1.22",
64
64
  "@xynogen/pix-gate": "^0.2.3",
65
65
  "@xynogen/pix-subagent": "^0.6.5"
package/src/compaction.ts CHANGED
@@ -196,14 +196,9 @@ export default function registerCompaction(pi: ExtensionAPI): void {
196
196
  ctx.compact({
197
197
  onComplete: (result) => {
198
198
  compacting = false;
199
- const decision = resumeDecisionAfterCompaction({
200
- estimatedTokensAfter: result.estimatedTokensAfter,
201
- threshold,
202
- idle: ctx.isIdle(),
203
- hasPending: ctx.hasPendingMessages(),
204
- });
205
-
206
- if (decision === "loop") {
199
+
200
+ // Loop check is token-based (timing-independent) — decide it now.
201
+ if ((result.estimatedTokensAfter ?? 0) >= threshold) {
207
202
  // Still over the line (e.g. large minimumTokens floor on a small model).
208
203
  // Latch the auto-trigger off so the resume turn doesn't re-fire
209
204
  // agent_settled and compact every turn. Reset by /compact or new session.
@@ -215,27 +210,48 @@ export default function registerCompaction(pi: ExtensionAPI): void {
215
210
  return;
216
211
  }
217
212
 
218
- if (decision === "skip") {
219
- // User is already driving; skipping avoids colliding with their in-flight
220
- // prompt and avoids overriding an explicit redirect. The fresh summary is
221
- // already in context, so the agent continues without a nudge.
222
- ctx.ui.notify("Compaction: resume nudge skipped (your prompt is in flight)", "info");
223
- return;
224
- }
213
+ // Defer the resume/skip decision by one macrotask. A message the user
214
+ // typed DURING compaction lives in the TUI's own compactionQueuedMessages
215
+ // buffer, which ctx.hasPendingMessages()/isIdle() cannot see. The host
216
+ // flushes that buffer on compaction_end by starting a new turn; deferring
217
+ // lets that turn's prompt() microtask chain flip the session to streaming
218
+ // first, so the re-read below reports busy and we skip the nudge. Without
219
+ // this, our nudge races the user's steer, wins the turn, and leaves their
220
+ // message queued "stuck" behind it — and re-points the agent at the old task.
221
+ // ponytail: setTimeout(0) drains microtasks; if the flush's prompt() blocks
222
+ // on a real-async auth check it may still be idle here and the nudge fires
223
+ // (user steer then queues behind it). Rare; upgrade path is a host API that
224
+ // exposes the TUI compaction buffer or a post-flush event.
225
+ setTimeout(() => {
226
+ const decision = resumeDecisionAfterCompaction({
227
+ estimatedTokensAfter: 0, // loop already handled above
228
+ threshold,
229
+ idle: ctx.isIdle(),
230
+ hasPending: ctx.hasPendingMessages(),
231
+ });
225
232
 
226
- // Idle: nudge the agent to pick its task back up. Visible as a normal user
227
- // message in the transcript (no hidden automation). deliverAs "followUp" is
228
- // a backstop for the narrow await-window where a prompt starts between the
229
- // isIdle() check and this send; pi swallows a residual rejection into its
230
- // own emitError, so it can't crash the session. The try/catch only guards a
231
- // stale-ctx throw from sendUserMessage.
232
- ctx.ui.notify("Compaction: resuming task", "info");
233
- try {
234
- pi.sendUserMessage(RESUME_NUDGE, { deliverAs: "followUp" });
235
- } catch (err) {
236
- const message = err instanceof Error ? err.message : String(err);
237
- ctx.ui.notify(`Compaction: resume nudge skipped (${message})`, "warning");
238
- }
233
+ if (decision === "skip") {
234
+ // User is already driving (in-flight or freshly flushed prompt); skipping
235
+ // avoids colliding with it and avoids overriding an explicit redirect. The
236
+ // fresh summary is already in context, so the agent continues without a nudge.
237
+ ctx.ui.notify("Compaction: resume nudge skipped (your prompt is in flight)", "info");
238
+ return;
239
+ }
240
+
241
+ // Idle: nudge the agent to pick its task back up. Visible as a normal user
242
+ // message in the transcript (no hidden automation). deliverAs "followUp" is
243
+ // a backstop for the narrow await-window where a prompt starts between the
244
+ // isIdle() check and this send; pi swallows a residual rejection into its
245
+ // own emitError, so it can't crash the session. The try/catch only guards a
246
+ // stale-ctx throw from sendUserMessage.
247
+ ctx.ui.notify("Compaction: resuming task", "info");
248
+ try {
249
+ pi.sendUserMessage(RESUME_NUDGE, { deliverAs: "followUp" });
250
+ } catch (err) {
251
+ const message = err instanceof Error ? err.message : String(err);
252
+ ctx.ui.notify(`Compaction: resume nudge skipped (${message})`, "warning");
253
+ }
254
+ }, 0);
239
255
  },
240
256
  onError: (err) => {
241
257
  compacting = false;