billion-context-dsh 0.1.4 → 0.1.6

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
@@ -7,42 +7,6 @@ import { createCore } from "acp-kernel";
7
7
 
8
8
  // src/state.ts
9
9
  import { createInitialState } from "acp-kernel";
10
- var AcpStateStore = class {
11
- states = /* @__PURE__ */ new Map();
12
- /** Kernel state for one session, initialised on first access. */
13
- stateFor(session) {
14
- const id = session.id;
15
- const existing = this.states.get(id);
16
- if (existing !== void 0) return existing;
17
- const state = createInitialState();
18
- this.states.set(id, state);
19
- return state;
20
- }
21
- set(session, state) {
22
- this.states.set(session.id, state);
23
- }
24
- delete(session) {
25
- this.states.delete(session.id);
26
- }
27
- };
28
-
29
- // src/tools.ts
30
- import { defineTool } from "@deepseek-ai/dsh-tools";
31
- import { defaultCountTokens as defaultCountTokens2 } from "acp-kernel";
32
-
33
- // src/config.ts
34
- import { defaultConfig } from "acp-kernel";
35
- function kernelConfigFor(input) {
36
- const nudgePatch = {};
37
- if (input.nudgeMinContextLimitPct !== void 0) nudgePatch.minContextLimitPct = input.nudgeMinContextLimitPct;
38
- if (input.nudgeMaxContextLimitPct !== void 0) nudgePatch.maxContextLimitPct = input.nudgeMaxContextLimitPct;
39
- if (input.nudgeEmergencyThresholdPct !== void 0) nudgePatch.emergencyThresholdPct = input.nudgeEmergencyThresholdPct;
40
- const overrides = { ...input.coreOverrides };
41
- if (Object.keys(nudgePatch).length > 0) {
42
- overrides.nudge = { ...defaultConfig(input.modelContextLimit).nudge, ...nudgePatch };
43
- }
44
- return defaultConfig(input.modelContextLimit, overrides);
45
- }
46
10
 
47
11
  // src/region.ts
48
12
  import { randomUUID } from "crypto";
