billion-context-omp 0.2.1 → 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.1" ? `billion-context-omp@${"0.2.1"}` : 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}
@@ -5051,9 +5101,44 @@ function wireToolGuardrails(pi, runtime) {
5051
5101
  });
5052
5102
  }
5053
5103
 
5104
+ // src/instance-guard.ts
5105
+ import { readFileSync as readFileSync2, writeFileSync } from "fs";
5106
+ import { join as join4 } from "path";
5107
+ var MARKER_FILE = ".billion-context-omp-instance.json";
5108
+ var FRESH_MS = 6e4;
5109
+ function markerPath() {
5110
+ return join4(homeDir(), ".omp", MARKER_FILE);
5111
+ }
5112
+ function readMarker() {
5113
+ try {
5114
+ const raw = JSON.parse(readFileSync2(markerPath(), "utf8"));
5115
+ if (typeof raw?.path === "string" && typeof raw?.ts === "number") return raw;
5116
+ } catch {
5117
+ }
5118
+ return void 0;
5119
+ }
5120
+ function detectDualInstance(selfPath, now = Date.now()) {
5121
+ const m = readMarker();
5122
+ if (!m) return void 0;
5123
+ if (m.path === selfPath) return void 0;
5124
+ if (now - m.ts > FRESH_MS) return void 0;
5125
+ return m;
5126
+ }
5127
+ function stampInstance(selfPath, version, pid = process.pid, now = Date.now()) {
5128
+ try {
5129
+ writeFileSync(markerPath(), JSON.stringify({ path: selfPath, version, pid, ts: now }));
5130
+ } catch {
5131
+ }
5132
+ }
5133
+ function stampAndDetect(selfPath, version, now = Date.now()) {
5134
+ const conflict = detectDualInstance(selfPath, now);
5135
+ stampInstance(selfPath, version, void 0, now);
5136
+ return conflict;
5137
+ }
5138
+
5054
5139
  // src/update.ts
5055
5140
  import { readFile, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
5056
- import { join as join4, dirname as dirname3 } from "path";
5141
+ import { join as join5, dirname as dirname3 } from "path";
5057
5142
  import { fileURLToPath } from "url";
5058
5143
  import { execFile } from "child_process";
5059
5144
  import { CONFIG_DIR_NAME as CONFIG_DIR_NAME3 } from "@oh-my-pi/pi-utils";
@@ -5061,7 +5146,7 @@ var PACKAGE_NAME = "billion-context-omp";
5061
5146
  var REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
5062
5147
  var SEMVER_RE = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z-.]+)?$/;
5063
5148
  var CHECK_INTERVAL_MS = 3 * 60 * 1e3;
5064
- var throttleFile = () => join4(homeDir(), CONFIG_DIR_NAME3, ".billion-context-omp-update-check");
5149
+ var throttleFile = () => join5(homeDir(), CONFIG_DIR_NAME3, ".billion-context-omp-update-check");
5065
5150
  var updateInFlight = false;
