@xynogen/pix-core 0.5.1 → 0.5.6

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 +26 -26
  2. package/src/compaction.ts +48 -29
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xynogen/pix-core",
3
- "version": "0.5.1",
3
+ "version": "0.5.6",
4
4
  "description": "Pi extension bundle — installs and activates all core pix-* extensions",
5
5
  "type": "module",
6
6
  "main": "src/extension.ts",
@@ -38,31 +38,31 @@
38
38
  "access": "public"
39
39
  },
40
40
  "dependencies": {
41
- "@xynogen/pix-runtime": "^0.5.2",
42
- "@xynogen/pix-data": "^0.4.1",
43
- "@xynogen/pix-pretty": "^1.11.0",
44
- "@xynogen/pix-welcome": "^0.1.19",
45
- "@xynogen/pix-footer": "^0.1.21",
46
- "@xynogen/pix-diagnostics": "^0.1.10",
47
- "@xynogen/pix-display": "^0.1.14",
48
- "@xynogen/pix-prompts": "^0.3.5",
49
- "@xynogen/pix-skills": "^0.7.8",
50
- "@xynogen/pix-models": "^0.1.27",
51
- "@xynogen/pix-update": "^0.1.18",
52
- "@xynogen/pix-commands": "^0.3.2",
53
- "@xynogen/pix-nudge": "^0.1.13",
54
- "@xynogen/pix-read": "^0.1.20",
55
- "@xynogen/pix-write": "^0.1.19",
56
- "@xynogen/pix-edit": "^0.1.21",
57
- "@xynogen/pix-find": "^0.1.20",
58
- "@xynogen/pix-grep": "^0.1.20",
59
- "@xynogen/pix-ls": "^0.1.20",
60
- "@xynogen/pix-bash": "^0.1.19",
61
- "@xynogen/pix-todo": "^0.1.21",
62
- "@xynogen/pix-ask": "^0.2.16",
63
- "@xynogen/pix-optimizer": "^1.1.22",
64
- "@xynogen/pix-gate": "^0.2.3",
65
- "@xynogen/pix-subagent": "^0.6.5"
41
+ "@xynogen/pix-runtime": "^0.5.3",
42
+ "@xynogen/pix-data": "^0.4.3",
43
+ "@xynogen/pix-pretty": "^1.11.2",
44
+ "@xynogen/pix-welcome": "^0.1.21",
45
+ "@xynogen/pix-footer": "^0.1.24",
46
+ "@xynogen/pix-diagnostics": "^0.1.12",
47
+ "@xynogen/pix-display": "^0.1.16",
48
+ "@xynogen/pix-prompts": "^0.3.7",
49
+ "@xynogen/pix-skills": "^0.7.12",
50
+ "@xynogen/pix-models": "^0.1.31",
51
+ "@xynogen/pix-update": "^0.1.20",
52
+ "@xynogen/pix-commands": "^0.3.5",
53
+ "@xynogen/pix-nudge": "^0.1.15",
54
+ "@xynogen/pix-read": "^0.1.22",
55
+ "@xynogen/pix-write": "^0.1.21",
56
+ "@xynogen/pix-edit": "^0.1.24",
57
+ "@xynogen/pix-find": "^0.1.23",
58
+ "@xynogen/pix-grep": "^0.1.23",
59
+ "@xynogen/pix-ls": "^0.1.23",
60
+ "@xynogen/pix-bash": "^0.1.22",
61
+ "@xynogen/pix-todo": "^0.1.23",
62
+ "@xynogen/pix-ask": "^0.2.19",
63
+ "@xynogen/pix-optimizer": "^1.1.24",
64
+ "@xynogen/pix-gate": "^0.2.6",
65
+ "@xynogen/pix-subagent": "^0.6.7"
66
66
  },