@@ -145,6 +109,9 @@ function eventsToCoreMessages(events) {
145
109
  function surfaceEventsOf(session) {
146
110
  return session.surface.nodes.map((seq) => session.events[seq]).filter((event) => event !== void 0);
147
111
  }
112
+ function allLogMessages(session) {
113
+ return eventsToCoreMessages(session.events);
114
+ }
148
115
  function extractEventText(event) {
149
116
  switch (event.type) {
150
117
  case "user/message":
@@ -245,6 +212,9 @@ function shadowedSeqsOf(session, start, end) {
245
212
  const endIdx = nodes.indexOf(end);
246
213
  return nodes.slice(startIdx, endIdx + 1);
247
214
  }
215
+ function readCompactionSummary(event) {
216
+ return event.data;
217
+ }
248
218
  function runCompactionTransaction(session, input) {
249
219
  assertNoActiveCompaction(session.events);
250
220
  const turn = findOpenTurn(session.events);
@@ -258,7 +228,12 @@ function runCompactionTransaction(session, input) {
258
228
  shadowedSeqs: [...input.shadowedSeqs],
259
229
  shadowedTokenCount: input.shadowedTokenCount,
260
230
  provider: input.provider,
261
- model: input.model
231
+ model: input.model,
232
+ tier: input.tier ?? 1,
233
+ ...input.kernelBlockId === void 0 ? {} : { kernelBlockId: input.kernelBlockId },
234
+ ...input.parentBlockIds === void 0 || input.parentBlockIds.length === 0 ? {} : { parentBlockIds: [...input.parentBlockIds] },
235
+ ...input.directMessageIds === void 0 ? {} : { directMessageIds: [...input.directMessageIds] },
236
+ ...input.effectiveMessageIds === void 0 ? {} : { effectiveMessageIds: [...input.effectiveMessageIds] }
262
237
  }).seq);
263
238
  const message = createUserMessage({
264
239
  content: input.summary,
@@ -271,11 +246,19 @@ function runCompactionTransaction(session, input) {
271
246
  seqs.push(session.append("compaction/end", { compactionId, turn }).seq);
272
247
  return { compactionId, seqs };
273
248
  }
249
+ function summarySeqOfCompaction(events, compactionId) {
250
+ for (const event of events) {
251
+ if (event.type !== "user/message") continue;
252
+ const source = event.data.source;
253
+ if (source?.plugin === "compact" && source.compactionId === compactionId) return event.seq;
254
+ }
255
+ return null;
256
+ }
274
257
  function rebuildBlockLedger(events) {
275
258
  const ledger = [];
276
259
  for (const event of events) {
277
260
  if (event.type !== "compaction/summary") continue;
278
- const data = event.data;
261
+ const data = readCompactionSummary(event);
279
262
  let shadowedTokenCount = data.shadowedTokenCount;
280
263
  if (shadowedTokenCount === 0) {
281
264
  shadowedTokenCount = 0;
@@ -284,13 +267,25 @@ function rebuildBlockLedger(events) {
284
267
  if (original !== void 0) shadowedTokenCount += defaultCountTokens(extractEventText(original));
285
268
  }
286
269
  }
270
+ const tier = data.tier === 2 || data.tier === 3 ? data.tier : 1;
271
+ const parentBlockIds = Array.isArray(data.parentBlockIds) ? [...data.parentBlockIds] : [];
272
+ const directMessageIds = Array.isArray(data.directMessageIds) ? [...data.directMessageIds] : void 0;
273
+ const effectiveMessageIds = Array.isArray(data.effectiveMessageIds) ? [...data.effectiveMessageIds] : void 0;
274
+ const summarySeq = summarySeqOfCompaction(events, data.compactionId);
287
275
  ledger.push({
288
276
  blockId: data.compactionId,
289
277
  summary: extractText(data.summary),
290
278
  shadowedSeqs: [...data.shadowedSeqs],
291
279
  shadowedTokenCount,
292
280
  start: data.shadowedRange.start,
293
- end: data.shadowedRange.end
281
+ end: data.shadowedRange.end,
282
+ tier,
283
+ parentBlockIds,
284
+ ...typeof data.kernelBlockId === "string" ? { kernelBlockId: data.kernelBlockId } : {},
285
+ ...summarySeq === null ? {} : { summarySeq },
286
+ ...directMessageIds === void 0 ? {} : { directMessageIds },
287
+ ...effectiveMessageIds === void 0 ? {} : { effectiveMessageIds },
288
+ createdAt: event.time
294
289
  });
295
290
  }
296
291
  return ledger;
@@ -354,6 +349,206 @@ function surfaceSummary(session) {
354
349
  const last = nodes[nodes.length - 1];
355
350
  return `${nodes.length} nodes, seqs ${first}..${last}`;
356
351
  }
352
+ function blockRegistry(session) {
353
+ const ledger = rebuildBlockLedger(session.events);
354
+ const kernelIdOf = /* @__PURE__ */ new Map();
355
+ const raw = [];
356
+ let next = 1;
357
+ for (const entry of ledger) {
358
+ let kernelBlockId;
359
+ if (entry.kernelBlockId !== void 0 && /^b\d+$/.test(entry.kernelBlockId)) {
360
+ kernelBlockId = entry.kernelBlockId;
361
+ const num = Number(kernelBlockId.slice(1));
362
+ if (Number.isInteger(num)) next = Math.max(next, num + 1);
363
+ } else {
364
+ kernelBlockId = `b${next}`;
365
+ next += 1;
366
+ }
367
+ kernelIdOf.set(entry.blockId, kernelBlockId);
368
+ raw.push({
369
+ blockId: entry.blockId,
370
+ kernelBlockId,
371
+ tier: entry.tier,
372
+ summarySeq: entry.summarySeq ?? null,
373
+ active: true,
374
+ parentBlockIds: [...entry.parentBlockIds]
375
+ });
376
+ }
377
+ const consumed = /* @__PURE__ */ new Set();
378
+ for (const entry of raw) {
379
+ for (const parent of entry.parentBlockIds) consumed.add(parent);
380
+ }
381
+ return raw.map((entry) => ({
382
+ ...entry,
383
+ active: !consumed.has(entry.blockId)
384
+ }));
385
+ }
386
+ function blockRefForSummarySeq(session, seq) {
387
+ const event = session.events[seq];
388
+ if (event?.type !== "user/message") return null;
389
+ const source = event.data.source;
390
+ if (source?.plugin !== "compact" || source.compactionId === void 0) return null;
391
+ const entry = blockRegistry(session).find((r) => r.blockId === source.compactionId);
392
+ if (entry === void 0) return null;
393
+ return entry.kernelBlockId;
394
+ }
395
+ function compactionIdsOfKernelBlocks(session, kernelBlockIds) {
396
+ if (kernelBlockIds.length === 0) return [];
397
+ const byKernel = new Map(blockRegistry(session).map((r) => [r.kernelBlockId, r.blockId]));
398
+ return kernelBlockIds.map((id) => byKernel.get(id)).filter((id) => id !== void 0);
399
+ }
400
+ function summarySeqOfKernelBlock(session, kernelBlockId) {
401
+ const entry = blockRegistry(session).find((r) => r.kernelBlockId === kernelBlockId);
402
+ return entry?.active ? entry.summarySeq : null;
403
+ }
404
+ function checkpointBlockIdOf(events, seq) {
405
+ const event = events[seq];
406
+ if (event?.type !== "user/message") return null;
407
+ const source = event.data.source;
408
+ if (source?.plugin !== "compact" || source.compactionId === void 0) return null;
409
+ return source.compactionId;
410
+ }
411
+ function expandShadowedSeqs(session, blockId) {
412
+ const ledger = rebuildBlockLedger(session.events);
413
+ const byId = new Map(ledger.map((entry) => [entry.blockId, entry]));
414
+ const root = byId.get(blockId);
415
+ if (root === void 0) return [];
416
+ const out = [];
417
+ const seen = /* @__PURE__ */ new Set();
418
+ const visit = (entry) => {
419
+ if (seen.has(entry.blockId)) return;
420
+ seen.add(entry.blockId);
421
+ for (const seq of entry.shadowedSeqs) {
422
+ const childId = checkpointBlockIdOf(session.events, seq);
423
+ const child = childId === null ? void 0 : byId.get(childId);
424
+ if (child !== void 0) visit(child);
425
+ else out.push(seq);
426
+ }
427
+ };
428
+ visit(root);
429
+ return out;
430
+ }
431
+
432
+ // src/state.ts
433
+ function rebuildKernelBlocks(events) {
434
+ const ledger = rebuildBlockLedger(events);
435
+ if (ledger.length === 0) return [];
436
+ const kernelIdOf = /* @__PURE__ */ new Map();
437
+ const parentKernelIds = /* @__PURE__ */ new Map();
438
+ let next = 1;
439
+ for (const entry of ledger) {
440
+ let kernelBlockId;
441
+ if (entry.kernelBlockId !== void 0 && /^b\d+$/.test(entry.kernelBlockId)) {
442
+ kernelBlockId = entry.kernelBlockId;
443
+ const num = Number(kernelBlockId.slice(1));
444
+ if (Number.isInteger(num)) next = Math.max(next, num + 1);
445
+ } else {
446
+ kernelBlockId = `b${next}`;
447
+ next += 1;
448
+ }
449
+ kernelIdOf.set(entry.blockId, kernelBlockId);
450
+ parentKernelIds.set(
451
+ entry.blockId,
452
+ entry.parentBlockIds.map((parent) => kernelIdOf.get(parent)).filter((id) => id !== void 0)
453
+ );
454
+ }
455
+ const consumed = /* @__PURE__ */ new Set();
456
+ for (const entry of ledger) {
457
+ for (const parent of entry.parentBlockIds) consumed.add(parent);
458
+ }
459
+ const blocks = [];
460
+ for (const entry of ledger) {
461
+ const blockId = kernelIdOf.get(entry.blockId);
462
+ const direct = entry.directMessageIds ?? [...entry.shadowedSeqs.map(String)];
463
+ const effective = entry.effectiveMessageIds ?? (entry.tier > 1 ? entry.summarySeq === void 0 ? [...entry.shadowedSeqs.map(String)] : [String(entry.summarySeq)] : [...entry.shadowedSeqs.map(String)]);
464
+ blocks.push({
465
+ blockId,
466
+ runId: `r${blocks.length + 1}`,
467
+ tier: entry.tier,
468
+ summary: entry.summary,
469
+ directMessageIds: [...direct],
470
+ effectiveMessageIds: [...effective],
471
+ directBlockIds: parentKernelIds.get(entry.blockId) ?? [],
472
+ compressedTokens: entry.shadowedTokenCount,
473
+ createdAt: entry.createdAt,
474
+ survivedCount: 0,
475
+ generation: "young",
476
+ active: !consumed.has(entry.blockId)
477
+ });
478
+ }
479
+ return blocks;
480
+ }
481
+ function nextBlockIdAfter(events) {
482
+ const blocks = rebuildKernelBlocks(events);
483
+ let max = 0;
484
+ for (const block of blocks) {
485
+ const num = Number(block.blockId.slice(1));
486
+ if (Number.isInteger(num)) max = Math.max(max, num);
487
+ }
488
+ return max + 1;
489
+ }
490
+ var AcpStateStore = class {
491
+ states = /* @__PURE__ */ new Map();
492
+ /** Kernel state for one session, initialised on first access. */
493
+ stateFor(session) {
494
+ const id = session.id;
495
+ const existing = this.states.get(id);
496
+ if (existing !== void 0) return existing;
497
+ const state = createInitialState();
498
+ if (session.events.some((event) => event.type === "compaction/summary")) {
499
+ state.blocks = rebuildKernelBlocks(session.events);
500
+ state.nextBlockId = nextBlockIdAfter(session.events);
501
+ }
502
+ this.states.set(id, state);
503
+ return state;
504
+ }
505
+ set(session, state) {
506
+ this.states.set(session.id, state);
507
+ }
508
+ delete(session) {
509
+ this.states.delete(session.id);
510
+ }
511
+ };
512
+
513
+ // src/tools.ts
514
+ import { defineTool } from "@deepseek-ai/dsh-tools";
515
+ import { defaultCountTokens as defaultCountTokens2 } from "acp-kernel";
516
+
517
+ // src/config.ts
518
+ import { defaultConfig } from "acp-kernel";
519
+ function kernelConfigFor(input) {
520
+ const nudgePatch = {};
521
+ if (input.nudgeMinContextLimitPct !== void 0) nudgePatch.minContextLimitPct = input.nudgeMinContextLimitPct;
522
+ if (input.nudgeMaxContextLimitPct !== void 0) nudgePatch.maxContextLimitPct = input.nudgeMaxContextLimitPct;
523
+ if (input.nudgeEmergencyThresholdPct !== void 0) nudgePatch.emergencyThresholdPct = input.nudgeEmergencyThresholdPct;
524
+ const overrides = { ...input.coreOverrides };
525
+ if (Object.keys(nudgePatch).length > 0) {
526
+ overrides.nudge = { ...defaultConfig(input.modelContextLimit).nudge, ...nudgePatch };
527
+ }
528
+ return defaultConfig(input.modelContextLimit, overrides);
529
+ }
530
+
531
+ // src/window.ts
532
+ var DEFAULT_CONTEXT_WINDOW = 128e3;
533
+ function windowSourceLabel(window) {
534
+ if (window.source === "explicit") return "configured";
535
+ if (window.source === "auto") {
536
+ return `auto-detected from ${window.provider ?? "?"}/${window.model ?? "?"}`;
537
+ }
538
+ return "default (auto-detection unavailable)";
539
+ }
540
+ async function detectContextWindow(agent, provider, model) {
541
+ const llm = agent.ctx?.get?.("llm");
542
+ if (llm?.resolveModelInfo === void 0) return null;
543
+ try {
544
+ const info = await llm.resolveModelInfo(provider, model);
545
+ const window = info?.context?.contextWindow;
546
+ if (typeof window === "number" && Number.isInteger(window) && window > 0) return window;
547
+ return null;
548
+ } catch {
549
+ return null;
550
+ }
551
+ }
357
552
 
358
553
  // src/tools.ts
359
554
  function textOutput() {
@@ -412,8 +607,9 @@ async function handleCompress(env, args, exec) {
412
607
  const agent = requireAgent(exec);
413
608
  const session = agent.session;
414
609
  const state = env.store.stateFor(session);
415
- const coreMessages = eventsToCoreMessages(surfaceEventsOf(session));
416
- const tokenCount = coreMessages.reduce((sum, message) => sum + defaultCountTokens2(message.text ?? ""), 0);
610
+ const coreMessages = allLogMessages(session);
611
+ const surfaceMessages = eventsToCoreMessages(surfaceEventsOf(session));
612
+ const tokenCount = surfaceMessages.reduce((sum, message) => sum + defaultCountTokens2(message.text ?? ""), 0);
417
613
  const config = kernelConfigFor(env);
418
614
  const turn = env.kernel.processTurn({ messages: coreMessages, state, config, tokenCount });
419
615
  env.store.set(session, turn.state);
@@ -422,8 +618,10 @@ async function handleCompress(env, args, exec) {
422
618
  const startSeq = parseSeq(range.startSeq);
423
619
  const endSeq = parseSeq(range.endSeq);
424
620
  const { start, end } = resolveSurfaceRange(session, startSeq, endSeq);
425
- const startRef = byRaw[String(start)];
426
- const endRef = byRaw[String(end)];
621
+ const startBlockRef = blockRefForSummarySeq(session, start);
622
+ const endBlockRef = blockRefForSummarySeq(session, end);
623
+ const startRef = startBlockRef ?? byRaw[String(start)];
624
+ const endRef = endBlockRef ?? byRaw[String(end)];
427
625
  if (startRef === void 0 || endRef === void 0) {
428
626
  throw new Error(
429
627
  `billion-context-dsh: seq ${start}..${end} has no assigned ref \u2014 the range must be on the current surface (run acp_status for the live seq list)`
@@ -445,15 +643,44 @@ async function handleCompress(env, args, exec) {
445
643
  messages: coreMessages,
446
644
  state: turn.state,
447
645
  config
646
+ // Deliberately NOT overriding protectedMessageIds: with the full log the
647
+ // kernel's recent/last-user protection is computed over the same
648
+ // non-block-covered messages as the visible feed, so default behavior is
649
+ // preserved. Any 'Excluded N protected message(s)' warning is surfaced.
448
650
  });
449
651
  if (applied.result.errors.length > 0) {
450
652
  return { text: `compress failed: ${applied.result.errors.join("; ")}` };
451
653
  }
452
654
  env.store.set(session, applied.state);
655
+ const previousIds = new Set(turn.state.blocks.map((block) => block.blockId));
656
+ const newBlocks = applied.state.blocks.filter((block) => !previousIds.has(block.blockId));
657
+ const blockByRangeKey = new Map(newBlocks.map((block) => [`${block.startRef}::${block.endRef}`, block]));
658
+ const warningByRangeKey = /* @__PURE__ */ new Map();
659
+ const freeWarnings = [];
660
+ for (const warning of applied.result.warnings) {
661
+ const match = /^Skipped range \((.+?)\.\.(.+?)\)/.exec(warning);
662
+ if (match !== null) {
663
+ const key = `${match[1]}::${match[2]}`;
664
+ const list = warningByRangeKey.get(key) ?? [];
665
+ list.push(warning);
666
+ warningByRangeKey.set(key, list);
667
+ } else {
668
+ freeWarnings.push(warning);
669
+ }
670
+ }
453
671
  const lines = [];
672
+ let skippedRanges = 0;
454
673
  for (let index = 0; index < ranges.length; index += 1) {
455
674
  const range = ranges[index];
456
675
  const original = args.content[index];
676
+ const key = `${range.startRef}::${range.endRef}`;
677
+ const block = blockByRangeKey.get(key);
678
+ if (block === void 0) {
679
+ skippedRanges += 1;
680
+ const warnings = warningByRangeKey.get(key) ?? [];
681
+ for (const warning of warnings) lines.push(` ${warning}`);
682
+ continue;
683
+ }
457
684
  const { start, end } = range;
458
685
  const shadowed = shadowedSeqsOf(session, start, end);
459
686
  let shadowedTokens = 0;
@@ -461,6 +688,8 @@ async function handleCompress(env, args, exec) {
461
688
  const event = session.events[seq];
462
689
  if (event !== void 0) shadowedTokens += defaultCountTokens2(extractEventText(event));
463
690
  }
691
+ const tier = block.tier === 2 || block.tier === 3 ? block.tier : 1;
692
+ const parentBlockIds = compactionIdsOfKernelBlocks(session, block.directBlockIds);
464
693
  const { compactionId } = runCompactionTransaction(session, {
465
694
  start,
466
695
  end,
@@ -468,17 +697,27 @@ async function handleCompress(env, args, exec) {
468
697
  summary: [{ type: "text", text: original.summary }],
469
698
  shadowedTokenCount: shadowedTokens,
470
699
  provider: agent.options.provider ?? "",
471
- model: agent.options.model ?? ""
700
+ model: agent.options.model ?? "",
701
+ tier,
702
+ kernelBlockId: block.blockId,
703
+ ...parentBlockIds.length === 0 ? {} : { parentBlockIds },
704
+ // Record the kernel block's raw coverage so a restarted engine
705
+ // rehydrates the SAME effective messages (a tier-2 block's coverage is
706
+ // its parents' originals, not the checkpoint node).
707
+ directMessageIds: block.directMessageIds,
708
+ effectiveMessageIds: block.effectiveMessageIds
472
709
  });
473
710
  const adjusted = start !== range.startSeq || end !== range.endSeq;
711
+ const tierLabel = tier === 1 ? "" : `, tier ${tier}`;
474
712
  lines.push(
475
- ` block ${compactionId.slice(0, 8)}: seqs ${start}..${end}, ${shadowed.length} messages shadowed` + (adjusted ? ` (adjusted from ${range.startSeq}..${range.endSeq} to balanced edges)` : "")
713
+ ` block ${compactionId.slice(0, 8)}: seqs ${start}..${end}, ${shadowed.length} messages shadowed${tierLabel}` + (adjusted ? ` (adjusted from ${range.startSeq}..${range.endSeq} to balanced edges)` : "")
476
714
  );
477
715
  }
478
- return {
479
- text: `Compressed ${applied.result.blocksCreated} block(s), ~${applied.result.tokensCompressed} tokens reclaimed.
480
- ${lines.join("\n")}`
481
- };
716
+ const summaryLine = `Compressed ${applied.result.blocksCreated} block(s), ~${applied.result.tokensCompressed} tokens reclaimed.`;
717
+ const warningLines = [...freeWarnings.map((warning) => ` ${warning}`), ...lines];
718
+ const footer = skippedRanges > 0 ? ` (${skippedRanges} range(s) skipped \u2014 see warnings above)` : "";
719
+ return { text: `${summaryLine}
720
+ ${[...warningLines, footer].filter((line) => line !== "").join("\n")}` };
482
721
  }
483
722
  var decompressParameters = {
484
723
  blockId: { type: "string", required: true, description: "Block id from acp_status or search_context (the compaction id)." }
@@ -491,13 +730,14 @@ function handleDecompress(_env, args, exec) {
491
730
  return { text: `decompress: block "${args.blockId}" not found (see acp_status for the block list)` };
492
731
  }
493
732
  const parts = [];
494
- for (const seq of block.shadowedSeqs) {
733
+ for (const seq of expandShadowedSeqs(session, block.blockId)) {
495
734
  const event = session.events[seq];
496
735
  const text = event === void 0 ? "" : extractEventText(event);
497
736
  if (text.length > 0) parts.push(`[seq ${seq}] ${text}`);
498
737
  }
738
+ const tierNote = block.tier > 1 ? ` (tier ${block.tier}, distills ${block.parentBlockIds.length} block(s))` : "";
499
739
  return {
500
- text: `Block ${block.blockId} \u2014 ${block.summary}
740
+ text: `Block ${block.blockId} \u2014 ${block.summary}${tierNote}
501
741
 
502
742
  ${parts.join("\n\n") || "(no recoverable content)"}`
503
743
  };
@@ -528,18 +768,21 @@ ${original}`.toLowerCase();
528
768
  };
529
769
  }
530
770
  var statusParameters = {};
531
- function handleStatus(env, _args, exec) {
532
- const session = requireAgent(exec).session;
771
+ async function handleStatus(env, _args, exec) {
772
+ const agent = requireAgent(exec);
773
+ const session = agent.session;
533
774
  const ledger = rebuildBlockLedger(session.events);
534
775
  const totalTokens = ledger.reduce((sum, block) => sum + block.shadowedTokenCount, 0);
535
776
  const coreMessages = eventsToCoreMessages(surfaceEventsOf(session));
536
777
  const estimated = coreMessages.reduce((sum, message) => sum + defaultCountTokens2(message.text ?? ""), 0);
537
- const limit = env.modelContextLimit;
778
+ const window = env.windowFor === void 0 ? { limit: env.modelContextLimit, source: "explicit" } : await env.windowFor(agent);
779
+ const limit = window.limit;
538
780
  const lines = [
539
781
  `ACP status \u2014 session ${session.id}`,
540
782
  ` blocks: ${ledger.length}`,
541
783
  ` tokens compressed: ${totalTokens}`,
542
784
  ` estimated context: ${estimated} / ${limit} (${Math.round(estimated / limit * 100)}%)`,
785
+ ` context window: ${limit} (${windowSourceLabel(window)})`,
543
786
  ` surface: ${surfaceSummary(session)}`
544
787
  ];
545
788
  for (const block of ledger.slice(0, 10)) {
@@ -582,7 +825,7 @@ function makeTools(env) {
582
825
  parameters: statusParameters,
583
826
  output: textOutput(),
584
827
  execute(args, exec) {
585
- return Promise.resolve(handleStatus(env, args, exec));
828
+ return handleStatus(env, args, exec);
586
829
  }
587
830
  })
588
831
  ];
@@ -590,21 +833,24 @@ function makeTools(env) {
590
833
 
591
834
  // src/commands.ts
592
835
  import { defaultCountTokens as defaultCountTokens3 } from "acp-kernel";
593
- function statusText(env, agent) {
836
+ async function statusText(env, agent) {
594
837
  const session = agent.session;
595
838
  const ledger = rebuildBlockLedger(session.events);
596
839
  const totalTokens = ledger.reduce((sum, block) => sum + block.shadowedTokenCount, 0);
597
840
  const coreMessages = eventsToCoreMessages(surfaceEventsOf(session));
598
841
  const estimated = coreMessages.reduce((sum, message) => sum + defaultCountTokens3(message.text ?? ""), 0);
599
- const limit = env.modelContextLimit;
842
+ const window = env.windowFor === void 0 ? { limit: env.modelContextLimit, source: "explicit" } : await env.windowFor(agent);
843
+ const limit = window.limit;
600
844
  const lines = [
601
845
  `ACP status \u2014 session ${session.id}`,
602
846
  ` blocks: ${ledger.length}`,
603
847
  ` tokens compressed: ${totalTokens}`,
604
- ` estimated context: ${estimated} / ${limit} (${Math.round(estimated / limit * 100)}%)`
848
+ ` estimated context: ${estimated} / ${limit} (${Math.round(estimated / limit * 100)}%)`,
849
+ ` context window: ${limit} (${windowSourceLabel(window)})`
605
850
  ];
606
851
  for (const block of ledger.slice(0, 10)) {
607
- lines.push(` - ${block.blockId.slice(0, 8)}: seqs ${block.start}..${block.end} \u2014 ${block.summary.slice(0, 80)}`);
852
+ const tier = block.tier > 1 ? ` [T${block.tier}]` : "";
853
+ lines.push(` - ${block.blockId.slice(0, 8)}${tier}: seqs ${block.start}..${block.end} \u2014 ${block.summary.slice(0, 80)}`);
608
854
  }
609
855
  return lines.join("\n");
610
856
  }
@@ -620,6 +866,9 @@ function compressText(env, agent, args) {
620
866
  }
621
867
  const session = agent.session;
622
868
  const { start, end } = resolveSurfaceRange(session, startSeq, endSeq);
869
+ if (blockRefForSummarySeq(session, start) !== null || blockRefForSummarySeq(session, end) !== null) {
870
+ return "/acp compress: the range touches a compressed block summary node \u2014 distill it with the compress tool (seq-based batch), not /acp compress";
871
+ }
623
872
  const shadowed = shadowedSeqsOf(session, startSeq, endSeq);
624
873
  let shadowedTokens = 0;
625
874
  for (const seq of shadowed) {
@@ -643,7 +892,7 @@ function decompressText(_env, agent, args) {
643
892
  const ledger = rebuildBlockLedger(session.events);
644
893
  const block = ledger.find((entry) => entry.blockId.startsWith(args[0]));
645
894
  if (block === void 0) return `block "${args[0]}" not found (see /acp status)`;
646
- const parts = block.shadowedSeqs.map((seq) => extractEventText(session.events[seq])).filter((text) => text.length > 0);
895
+ const parts = expandShadowedSeqs(session, block.blockId).map((seq) => extractEventText(session.events[seq])).filter((text) => text.length > 0);
647
896
  return `Block ${block.blockId} \u2014 ${block.summary}
648
897
 
649
898
  ${parts.join("\n\n") || "(no recoverable content)"}`;
@@ -655,7 +904,7 @@ function acpCommand(env) {
655
904
  handler: async (invocation) => {
656
905
  const raw = invocation.rawInput.trim();
657
906
  if (raw === "" || raw === "status") {
658
- return { kind: "success", text: statusText(env, invocation.agent) };
907
+ return { kind: "success", text: await statusText(env, invocation.agent) };
659
908
  }
660
909
  if (raw.startsWith("compress")) {
661
910
  return { kind: "success", text: compressText(env, invocation.agent, raw.slice("compress".length).trim().split(/\s+/)) };
@@ -694,8 +943,9 @@ function measuredTokenCount(agent, coreMessages) {
694
943
  function buildNudge(agent, env, lastNudgeTurn) {
695
944
  const session = agent.session;
696
945
  const state = env.store.stateFor(session);
697
- const coreMessages = eventsToCoreMessages(surfaceEventsOf(session));
698
- const tokenCount = measuredTokenCount(agent, coreMessages);
946
+ const coreMessages = allLogMessages(session);
947
+ const surfaceMessages = eventsToCoreMessages(surfaceEventsOf(session));
948
+ const tokenCount = measuredTokenCount(agent, surfaceMessages);
699
949
  const config = kernelConfigFor(env);
700
950
  const turn = env.kernel.processTurn({ messages: coreMessages, state, config, tokenCount });
701
951
  env.store.set(session, turn.state);
@@ -717,7 +967,18 @@ function buildNudgeText(nudge, emergency, session) {
717
967
  const pct = Math.round(Math.min(nudge.contextUsage, 1) * 100);
718
968
  const frame = emergency ? `\u26A0\uFE0F Context usage is at ${pct}% of the window \u2014 nearly full. Consider compressing consumed ranges soon so working context stays available; the choice and timing are yours.` : `Context usage is at ${pct}%. This is a suggestion, not a requirement \u2014 you decide whether and when to compress.`;
719
969
  const guidance = "Compress by need, not by percentage: replace only ranges you have genuinely consumed, with dense self-contained summaries.";
720
- return [frame, "", guidance, rangeTable(session)].join("\n");
970
+ const parts = [frame, "", guidance];
971
+ if ((nudge.tier === 2 || nudge.tier === 3) && (nudge.tierTargetBlocks?.length ?? 0) > 0) {
972
+ const targets = nudge.tierTargetBlocks;
973
+ const summarySeqs = targets.map((block) => summarySeqOfKernelBlock(session, block.blockId)).filter((seq) => seq !== null);
974
+ const pending = nudge.tier === 2 ? nudge.breakdown?.pendingT2 : nudge.breakdown?.pendingT3;
975
+ const tokens = typeof pending === "number" ? pending : 0;
976
+ parts.push(
977
+ `Tier ${nudge.tier}: ${targets.length} tier-${nudge.tier - 1} block(s) distillable (${tokens} tokens) \u2014 compress their summary node(s) [seqs ${summarySeqs.join(", ")}] to reclaim the original messages.`
978
+ );
979
+ }
980
+ parts.push(rangeTable(session));
981
+ return parts.join("\n");
721
982
  }
722
983
 
723
984
  // src/system-prompt.ts
@@ -734,15 +995,24 @@ Compression tools (refs are SURFACE SEQS, not ids):
734
995
  - search_context: find information inside compressed blocks BEFORE decompressing. search_context({ query }).
735
996
  - acp_status: current context usage and the live compressible-range list. Run it before compressing when in doubt.
736
997
 
998
+ 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.
999
+
737
1000
  When you write a summary, it becomes the ONLY record of that range: keep file paths, signatures, exact values, decisions, and error strings verbatim so a later reader (or you, after decompress) can continue without the original. Never reuse historical seqs \u2014 the surface moves as messages land and compress; verify with acp_status.`;
738
1001
  var ACP_SYSTEM_PROMPT_ORDER = 150;
739
1002
 
740
1003
  // src/index.ts
741
1004
  var DEFAULT_CONFIG = {
742
- modelContextLimit: 128e3,
1005
+ autoModelContextLimit: true,
743
1006
  autoTools: true,
744
1007
  autoCommand: true,
745
- autoNudge: true
1008
+ autoNudge: true,
1009
+ // Nudge thresholds: engine defaults 0.70/0.85 — deliberately below the
1010
+ // kernel/billion-context-pi 0.75/0.95. 0.95 leaves no room to act before
1011
+ // the API rejects, and the host's compaction-basic line (thresholdRatio
1012
+ // 0.80) shadows it in standard/code/cordis modes; 0.70 keeps the forced
1013
+ // over-limit nudge ahead of that 80% line. Explicit values always win.
1014
+ nudgeMaxContextLimitPct: 0.7,
1015
+ nudgeEmergencyThresholdPct: 0.85
746
1016
  };
747
1017
  function resolveAcpConfig(config = {}) {
748
1018
  return { ...DEFAULT_CONFIG, ...config };
@@ -755,6 +1025,8 @@ var AcpCompactionEngine = class extends CompactionEngine {
755
1025
  /** Resolved engine configuration. */
756
1026
  config;
757
1027
  lastNudgeTurn = /* @__PURE__ */ new Map();
1028
+ /** Per provider/model route the resolved window (probe failures cached too). */
1029
+ windowCache = /* @__PURE__ */ new Map();
758
1030
  constructor(ctx, config = {}) {
759
1031
  super(ctx);
760
1032
  this.config = resolveAcpConfig(config);
@@ -763,11 +1035,13 @@ var AcpCompactionEngine = class extends CompactionEngine {
763
1035
  const env = {
764
1036
  kernel: this.kernel,
765
1037
  store: this.store,
766
- modelContextLimit: this.config.modelContextLimit,
1038
+ // Initial value before any probe; windowFor() replaces it per pre-step.
1039
+ modelContextLimit: this.config.modelContextLimit ?? DEFAULT_CONTEXT_WINDOW,
767
1040
  nudgeMinContextLimitPct: this.config.nudgeMinContextLimitPct,
768
1041
  nudgeMaxContextLimitPct: this.config.nudgeMaxContextLimitPct,
769
1042
  nudgeEmergencyThresholdPct: this.config.nudgeEmergencyThresholdPct,
770
- coreOverrides: this.config.coreOverrides
1043
+ coreOverrides: this.config.coreOverrides,
1044
+ windowFor: (agent) => this.windowFor(agent)
771
1045
  };
772
1046
  const tools = ctx.get("tools");
773
1047
  if (tools !== void 0) {
@@ -805,7 +1079,8 @@ var AcpCompactionEngine = class extends CompactionEngine {
805
1079
  ctx.on("agent/pre-step", async (payload, next) => {
806
1080
  const decision = await next();
807
1081
  if (decision.kind === "reject") return decision;
808
- const outcome = buildNudge(payload.agent, env, this.lastNudgeTurn);
1082
+ const window = await this.windowFor(payload.agent);
1083
+ const outcome = buildNudge(payload.agent, { ...env, modelContextLimit: window.limit }, this.lastNudgeTurn);
809
1084
  if (outcome === null) return decision;
810
1085
  return { kind: "enter", messages: [...decision.messages, outcome.message] };
811
1086
  });
@@ -819,6 +1094,32 @@ var AcpCompactionEngine = class extends CompactionEngine {
819
1094
  });
820
1095
  }
821
1096
  }
1097
+ /**
1098
+ * Resolve the effective context window for an agent. An explicitly
1099
+ * configured `modelContextLimit` always wins (no probe). Otherwise probe the
1100
+ * model's real window via `agent.ctx.llm.resolveModelInfo` (cached per
1101
+ * provider/model route, probe failures cached too) and fall back to
1102
+ * DEFAULT_CONTEXT_WINDOW when auto-detection is disabled or unavailable.
1103
+ */
1104
+ async windowFor(agent) {
1105
+ if (this.config.modelContextLimit !== void 0) {
1106
+ return { limit: this.config.modelContextLimit, source: "explicit" };
1107
+ }
1108
+ const provider = agent.options.provider ?? "";
1109
+ const model = agent.options.model ?? "";
1110
+ const key = `${provider}\0${model}`;
1111
+ const cached = this.windowCache.get(key);
1112
+ if (cached !== void 0) return cached;
1113
+ let window;
1114
+ if (!this.config.autoModelContextLimit) {
1115
+ window = { limit: DEFAULT_CONTEXT_WINDOW, source: "default", provider, model };
1116
+ } else {
1117
+ const detected = await detectContextWindow(agent, provider, model);
1118
+ window = detected === null ? { limit: DEFAULT_CONTEXT_WINDOW, source: "default", provider, model } : { limit: detected, source: "auto", provider, model };
1119
+ }
1120
+ this.windowCache.set(key, window);
1121
+ return window;
1122
+ }
822
1123
  /** ACP is model-driven: automatic pressure policy never summarizes by itself. */
823
1124
  async compactIfNeeded(_agent, _trigger, signal) {
824
1125
  signal.throwIfAborted();
@@ -848,11 +1149,17 @@ export {
848
1149
  ACP_SYSTEM_PROMPT_ORDER,
849
1150
  AcpCompactionEngine,
850
1151
  AcpStateStore,
1152
+ DEFAULT_CONTEXT_WINDOW,
851
1153
  acpCommand,
852
1154
  assertNoActiveCompaction,
1155
+ blockRefForSummarySeq,
1156
+ blockRegistry,
853
1157
  buildNudge,
1158
+ compactionIdsOfKernelBlocks,
854
1159
  index_default as default,
1160
+ detectContextWindow,
855
1161
  eventsToCoreMessages,
1162
+ expandShadowedSeqs,
856
1163
  extractEventText,
857
1164
  findOpenTurn,
858
1165
  kernelConfigFor,
@@ -863,6 +1170,8 @@ export {
863
1170
  resolveSurfaceRange,
864
1171
  runCompactionTransaction,
865
1172
  shadowedSeqsOf,
866
- surfaceEventsOf
1173
+ summarySeqOfKernelBlock,
1174
+ surfaceEventsOf,
1175
+ windowSourceLabel
867
1176
  };
868
1177
  //# sourceMappingURL=index.js.map