5066
5151
  function parseVersion(v) {
5067
5152
  return v.replace(/^v/, "").split(".").map((n) => parseInt(n, 10) || 0);
@@ -5111,7 +5196,7 @@ function findNpmRoot(extDir) {
5111
5196
  async function findExtensionDir() {
5112
5197
  let dir = dirname3(fileURLToPath(import.meta.url));
5113
5198
  for (; ; ) {
5114
- const pkg = await readPackageJson(join4(dir, "package.json"));
5199
+ const pkg = await readPackageJson(join5(dir, "package.json"));
5115
5200
  if (pkg?.name === PACKAGE_NAME) return dir;
5116
5201
  const parent = dirname3(dir);
5117
5202
  if (parent === dir) return void 0;
@@ -5181,7 +5266,7 @@ async function checkForUpdate(autoUpdate, notify) {
5181
5266
  const data = await res.json();
5182
5267
  const latest = data.version;
5183
5268
  if (!latest) return;
5184
- const current = runtimeVersion ?? "0.2.1";
5269
+ const current = runtimeVersion ?? "0.2.3";
5185
5270
  const hasUpdate = isNewer(latest, current);
5186
5271
  debug.event("update-check", {
5187
5272
  current,
@@ -5211,25 +5296,25 @@ async function checkForUpdate(autoUpdate, notify) {
5211
5296
  async function getRuntimeVersion() {
5212
5297
  const extDir = await findExtensionDir();
5213
5298
  if (!extDir) return void 0;
5214
- const pkg = await readPackageJson(join4(extDir, "package.json"));
5299
+ const pkg = await readPackageJson(join5(extDir, "package.json"));
5215
5300
  return pkg?.version;
5216
5301
  }
5217
5302
 
5218
5303
  // src/dump.ts
5219
- import { mkdirSync as mkdirSync2, writeFileSync, readdirSync, unlinkSync } from "fs";
5304
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, readdirSync as readdirSync2, unlinkSync as unlinkSync2 } from "fs";
5220
5305
  import * as path2 from "path";
5221
5306
  import { CONFIG_DIR_NAME as CONFIG_DIR_NAME4 } from "@oh-my-pi/pi-utils";
5222
5307
  var counters = {};
5223
5308
  var MAX_FILES_PER_PREFIX = 200;
5224
5309
  function pruneDumps(dir, prefixTest, seqOf) {
5225
5310
  try {
5226
- 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));
5227
5312
  if (files.length <= MAX_FILES_PER_PREFIX) return;
5228
5313
  files.sort((a, b) => a.n - b.n);
5229
5314
  const excess = files.slice(0, files.length - MAX_FILES_PER_PREFIX);
5230
5315
  for (const { f } of excess) {
5231
5316
  try {
5232
- unlinkSync(path2.join(dir, f));
5317
+ unlinkSync2(path2.join(dir, f));
5233
5318
  } catch {
5234
5319
  }
5235
5320
  }
@@ -5247,7 +5332,7 @@ function dumpContextMessages(messages, meta) {
5247
5332
  mkdirSync2(dir, { recursive: true });
5248
5333
  if (!(dir in counters)) {
5249
5334
  try {
5250
- const existing = readdirSync(dir).filter((f) => /^\d{4}\.json$/.test(f));
5335
+ const existing = readdirSync2(dir).filter((f) => /^\d{4}\.json$/.test(f));
5251
5336
  const max = existing.reduce((mx, f) => {
5252
5337
  const n = parseInt(f, 10);
5253
5338
  return Number.isNaN(n) ? mx : Math.max(mx, n);
@@ -5261,7 +5346,7 @@ function dumpContextMessages(messages, meta) {
5261
5346
  counters[dir] = seq + 1;
5262
5347
  const name = `${String(seq).padStart(4, "0")}.json`;
5263
5348
  const fullPath = path2.join(dir, name);
5264
- writeFileSync(
5349
+ writeFileSync2(
5265
5350
  fullPath,
5266
5351
  JSON.stringify({
5267
5352
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -5330,7 +5415,7 @@ function dumpProviderRequest(payload, meta) {
5330
5415
  mkdirSync2(dir, { recursive: true });
5331
5416
  if (!("req" in counters)) {
5332
5417
  try {
5333
- const existing = readdirSync(dir).filter((f) => /^req_\d+\.json$/.test(f));
5418
+ const existing = readdirSync2(dir).filter((f) => /^req_\d+\.json$/.test(f));
5334
5419
  const max = existing.reduce((mx, f) => {
5335
5420
  const n = parseInt(f.slice(4, -5), 10);
5336
5421
  return Number.isNaN(n) ? mx : Math.max(mx, n);
@@ -5345,7 +5430,7 @@ function dumpProviderRequest(payload, meta) {
5345
5430
  const name = `req_${String(seq).padStart(4, "0")}.json`;
5346
5431
  const fullPath = path2.join(dir, name);
5347
5432
  const summary = summarizeProviderPayload(payload);
5348
- writeFileSync(
5433
+ writeFileSync2(
5349
5434
  fullPath,
5350
5435
  JSON.stringify({
5351
5436
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -5369,10 +5454,11 @@ import * as path3 from "path";
5369
5454
  import { CONFIG_DIR_NAME as CONFIG_DIR_NAME5 } from "@oh-my-pi/pi-utils";
5370
5455
  async function loadUserConfig(cwd) {
5371
5456
  const home = homeDir();
5457
+ const globalDir = join8(home, CONFIG_DIR_NAME5);
5372
5458
  const merged = {};
5373
- for (const base of [join7(home, CONFIG_DIR_NAME5), join7(cwd, CONFIG_DIR_NAME5)]) {
5374
- const file = join7(base, "acp-omp.json");
5375
- const allowPrompts = base.startsWith(home);
5459
+ for (const base of [globalDir, join8(cwd, CONFIG_DIR_NAME5)]) {
5460
+ const file = join8(base, "acp-omp.json");
5461
+ const allowPrompts = base === globalDir;
5376
5462
  try {
5377
5463
  const raw = await fs.readFile(file, "utf8");
5378
5464
  const parsed = JSON.parse(raw);
@@ -5389,7 +5475,7 @@ async function loadUserConfig(cwd) {
5389
5475
  }
5390
5476
  return merged;
5391
5477
  }
5392
- function join7(...parts) {
5478
+ function join8(...parts) {
5393
5479
  return path3.join(...parts);
5394
5480
  }
5395
5481
  var KNOWN = /* @__PURE__ */ new Set([
@@ -5497,7 +5583,18 @@ function wireCompactionDisable(pi, runtime) {
5497
5583
  function wireSessionLifecycle(pi, runtime) {
5498
5584
  pi.on("session_start", async (_event, ctx) => {
5499
5585
  const sid = ctx.sessionManager.getSessionId();
5500
- logInfo("session", { event: "start", sid, cwd: ctx.cwd, debug: runtime.adapter.debug ?? null, version: true ? "0.2.1" : null });
5586
+ logInfo("session", { event: "start", sid, cwd: ctx.cwd, debug: runtime.adapter.debug ?? null, version: true ? "0.2.3" : null });
5587
+ const selfPath = import.meta.url;
5588
+ const conflict = stampAndDetect(selfPath, true ? "0.2.3" : null);
5589
+ if (conflict) {
5590
+ logWarn("instance", { event: "dual-instance", self: selfPath, other: conflict.path, otherPid: conflict.pid, otherVersion: conflict.version });
5591
+ try {
5592
+ if (ctx.hasUI) {
5593
+ ctx.ui.notify(`\u26A0 billion-context-omp loaded TWICE (also from ${conflict.path}). Two instances corrupt compression state \u2014 remove one (check 'omp plugin list' vs config.yml extensions).`);
5594
+ }
5595
+ } catch {
5596
+ }
5597
+ }
5501
5598
  try {
5502
5599
  const user = await loadUserConfig(ctx.cwd);
5503
5600
  runtime.setAdapter(applyUserConfig(runtime.adapter, user));
@@ -5527,6 +5624,7 @@ function wireSessionLifecycle(pi, runtime) {
5527
5624
  async function transformStream(ctx, runtime, input, mode) {
5528
5625
  const sid = ctx.sessionManager.getSessionId();
5529
5626
  const release = await runtime.acquireLock(sid);
5627
+ let result;
5530
5628
  try {
5531
5629
  if (input.length === 0) {
5532
5630
  debug.event("empty-stream-bypass", { sid });
@@ -5632,21 +5730,25 @@ ${rendered.text}${example}`);
5632
5730
  }
5633
5731
  }
5634
5732
  }
5733
+ if (mode === "context") {
5734
+ runtime.recordRebuiltOutput(ctx, rebuilt);
5735
+ }
5635
5736
  dumpContextMessages(rebuilt, {
5636
5737
  sid,
5637
5738
  injected: nudgeInjected,
5638
5739
  emergency: turn.nudge?.breakdown?.emergencyOverride === 1
5639
5740
  });
5640
- await checkForUpdate(runtime.adapter.autoUpdate ?? true, (msg) => {
5641
- if (ctx.hasUI) ctx.ui.notify(msg);
5642
- });
5643
- return { rebuilt, nudgeInjected };
5741
+ result = { rebuilt, nudgeInjected };
5644
5742
  } catch (e) {
5645
5743
  logThrow("context", e, { sid, phase: "transform", mode });
5646
5744
  throw e;
5647
5745
  } finally {
5648
5746
  release();
5649
5747
  }
5748
+ await checkForUpdate(runtime.adapter.autoUpdate ?? true, (msg) => {
5749
+ if (ctx.hasUI) ctx.ui.notify(msg);
5750
+ });
5751
+ return result;
5650
5752
  }
5651
5753
  function wireContextTransform(pi, runtime) {
5652
5754
  pi.on("context", async (event, ctx) => {