billion-context-dsh 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/dist/index.js CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  toolPairingBalancedAfter,
17
17
  toolPairingBalancedBefore
18
18
  } from "@deepseek-ai/dsh-compaction";
19
- import { createUserMessage } from "@deepseek-ai/dsh-llm";
19
+ import { createAssistantMessage, createUserMessage } from "@deepseek-ai/dsh-llm";
20
20
  import { defaultCountTokens } from "acp-kernel";
21
21
 
22
22
  // src/messages.ts
@@ -48,7 +48,29 @@ function stringifyArgs(args) {
48
48
  return String(args);
49
49
  }
50
50
  }
51
- function projectEvent(event) {
51
+ function toolCallIdOfResultEvent(event) {
52
+ if (event.type !== "tool/result") return null;
53
+ const message = event.data.message;
54
+ const block = Array.isArray(message?.content) ? message.content.find((candidate) => candidate?.type === "tool-result") : void 0;
55
+ const id = block?.toolCallId ?? message?.source?.callId;
56
+ return typeof id === "string" ? id : null;
57
+ }
58
+ function buildToolCallIndex(events) {
59
+ const index = /* @__PURE__ */ new Map();
60
+ for (const event of events) {
61
+ if (event.type !== "assistant/message") continue;
62
+ const content = event.data.message?.content;
63
+ if (!Array.isArray(content)) continue;
64
+ for (const block of content) {
65
+ const candidate = block;
66
+ if (candidate !== null && typeof candidate === "object" && candidate.type === "tool-call" && typeof candidate.id === "string") {
67
+ index.set(candidate.id, typeof candidate.name === "string" ? candidate.name : "");
68
+ }
69
+ }
70
+ }
71
+ return index;
72
+ }
73
+ function projectEvent(event, toolNames) {
52
74
  switch (event.type) {
53
75
  case "user/message": {
54
76
  const text = extractText(event.data.content);
@@ -88,12 +110,13 @@ ${argStr}` : argStr || text;
88
110
  const message = event.data.message;
89
111
  const text = extractText(message?.content);
90
112
  if (text.length === 0) return [];
113
+ const key = toolCallIdOfResultEvent(event);
91
114
  return [{
92
115
  id: String(event.seq),
93
116
  role: "tool",
94
117
  contentType: "tool-result",
95
- toolName: message?.toolName ?? "",
96
- toolCallId: message?.toolCallId ?? "",
118
+ toolName: toolNames?.get(key ?? "") ?? "",
119
+ toolCallId: message?.toolCallId ?? key ?? "",
97
120
  text
98
121
  }];
99
122
  }
@@ -101,9 +124,10 @@ ${argStr}` : argStr || text;
101
124
  return [];
102
125
  }
103
126
  }
104
- function eventsToCoreMessages(events) {
127
+ function eventsToCoreMessages(events, toolNames) {
128
+ const index = toolNames ?? buildToolCallIndex(events);
105
129
  const out = [];
106
- for (const event of events) out.push(...projectEvent(event));
130
+ for (const event of events) out.push(...projectEvent(event, index));
107
131
  return out;
108
132
  }
109
133
  function surfaceEventsOf(session) {
@@ -347,7 +371,191 @@ function isCheckpointNode(event) {
347
371
  const source = event.data.source;
348
372
  return source?.plugin === "compact";
349
373
  }
374
+ function toolCallIdsOfEvent(event) {
375
+ if (event.type !== "assistant/message") return [];
376
+ const content = event.data.message?.content;
377
+ if (!Array.isArray(content)) return [];
378
+ const ids = [];
379
+ for (const block of content) {
380
+ if (block === null || typeof block !== "object") continue;
381
+ const b = block;
382
+ if (b.type === "tool-call" && typeof b.id === "string") ids.push(b.id);
383
+ }
384
+ return ids;
385
+ }
386
+ function assistantProviderModel(event) {
387
+ if (event.type === "assistant/message") {
388
+ const message = event.data.message;
389
+ return {
390
+ provider: typeof message?.source?.provider === "string" ? message.source.provider : "billion-context-dsh",
391
+ model: typeof message?.source?.model === "string" ? message.source.model : "surface-prune"
392
+ };
393
+ }
394
+ return { provider: "billion-context-dsh", model: "surface-prune" };
395
+ }
396
+ function hideSurfaceSeqs(session, seqs, provider, model, text) {
397
+ if (seqs.length === 0) return;
398
+ const start = seqs[0];
399
+ const end = seqs[seqs.length - 1];
400
+ let shadowedTokenCount = 0;
401
+ for (const seq of seqs) {
402
+ const event = session.events[seq];
403
+ if (event !== void 0) shadowedTokenCount += defaultCountTokens(extractEventText(event));
404
+ }
405
+ session.append("compaction/prune", {
406
+ shadowedRange: { start, end },
407
+ shadowedSeqs: [...seqs],
408
+ shadowedTokenCount
409
+ });
410
+ if (text !== void 0) {
411
+ session.append("user/message", createUserMessage({
412
+ content: [{ type: "text", text }],
413
+ source: { kind: "plugin", plugin: "billion-context-dsh" }
414
+ }), {
415
+ surfaceOp: { op: "replace", start, end },
416
+ sourceEventSeqs: [...seqs]
417
+ });
418
+ return;
419
+ }
420
+ session.append("assistant/message", {
421
+ turn: findOpenTurn(session.events) ?? 0,
422
+ step: 0,
423
+ message: createAssistantMessage({ content: [], source: { provider, model } })
424
+ }, {
425
+ surfaceOp: { op: "replace", start, end },
426
+ sourceEventSeqs: [...seqs]
427
+ });
428
+ }
429
+ function hideCompressToolPair(session, callId, resultSeq) {
430
+ let callSeq = null;
431
+ for (const event of session.events) {
432
+ if (event.type !== "assistant/message") continue;
433
+ if (toolCallIdsOfEvent(event).includes(callId)) {
434
+ callSeq = event.seq;
435
+ break;
436
+ }
437
+ }
438
+ if (callSeq === null) return false;
439
+ const callNodeIds = toolCallIdsOfEvent(session.events[callSeq]);
440
+ if (callNodeIds.length !== 1 || callNodeIds[0] !== callId) return false;
441
+ let resolvedResultSeq = resultSeq ?? null;
442
+ if (resolvedResultSeq === null) {
443
+ for (const event of session.events) {
444
+ if (event.type === "tool/result" && toolCallIdOfResultEvent(event) === callId) {
445
+ resolvedResultSeq = event.seq;
446
+ break;
447
+ }
448
+ }
449
+ }
450
+ if (resolvedResultSeq === null) return false;
451
+ const nodes = session.surface.nodes;
452
+ const startIdx = nodes.indexOf(callSeq);
453
+ const endIdx = nodes.indexOf(resolvedResultSeq);
454
+ if (startIdx < 0 || endIdx < 0 || endIdx - startIdx !== 1) return false;
455
+ const { provider, model } = assistantProviderModel(session.events[callSeq]);
456
+ const resultEvent = session.events[resolvedResultSeq];
457
+ const resultText = resultEvent === void 0 ? "" : extractEventText(resultEvent);
458
+ hideSurfaceSeqs(session, [callSeq, resolvedResultSeq], provider, model, resultText.trim().length > 0 ? resultText : void 0);
459
+ return true;
460
+ }
461
+ function stripOrphanedSurfaceToolMessages(session, inFlightCallIds = /* @__PURE__ */ new Set()) {
462
+ const nodes = session.surface.nodes;
463
+ const callIdsBySeq = /* @__PURE__ */ new Map();
464
+ const open = /* @__PURE__ */ new Map();
465
+ const orphanResultSeqs = [];
466
+ const brokenResults = /* @__PURE__ */ new Map();
467
+ for (let index = 0; index < nodes.length; index += 1) {
468
+ const seq = nodes[index];
469
+ const event = session.events[seq];
470
+ if (event === void 0) continue;
471
+ if (event.type === "assistant/message") {
472
+ const ids = toolCallIdsOfEvent(event);
473
+ if (ids.length === 0) continue;
474
+ callIdsBySeq.set(seq, ids);
475
+ for (const id of ids) {
476
+ if (!open.has(id)) open.set(id, { seq, index });
477
+ }
478
+ } else if (event.type === "tool/result") {
479
+ const id = toolCallIdOfResultEvent(event);
480
+ if (id === null) continue;
481
+ const call = open.get(id);
482
+ if (call === void 0) {
483
+ orphanResultSeqs.push(seq);
484
+ continue;
485
+ }
486
+ const callNodeIds = callIdsBySeq.get(call.seq);
487
+ let adjacent = false;
488
+ if (callNodeIds !== void 0) {
489
+ adjacent = true;
490
+ for (let mid = call.index + 1; mid < index; mid += 1) {
491
+ const midEvent = session.events[nodes[mid]];
492
+ if (midEvent === void 0 || midEvent.type !== "tool/result") {
493
+ adjacent = false;
494
+ break;
495
+ }
496
+ const midId = toolCallIdOfResultEvent(midEvent);
497
+ if (midId === null || !callNodeIds.includes(midId)) {
498
+ adjacent = false;
499
+ break;
500
+ }
501
+ }
502
+ }
503
+ open.delete(id);
504
+ if (!adjacent) brokenResults.set(seq, call.seq);
505
+ }
506
+ }
507
+ const brokenIdsByCallSeq = /* @__PURE__ */ new Map();
508
+ for (const [resultSeq, callSeq] of brokenResults) {
509
+ const id = toolCallIdOfResultEvent(session.events[resultSeq]);
510
+ if (id !== null) {
511
+ const list = brokenIdsByCallSeq.get(callSeq) ?? [];
512
+ list.push(id);
513
+ brokenIdsByCallSeq.set(callSeq, list);
514
+ }
515
+ }
516
+ const hiddenSet = new Set(orphanResultSeqs);
517
+ for (const resultSeq of brokenResults.keys()) hiddenSet.add(resultSeq);
518
+ for (const [callSeq, ids] of callIdsBySeq) {
519
+ const brokenIds = brokenIdsByCallSeq.get(callSeq);
520
+ const allUnpaired = !ids.some((candidate) => inFlightCallIds.has(candidate)) && ids.every((candidate) => open.has(candidate) || brokenIds?.includes(candidate) === true);
521
+ if (allUnpaired) hiddenSet.add(callSeq);
522
+ }
523
+ const hidden = [...hiddenSet].sort((a, b) => a - b);
524
+ let count = 0;
525
+ for (const seq of hidden) {
526
+ const event = session.events[seq];
527
+ if (event === void 0) continue;
528
+ const { provider, model } = assistantProviderModel(event);
529
+ hideSurfaceSeqs(session, [seq], provider, model);
530
+ count += 1;
531
+ }
532
+ return count;
533
+ }
534
+ function openToolCallIds(session) {
535
+ const open = /* @__PURE__ */ new Set();
536
+ for (const seq of session.surface.nodes) {
537
+ const event = session.events[seq];
538
+ if (event === void 0) continue;
539
+ if (event.type === "assistant/message") {
540
+ for (const id of toolCallIdsOfEvent(event)) open.add(id);
541
+ } else if (event.type === "tool/result") {
542
+ const id = toolCallIdOfResultEvent(event);
543
+ if (id !== null) open.delete(id);
544
+ }
545
+ }
546
+ return open;
547
+ }
548
+ function deferCompressPairHide(session, callId, resultSeq, onError) {
549
+ queueMicrotask(() => {
550
+ try {
551
+ hideCompressToolPair(session, callId, resultSeq);
552
+ } catch (error) {
553
+ onError?.(error);
554
+ }
555
+ });
556
+ }
350
557
  function buildCompressibleSeqRanges(session, opts = {}) {
558
+ stripOrphanedSurfaceToolMessages(session);
351
559
  const nodes = session.surface.nodes;
352
560
  const preserve = opts.preserveRecent ?? 5;
353
561
  const protectedSeqs = /* @__PURE__ */ new Set();
@@ -397,8 +605,12 @@ function buildCompressibleSeqRanges(session, opts = {}) {
397
605
  function surfaceSummary(session) {
398
606
  const nodes = session.surface.nodes;
399
607
  if (nodes.length === 0) return "empty";
400
- const first = nodes[0];
401
- const last = nodes[nodes.length - 1];
608
+ let first = nodes[0];
609
+ let last = nodes[0];
610
+ for (const seq of nodes) {
611
+ if (seq < first) first = seq;
612
+ if (seq > last) last = seq;
613
+ }
402
614
  return `${nodes.length} nodes, seqs ${first}..${last}`;
403
615
  }
404
616
  function blockRegistry(session) {
@@ -449,6 +661,11 @@ function compactionIdsOfKernelBlocks(session, kernelBlockIds) {
449
661
  const byKernel = new Map(blockRegistry(session).map((r) => [r.kernelBlockId, r.blockId]));
450
662
  return kernelBlockIds.map((id) => byKernel.get(id)).filter((id) => id !== void 0);
451
663
  }
664
+ function blockIdOfKernelRef(session, kernelRef) {
665
+ if (!/^b\d+$/.test(kernelRef)) return null;
666
+ const entry = blockRegistry(session).find((r) => r.kernelBlockId === kernelRef);
667
+ return entry?.blockId ?? null;
668
+ }
452
669
  function summarySeqOfKernelBlock(session, kernelBlockId) {
453
670
  const entry = blockRegistry(session).find((r) => r.kernelBlockId === kernelBlockId);
454
671
  return entry?.active ? entry.summarySeq : null;
@@ -564,7 +781,7 @@ var AcpStateStore = class {
564
781
 
565
782
  // src/tools.ts
566
783
  import { defineTool } from "@deepseek-ai/dsh-tools";
567
- import { defaultCountTokens as defaultCountTokens3 } from "acp-kernel";
784
+ import { buildStatusReport, defaultCountTokens as defaultCountTokens3 } from "acp-kernel";
568
785
 
569
786
  // src/config.ts
570
787
  import { defaultConfig } from "acp-kernel";
@@ -683,10 +900,10 @@ var DEFAULT_PROMPTS = {
683
900
  footer: "Compress with: compress({ content: [{ startSeq, endSeq, summary }] }) \u2014 content is an array: batch multiple unrelated segments in one call, each entry its own block. Keep ranges disjoint.\nSnapshot taken at nudge time: the seqs go stale once the surface moves (a later compress shadows them), so re-run acp_status for fresh refs before compressing."
684
901
  },
685
902
  tools: {
686
- compress: "Replace older conversation ranges with dense summaries you write. Each message seq is a surface reference. Single range: compress({ content: [{ startSeq, endSeq, summary }] }). Batch multiple unrelated ranges in one call (each content entry becomes its own block); keep ranges disjoint. Never compress content the current step is actively using. Seq refs must come from the CURRENT surface (acp_status or the latest nudge): a span whose edges were shadowed by an earlier compress is auto-remapped to its still-live content, a fully compressed span is reported as already compressed, and invented/other-session seqs fail with guidance.",
687
- decompress: "Recover the original content of a compressed block by its blockId (read-only; does not unshadow the range).",
903
+ compress: "Replace older conversation ranges with dense summaries you write. Each message seq is a surface reference. Single range: compress({ content: [{ startSeq, endSeq, summary }] }). Batch multiple unrelated ranges in one call (each content entry becomes its own block); keep ranges disjoint. Never compress content the current step is actively using. Compress boundaries are SURFACE SEQS (acp_status Surface: row, latest nudge table) \u2014 NOT the block refs (bN, e.g. b1) that acp_status COMPRESSED BLOCKS shows, which are for decompress only. Seq refs must come from the CURRENT surface (acp_status or the latest nudge): a span whose edges were shadowed by an earlier compress is auto-remapped to its still-live content, a fully compressed span is reported as already compressed, and invented/other-session seqs fail with guidance.",
904
+ decompress: "Recover the original content of a compressed block by its blockId \u2014 the kernel block ref `bN` shown by acp_status (e.g. b1), or a compaction id from search_context (read-only; does not unshadow the range).",
688
905
  searchContext: "Search inside compressed blocks (summaries and original content) for information the model no longer sees in context.",
689
- acpStatus: "Report the ACP block ledger: compressed blocks, reclaimed tokens, and current context pressure."
906
+ acpStatus: 'Context status: overview of the current context \u2014 CONTEXT BREAKDOWN (tool/text/summaries token shares of the visible total), COMPRESSED BLOCKS ledger, and the nudge decision. No args = overview. Percentages are shares of the visible content, not the context window. Note: the block refs in COMPRESSED BLOCKS (bN, e.g. b1) are for decompress; compress uses the Surface: seq range, not bN. Drilldown: pass scope:"compressed" for a per-block list, or scope:"uncompressed" with view:"messages" (every visible message) / view:"ranges" (merged ranges); tool filters to one tool name, sort reorders (size/time/tool; age for compressed), limit caps rows (default 30). Drilldown row refs are kernel ids (mN) for size awareness only \u2014 compress always uses the Surface: seqs, never mN.'
690
907
  },
691
908
  systemPromptTemplate: `Active Context Pruning \u2014 model-driven context management
692
909
 
@@ -711,10 +928,10 @@ WHEN NOT TO COMPRESS:
711
928
  {howToCompressRules}
712
929
 
713
930
  Compression tools (refs are SURFACE SEQS, not ids):
714
- - compress: replace one or more seq ranges, each with your own dense summary. Single range: compress({ content: [{ startSeq, endSeq, summary }] }). Batch multiple unrelated segments in one call (each entry becomes its own block): compress({ content: [{ startSeq: 1, endSeq: 5, summary: '...' }, { startSeq: 12, endSeq: 18, summary: '...' }] }). Keep ranges disjoint \u2014 overlapping entries in one batch are skipped. Edges are auto-balanced to tool-call/result boundaries; a trailing #callId fragment in a seq is ignored. Seq refs must be on the current surface: seqs from older nudges or earlier compresses go stale as the surface moves, so a stale span is auto-remapped to its still-live remainder (the result reports the adjusted span), a fully compressed span is reported as already compressed, and invented/other-session seqs fail with guidance.
715
- - decompress: recover a compressed block's original content, read-only. decompress({ blockId }).
931
+ - compress: replace one or more seq ranges, each with your own dense summary. Single range: compress({ content: [{ startSeq, endSeq, summary }] }). Batch multiple unrelated segments in one call (each entry becomes its own block): compress({ content: [{ startSeq: 1, endSeq: 5, summary: '...' }, { startSeq: 12, endSeq: 18, summary: '...' }] }). Keep ranges disjoint \u2014 overlapping entries in one batch are skipped. Edges are auto-balanced to tool-call/result boundaries; a trailing #callId fragment in a seq is ignored. Seq refs must be on the current surface: seqs from older nudges or earlier compresses go stale as the surface moves, so a stale span is auto-remapped to its still-live remainder (the result reports the adjusted span), a fully compressed span is reported as already compressed, and invented/other-session seqs fail with guidance. The block refs (bN, e.g. b1) in acp_status COMPRESSED BLOCKS are for decompress, NOT compress boundaries.
932
+ - decompress: recover a compressed block's original content, read-only. decompress({ blockId }) \u2014 accept the bN ref shown by acp_status (e.g. b1) or a compaction id.
716
933
  - search_context: find information inside compressed blocks BEFORE decompressing. search_context({ query }).
717
- - acp_status: current context usage and the live compressible-range list. Run it right before compressing \u2014 the only seqs that never go stale are the ones you just read.
934
+ - acp_status: current context usage and the live compressible-range list. Run it right before compressing \u2014 the only seqs that never go stale are the ones you just read. Drilldown (scope/view/tool/sort/limit) lists per-message or per-block sizes; drilldown rows are kernel ids (mN) for size awareness \u2014 compress uses seqs, never mN.
718
935
 
719
936
  Tiered compression: each compressed block appears on the surface as one summary node. Compressing that node again DISTILLS the block (tier 2): the parent summary folds into your new summary and the original messages are freed. Distilling a tier-2 block yields tier 3. Distill when a summary itself is consumed \u2014 decompress on the tier-2 block recovers the full originals.
720
937
 
@@ -884,28 +1101,6 @@ function renderNudgeFromTemplates(nudge, emergency, session, prompts) {
884
1101
  return parts.join("\n");
885
1102
  }
886
1103
 
887
- // src/window.ts
888
- var DEFAULT_CONTEXT_WINDOW = 128e3;
889
- function windowSourceLabel(window) {
890
- if (window.source === "explicit") return "configured";
891
- if (window.source === "auto") {
892
- return `auto-detected from ${window.provider ?? "?"}/${window.model ?? "?"}`;
893
- }
894
- return "default (auto-detection unavailable)";
895
- }
896
- async function detectContextWindow(agent, provider, model) {
897
- const llm = agent.ctx?.get?.("llm");
898
- if (llm?.resolveModelInfo === void 0) return null;
899
- try {
900
- const info = await llm.resolveModelInfo(provider, model);
901
- const window = info?.context?.contextWindow;
902
- if (typeof window === "number" && Number.isInteger(window) && window > 0) return window;
903
- return null;
904
- } catch {
905
- return null;
906
- }
907
- }
908
-
909
1104
  // src/tools.ts
910
1105
  function textOutput() {
911
1106
  return {
@@ -986,9 +1181,24 @@ function unwrapCompressArgs(args) {
986
1181
  if (content === void 0) return null;
987
1182
  return { ...args, content };
988
1183
  }
1184
+ function unwrapEnvelope(args) {
1185
+ const envelope = args.arguments;
1186
+ if (envelope === void 0) return args;
1187
+ let inner = envelope;
1188
+ if (typeof inner === "string") {
1189
+ try {
1190
+ inner = JSON.parse(inner);
1191
+ } catch {
1192
+ return args;
1193
+ }
1194
+ }
1195
+ if (typeof inner !== "object" || inner === null || Array.isArray(inner)) return args;
1196
+ return { ...args, ...inner };
1197
+ }
989
1198
  async function handleCompress(env, args, exec) {
990
1199
  const agent = requireAgent(exec);
991
1200
  const session = agent.session;
1201
+ stripOrphanedSurfaceToolMessages(session, openToolCallIds(session));
992
1202
  const state = env.store.stateFor(session);
993
1203
  const coreMessages = allLogMessages(session);
994
1204
  const surfaceMessages = eventsToCoreMessages(surfaceEventsOf(session));
@@ -1059,10 +1269,13 @@ async function handleCompress(env, args, exec) {
1059
1269
  // non-block-covered messages as the visible feed, so default behavior is
1060
1270
  // preserved. Any 'Excluded N protected message(s)' warning is surfaced.
1061
1271
  });
1062
- if (applied.result.errors.length > 0) {
1272
+ if (applied.result.errors.length > 0 && applied.result.blocksCreated === 0) {
1063
1273
  return { text: `compress failed: ${applied.result.errors.join("; ")}` };
1064
1274
  }
1065
1275
  env.store.set(session, applied.state);
1276
+ if (applied.result.blocksCreated > 0) {
1277
+ env.compressCallIdsToHide?.add(exec.callId);
1278
+ }
1066
1279
  const previousIds = new Set(turn.state.blocks.map((block) => block.blockId));
1067
1280
  const newBlocks = applied.state.blocks.filter((block) => !previousIds.has(block.blockId));
1068
1281
  const blockByRangeKey = new Map(newBlocks.map((block) => [`${block.startRef}::${block.endRef}`, block]));
@@ -1125,18 +1338,31 @@ async function handleCompress(env, args, exec) {
1125
1338
  }
1126
1339
  const summaryLine = `Compressed ${applied.result.blocksCreated} block(s), ~${applied.result.tokensCompressed} tokens reclaimed.`;
1127
1340
  const totalSkipped = skippedRanges + alreadyCompressedNotes.length;
1128
- const warningLines = [...freeWarnings.map((warning) => ` ${warning}`), ...alreadyCompressedNotes, ...lines];
1129
- const footer = totalSkipped > 0 ? ` (${totalSkipped} range(s) skipped \u2014 see warnings above)` : "";
1341
+ const failedLines = applied.result.errors.map((error) => ` ${error}`);
1342
+ const warningLines = [...freeWarnings.map((warning) => ` ${warning}`), ...failedLines, ...alreadyCompressedNotes, ...lines];
1343
+ const footer = totalSkipped > 0 ? ` (${totalSkipped} range(s) skipped or failed \u2014 see above)` : "";
1130
1344
  return { text: `${summaryLine}
1131
1345
  ${[...warningLines, footer].filter((line) => line !== "").join("\n")}` };
1132
1346
  }
1133
1347
  var decompressParameters = {
1134
- blockId: { type: "string", required: true, description: "Block id from acp_status or search_context (the compaction id)." }
1348
+ blockId: { type: "string", required: true, description: "Block id: the kernel block ref `bN` shown by acp_status (e.g. b1), or a compaction id / prefix from search_context." }
1135
1349
  };
1136
- function handleDecompress(_env, args, exec) {
1350
+ function resolveBlockId(session, arg) {
1351
+ const byKernelRef = blockIdOfKernelRef(session, arg);
1352
+ if (byKernelRef !== null) return byKernelRef;
1353
+ const ledger = rebuildBlockLedger(session.events);
1354
+ const byPrefix = ledger.find((entry) => entry.blockId.startsWith(arg));
1355
+ return byPrefix?.blockId ?? null;
1356
+ }
1357
+ function handleDecompress(_env, rawArgs, exec) {
1358
+ const args = unwrapEnvelope(rawArgs);
1137
1359
  const session = requireAgent(exec).session;
1360
+ const blockId = resolveBlockId(session, args.blockId);
1361
+ if (blockId === null) {
1362
+ return { text: `decompress: block "${args.blockId}" not found (see acp_status for the block list)` };
1363
+ }
1138
1364
  const ledger = rebuildBlockLedger(session.events);
1139
- const block = ledger.find((entry) => entry.blockId.startsWith(args.blockId));
1365
+ const block = ledger.find((entry) => entry.blockId === blockId);
1140
1366
  if (block === void 0) {
1141
1367
  return { text: `decompress: block "${args.blockId}" not found (see acp_status for the block list)` };
1142
1368
  }
@@ -1157,7 +1383,8 @@ var searchParameters = {
1157
1383
  query: { type: "string", required: true, description: "Search terms to find inside compressed blocks." },
1158
1384
  limit: { type: "integer", description: "Maximum results (default 5)." }
1159
1385
  };
1160
- function handleSearch(_env, args, exec) {
1386
+ function handleSearch(_env, rawArgs, exec) {
1387
+ const args = unwrapEnvelope(rawArgs);
1161
1388
  const session = requireAgent(exec).session;
1162
1389
  const ledger = rebuildBlockLedger(session.events);
1163
1390
  const terms = args.query.toLowerCase().split(/\s+/).filter(Boolean);
@@ -1178,26 +1405,63 @@ ${original}`.toLowerCase();
1178
1405
  ` + top.map((hit) => ` - ${hit.blockId} (score ${hit.score}): ${hit.summary.slice(0, 160)}`).join("\n") + "\n\nDecompress with: decompress({ blockId })"
1179
1406
  };
1180
1407
  }
1181
- var statusParameters = {};
1182
- async function handleStatus(env, _args, exec) {
1408
+ var statusParameters = {
1409
+ scope: {
1410
+ type: "string",
1411
+ enum: ["compressed", "uncompressed"],
1412
+ description: 'Drilldown scope: "compressed" lists compressed blocks, "uncompressed" lists visible messages. Omit for the overview.'
1413
+ },
1414
+ view: {
1415
+ type: "string",
1416
+ enum: ["ranges", "messages"],
1417
+ description: 'Drilldown view under scope:"uncompressed": "ranges" merges visible messages into ranges (default), "messages" lists every message.'
1418
+ },
1419
+ tool: {
1420
+ type: "string",
1421
+ description: 'Filter drilldown rows to one tool name (scope:"uncompressed" + view:"messages" only).'
1422
+ },
1423
+ sort: {
1424
+ type: "string",
1425
+ enum: ["size", "time", "tool", "age"],
1426
+ description: 'Row order: size (default, most tokens first), time, tool; "age" applies to compressed blocks.'
1427
+ },
1428
+ limit: {
1429
+ type: "integer",
1430
+ description: "Cap on rows or blocks shown (default 30)."
1431
+ }
1432
+ };
1433
+ function isCheckpointEvent(event) {
1434
+ if (event.type !== "user/message") return false;
1435
+ const source = event.data.source;
1436
+ return source?.plugin === "compact";
1437
+ }
1438
+ async function handleStatus(env, rawArgs, exec) {
1439
+ const args = unwrapEnvelope(rawArgs);
1183
1440
  const agent = requireAgent(exec);
1184
1441
  const session = agent.session;
1185
- const ledger = rebuildBlockLedger(session.events);
1186
- const totalTokens = ledger.reduce((sum, block) => sum + block.shadowedTokenCount, 0);
1187
- const coreMessages = eventsToCoreMessages(surfaceEventsOf(session));
1188
- const estimated = resolveTokenCount(agent, coreMessages);
1189
- const window = env.windowFor === void 0 ? { limit: env.modelContextLimit, source: "explicit" } : await env.windowFor(agent);
1190
- const limit = window.limit;
1191
- const lines = [
1192
- `ACP status \u2014 session ${session.id}`,
1193
- ` blocks: ${ledger.length}`,
1194
- ` tokens compressed: ${totalTokens}`,
1195
- ` estimated context: ${estimated} / ${limit} (${Math.round(estimated / limit * 100)}%)`,
1196
- ` context window: ${limit} (${windowSourceLabel(window)})`,
1197
- ` surface: ${surfaceSummary(session)}`
1198
- ];
1199
- for (const block of ledger.slice(0, 10)) {
1200
- lines.push(` - ${block.blockId.slice(0, 8)}: seqs ${block.start}..${block.end} (${block.shadowedSeqs.length} msgs) \u2014 ${block.summary.slice(0, 80)}`);
1442
+ const state = env.store.stateFor(session);
1443
+ const surface = surfaceEventsOf(session);
1444
+ const toolNames = buildToolCallIndex(surface);
1445
+ const coreMessages = allLogMessages(session);
1446
+ const surfaceMessages = eventsToCoreMessages(surface, toolNames);
1447
+ const tokenCount = resolveTokenCount(agent, surfaceMessages);
1448
+ const config = kernelConfigFor(env);
1449
+ const turn = env.kernel.processTurn({ messages: coreMessages, state, config, tokenCount });
1450
+ const statusMessages = eventsToCoreMessages(
1451
+ surface.filter((event) => !isCheckpointEvent(event)),
1452
+ toolNames
1453
+ );
1454
+ const report = buildStatusReport(turn.state, statusMessages, defaultCountTokens3, args);
1455
+ const lines = [report];
1456
+ if (args.scope === void 0) {
1457
+ const nudge = turn.nudge;
1458
+ if (nudge !== void 0) {
1459
+ lines.push("", `Nudge: ${nudge.shouldInject ? "ACTIVE" : "idle"} \u2014 ${nudge.reason}`);
1460
+ }
1461
+ }
1462
+ lines.push("", `Surface: ${surfaceSummary(session)}`);
1463
+ if (args.scope === "uncompressed") {
1464
+ lines.push("", "Note: drilldown rows are kernel refs (mN) for size awareness \u2014 compress uses the Surface: seqs above, never mN.");
1201
1465
  }
1202
1466
  return { text: lines.join("\n") };
1203
1467
  }
@@ -1245,6 +1509,30 @@ function makeTools(env) {
1245
1509
 
1246
1510
  // src/commands.ts
1247
1511
  import { defaultCountTokens as defaultCountTokens4 } from "acp-kernel";
1512
+
1513
+ // src/window.ts
1514
+ var DEFAULT_CONTEXT_WINDOW = 128e3;
1515
+ function windowSourceLabel(window) {
1516
+ if (window.source === "explicit") return "configured";
1517
+ if (window.source === "auto") {
1518
+ return `auto-detected from ${window.provider ?? "?"}/${window.model ?? "?"}`;
1519
+ }
1520
+ return "default (auto-detection unavailable)";
1521
+ }
1522
+ async function detectContextWindow(agent, provider, model) {
1523
+ const llm = agent.ctx?.get?.("llm");
1524
+ if (llm?.resolveModelInfo === void 0) return null;
1525
+ try {
1526
+ const info = await llm.resolveModelInfo(provider, model);
1527
+ const window = info?.context?.contextWindow;
1528
+ if (typeof window === "number" && Number.isInteger(window) && window > 0) return window;
1529
+ return null;
1530
+ } catch {
1531
+ return null;
1532
+ }
1533
+ }
1534
+
1535
+ // src/commands.ts
1248
1536
  async function statusText(env, agent) {
1249
1537
  const session = agent.session;
1250
1538
  const ledger = rebuildBlockLedger(session.events);
@@ -1301,8 +1589,9 @@ function compressText(env, agent, args) {
1301
1589
  function decompressText(_env, agent, args) {
1302
1590
  if (args.length < 1) return "/acp decompress <blockId>";
1303
1591
  const session = agent.session;
1592
+ const blockId = blockIdOfKernelRef(session, args[0]);
1304
1593
  const ledger = rebuildBlockLedger(session.events);
1305
- const block = ledger.find((entry) => entry.blockId.startsWith(args[0]));
1594
+ const block = blockId === null ? ledger.find((entry) => entry.blockId.startsWith(args[0])) : ledger.find((entry) => entry.blockId === blockId);
1306
1595
  if (block === void 0) return `block "${args[0]}" not found (see /acp status)`;
1307
1596
  const parts = expandShadowedSeqs(session, block.blockId).map((seq) => extractEventText(session.events[seq])).filter((text) => text.length > 0);
1308
1597
  return `Block ${block.blockId} \u2014 ${block.summary}
@@ -1360,6 +1649,8 @@ var AcpCompactionEngine = class extends CompactionEngine {
1360
1649
  /** Resolved prompt templates (validated at construction — fail-fast on template typos). */
1361
1650
  prompts;
1362
1651
  lastNudgeTurn = /* @__PURE__ */ new Map();
1652
+ /** Successful compress call ids awaiting their tool/result so the pair can be hidden. */
1653
+ compressCallIdsToHide = /* @__PURE__ */ new Set();
1363
1654
  /** Per provider/model route the resolved window (probe failures cached too). */
1364
1655
  windowCache = /* @__PURE__ */ new Map();
1365
1656
  constructor(ctx, config = {}) {
@@ -1379,7 +1670,8 @@ var AcpCompactionEngine = class extends CompactionEngine {
1379
1670
  nudgeEmergencyThresholdPct: this.config.nudgeEmergencyThresholdPct,
1380
1671
  coreOverrides: this.config.coreOverrides,
1381
1672
  windowFor: (agent) => this.windowFor(agent),
1382
- prompts: this.prompts
1673
+ prompts: this.prompts,
1674
+ compressCallIdsToHide: this.compressCallIdsToHide
1383
1675
  };
1384
1676
  const tools = ctx.get("tools");
1385
1677
  if (tools !== void 0) {
@@ -1413,16 +1705,27 @@ var AcpCompactionEngine = class extends CompactionEngine {
1413
1705
  if (name === "commands") registerCommand();
1414
1706
  });
1415
1707
  }
1416
- if (this.config.autoNudge) {
1417
- ctx.on("agent/pre-step", async (payload, next) => {
1418
- const decision = await next();
1419
- if (decision.kind === "reject") return decision;
1420
- const window = await this.windowFor(payload.agent);
1421
- const outcome = buildNudge(payload.agent, { ...env, modelContextLimit: window.limit }, this.lastNudgeTurn);
1422
- if (outcome === null) return decision;
1423
- return { kind: "enter", messages: [...decision.messages, outcome.message] };
1708
+ ctx.on("session/event", (session, event) => {
1709
+ if (event.type !== "tool/result") return;
1710
+ const message = event.data.message;
1711
+ const block = message.content[0];
1712
+ const callId = block?.toolCallId ?? message.source.callId;
1713
+ if (typeof callId !== "string" || !this.compressCallIdsToHide.has(callId)) return;
1714
+ this.compressCallIdsToHide.delete(callId);
1715
+ deferCompressPairHide(session, callId, event.seq, (error) => {
1716
+ ctx.logger.warn(`billion-context-dsh: hide compress call/result pair failed: ${String(error)}`);
1424
1717
  });
1425
- }
1718
+ });
1719
+ ctx.on("agent/pre-step", async (payload, next) => {
1720
+ stripOrphanedSurfaceToolMessages(payload.agent.session);
1721
+ if (!this.config.autoNudge) return next();
1722
+ const decision = await next();
1723
+ if (decision.kind === "reject") return decision;
1724
+ const window = await this.windowFor(payload.agent);
1725
+ const outcome = buildNudge(payload.agent, { ...env, modelContextLimit: window.limit }, this.lastNudgeTurn);
1726
+ if (outcome === null) return decision;
1727
+ return { kind: "enter", messages: [...decision.messages, outcome.message] };
1728
+ });
1426
1729
  const systemPrompt = ctx.get("systemPrompt");
1427
1730
  if (systemPrompt !== void 0) {
1428
1731
  systemPrompt.section({
@@ -1519,6 +1822,7 @@ export {
1519
1822
  expandShadowedSeqs,
1520
1823
  extractEventText,
1521
1824
  findOpenTurn,
1825
+ hideCompressToolPair,
1522
1826
  kernelConfigFor,
1523
1827
  makeTools,
1524
1828
  projectEvent,
@@ -1531,6 +1835,7 @@ export {
1531
1835
  resolveTokenCount,
1532
1836
  runCompactionTransaction,
1533
1837
  shadowedSeqsOf,
1838
+ stripOrphanedSurfaceToolMessages,
1534
1839
  summarySeqOfKernelBlock,
1535
1840
  surfaceEventsOf,
1536
1841
  windowSourceLabel