billion-context-omp 0.2.2 → 0.2.3

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/README.md CHANGED
@@ -77,7 +77,7 @@ omp's built-in `/compact` is intercepted and replaced by an ACP model-summarized
77
77
  |------|-------------|
78
78
  | `compress` | Replace a contiguous message range with a detailed summary |
79
79
  | `decompress` | Restore a previously compressed block's content (to file by default; `inline:true` for single messages) |
80
- | `search_context` | Search compressed block summaries (and visible messages) by keyword |
80
+ | `search_context` | Search compressed block summaries and the original messages folded into them by keyword (visible messages are not indexed) |
81
81
  | `acp_status` | Show context usage, compressed blocks, compressible ranges |
82
82
 
83
83
  > The `acp_delegate` sub-agent subsystem from the Pi build is intentionally **not** registered — omp ships its own multi-agent orchestration, and duplicate delegation tools would conflict.
package/README.zh-CN.md CHANGED
@@ -77,7 +77,7 @@ omp 内置的 `/compact` 被拦截,替换为 ACP 模型摘要式 compaction,
77
77
  |------|------|
78
78
  | `compress` | 把一段连续消息区间替换为详细摘要 |
79
79
  | `decompress` | 恢复之前压缩的块内容(默认写文件;单条消息可 `inline:true`) |
80
- | `search_context` | 按关键字搜索压缩块摘要(及可见消息) |
80
+ | `search_context` | 按关键字搜索压缩块摘要及被折叠其中的原始消息(可见消息不建索引) |
81
81
  | `acp_status` | 显示上下文用量、压缩块、可压缩区间 |
82
82
 
83
83
  > Pi 版中的 `acp_delegate` 子代理系统在此**有意不注册**——omp 自带多代理编排,重复的委派工具会冲突。
@@ -12,4 +12,5 @@ declare const DecompressParams: import("@oh-my-pi/omptype").FluentType<{
12
12
  toFile?: string | undefined;
13
13
  }>;
14
14
  export declare function makeDecompressTool(runtime: AcpRuntime): ToolDefinition<typeof DecompressParams>;
15
+ export declare function pruneAutoFiles(): void;
15
16
  export {};
package/dist/index.js CHANGED
@@ -2769,8 +2769,12 @@ function resolveConfig(adapter, liveContextLimit) {
2769
2769
  return config;
2770
2770
  }
2771
2771
  function parsePercent(v) {
2772
- const n = typeof v === "number" ? v : v.trim().endsWith("%") ? Number(v.trim().slice(0, -1)) / 100 : Number(v);
2773
- if (!Number.isFinite(n)) return 0;
2772
+ const raw = typeof v === "number" ? v : v.trim().endsWith("%") ? Number(v.trim().slice(0, -1)) : Number(v);
2773
+ if (!Number.isFinite(raw)) return 0;
2774
+ const n = raw > 1 ? raw / 100 : raw;
2775
+ if (n > 1 || n < 0) {
2776
+ logWarn("config", { event: "percent-clamped", value: String(v), clamped: Math.min(1, Math.max(0, n)) });
2777
+ }
2774
2778
  return Math.min(1, Math.max(0, n));
2775
2779
  }
2776
2780
 
