billion-context-omp 0.2.2 → 0.2.4

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 自带多代理编排,重复的委派工具会冲突。
@@ -43,8 +43,11 @@ export declare function buildSummaryPrompt(prompts: Prompts): string;
43
43
  * deliberately NOT used here: fold blocks only replay from in-stream
44
44
  * compress tool calls, which the truncation removes. `previousSummary` (an
45
45
  * earlier compaction's summary) is folded in so iterative compactions never
46
- * drop it. Returns null on any failure so the caller falls back to Pi's
47
- * native compaction. */
46
+ * drop it. On a failed attempt the LLM call is
47
+ * retried ONCE (stochastic formatting failures recover on a fresh call);
48
+ * returns null only when the retry also fails — the caller then CANCELS the
49
+ * compaction (no native fallback: ACP owns compression, host-default
50
+ * summaries are the failure mode this hook exists to prevent). */
48
51
  export declare function summarizeMessages(ctx: ExtensionContext, messages: AgentMessage[], prompts: Prompts, configuredModel?: string | null, opts?: {
49
52
  previousSummary?: string;
50
53
  customInstructions?: string;
@@ -58,11 +61,9 @@ export declare function summarizeMessages(ctx: ExtensionContext, messages: Agent
58
61
  summary: string;
59
62
  model: string;
60
63
  } | null>;
61
- /** Generate a summary for a message range using the compression model. Shared
62
- * entry point for the `/compact` handler. Returns null when no model is
63
- * usable, the slice is empty, the model is unauthenticated, or the response
64
- * is unparseable — the caller then returns `undefined` so Pi falls back to
65
- * its native compaction. */
64
+ /** Generate a summary for a message range using the compression model.
65
+ * Currently unused by the extension (kept as the shared range-summary
66
+ * surface); null = hard failure, caller decides. */
66
67
  export declare function summarizeRange(ctx: ExtensionContext, messages: CoreMessage[], state: CompressionState, startRef: string, endRef: string, prompts: Prompts, configuredModel?: string | null): Promise<{
67
68
  summary: string;
68
69
  model: string;
@@ -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.4" ? `billion-context-omp@${"0.2.4"}` : void 0;
4512
4562
  return buildStatusPanel({
4513
4563
  version: versionStr,
4514
4564
  tokenCount: sessionTokens,
@@ -4757,7 +4807,7 @@ import { join as join3 } from "path";
4757
4807
  import { complete } from "@oh-my-pi/pi-ai";
4758
4808
  import { CONFIG_DIR_NAME as CONFIG_DIR_NAME2 } from "@oh-my-pi/pi-utils";
4759
4809
  var TIMEOUT_MS = 6e4;
4760
- var MAX_OUTPUT_TOKENS = 3e3;
4810
+ var MAX_OUTPUT_TOKENS = 8e3;
4761
4811
  var MAX_SLICE_CHARS = 15e4;
4762
4812
  var MAX_MSG_CHARS = 4e3;
4763
4813
  function readCompressModel() {
@@ -4807,9 +4857,10 @@ function parseSummary(text) {
4807
4857
  try {
4808
4858
  const obj = JSON.parse(cleaned);
4809
4859
  if (typeof obj.summary === "string" && obj.summary.length > 0) return obj.summary;
4860
+ return null;
4810
4861
  } catch {
4811
4862
  }
4812
- return null;
4863
+ return cleaned.length >= 50 ? cleaned : null;
4813
4864
  }
4814
4865
  function buildSummaryPrompt(prompts) {
4815
4866
  return prompts.compressPhilosophy.trim() + "\n\n" + prompts.howToCompressRules.trim() + '\n\nCompress the message range provided below into ONE dense, self-contained technical summary following the rules above. Output ONLY a JSON object: {"summary": "..."} where the value is the full summary as a single string.';
@@ -4846,14 +4897,32 @@ User instructions for this compaction: ${custom}`;
4846
4897
  const userText = `ENTIRE conversation to compress (${slice.length} messages, ~${tokens} tokens). Compress it:
4847
4898
 
4848
4899
  ` + formatSlice(slice, opts?.messageRefs ? { ...createInitialState(), messageRefs: opts.messageRefs } : createInitialState());
4849
- const response = await run(
4850
- model,
4851
- { systemPrompt: [instructions], messages: [{ role: "user", content: [{ type: "text", text: userText }], timestamp: Date.now() }] },
4852
- { apiKey: auth.apiKey, headers: auth.headers, maxTokens: MAX_OUTPUT_TOKENS, signal: ac.signal }
4853
- );
4854
- const summary = parseSummary(
4855
- response.content.filter((c) => c.type === "text").map((c) => c.text).join("\n")
4856
- );
4900
+ const attempt = async () => {
4901
+ const response = await run(
4902
+ model,
4903
+ { systemPrompt: [instructions], messages: [{ role: "user", content: [{ type: "text", text: userText }], timestamp: Date.now() }] },
4904
+ { apiKey: auth.apiKey, headers: auth.headers, maxTokens: MAX_OUTPUT_TOKENS, signal: ac.signal }
4905
+ );
4906
+ return parseSummary(
4907
+ response.content.filter((c) => c.type === "text").map((c) => c.text).join("\n")
4908
+ );
4909
+ };
4910
+ let summary = null;
4911
+ try {
4912
+ summary = await attempt();
4913
+ } catch (e) {
4914
+ if (opts?.signal?.aborted || ac.signal.aborted) throw e;
4915
+ logWarn("summarize-messages", { event: "attempt-failed", model: label, error: String(e) });
4916
+ }
4917
+ if (!summary) {
4918
+ try {
4919
+ summary = await attempt();
4920
+ if (summary) logInfo("summarize-messages", { event: "recovered-on-retry", model: label, messages: slice.length });
4921
+ } catch (e) {
4922
+ if (opts?.signal?.aborted || ac.signal.aborted) throw e;
4923
+ logWarn("summarize-messages", { event: "retry-failed", model: label, error: String(e) });
4924
+ }
4925
+ }
4857
4926
  if (!summary) {
4858
4927
  logWarn("summarize-messages", { event: "unparseable-summary", model: label, messages: slice.length });
4859
4928
  return null;
@@ -4893,7 +4962,7 @@ You have four context-management tools:
4893
4962
 
4894
4963
  - 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
4964
  - 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" }).
4965
+ - 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
4966
  - 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
4967
 
4899
4968
  ${prompts.compressPhilosophy}
@@ -5216,7 +5285,7 @@ async function checkForUpdate(autoUpdate, notify) {
5216
5285
  const data = await res.json();
5217
5286
  const latest = data.version;
5218
5287
  if (!latest) return;
5219
- const current = runtimeVersion ?? "0.2.2";
5288
+ const current = runtimeVersion ?? "0.2.4";
5220
5289
  const hasUpdate = isNewer(latest, current);
5221
5290
  debug.event("update-check", {
5222
5291
  current,
@@ -5251,20 +5320,20 @@ async function getRuntimeVersion() {
5251
5320
  }
5252
5321
 
5253
5322
  // src/dump.ts
5254
- import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, readdirSync, unlinkSync } from "fs";
5323
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, readdirSync as readdirSync2, unlinkSync as unlinkSync2 } from "fs";
5255
5324
  import * as path2 from "path";
5256
5325
  import { CONFIG_DIR_NAME as CONFIG_DIR_NAME4 } from "@oh-my-pi/pi-utils";
5257
5326
  var counters = {};
5258
5327
  var MAX_FILES_PER_PREFIX = 200;
5259
5328
  function pruneDumps(dir, prefixTest, seqOf) {
5260
5329
  try {
5261
- const files = readdirSync(dir).filter(prefixTest).map((f) => ({ f, n: seqOf(f) })).filter((x) => !Number.isNaN(x.n));
5330
+ const files = readdirSync2(dir).filter(prefixTest).map((f) => ({ f, n: seqOf(f) })).filter((x) => !Number.isNaN(x.n));
5262
5331
  if (files.length <= MAX_FILES_PER_PREFIX) return;
5263
5332
  files.sort((a, b) => a.n - b.n);
5264
5333
  const excess = files.slice(0, files.length - MAX_FILES_PER_PREFIX);
5265
5334
  for (const { f } of excess) {
5266
5335
  try {
5267
- unlinkSync(path2.join(dir, f));
5336
+ unlinkSync2(path2.join(dir, f));
5268
5337
  } catch {
5269
5338
  }
5270
5339
  }
@@ -5282,7 +5351,7 @@ function dumpContextMessages(messages, meta) {
5282
5351
  mkdirSync2(dir, { recursive: true });
5283
5352
  if (!(dir in counters)) {
5284
5353
  try {
5285
- const existing = readdirSync(dir).filter((f) => /^\d{4}\.json$/.test(f));
5354
+ const existing = readdirSync2(dir).filter((f) => /^\d{4}\.json$/.test(f));
5286
5355
  const max = existing.reduce((mx, f) => {
5287
5356
  const n = parseInt(f, 10);
5288
5357
  return Number.isNaN(n) ? mx : Math.max(mx, n);
@@ -5365,7 +5434,7 @@ function dumpProviderRequest(payload, meta) {
5365
5434
  mkdirSync2(dir, { recursive: true });
5366
5435
  if (!("req" in counters)) {
5367
5436
  try {
5368
- const existing = readdirSync(dir).filter((f) => /^req_\d+\.json$/.test(f));
5437
+ const existing = readdirSync2(dir).filter((f) => /^req_\d+\.json$/.test(f));
5369
5438
  const max = existing.reduce((mx, f) => {
5370
5439
  const n = parseInt(f.slice(4, -5), 10);
5371
5440
  return Number.isNaN(n) ? mx : Math.max(mx, n);
@@ -5404,10 +5473,11 @@ import * as path3 from "path";
5404
5473
  import { CONFIG_DIR_NAME as CONFIG_DIR_NAME5 } from "@oh-my-pi/pi-utils";
5405
5474
  async function loadUserConfig(cwd) {
5406
5475
  const home = homeDir();
5476
+ const globalDir = join8(home, CONFIG_DIR_NAME5);
5407
5477
  const merged = {};
5408
- for (const base of [join8(home, CONFIG_DIR_NAME5), join8(cwd, CONFIG_DIR_NAME5)]) {
5478
+ for (const base of [globalDir, join8(cwd, CONFIG_DIR_NAME5)]) {
5409
5479
  const file = join8(base, "acp-omp.json");
5410
- const allowPrompts = base.startsWith(home);
5480
+ const allowPrompts = base === globalDir;
5411
5481
  try {
5412
5482
  const raw = await fs.readFile(file, "utf8");
5413
5483
  const parsed = JSON.parse(raw);
@@ -5504,8 +5574,8 @@ function wireCompactionDisable(pi, runtime) {
5504
5574
  messageRefs: slot.state.messageRefs
5505
5575
  });
5506
5576
  if (!result) {
5507
- ctx.ui?.notify?.("ACP: compression fell back to Pi native compaction", "warning");
5508
- return void 0;
5577
+ ctx.ui?.notify?.("ACP: /compact aborted \u2014 summary generation failed after retry (details in ~/.omp/acp-omp.log)", "error");
5578
+ return { cancel: true };
5509
5579
  }
5510
5580
  logInfo("compact", {
5511
5581
  sid,
@@ -5532,9 +5602,9 @@ function wireCompactionDisable(pi, runtime) {
5532
5602
  function wireSessionLifecycle(pi, runtime) {
5533
5603
  pi.on("session_start", async (_event, ctx) => {
5534
5604
  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 });
5605
+ logInfo("session", { event: "start", sid, cwd: ctx.cwd, debug: runtime.adapter.debug ?? null, version: true ? "0.2.4" : null });
5536
5606
  const selfPath = import.meta.url;
5537
- const conflict = stampAndDetect(selfPath, true ? "0.2.2" : null);
5607
+ const conflict = stampAndDetect(selfPath, true ? "0.2.4" : null);
5538
5608
  if (conflict) {
5539
5609
  logWarn("instance", { event: "dual-instance", self: selfPath, other: conflict.path, otherPid: conflict.pid, otherVersion: conflict.version });
5540
5610
  try {
@@ -5573,6 +5643,7 @@ function wireSessionLifecycle(pi, runtime) {
5573
5643
  async function transformStream(ctx, runtime, input, mode) {
5574
5644
  const sid = ctx.sessionManager.getSessionId();
5575
5645
  const release = await runtime.acquireLock(sid);
5646
+ let result;
5576
5647
  try {
5577
5648
  if (input.length === 0) {
5578
5649
  debug.event("empty-stream-bypass", { sid });
@@ -5678,21 +5749,25 @@ ${rendered.text}${example}`);
5678
5749
  }
5679
5750
  }
5680
5751
  }
5752
+ if (mode === "context") {
5753
+ runtime.recordRebuiltOutput(ctx, rebuilt);
5754
+ }
5681
5755
  dumpContextMessages(rebuilt, {
5682
5756
  sid,
5683
5757
  injected: nudgeInjected,
5684
5758
  emergency: turn.nudge?.breakdown?.emergencyOverride === 1
5685
5759
  });
5686
- await checkForUpdate(runtime.adapter.autoUpdate ?? true, (msg) => {
5687
- if (ctx.hasUI) ctx.ui.notify(msg);
5688
- });
5689
- return { rebuilt, nudgeInjected };
5760
+ result = { rebuilt, nudgeInjected };
5690
5761
  } catch (e) {
5691
5762
  logThrow("context", e, { sid, phase: "transform", mode });
5692
5763
  throw e;
5693
5764
  } finally {
5694
5765
  release();
5695
5766
  }
5767
+ await checkForUpdate(runtime.adapter.autoUpdate ?? true, (msg) => {
5768
+ if (ctx.hasUI) ctx.ui.notify(msg);
5769
+ });
5770
+ return result;
5696
5771
  }
5697
5772
  function wireContextTransform(pi, runtime) {
5698
5773
  pi.on("context", async (event, ctx) => {