billion-context-omp 0.1.8 → 0.1.9

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.
@@ -50,6 +50,10 @@ export declare function summarizeMessages(ctx: ExtensionContext, messages: Agent
50
50
  customInstructions?: string;
51
51
  signal?: AbortSignal;
52
52
  completeFn?: typeof complete;
53
+ /** Fold-slot messageRefs — when provided, formatSlice renders stable
54
+ * mNNNNN refs instead of raw pN position ids (issue #14 Minor1: the
55
+ * model was shown p-ids it must never echo back). */
56
+ messageRefs?: CompressionState["messageRefs"];
53
57
  }): Promise<{
54
58
  summary: string;
55
59
  model: string;
package/dist/index.js CHANGED
@@ -3158,8 +3158,13 @@ function rangeFingerprints(ranges, coreMessages, byRef, blocks) {
3158
3158
  }
3159
3159
 
3160
3160
  // src/runtime.ts
3161
- function freshSlot() {
3162
- return { identities: [], foldedLen: 0, preview: false, state: createInitialState(), coreMessages: [], appliedCallIds: /* @__PURE__ */ new Set() };
3161
+ function freshSlot(preserveFrom) {
3162
+ const slot = { identities: [], foldedLen: 0, preview: false, state: createInitialState(), coreMessages: [], appliedCallIds: /* @__PURE__ */ new Set(), rejectStreak: 0 };
3163
+ if (preserveFrom) {
3164
+ slot.state = { ...slot.state, nudge: preserveFrom.state.nudge };
3165
+ slot.rejectStreak = preserveFrom.rejectStreak;
3166
+ }
3167
+ return slot;
3163
3168
  }
3164
3169
  function stateHasCompressCall(state, callId) {
3165
3170
  return state.blocks.some((b) => b.compressCallId === callId);
@@ -3205,7 +3210,7 @@ function createRuntime(adapter) {
3205
3210
  let slot = slotFor(sid);
3206
3211
  if (slot.preview) {
3207
3212
  debug.event("fold-refold", { sid, foldedLen: slot.foldedLen, lcp: 0, streamLen: stream.length, reason: "preview" });
3208
- slot = freshSlot();
3213
+ slot = freshSlot(slot);
3209
3214
  slots.set(sid, slot);
3210
3215
  }
3211
3216
  const ids = stream.map(messageIdentity);
@@ -3213,7 +3218,7 @@ function createRuntime(adapter) {
3213
3218
  while (lcp < Math.min(ids.length, slot.identities.length) && ids[lcp] === slot.identities[lcp]) lcp++;
3214
3219
  if (lcp < slot.foldedLen) {
3215
3220
  debug.event("fold-refold", { sid, foldedLen: slot.foldedLen, lcp, streamLen: ids.length });
3216
- slot = freshSlot();
3221
+ slot = freshSlot(slot);
3217
3222
  slots.set(sid, slot);
3218
3223
  lcp = 0;
3219
3224
  }
@@ -3295,6 +3300,11 @@ function createRuntime(adapter) {
3295
3300
  slot.state = state;
3296
3301
  if (toolCallId) slot.appliedCallIds.add(toolCallId);
3297
3302
  }
3303
+ function noteCompressOutcome(ctx, ok) {
3304
+ const slot = slotFor(sidOf(ctx));
3305
+ slot.rejectStreak = ok ? 0 : slot.rejectStreak + 1;
3306
+ return slot.rejectStreak;
3307
+ }
3298
3308
  return {
3299
3309
  core,
3300
3310
  get adapter() {
@@ -3314,6 +3324,7 @@ function createRuntime(adapter) {
3314
3324
  foldStream,
3315
3325
  stateFor,
3316
3326
  commitFoldState,
3327
+ noteCompressOutcome,
3317
3328
  forgetSession,
3318
3329
  primeFold,
3319
3330
  acquireLock
@@ -3444,7 +3455,7 @@ async function handleCompress(args, runtime, ctx, toolCallId) {
3444
3455
  const invalidRanges = rangeSpecs.filter((r) => !r.startRef || !r.endRef || typeof r.startRef !== "string" || typeof r.endRef !== "string");
3445
3456
  if (invalidRanges.length > 0) {
3446
3457
  logError("compress", { sid: ctx.sessionManager.getSessionId(), event: "invalid-ranges", count: invalidRanges.length, ranges: invalidRanges.map((r) => `${r.startRef}..${r.endRef}`) });
3447
- return `Rejected: ${invalidRanges.length} range(s) have invalid startId or endId (missing or non-string). All ranges must have valid message refs (e.g. "m00005") or block IDs (e.g. "b3"). No changes applied \u2014 run acp_status for current refs.`;
3458
+ return rejectionMessage(ctx, runtime, `Rejected: ${invalidRanges.length} range(s) have invalid startId or endId (missing or non-string). All ranges must have valid message refs (e.g. "m00005") or block IDs (e.g. "b3"). No changes applied \u2014 run acp_status for current refs.`);
3448
3459
  }
3449
3460
  let applied;
3450
3461
  try {
@@ -3456,12 +3467,13 @@ async function handleCompress(args, runtime, ctx, toolCallId) {
3456
3467
  });
3457
3468
  } catch (e) {
3458
3469
  logThrow("compress", e, { sid: ctx.sessionManager.getSessionId(), phase: "applyCompression", ranges: rangeSpecs.length });
3459
- return `Compression failed: ${e instanceof Error ? e.message : String(e)}. No changes applied \u2014 state is unchanged.`;
3470
+ return rejectionMessage(ctx, runtime, `Compression failed: ${e instanceof Error ? e.message : String(e)}. No changes applied \u2014 state is unchanged.`);
3460
3471
  }
3461
3472
  if (applied.result.errors.length > 0) {
3462
3473
  logError("compress", { sid: ctx.sessionManager.getSessionId(), event: "apply-errors", count: applied.result.errors.length, errors: applied.result.errors.slice(0, 5) });
3463
- return `Compression rejected: ${applied.result.errors.join("; ")}. No changes applied \u2014 run acp_status to verify current state.`;
3474
+ return rejectionMessage(ctx, runtime, `Compression rejected: ${applied.result.errors.join("; ")}. No changes applied \u2014 run acp_status to verify current state.`);
3464
3475
  }
3476
+ runtime.noteCompressOutcome(ctx, true);
3465
3477
  await runtime.commitFoldState(ctx, applied.state, toolCallId);
3466
3478
  const { blocksCreated, tokensCompressed, warnings } = applied.result;
3467
3479
  const afterTokens = Math.max(0, beforeTokens - tokensCompressed);
@@ -3502,6 +3514,22 @@ async function handleCompress(args, runtime, ctx, toolCallId) {
3502
3514
  releaseLock();
3503
3515
  }
3504
3516
  }
3517
+ var LOOP_GUARD_STOP = 3;
3518
+ var LOOP_GUARD_SUPPRESS = 4;
3519
+ function rejectionMessage(ctx, runtime, base) {
3520
+ const streak = runtime.noteCompressOutcome(ctx, false);
3521
+ if (streak >= LOOP_GUARD_SUPPRESS) {
3522
+ logWarn("compress", { sid: ctx.sessionManager.getSessionId(), event: "loop-guard", streak, mode: "suppressed" });
3523
+ return `Compression rejected (again \u2014 ${streak} consecutive rejections). No changes applied. STOP calling compress; it is not converging. Continue the task. Compress stays available: a fresh attempt works when acp_status shows a range that can meet the minimum size.`;
3524
+ }
3525
+ if (streak >= LOOP_GUARD_STOP) {
3526
+ logWarn("compress", { sid: ctx.sessionManager.getSessionId(), event: "loop-guard", streak, mode: "stop-directive" });
3527
+ return `${base}
3528
+
3529
+ STOP: ${streak} compress calls rejected in a row. Do NOT retry the same range. Run acp_status to see what is actually compressible now; if no range can meet the minimum size, nothing is left to compress \u2014 stop and continue the actual task.`;
3530
+ }
3531
+ return base;
3532
+ }
3505
3533
 
3506
3534
  // src/decompress-tool.ts
3507
3535
  import { type as type2 } from "@oh-my-pi/omptype";
@@ -4464,7 +4492,7 @@ async function statusReport(runtime, ctx) {
4464
4492
  const coveredIds = collectCoveredMessageIds(state);
4465
4493
  const sentTokens = estimateTokens(coreMessages, coveredIds) + systemPromptTokens;
4466
4494
  const turn = runtime.core.processTurn({ messages: coreMessages, state, config, tokenCount: sentTokens });
4467
- const versionStr = "0.1.8" ? `billion-context-omp@${"0.1.8"}` : void 0;
4495
+ const versionStr = "0.1.9" ? `billion-context-omp@${"0.1.9"}` : void 0;
4468
4496
  return buildStatusPanel({
4469
4497
  version: versionStr,
4470
4498
  tokenCount: sessionTokens,
@@ -4570,7 +4598,7 @@ async function summarizeMessages(ctx, messages, prompts, configuredModel, opts)
4570
4598
  User instructions for this compaction: ${custom}`;
4571
4599
  const userText = `ENTIRE conversation to compress (${slice.length} messages, ~${tokens} tokens). Compress it:
4572
4600
 
4573
- ` + formatSlice(slice, createInitialState());
4601
+ ` + formatSlice(slice, opts?.messageRefs ? { ...createInitialState(), messageRefs: opts.messageRefs } : createInitialState());
4574
4602
  const response = await run(
4575
4603
  model,
4576
4604
  { systemPrompt: [instructions], messages: [{ role: "user", content: [{ type: "text", text: userText }], timestamp: Date.now() }] },
@@ -4638,6 +4666,8 @@ WHEN NOT TO COMPRESS
4638
4666
  - Content the current task step is actively reading or reasoning about.
4639
4667
  - Important user messages \u2014 preserve their exact intent, constraints, and acceptance criteria. If a message in the range must stay verbatim, exclude it from the compress range instead of compressing it.
4640
4668
  - Protected tool outputs \u2014 hard-excluded from compression ranges, survive intact in visible context.
4669
+ - Nothing left worth compressing \u2014 if no compressible range can meet the minimum size, do NOT call compress at all. Compression is maintenance, never the task goal; when there is nothing to compress, do the task.
4670
+ - A rejected compress call \u2014 never retry a rejected range unchanged. Re-check acp_status first; after 3 rejections, stop compressing and continue the task.
4641
4671
 
4642
4672
  ${prompts.howToCompressRules}
4643
4673
 
@@ -4784,7 +4814,7 @@ var PACKAGE_NAME = "billion-context-omp";
4784
4814
  var REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
4785
4815
  var SEMVER_RE = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z-.]+)?$/;
4786
4816
  var CHECK_INTERVAL_MS = 3 * 60 * 1e3;
4787
- var THROTTLE_FILE = join4(homeDir(), CONFIG_DIR_NAME3, ".billion-context-omp-update-check");
4817
+ var throttleFile = () => join4(homeDir(), CONFIG_DIR_NAME3, ".billion-context-omp-update-check");
4788
4818
  var updateInFlight = false;
4789
4819
  function parseVersion(v) {
4790
4820
  return v.replace(/^v/, "").split(".").map((n) => parseInt(n, 10) || 0);
@@ -4800,7 +4830,7 @@ function isNewer(latest, current) {
4800
4830
  }
4801
4831
  async function readLastCheck() {
4802
4832
  try {
4803
- const data = await readFile(THROTTLE_FILE, "utf-8");
4833
+ const data = await readFile(throttleFile(), "utf-8");
4804
4834
  return parseInt(data.trim(), 10) || 0;
4805
4835
  } catch {
4806
4836
  return 0;
@@ -4808,8 +4838,8 @@ async function readLastCheck() {
4808
4838
  }
4809
4839
  async function writeLastCheck(timestamp) {
4810
4840
  try {
4811
- await mkdir2(dirname3(THROTTLE_FILE), { recursive: true });
4812
- await writeFile2(THROTTLE_FILE, String(timestamp), "utf-8");
4841
+ await mkdir2(dirname3(throttleFile()), { recursive: true });
4842
+ await writeFile2(throttleFile(), String(timestamp), "utf-8");
4813
4843
  } catch {
4814
4844
  }
4815
4845
  }
@@ -4891,12 +4921,12 @@ async function checkForUpdate(autoUpdate, notify) {
4891
4921
  const now = Date.now();
4892
4922
  const lastCheck = await readLastCheck();
4893
4923
  if (now - lastCheck < CHECK_INTERVAL_MS) return;
4894
- await writeLastCheck(now);
4895
4924
  const runtimeVersion = await getRuntimeVersion();
4896
4925
  const res = await fetch(REGISTRY_URL, {
4897
4926
  signal: AbortSignal.timeout(5e3),
4898
4927
  headers: { Accept: "application/json" }
4899
4928
  });
4929
+ await writeLastCheck(now);
4900
4930
  if (!res.ok) {
4901
4931
  logWarn("update", { event: "check-http", status: res.status });
4902
4932
  return;
@@ -4904,7 +4934,7 @@ async function checkForUpdate(autoUpdate, notify) {
4904
4934
  const data = await res.json();
4905
4935
  const latest = data.version;
4906
4936
  if (!latest) return;
4907
- const current = runtimeVersion ?? "0.1.8";
4937
+ const current = runtimeVersion ?? "0.1.9";
4908
4938
  const hasUpdate = isNewer(latest, current);
4909
4939
  debug.event("update-check", {
4910
4940
  current,
@@ -5181,11 +5211,13 @@ function wireCompactionDisable(pi, runtime) {
5181
5211
  const prep = event.preparation;
5182
5212
  const toSummarize = [...prep.messagesToSummarize ?? [], ...prep.turnPrefixMessages ?? []];
5183
5213
  if (toSummarize.length === 0) return void 0;
5214
+ const slot = await runtime.stateFor(ctx);
5184
5215
  ctx.ui?.notify?.(`ACP: compacting ${toSummarize.length} messages\u2026`, "info");
5185
5216
  const result = await summarizeMessages(ctx, toSummarize, runtime.prompts, runtime.adapter.compress?.compressModel, {
5186
5217
  previousSummary: prep.previousSummary,
5187
5218
  customInstructions: event.customInstructions,
5188
- signal: event.signal
5219
+ signal: event.signal,
5220
+ messageRefs: slot.state.messageRefs
5189
5221
  });
5190
5222
  if (!result) {
5191
5223
  ctx.ui?.notify?.("ACP: compression fell back to Pi native compaction", "warning");
@@ -5216,7 +5248,7 @@ function wireCompactionDisable(pi, runtime) {
5216
5248
  function wireSessionLifecycle(pi, runtime) {
5217
5249
  pi.on("session_start", async (_event, ctx) => {
5218
5250
  const sid = ctx.sessionManager.getSessionId();
5219
- logInfo("session", { event: "start", sid, cwd: ctx.cwd, debug: runtime.adapter.debug ?? null, version: true ? "0.1.8" : null });
5251
+ logInfo("session", { event: "start", sid, cwd: ctx.cwd, debug: runtime.adapter.debug ?? null, version: true ? "0.1.9" : null });
5220
5252
  try {
5221
5253
  const user = await loadUserConfig(ctx.cwd);
5222
5254
  runtime.setAdapter(applyUserConfig(runtime.adapter, user));
@@ -5255,6 +5287,9 @@ function wireContextTransform(pi, runtime) {
5255
5287
  }
5256
5288
  debug.event("context-in-raw", { sid, msgs: input.length });
5257
5289
  const { state, coreMessages, originalById, streamLen } = runtime.foldStream(ctx, input);
5290
+ const preTurnNudgeBaseline = state.nudge.lastPerMessageNudgeTokens;
5291
+ const preTurnNudgeShownTokens = state.nudge.lastNudgeShownTokens;
5292
+ const preTurnNudgeShownByTier = state.nudge.lastShownByTier;
5258
5293
  const config = runtime.configFor(ctx);
5259
5294
  const coveredIds = collectCoveredMessageIds(state);
5260
5295
  const systemPromptTokens = estimateTextTokens2(getSystemPromptText(ctx) ?? "");
@@ -5310,29 +5345,49 @@ function wireContextTransform(pi, runtime) {
5310
5345
  rebuiltMsgs: rebuilt.length
5311
5346
  });
5312
5347
  const debugOn2 = debug.enabled;
5348
+ let nudgeInjected = false;
5313
5349
  if (turn.nudge?.shouldInject) {
5314
- const emergency = turn.nudge.breakdown?.emergencyOverride === 1;
5315
- {
5316
- turn.nudge.compressibleRanges = viableRanges(turn.nudge.compressibleRanges);
5317
- const rendered = renderNudgeText(turn.nudge, runtime.prompts);
5318
- const top = [...turn.nudge.compressibleRanges].sort((a, b) => b.tokens - a.tokens)[0];
5319
- const example = top ? `
5350
+ const lastUser = [...input].reverse().find((m) => m.role === "user");
5351
+ const tailText = lastUser ? JSON.stringify(lastUser.content ?? "") : "";
5352
+ const isFeedbackView = tailText.includes("efficiency nudge to compress early") || tailText.includes("Context limit reached");
5353
+ if (isFeedbackView) {
5354
+ debug.event("nudge-feedback-skip", { sid: ctx.sessionManager.getSessionId(), msgs: input.length });
5355
+ } else {
5356
+ const emergency = turn.nudge.breakdown?.emergencyOverride === 1;
5357
+ const epochReset = turn.state.nudge.lastPerMessageNudgeTokens !== preTurnNudgeBaseline;
5358
+ const prevShown = epochReset ? 0 : preTurnNudgeShownTokens;
5359
+ const cadenceFloor = turn.nudge.breakdown?.growthFloor ?? 0;
5360
+ const suppressed = !emergency && prevShown > 0 && tokenCount - prevShown < cadenceFloor;
5361
+ if (suppressed) {
5362
+ turn.state.nudge.lastNudgeShownTokens = prevShown;
5363
+ turn.state.nudge.lastShownByTier = preTurnNudgeShownByTier;
5364
+ logInfo("nudge", { sid, event: "cadence-suppressed", growth: tokenCount - prevShown, floor: cadenceFloor, pct: Math.round(turn.nudge.contextUsage * 100), reason: turn.nudge.reason });
5365
+ debug.event("nudge-suppressed", { sid, growth: tokenCount - prevShown, floor: cadenceFloor, pct: Math.round(turn.nudge.contextUsage * 100), reason: turn.nudge.reason });
5366
+ } else {
5367
+ nudgeInjected = true;
5368
+ {
5369
+ turn.nudge.compressibleRanges = viableRanges(turn.nudge.compressibleRanges);
5370
+ const rendered = renderNudgeText(turn.nudge, runtime.prompts);
5371
+ const top = [...turn.nudge.compressibleRanges].sort((a, b) => b.tokens - a.tokens)[0];
5372
+ const example = top ? `
5320
5373
 
5321
5374
  Example: compress({ content: [{ startId: "${top.startRef}", endId: "${top.endRef}", summary: "..." }] })` : "";
5322
- rebuilt.push(nudgeMessage(turn.nudge, turn.state.blocks.filter((b) => b.active), runtime.prompts, example));
5323
- if (emergency) {
5324
- logWarn("nudge", { sid: ctx.sessionManager.getSessionId(), event: "emergency-inject", pct: Math.round(turn.nudge.contextUsage * 100), voice: rendered.voice, compressible: turn.nudge.compressibleRanges.length });
5325
- }
5326
- if (debugOn2 && ctx.hasUI) {
5327
- ctx.ui.notify(`[ACP nudge \u2192 context]${emergency ? " [EMERGENCY]" : ""}
5375
+ rebuilt.push(nudgeMessage(turn.nudge, turn.state.blocks.filter((b) => b.active), runtime.prompts, example));
5376
+ if (emergency) {
5377
+ logWarn("nudge", { sid: ctx.sessionManager.getSessionId(), event: "emergency-inject", pct: Math.round(turn.nudge.contextUsage * 100), voice: rendered.voice, compressible: turn.nudge.compressibleRanges.length });
5378
+ }
5379
+ if (debugOn2 && ctx.hasUI) {
5380
+ ctx.ui.notify(`[ACP nudge \u2192 context]${emergency ? " [EMERGENCY]" : ""}
5328
5381
  ${rendered.text}${example}`);
5382
+ }
5383
+ debug.event("nudge-injected", { sid: ctx.sessionManager.getSessionId(), voice: rendered.voice, channels: ["context", debugOn2 ? "terminal" : null].filter(Boolean), emergency, text: rendered.text + example });
5384
+ }
5329
5385
  }
5330
- debug.event("nudge-injected", { sid: ctx.sessionManager.getSessionId(), voice: rendered.voice, channels: ["context", debugOn2 ? "terminal" : null].filter(Boolean), emergency, text: rendered.text + example });
5331
5386
  }
5332
5387
  }
5333
5388
  dumpContextMessages(rebuilt, {
5334
5389
  sid,
5335
- injected: turn.nudge?.shouldInject ?? false,
5390
+ injected: nudgeInjected,
5336
5391
  emergency: turn.nudge?.breakdown?.emergencyOverride === 1
5337
5392
  });
5338
5393
  await checkForUpdate(runtime.adapter.autoUpdate ?? true, (msg) => {