@@ -3159,7 +3163,7 @@ function rangeFingerprints(ranges, coreMessages, byRef, blocks) {
3159
3163
 
3160
3164
  // src/runtime.ts
3161
3165
  function freshSlot(preserveFrom) {
3162
- const slot = { identities: [], foldedLen: 0, preview: false, state: createInitialState(), coreMessages: [], appliedCallIds: /* @__PURE__ */ new Set(), rejectStreak: 0 };
3166
+ const slot = { identities: [], foldedLen: 0, preview: false, state: createInitialState(), coreMessages: [], appliedCallIds: /* @__PURE__ */ new Set(), rejectStreak: 0, lastRebuiltOutput: null };
3163
3167
  if (preserveFrom) {
3164
3168
  slot.state = { ...slot.state, nudge: preserveFrom.state.nudge };
3165
3169
  slot.rejectStreak = preserveFrom.rejectStreak;
@@ -3223,6 +3227,17 @@ function createRuntime(adapter) {
3223
3227
  slots.set(sid, slot);
3224
3228
  }
3225
3229
  const ids = stream.map(messageIdentity);
3230
+ const lastOut = slot.lastRebuiltOutput;
3231
+ if (lastOut !== null && ids.length === lastOut.length && ids.every((id, i) => id === lastOut[i])) {
3232
+ const coreMessages2 = streamToCoreMessages(stream);
3233
+ const originalById2 = /* @__PURE__ */ new Map();
3234
+ stream.forEach((message, i) => originalById2.set(`p${i + 1}`, message));
3235
+ slot.identities = ids;
3236
+ slot.foldedLen = ids.length;
3237
+ slot.coreMessages = coreMessages2;
3238
+ debug.event("feedback-reuse", { sid, msgs: ids.length, blocks: slot.state.blocks.length });
3239
+ return { state: slot.state, coreMessages: coreMessages2, originalById: originalById2, streamLen: ids.length };
3240
+ }
3226
3241
  let lcp = 0;
3227
3242
  while (lcp < Math.min(ids.length, slot.identities.length) && ids[lcp] === slot.identities[lcp]) lcp++;
3228
3243
  if (lcp < slot.foldedLen) {
@@ -3311,6 +3326,10 @@ function createRuntime(adapter) {
3311
3326
  slot.state = state;
3312
3327
  if (toolCallId) slot.appliedCallIds.add(toolCallId);
3313
3328
  }
3329
+ function recordRebuiltOutput(ctx, rebuilt) {
3330
+ const slot = slotFor(sidOf(ctx));
3331
+ slot.lastRebuiltOutput = rebuilt.map(messageIdentity);
3332
+ }
3314
3333
  function noteCompressOutcome(ctx, ok) {
3315
3334
  const slot = slotFor(sidOf(ctx));
3316
3335
  slot.rejectStreak = ok ? 0 : slot.rejectStreak + 1;
@@ -3335,6 +3354,7 @@ function createRuntime(adapter) {
3335
3354
  foldStream,
3336
3355
  stateFor,
3337
3356
  commitFoldState,
3357
+ recordRebuiltOutput,
3338
3358
  noteCompressOutcome,
3339
3359
  forgetSession,
3340
3360
  primeFold,
@@ -3435,11 +3455,13 @@ function makeCompressTool(runtime) {
3435
3455
  }
3436
3456
  async function handleCompress(args, runtime, ctx, toolCallId) {
3437
3457
  const ranges = args.content ?? [];
3438
- if (ranges.length === 0) return "No ranges provided.";
3458
+ if (ranges.length === 0) {
3459
+ return rejectionMessage(ctx, runtime, "No ranges provided. No changes applied \u2014 send content: [{ startId, endId, summary }].");
3460
+ }
3439
3461
  const invalid = ranges.filter((r) => !r || typeof r.summary !== "string" || !r.summary.trim() || !r.startId || !r.endId);
3440
3462
  if (invalid.length > 0) {
3441
3463
  logWarn("compress", { sid: ctx.sessionManager.getSessionId(), event: "invalid-ranges", count: invalid.length });
3442
- return `Every range needs startId, endId and a summary (min 50 chars) \u2014 got ${invalid.length} range(s) missing fields. Re-send the FULL ranges array with summaries included.`;
3464
+ return rejectionMessage(ctx, runtime, `Every range needs startId, endId and a summary (min 50 chars) \u2014 got ${invalid.length} range(s) missing fields. Re-send the FULL ranges array with summaries included. No changes applied \u2014 run acp_status for current refs.`);
3443
3465
  }
3444
3466
  const releaseLock = await runtime.acquireLock(ctx.sessionManager.getSessionId());
3445
3467
  try {
@@ -3550,10 +3572,12 @@ STOP: ${streak} compress calls rejected in a row. Do NOT retry the same range. R
3550
3572
  // src/decompress-tool.ts
3551
3573
  import { type as type2 } from "@oh-my-pi/omptype";
3552
3574
  import { writeFile, mkdir } from "fs/promises";
3553
- import { realpathSync } from "fs";
3575
+ import { realpathSync, readdirSync, statSync as statSync2, unlinkSync } from "fs";
3554
3576
  import { resolve, relative, isAbsolute, join as join2, dirname as dirname2, basename } from "path";
3555
3577
  import { tmpdir } from "os";
3556
- var AUTO_DIR = join2(homeDir() || tmpdir(), ".cache", "omp", "acp-decompress");
3578
+ function autoDir() {
3579
+ return join2(homeDir() || tmpdir(), ".cache", "omp", "acp-decompress");
3580
+ }
3557
3581
  var PREVIEW_CHARS = 600;
3558
3582
  var MESSAGE_INLINE_THRESHOLD = 2e3;
3559
3583
  var DecompressParams = type2({
@@ -3618,7 +3642,31 @@ function resolveToFilePath(targetPath) {
3618
3642
  return resolved;
3619
3643
  }
3620
3644
  function autoFilePath(blockId) {
3621
- return join2(AUTO_DIR, `${blockId}-${Date.now()}.txt`);
3645
+ return join2(autoDir(), `${blockId}-${Date.now()}.txt`);
3646
+ }
3647
+ var MAX_AUTO_FILES = 100;
3648
+ function pruneAutoFiles() {
3649
+ const dir = autoDir();
3650
+ try {
3651
+ const files = readdirSync(dir).map((f) => {
3652
+ try {
3653
+ return { f, m: statSync2(join2(dir, f)).mtimeMs };
3654
+ } catch {
3655
+ return null;
3656
+ }
3657
+ }).filter((x) => x !== null);
3658
+ if (files.length <= MAX_AUTO_FILES) return;
3659
+ files.sort((a, b) => a.m - b.m);
3660
+ const excess = files.slice(0, files.length - MAX_AUTO_FILES);
3661
+ for (const { f } of excess) {
3662
+ try {
3663
+ unlinkSync(join2(dir, f));
3664
+ } catch {
3665
+ }
3666
+ }
3667
+ debug.event("decompress-pruned", { dir, removed: excess.length });
3668
+ } catch {
3669
+ }
3622
3670
  }
3623
3671
  function headPreview(text) {
3624
3672
  if (text.length <= PREVIEW_CHARS) return text;
@@ -3652,8 +3700,9 @@ ${text}`;
3652
3700
  logError("decompress", { sid: ctx.sessionManager.getSessionId(), event: "message-path-rejected", ref, toFile: args.toFile });
3653
3701
  return targetPath.error;
3654
3702
  }
3655
- await mkdir(AUTO_DIR, { recursive: true }).catch((e) => logError("decompress", { event: "mkdir-failed", dir: AUTO_DIR, error: e instanceof Error ? e.message : String(e) }));
3703
+ await mkdir(autoDir(), { recursive: true }).catch((e) => logError("decompress", { event: "mkdir-failed", dir: autoDir(), error: e instanceof Error ? e.message : String(e) }));
3656
3704
  await writeFile(targetPath, text, "utf8");
3705
+ if (!args.toFile) pruneAutoFiles();
3657
3706
  debug.event("decompress-message", { ref, ownerBlockId, mode: "file", path: targetPath, chars: text.length });
3658
3707
  logInfo("decompress", { sid: ctx.sessionManager.getSessionId(), event: "message", mode: "file", ref, ownerBlockId, path: targetPath, chars: text.length });
3659
3708
  return [
@@ -3695,8 +3744,9 @@ ${text}`;
3695
3744
  logError("decompress", { sid: ctx.sessionManager.getSessionId(), event: "block-path-rejected", blockId, toFile: args.toFile });
3696
3745
  return targetPath.error;
3697
3746
  }
3698
- await mkdir(AUTO_DIR, { recursive: true }).catch((e) => logError("decompress", { event: "mkdir-failed", dir: AUTO_DIR, error: e instanceof Error ? e.message : String(e) }));
3747
+ await mkdir(autoDir(), { recursive: true }).catch((e) => logError("decompress", { event: "mkdir-failed", dir: autoDir(), error: e instanceof Error ? e.message : String(e) }));
3699
3748
  await writeFile(targetPath, text, "utf8");
3749
+ if (!args.toFile) pruneAutoFiles();
3700
3750
  debug.event("decompress", { blockId, full, count, mode: "file", path: targetPath, chars: text.length });
3701
3751
  logInfo("decompress", { sid: ctx.sessionManager.getSessionId(), event: "block", mode: "file", blockId, full, count, path: targetPath, chars: text.length });
3702
3752
  const itemWord = count === 1 ? "item" : "items";
@@ -4508,7 +4558,7 @@ async function statusReport(runtime, ctx) {
4508
4558
  const coveredIds = collectCoveredMessageIds(state);
4509
4559
  const sentTokens = estimateTokens(coreMessages, coveredIds) + systemPromptTokens;
4510
4560
  const turn = runtime.core.processTurn({ messages: coreMessages, state, config, tokenCount: sentTokens });
4511
- const versionStr = "0.2.2" ? `billion-context-omp@${"0.2.2"}` : void 0;
4561
+ const versionStr = "0.2.3" ? `billion-context-omp@${"0.2.3"}` : void 0;
4512
4562
  return buildStatusPanel({
4513
4563
  version: versionStr,
4514
4564
  tokenCount: sessionTokens,
@@ -4893,7 +4943,7 @@ You have four context-management tools:
4893
4943
 
4894
4944
  - compress \u2014 Replace a contiguous range of older conversation with a single detailed summary you write. Use when content is genuinely consumed (no longer needed for the current task step). Single range: compress({ content: [{ topic: "Session Opener", startId: "m00150", endId: "m00220", summary: "..." }] }) \u2014 topic is recommended but optional. Batch (multiple unrelated ranges, each with its own topic): compress({ content: [{ topic: "Auth", startId: "m00150", endId: "m00220", summary: "..." }, { topic: "Deploy", startId: "m00300", endId: "m00350", summary: "..." }] }). Call it as a normal tool \u2014 summaries are plain string arguments.
4895
4945
  - decompress \u2014 Restore a previously compressed block's content. The block stays compressed \u2014 context and cache prefix are not disrupted. By DEFAULT content is written to an auto-generated file (avoids context bloat); use the read tool to view it. Pass inline:true to return content in the tool result instead (appends to context). full:true recurses to original messages. Example: decompress({ blockId: "b5" }) or decompress({ blockId: "b5", full: true }) or decompress({ blockId: "b5", inline: true }).
4896
- - search_context \u2014 Search compressed block summaries (and optionally visible messages) by keyword. Use BEFORE decompressing to find the right block. Example: search_context({ query: "auth token refresh" }).
4946
+ - search_context \u2014 Search compressed block summaries AND the original messages folded into them by keyword (messages still visible in context are not indexed \u2014 you can already see them). Use BEFORE decompressing to find the right block. Example: search_context({ query: "auth token refresh" }).
4897
4947
  - acp_status \u2014 Context status with compressible ranges. No args = overview + totals. scope:"uncompressed" for range view; add view:"messages" for per-message listing. scope:"compressed" for block details.
4898
4948
 
4899
4949
  ${prompts.compressPhilosophy}
@@ -5216,7 +5266,7 @@ async function checkForUpdate(autoUpdate, notify) {
5216
5266
  const data = await res.json();
5217
5267
  const latest = data.version;
5218
5268
  if (!latest) return;
5219
- const current = runtimeVersion ?? "0.2.2";
5269
+ const current = runtimeVersion ?? "0.2.3";
5220
5270
  const hasUpdate = isNewer(latest, current);
5221
5271
  debug.event("update-check", {
5222
5272
  current,
@@ -5251,20 +5301,20 @@ async function getRuntimeVersion() {
5251
5301
  }
5252
5302
 
5253
5303
  // src/dump.ts
5254
- import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, readdirSync, unlinkSync } from "fs";
5304
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, readdirSync as readdirSync2, unlinkSync as unlinkSync2 } from "fs";
5255
5305
  import * as path2 from "path";
5256
5306
  import { CONFIG_DIR_NAME as CONFIG_DIR_NAME4 } from "@oh-my-pi/pi-utils";
5257
5307
  var counters = {};
5258
5308
  var MAX_FILES_PER_PREFIX = 200;
5259
5309
  function pruneDumps(dir, prefixTest, seqOf) {
5260
5310
  try {
5261
- const files = readdirSync(dir).filter(prefixTest).map((f) => ({ f, n: seqOf(f) })).filter((x) => !Number.isNaN(x.n));
5311
+ const files = readdirSync2(dir).filter(prefixTest).map((f) => ({ f, n: seqOf(f) })).filter((x) => !Number.isNaN(x.n));
5262
5312
  if (files.length <= MAX_FILES_PER_PREFIX) return;
5263
5313
  files.sort((a, b) => a.n - b.n);
5264
5314
  const excess = files.slice(0, files.length - MAX_FILES_PER_PREFIX);
5265
5315
  for (const { f } of excess) {
5266
5316
  try {
5267
- unlinkSync(path2.join(dir, f));
5317
+ unlinkSync2(path2.join(dir, f));
5268
5318
  } catch {
5269
5319
  }
5270
5320
  }
@@ -5282,7 +5332,7 @@ function dumpContextMessages(messages, meta) {
5282
5332
  mkdirSync2(dir, { recursive: true });
5283
5333
  if (!(dir in counters)) {
5284
5334
  try {
5285
- const existing = readdirSync(dir).filter((f) => /^\d{4}\.json$/.test(f));
5335
+ const existing = readdirSync2(dir).filter((f) => /^\d{4}\.json$/.test(f));
5286
5336
  const max = existing.reduce((mx, f) => {
5287
5337
  const n = parseInt(f, 10);
5288
5338
  return Number.isNaN(n) ? mx : Math.max(mx, n);
@@ -5365,7 +5415,7 @@ function dumpProviderRequest(payload, meta) {
5365
5415
  mkdirSync2(dir, { recursive: true });
5366
5416
  if (!("req" in counters)) {
5367
5417
  try {
5368
- const existing = readdirSync(dir).filter((f) => /^req_\d+\.json$/.test(f));
5418
+ const existing = readdirSync2(dir).filter((f) => /^req_\d+\.json$/.test(f));
5369
5419
  const max = existing.reduce((mx, f) => {
5370
5420
  const n = parseInt(f.slice(4, -5), 10);
5371
5421
  return Number.isNaN(n) ? mx : Math.max(mx, n);
@@ -5404,10 +5454,11 @@ import * as path3 from "path";
5404
5454
  import { CONFIG_DIR_NAME as CONFIG_DIR_NAME5 } from "@oh-my-pi/pi-utils";
5405
5455
  async function loadUserConfig(cwd) {
5406
5456
  const home = homeDir();
5457
+ const globalDir = join8(home, CONFIG_DIR_NAME5);
5407
5458
  const merged = {};
5408
- for (const base of [join8(home, CONFIG_DIR_NAME5), join8(cwd, CONFIG_DIR_NAME5)]) {
5459
+ for (const base of [globalDir, join8(cwd, CONFIG_DIR_NAME5)]) {
5409
5460
  const file = join8(base, "acp-omp.json");
5410
- const allowPrompts = base.startsWith(home);
5461
+ const allowPrompts = base === globalDir;
5411
5462
  try {
5412
5463
  const raw = await fs.readFile(file, "utf8");
5413
5464
  const parsed = JSON.parse(raw);
@@ -5532,9 +5583,9 @@ function wireCompactionDisable(pi, runtime) {
5532
5583
  function wireSessionLifecycle(pi, runtime) {
5533
5584
  pi.on("session_start", async (_event, ctx) => {
5534
5585
  const sid = ctx.sessionManager.getSessionId();
5535
- logInfo("session", { event: "start", sid, cwd: ctx.cwd, debug: runtime.adapter.debug ?? null, version: true ? "0.2.2" : null });
5586
+ logInfo("session", { event: "start", sid, cwd: ctx.cwd, debug: runtime.adapter.debug ?? null, version: true ? "0.2.3" : null });
5536
5587
  const selfPath = import.meta.url;
5537
- const conflict = stampAndDetect(selfPath, true ? "0.2.2" : null);
5588
+ const conflict = stampAndDetect(selfPath, true ? "0.2.3" : null);
5538
5589
  if (conflict) {
5539
5590
  logWarn("instance", { event: "dual-instance", self: selfPath, other: conflict.path, otherPid: conflict.pid, otherVersion: conflict.version });
5540
5591
  try {
@@ -5573,6 +5624,7 @@ function wireSessionLifecycle(pi, runtime) {
5573
5624
  async function transformStream(ctx, runtime, input, mode) {
5574
5625
  const sid = ctx.sessionManager.getSessionId();
5575
5626
  const release = await runtime.acquireLock(sid);
5627
+ let result;
5576
5628
  try {
5577
5629
  if (input.length === 0) {
5578
5630
  debug.event("empty-stream-bypass", { sid });
@@ -5678,21 +5730,25 @@ ${rendered.text}${example}`);
5678
5730
  }
5679
5731
  }
5680
5732
  }
5733
+ if (mode === "context") {
5734
+ runtime.recordRebuiltOutput(ctx, rebuilt);
5735
+ }
5681
5736
  dumpContextMessages(rebuilt, {
5682
5737
  sid,
5683
5738
  injected: nudgeInjected,
5684
5739
  emergency: turn.nudge?.breakdown?.emergencyOverride === 1
5685
5740
  });
5686
- await checkForUpdate(runtime.adapter.autoUpdate ?? true, (msg) => {
5687
- if (ctx.hasUI) ctx.ui.notify(msg);
5688
- });
5689
- return { rebuilt, nudgeInjected };
5741
+ result = { rebuilt, nudgeInjected };
5690
5742
  } catch (e) {
5691
5743
  logThrow("context", e, { sid, phase: "transform", mode });
5692
5744
  throw e;
5693
5745
  } finally {
5694
5746
  release();
5695
5747
  }
5748
+ await checkForUpdate(runtime.adapter.autoUpdate ?? true, (msg) => {
5749
+ if (ctx.hasUI) ctx.ui.notify(msg);
5750
+ });
5751
+ return result;
5696
5752
  }
5697
5753
  function wireContextTransform(pi, runtime) {
5698
5754
  pi.on("context", async (event, ctx) => {