67
67
  "peerDependencies": {
68
68
  "@earendil-works/pi-ai": "*",
package/src/compaction.ts CHANGED
@@ -65,7 +65,10 @@ export function compactionThresholdTokens(
65
65
  triggerPercent: number,
66
66
  minimumTokens: number,
67
67
  ): number {
68
- return Math.max(minimumTokens, Math.ceil((contextWindow * triggerPercent) / 100));
68
+ const cw = Number.isFinite(contextWindow) ? Math.max(0, Math.floor(contextWindow)) : 0;
69
+ const pct = Number.isFinite(triggerPercent) ? Math.min(100, Math.max(0, triggerPercent)) : 0;
70
+ const floor = Number.isFinite(minimumTokens) ? Math.max(0, Math.round(minimumTokens)) : 100_000;
71
+ return Math.max(floor, Math.ceil((cw * pct) / 100));
69
72
  }
70
73
 
71
74
  /**
@@ -196,14 +199,9 @@ export default function registerCompaction(pi: ExtensionAPI): void {
196
199
  ctx.compact({
197
200
  onComplete: (result) => {
198
201
  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") {
202
+
203
+ // Loop check is token-based (timing-independent) — decide it now.
204
+ if ((result.estimatedTokensAfter ?? 0) >= threshold) {
207
205
  // Still over the line (e.g. large minimumTokens floor on a small model).
208
206
  // Latch the auto-trigger off so the resume turn doesn't re-fire
209
207
  // agent_settled and compact every turn. Reset by /compact or new session.
@@ -215,27 +213,48 @@ export default function registerCompaction(pi: ExtensionAPI): void {
215
213
  return;
216
214
  }
217
215
 
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
- }
216
+ // Defer the resume/skip decision by one macrotask. A message the user
217
+ // typed DURING compaction lives in the TUI's own compactionQueuedMessages
218
+ // buffer, which ctx.hasPendingMessages()/isIdle() cannot see. The host
219
+ // flushes that buffer on compaction_end by starting a new turn; deferring
220
+ // lets that turn's prompt() microtask chain flip the session to streaming
221
+ // first, so the re-read below reports busy and we skip the nudge. Without
222
+ // this, our nudge races the user's steer, wins the turn, and leaves their
223
+ // message queued "stuck" behind it — and re-points the agent at the old task.
224
+ // ponytail: setTimeout(0) drains microtasks; if the flush's prompt() blocks
225
+ // on a real-async auth check it may still be idle here and the nudge fires
226
+ // (user steer then queues behind it). Rare; upgrade path is a host API that
227
+ // exposes the TUI compaction buffer or a post-flush event.
228
+ setTimeout(() => {
229
+ const decision = resumeDecisionAfterCompaction({
230
+ estimatedTokensAfter: 0, // loop already handled above
231
+ threshold,
232
+ idle: ctx.isIdle(),
233
+ hasPending: ctx.hasPendingMessages(),
234
+ });
225
235
 
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
- }
236
+ if (decision === "skip") {
237
+ // User is already driving (in-flight or freshly flushed prompt); skipping
238
+ // avoids colliding with it and avoids overriding an explicit redirect. The
239
+ // fresh summary is already in context, so the agent continues without a nudge.
240
+ ctx.ui.notify("Compaction: resume nudge skipped (your prompt is in flight)", "info");
241
+ return;
242
+ }
243
+
244
+ // Idle: nudge the agent to pick its task back up. Visible as a normal user
245
+ // message in the transcript (no hidden automation). deliverAs "followUp" is
246
+ // a backstop for the narrow await-window where a prompt starts between the
247
+ // isIdle() check and this send; pi swallows a residual rejection into its
248
+ // own emitError, so it can't crash the session. The try/catch only guards a
249
+ // stale-ctx throw from sendUserMessage.
250
+ ctx.ui.notify("Compaction: resuming task", "info");
251
+ try {
252
+ pi.sendUserMessage(RESUME_NUDGE, { deliverAs: "followUp" });
253
+ } catch (err) {
254
+ const message = err instanceof Error ? err.message : String(err);
255
+ ctx.ui.notify(`Compaction: resume nudge skipped (${message})`, "warning");
256
+ }
257
+ }, 0);
239
258
  },
240
259
  onError: (err) => {
241
260
  compacting = false;