dsh-context-compression-improved 0.4.0 → 0.5.1

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.
Files changed (36) hide show
  1. package/CHANGELOG.ja.md +34 -0
  2. package/CHANGELOG.ko.md +34 -0
  3. package/CHANGELOG.md +38 -0
  4. package/CHANGELOG.zh.md +30 -0
  5. package/docs/installation.md +25 -1
  6. package/docs/installation.zh.md +24 -1
  7. package/package.json +1 -1
  8. package/packages/selector/lib/{review-registry.js → advisor-state.js} +105 -4
  9. package/packages/selector/lib/client.d.ts +7 -0
  10. package/packages/selector/lib/client.js +32 -2
  11. package/packages/selector/lib/index.d.ts +7 -0
  12. package/packages/selector/lib/index.js +150 -3
  13. package/packages/selector/lib/pruner.d.ts +82 -0
  14. package/packages/selector/lib/pruner.js +545 -11
  15. package/packages/selector/src/client/preset-options.ts +2 -0
  16. package/packages/selector/src/index.ts +108 -0
  17. package/packages/selector/src/preset-overlay.ts +60 -1
  18. package/packages/selector/src/profiles.ts +48 -0
  19. package/packages/selector/src/pruner/state.ts +3 -0
  20. package/packages/selector/src/pruner.ts +113 -0
  21. package/packages/selector/src/runtime/audit.ts +22 -0
  22. package/packages/selector/src/runtime/config.ts +58 -0
  23. package/packages/selector/src/runtime/tokenpilot/advisor-prompt.ts +188 -0
  24. package/packages/selector/src/runtime/tokenpilot/advisor-state.ts +133 -0
  25. package/packages/selector/src/runtime/tokenpilot/advisor.ts +419 -0
  26. package/packages/selector/src/runtime/tokenpilot/sidechannel.ts +24 -9
  27. package/packages/selector/src/runtime/types.ts +21 -0
  28. package/packages/selector/tests/advisor-report.host.spec.ts +223 -0
  29. package/packages/selector/tests/built/client-artifact.spec.ts +9 -5
  30. package/packages/selector/tests/runtime/advisor-invariant.spec.ts +272 -0
  31. package/packages/selector/tests/runtime/advisor.spec.ts +226 -0
  32. package/packages/selector/tests/runtime/audit.spec.ts +44 -0
  33. package/packages/selector/tests/runtime/tokenpilot/profile-baseline.spec.ts +12 -0
  34. package/packages/selector/tests/standing-generation.host.spec.ts +54 -5
  35. package/scripts/packed-components-smoke.mjs +30 -8
  36. package/scripts/packed-install-e2e.mjs +69 -15
@@ -1,4 +1,4 @@
1
- import { C as COMPRESSION_PROFILES, S as deepFreeze, _ as resolvePolicy, a as AUTO_COMPACT_THRESHOLD_LIMITS, b as resolveCustomPolicy, c as DEFAULTS, d as charsToTokens, f as codePointLength, g as resolveConfig, h as parseContextCompressionSettings, i as ReviewQueue, l as PRUNE_MARKER, m as isValidAutoCompactThresholdPercent, o as CONTEXT_COMPRESSION_SETTINGS_NAMESPACE, p as isCompressionProfile, r as sharedReviewStore, s as ContextCompressionSettingsSchema, t as registerReviewPruner, u as charsForTokens, v as CustomCompressionPolicySchema, x as assertNever, y as DEFAULT_CUSTOM_COMPRESSION_POLICY } from "./review-registry.js";
1
+ import { C as DEFAULT_CUSTOM_COMPRESSION_POLICY, D as COMPRESSION_PROFILES, E as deepFreeze, S as CustomCompressionPolicySchema, T as assertNever, _ as isCompressionProfile, a as registerReviewPruner, b as resolveConfig, c as ReviewQueue, d as ContextCompressionSettingsSchema, f as DEFAULTS, g as codePointLength, h as charsToTokens, i as recordScore, l as AUTO_COMPACT_THRESHOLD_LIMITS, m as charsForTokens, n as invalidateOnTaskChange, p as PRUNE_MARKER, r as recordRecertified, s as sharedReviewStore, t as getAdvisorState, u as CONTEXT_COMPRESSION_SETTINGS_NAMESPACE, v as isValidAutoCompactThresholdPercent, w as resolveCustomPolicy, x as resolvePolicy, y as parseContextCompressionSettings } from "./advisor-state.js";
2
2
  import { a as validatePublishedTailTrim, i as tailTrimStub, n as tailTrimMessage, o as eventBySeq, r as tailTrimRef, s as sessionEvents, t as parseTailTrimRef } from "./tail-trim.js";
3
3
  import z from "@deepseek-ai/schemastery";
4
4
  import { createHash } from "node:crypto";
@@ -2810,20 +2810,31 @@ function documentCensus(text, omittedLines) {
2810
2810
  var SideChannel = class {
2811
2811
  ctx;
2812
2812
  options;
2813
- constructor(ctx, options) {
2813
+ overrides;
2814
+ /**
2815
+ * @param overrides - per-consumer overrides of the estimator-named options.
2816
+ * The estimator itself never passes them (byte-identical behavior); the
2817
+ * advisory advisor passes its own mode/timeout/output budget so both
2818
+ * consumers share one transport without sharing one configuration.
2819
+ */
2820
+ constructor(ctx, options, overrides) {
2814
2821
  this.ctx = ctx;
2815
2822
  this.options = options;
2823
+ this.overrides = overrides;
2824
+ }
2825
+ get mode() {
2826
+ return this.overrides?.mode ?? this.options.estimatorMode ?? "";
2816
2827
  }
2817
2828
  get enabled() {
2818
- return this.options.estimatorMode === "host" || this.options.estimatorMode === "direct";
2829
+ return this.mode === "host" || this.mode === "direct";
2819
2830
  }
2820
2831
  async ask(request) {
2821
- const timeoutMs = this.options.estimatorTimeoutMs ?? 3e3;
2832
+ const timeoutMs = this.overrides?.timeoutMs ?? this.options.estimatorTimeoutMs ?? 3e3;
2822
2833
  const timeout = AbortSignal.timeout(timeoutMs);
2823
2834
  const signal = typeof AbortSignal.any === "function" ? AbortSignal.any([request.signal, timeout]) : timeout;
2824
2835
  try {
2825
- if (this.options.estimatorMode === "host") return await this.askHost(request.system, request.user, signal);
2826
- if (this.options.estimatorMode === "direct") return await this.askDirect(request.system, request.user, signal);
2836
+ if (this.mode === "host") return await this.askHost(request.system, request.user, signal);
2837
+ if (this.mode === "direct") return await this.askDirect(request.system, request.user, signal);
2827
2838
  return;
2828
2839
  } catch {
2829
2840
  return;
@@ -2837,7 +2848,7 @@ var SideChannel = class {
2837
2848
  ok: text !== void 0,
2838
2849
  latencyMs: Date.now() - now,
2839
2850
  ...this.identity() !== void 0 ? { channel: this.identity() } : {},
2840
- ...text === void 0 ? { reason: "channel returned no content (timeout, non-2xx, parse failure, or reasoning ate the 256-token budget)" } : {}
2851
+ ...text === void 0 ? { reason: "channel returned no content (timeout, non-2xx, parse failure, or reasoning ate the output-token budget)" } : {}
2841
2852
  };
2842
2853
  return {
2843
2854
  ...text === void 0 ? {} : { text },
@@ -2845,8 +2856,8 @@ var SideChannel = class {
2845
2856
  };
2846
2857
  }
2847
2858
  identity() {
2848
- if (this.options.estimatorMode === "direct") return `direct:${this.options.estimatorModel ?? ""}`;
2849
- if (this.options.estimatorMode === "host") {
2859
+ if (this.mode === "direct") return `direct:${this.options.estimatorModel ?? ""}`;
2860
+ if (this.mode === "host") {
2850
2861
  const route = this.resolveHostRoute();
2851
2862
  return route === void 0 ? "host" : `host:${route.provider}/${route.model}`;
2852
2863
  }
@@ -2893,7 +2904,7 @@ var SideChannel = class {
2893
2904
  system,
2894
2905
  temperature: 0,
2895
2906
  reasoningEffort: "off",
2896
- maxTokens: 256,
2907
+ maxTokens: this.overrides?.maxTokens ?? 256,
2897
2908
  signal
2898
2909
  });
2899
2910
  for await (const chunk of stream) if (chunk.type === "text-delta" && typeof chunk.text === "string") text += chunk.text;
@@ -2920,7 +2931,7 @@ var SideChannel = class {
2920
2931
  content: user
2921
2932
  }],
2922
2933
  temperature: 0,
2923
- max_tokens: 256
2934
+ max_tokens: this.overrides?.maxTokens ?? 256
2924
2935
  }),
2925
2936
  signal
2926
2937
  });
@@ -3332,6 +3343,445 @@ async function openReviewStorage(getService) {
3332
3343
  return new StorageDomainReviewStore((await service.open(reviewStorageSpec())).table(REVIEW_STORAGE_TABLE));
3333
3344
  }
3334
3345
  //#endregion
3346
+ //#region src/runtime/tokenpilot/advisor-prompt.ts
3347
+ function buildAdvisorSummarySystemPrompt() {
3348
+ return [
3349
+ "You summarize what an agent session is working on, for relevance statistics only.",
3350
+ "Input: the session todolist snapshot and a recent tail of assistant narration.",
3351
+ "Answer with ONLY one JSON object:",
3352
+ "{\"overallTask\":\"<one sentence>\",\"activeSubtasks\":[\"<subtask>\"],\"keywords\":[\"<task keyword>\"]}.",
3353
+ "keywords must be 3-10 short distinctive words describing the CURRENT task.",
3354
+ "Never add commentary; never invent tasks that the input does not support."
3355
+ ].join(" ");
3356
+ }
3357
+ function buildAdvisorSummaryUserPrompt(taskText, tailText) {
3358
+ return [`todolist:\n${taskText}`, tailText.trim().length > 0 ? `recent tail:\n${tailText.trim()}` : "recent tail: (none)"].join("\n\n");
3359
+ }
3360
+ function buildAdvisorScoringSystemPrompt() {
3361
+ return [
3362
+ "You score how relevant each historical session artifact is to the current task,",
3363
+ "for statistics only. Relevance covers both the artifact content and its comments",
3364
+ "(comment semantics count too). 0 means unrelated, 1 means the live agent will",
3365
+ "very likely need this exact content again.",
3366
+ "Answer with ONLY one JSON object per input line:",
3367
+ "{\"seq\":<number>,\"score\":<number between 0 and 1>,\"reason\":\"<short>\"}",
3368
+ "one per line, same order as the input. Never invent seq values; never add commentary."
3369
+ ].join(" ");
3370
+ }
3371
+ function buildAdvisorScoringUserPrompt(taskText, activeSubtasks, candidates) {
3372
+ return [
3373
+ `task: ${taskText.replace(/\s+/gu, " ").slice(0, 600)}`,
3374
+ activeSubtasks.length > 0 ? `active subtasks: ${activeSubtasks.join("; ").slice(0, 300)}` : "active subtasks: (none)",
3375
+ "",
3376
+ ...candidates.map((candidate) => `seq=${String(candidate.seq)} | ${candidate.preview.replace(/\s+/gu, " ")}`)
3377
+ ].join("\n");
3378
+ }
3379
+ /** Pull the first balanced JSON object out of a possibly chatty answer. */
3380
+ function firstJsonObject(text) {
3381
+ const start = text.indexOf("{");
3382
+ if (start < 0) return void 0;
3383
+ let depth = 0;
3384
+ let inString = false;
3385
+ let escaped = false;
3386
+ for (let index = start; index < text.length; index += 1) {
3387
+ const char = text[index];
3388
+ if (inString) {
3389
+ if (escaped) escaped = false;
3390
+ else if (char === "\\") escaped = true;
3391
+ else if (char === "\"") inString = false;
3392
+ continue;
3393
+ }
3394
+ if (char === "\"") inString = true;
3395
+ else if (char === "{") depth += 1;
3396
+ else if (char === "}") {
3397
+ depth -= 1;
3398
+ if (depth === 0) try {
3399
+ const parsed = JSON.parse(text.slice(start, index + 1));
3400
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
3401
+ } catch {
3402
+ return;
3403
+ }
3404
+ }
3405
+ }
3406
+ }
3407
+ function stringList(value, limit) {
3408
+ if (!Array.isArray(value)) return [];
3409
+ return value.filter((entry) => typeof entry === "string" && entry.trim().length > 0).slice(0, limit).map((entry) => entry.trim());
3410
+ }
3411
+ /**
3412
+ * Parse one summary answer. Fail-open: `undefined` on any malformed or
3413
+ * missing field, so a broken channel can never poison the cached summary.
3414
+ */
3415
+ function parseAdvisorSummary(text) {
3416
+ if (text === void 0 || text.trim().length === 0) return void 0;
3417
+ const object = firstJsonObject(text);
3418
+ if (object === void 0) return void 0;
3419
+ const overallTask = object.overallTask;
3420
+ if (typeof overallTask !== "string" || overallTask.trim().length === 0) return void 0;
3421
+ const activeSubtasks = stringList(object.activeSubtasks, 12);
3422
+ const keywords = stringList(object.keywords, 12);
3423
+ if (keywords.length === 0 && activeSubtasks.length === 0) return void 0;
3424
+ return {
3425
+ overallTask: overallTask.trim(),
3426
+ activeSubtasks,
3427
+ keywords
3428
+ };
3429
+ }
3430
+ /** Extract every balanced JSON object from a JSON-lines or chatty answer. */
3431
+ function jsonObjects(text) {
3432
+ const objects = [];
3433
+ let depth = 0;
3434
+ let start = -1;
3435
+ let inString = false;
3436
+ let escaped = false;
3437
+ for (let index = 0; index < text.length; index += 1) {
3438
+ const char = text[index];
3439
+ if (inString) {
3440
+ if (escaped) escaped = false;
3441
+ else if (char === "\\") escaped = true;
3442
+ else if (char === "\"") inString = false;
3443
+ continue;
3444
+ }
3445
+ if (char === "\"") inString = true;
3446
+ else if (char === "{") {
3447
+ if (depth === 0) start = index;
3448
+ depth += 1;
3449
+ } else if (char === "}") {
3450
+ depth -= 1;
3451
+ if (depth === 0 && start >= 0) {
3452
+ try {
3453
+ const parsed = JSON.parse(text.slice(start, index + 1));
3454
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) objects.push(parsed);
3455
+ } catch {}
3456
+ start = -1;
3457
+ }
3458
+ }
3459
+ }
3460
+ return objects;
3461
+ }
3462
+ /**
3463
+ * Parse one scoring answer. Fail-open: returns the valid rows it could read
3464
+ * (`undefined` when nothing valid remains) — a partially garbage answer still
3465
+ * contributes its good rows, mirroring the estimator's per-item tolerance.
3466
+ */
3467
+ function parseAdvisorScores(text, validSeqs) {
3468
+ if (text === void 0 || text.trim().length === 0) return void 0;
3469
+ const scores = /* @__PURE__ */ new Map();
3470
+ for (const object of jsonObjects(text)) {
3471
+ const seq = object.seq;
3472
+ const score = object.score;
3473
+ if (typeof seq !== "number" || !Number.isSafeInteger(seq) || !validSeqs.has(seq)) continue;
3474
+ if (typeof score !== "number" || !Number.isFinite(score) || score < 0 || score > 1) continue;
3475
+ const reason = typeof object.reason === "string" && object.reason.trim().length > 0 ? object.reason.trim() : void 0;
3476
+ scores.set(seq, {
3477
+ seq,
3478
+ score,
3479
+ ...reason !== void 0 ? { reason } : {}
3480
+ });
3481
+ }
3482
+ return scores.size > 0 ? scores : void 0;
3483
+ }
3484
+ //#endregion
3485
+ //#region src/runtime/tokenpilot/advisor.ts
3486
+ /** Character cap for the recent-text fallback and the tail-text summary input. */
3487
+ const TAIL_TEXT_CHAR_BUDGET = 4e3;
3488
+ /** Score the advisor assigns to candidates it has no answer for. */
3489
+ const NEUTRAL_RELEVANCE = .5;
3490
+ /** Deterministic djb2-derived hex digest for task-semantics versioning. */
3491
+ function versionDigest(text) {
3492
+ let hash = 5381;
3493
+ for (let index = 0; index < text.length; index += 1) hash = (hash * 33 ^ text.charCodeAt(index)) >>> 0;
3494
+ return hash.toString(16).padStart(8, "0");
3495
+ }
3496
+ /** Truncate on the character basis (Unicode code points), never UTF-16 units. */
3497
+ function truncateChars(text, budget) {
3498
+ if (codePointLength(text) <= budget) return text;
3499
+ return Array.from(text).slice(0, budget).join("");
3500
+ }
3501
+ /** Character cap of one candidate preview line offered to the scoring prompt. */
3502
+ const PREVIEW_CHAR_BUDGET = 200;
3503
+ /**
3504
+ * One candidate face for the scoring prompt: tool-call name plus the head of
3505
+ * the result text. Pure and shape-defensive.
3506
+ */
3507
+ function advisorCandidatePreview(callName, blocks) {
3508
+ return truncateChars(`${callName} ${textBlocks(blocks)}`.trim(), PREVIEW_CHAR_BUDGET);
3509
+ }
3510
+ /**
3511
+ * Collect the recent assistant narration tail (bounded, oldest-first join) as
3512
+ * summary-prompt context. Pure.
3513
+ */
3514
+ function collectTailText(events, budget = TAIL_TEXT_CHAR_BUDGET) {
3515
+ const parts = [];
3516
+ let size = 0;
3517
+ for (let index = events.length - 1; index >= 0; index -= 1) {
3518
+ const event = events[index];
3519
+ if (event?.type !== "assistant/message") continue;
3520
+ const text = textBlocks(event.data).trim();
3521
+ if (text.length === 0) continue;
3522
+ parts.unshift(text);
3523
+ size += codePointLength(text);
3524
+ if (size >= budget) break;
3525
+ }
3526
+ return truncateChars(parts.join("\n"), budget);
3527
+ }
3528
+ function textBlocks(data) {
3529
+ const content = data?.content;
3530
+ if (!Array.isArray(content)) return "";
3531
+ const parts = [];
3532
+ for (const block of content) if (block?.type === "text" && typeof block.text === "string") parts.push(block.text);
3533
+ return parts.join("\n");
3534
+ }
3535
+ /** Structured probe of one `todo/write` payload: the list of task strings, or undefined. */
3536
+ function extractTodoItems(data) {
3537
+ const todos = data?.todos;
3538
+ const list = Array.isArray(todos) ? todos : Array.isArray(data) ? data : void 0;
3539
+ if (list === void 0 || list.length === 0) return void 0;
3540
+ const items = [];
3541
+ for (const entry of list) {
3542
+ if (typeof entry === "string") {
3543
+ if (entry.trim().length > 0) items.push(entry.trim());
3544
+ continue;
3545
+ }
3546
+ if (entry !== null && typeof entry === "object") {
3547
+ const record = entry;
3548
+ const text = [
3549
+ record.content,
3550
+ record.text,
3551
+ record.title,
3552
+ record.name
3553
+ ].find((candidate) => typeof candidate === "string" && candidate.trim().length > 0);
3554
+ if (typeof text === "string") {
3555
+ items.push(text.trim());
3556
+ continue;
3557
+ }
3558
+ items.push(JSON.stringify(record));
3559
+ }
3560
+ }
3561
+ return items.length > 0 ? items : void 0;
3562
+ }
3563
+ /**
3564
+ * Harvest task semantics for the summary/scoring prompts: the most recent
3565
+ * `todo/write` event (structured probe first, then the raw JSON string),
3566
+ * falling back to recent user/message text. Pure — log in, semantics out.
3567
+ */
3568
+ function collectTaskSemantics(events) {
3569
+ for (let index = events.length - 1; index >= 0; index -= 1) {
3570
+ const event = events[index];
3571
+ if (event === void 0 || event.type !== "todo/write") continue;
3572
+ const data = event.data;
3573
+ const items = extractTodoItems(data);
3574
+ if (items !== void 0) {
3575
+ const taskText = truncateChars(items.join("\n"), TAIL_TEXT_CHAR_BUDGET);
3576
+ return {
3577
+ source: "todos",
3578
+ todoVersion: versionDigest(taskText),
3579
+ taskText
3580
+ };
3581
+ }
3582
+ const raw = truncateChars(JSON.stringify(event.data) ?? "", TAIL_TEXT_CHAR_BUDGET);
3583
+ if (raw.length > 2) return {
3584
+ source: "raw-todo",
3585
+ todoVersion: versionDigest(raw),
3586
+ taskText: raw
3587
+ };
3588
+ }
3589
+ for (let index = events.length - 1; index >= 0; index -= 1) {
3590
+ const event = events[index];
3591
+ if (event?.type !== "user/message") continue;
3592
+ const text = truncateChars(textBlocks(event.data).trim(), TAIL_TEXT_CHAR_BUDGET);
3593
+ if (text.length === 0) continue;
3594
+ return {
3595
+ source: "messages",
3596
+ todoVersion: versionDigest(text),
3597
+ taskText: text
3598
+ };
3599
+ }
3600
+ }
3601
+ /**
3602
+ * Prefix-decay figure: 1 minus the character-pressure-weighted mean relevance
3603
+ * of the prefix candidates. Unscored candidates count as neutral 0.5. Pure,
3604
+ * deterministic, no LLM and no I/O.
3605
+ */
3606
+ function prefixDecay(candidates, scores) {
3607
+ let totalWeight = 0;
3608
+ let weightedRelevance = 0;
3609
+ for (const candidate of candidates) {
3610
+ const weight = candidate.characterPressure > 0 ? candidate.characterPressure : 0;
3611
+ if (weight === 0) continue;
3612
+ totalWeight += weight;
3613
+ weightedRelevance += weight * (scores.get(candidate.seq)?.score ?? NEUTRAL_RELEVANCE);
3614
+ }
3615
+ if (totalWeight === 0) return {
3616
+ decay: 0,
3617
+ weightedChars: 0
3618
+ };
3619
+ return {
3620
+ decay: 1 - weightedRelevance / totalWeight,
3621
+ weightedChars: totalWeight
3622
+ };
3623
+ }
3624
+ /** Lowercase word tokens used by the local keyword-overlap prescreen. */
3625
+ function keywordsOf(text) {
3626
+ const matches = text.toLowerCase().match(/[\p{L}\p{N}_-]{3,}/gu) ?? [];
3627
+ return new Set(matches);
3628
+ }
3629
+ function overlapCount(left, right) {
3630
+ let count = 0;
3631
+ for (const token of right) if (left.has(token)) count += 1;
3632
+ return count;
3633
+ }
3634
+ /**
3635
+ * Incremental scoring selection: candidates newer than the watermark whose
3636
+ * character pressure reaches the token-named floor, ranked by local keyword
3637
+ * overlap with the task semantics and cut at the sample limit. When the task
3638
+ * semantics changed, the watermark is ignored so every eligible candidate can
3639
+ * rescore. Pure.
3640
+ */
3641
+ function selectScoringCandidates(candidates, state, input) {
3642
+ const eligible = [];
3643
+ for (const candidate of candidates) {
3644
+ if (!input.taskChanged && candidate.seq <= state.watermarkSeq) continue;
3645
+ if (candidate.characterPressure < input.minChars) continue;
3646
+ eligible.push({
3647
+ ...candidate,
3648
+ overlap: overlapCount(input.taskKeywords, keywordsOf(candidate.preview))
3649
+ });
3650
+ }
3651
+ eligible.sort((left, right) => right.overlap - left.overlap || right.characterPressure - left.characterPressure);
3652
+ return eligible.slice(0, input.sampleLimit).map(({ overlap: _overlap, ...candidate }) => candidate);
3653
+ }
3654
+ function advisorAudit(input, phase, fields) {
3655
+ return {
3656
+ schemaVersion: 1,
3657
+ kind: "advisor-outcome",
3658
+ sessionId: input.sessionId,
3659
+ phase,
3660
+ turnIndex: input.turn,
3661
+ ...fields
3662
+ };
3663
+ }
3664
+ /**
3665
+ * One full advisor pass: summary refresh (todo change or every refreshTurns),
3666
+ * incremental batch scoring with recertification marks, then the decay
3667
+ * figure. State is written only on success; any failure leaves state
3668
+ * untouched, emits ok:false audits with reason codes, and never throws.
3669
+ */
3670
+ async function runAdvisorPass(state, channel, emit, input) {
3671
+ if (input.task === void 0) return void 0;
3672
+ const taskChanged = invalidateOnTaskChange(state, input.task.todoVersion);
3673
+ const turn = input.turn;
3674
+ if (state.summary === void 0 || turn - state.lastSummaryTurn >= input.advisor.refreshTurns) {
3675
+ const summaryStarted = Date.now();
3676
+ const summaryText = await channel.ask({
3677
+ system: buildAdvisorSummarySystemPrompt(),
3678
+ user: buildAdvisorSummaryUserPrompt(input.task.taskText, input.tailText),
3679
+ signal: input.signal
3680
+ });
3681
+ const summaryLatencyMs = Date.now() - summaryStarted;
3682
+ const summary = input.signal.aborted ? void 0 : parseAdvisorSummary(summaryText);
3683
+ if (summary === void 0) {
3684
+ emit(advisorAudit(input, "summary", {
3685
+ ok: false,
3686
+ latencyMs: summaryLatencyMs,
3687
+ ...input.signal.aborted ? { reason: "aborted" } : summaryText === void 0 ? { reason: "channel-empty" } : { reason: "parse-failed" }
3688
+ }));
3689
+ return;
3690
+ }
3691
+ state.summary = {
3692
+ ...summary,
3693
+ todoVersion: input.task.todoVersion,
3694
+ turn
3695
+ };
3696
+ state.lastSummaryTurn = turn;
3697
+ emit(advisorAudit(input, "summary", {
3698
+ ok: true,
3699
+ latencyMs: summaryLatencyMs
3700
+ }));
3701
+ }
3702
+ const summary = state.summary;
3703
+ if (summary === void 0) return void 0;
3704
+ const sampled = selectScoringCandidates(input.candidates, state, {
3705
+ taskKeywords: keywordsOf(`${input.task.taskText}\n${summary.keywords.join(" ")}`),
3706
+ minChars: charsForTokens(input.advisor.minTokens),
3707
+ sampleLimit: input.advisor.sampleLimit,
3708
+ taskChanged
3709
+ });
3710
+ let scored = 0;
3711
+ let highestScored = 0;
3712
+ if (sampled.length > 0) {
3713
+ const scoringStarted = Date.now();
3714
+ const scoresText = await channel.ask({
3715
+ system: buildAdvisorScoringSystemPrompt(),
3716
+ user: buildAdvisorScoringUserPrompt(input.task.taskText, summary.activeSubtasks, sampled),
3717
+ signal: input.signal
3718
+ });
3719
+ const scoringLatencyMs = Date.now() - scoringStarted;
3720
+ const scores = input.signal.aborted ? void 0 : parseAdvisorScores(scoresText, new Set(sampled.map((item) => item.seq)));
3721
+ if (scores === void 0 || scores.size === 0) {
3722
+ emit(advisorAudit(input, "scoring", {
3723
+ ok: false,
3724
+ sampledCount: sampled.length,
3725
+ latencyMs: scoringLatencyMs,
3726
+ ...input.signal.aborted ? { reason: "aborted" } : scoresText === void 0 ? { reason: "channel-empty" } : { reason: "parse-failed" }
3727
+ }));
3728
+ return;
3729
+ }
3730
+ for (const candidate of sampled) {
3731
+ const answer = scores.get(candidate.seq);
3732
+ if (answer === void 0) continue;
3733
+ recordScore(state, candidate.seq, {
3734
+ score: answer.score,
3735
+ turn
3736
+ });
3737
+ scored += 1;
3738
+ if (candidate.seq > highestScored) highestScored = candidate.seq;
3739
+ if (answer.score < input.advisor.scoreThreshold) recordRecertified(state, candidate.seq, turn);
3740
+ }
3741
+ emit(advisorAudit(input, "scoring", {
3742
+ ok: true,
3743
+ sampledCount: sampled.length,
3744
+ latencyMs: scoringLatencyMs
3745
+ }));
3746
+ if (highestScored > state.watermarkSeq) state.watermarkSeq = highestScored;
3747
+ }
3748
+ const decay = prefixDecay(input.candidates, state.scores);
3749
+ state.lastDecay = {
3750
+ decay: decay.decay,
3751
+ weightedChars: decay.weightedChars,
3752
+ turn
3753
+ };
3754
+ emit(advisorAudit(input, "decay", {
3755
+ ok: true,
3756
+ sampledCount: scored,
3757
+ decay: decay.decay,
3758
+ weightedChars: decay.weightedChars,
3759
+ latencyMs: 0
3760
+ }));
3761
+ return {
3762
+ decay: decay.decay,
3763
+ weightedChars: decay.weightedChars,
3764
+ sampled: sampled.length
3765
+ };
3766
+ }
3767
+ /**
3768
+ * Convenience entry used by the pruner: fetch-or-create the session state and
3769
+ * run one pass against it.
3770
+ */
3771
+ async function runSessionAdvisorPass(session, channel, emit, input) {
3772
+ const state = getAdvisorState(session);
3773
+ if (state.inFlight) return void 0;
3774
+ state.inFlight = true;
3775
+ try {
3776
+ return await runAdvisorPass(state, channel, emit, {
3777
+ ...input,
3778
+ sessionId: input.sessionId ?? String(session.id)
3779
+ });
3780
+ } finally {
3781
+ state.inFlight = false;
3782
+ }
3783
+ }
3784
+ //#endregion
3335
3785
  //#region src/runtime/deepseek-official-pricing.ts
3336
3786
  /** Checked-in DeepSeek official prices and fixed-point provider-usage accounting. */
3337
3787
  const DEEPSEEK_OFFICIAL_PRICE_CATALOG_VERSION = "deepseek-official-2026-08-25";
@@ -3766,6 +4216,7 @@ var ToolResultPruner = class extends Service {
3766
4216
  reviewQueues: /* @__PURE__ */ new WeakMap(),
3767
4217
  reviewClocks: /* @__PURE__ */ new WeakMap(),
3768
4218
  estimatorRemainingTurns: /* @__PURE__ */ new WeakMap(),
4219
+ advisorChannels: /* @__PURE__ */ new WeakMap(),
3769
4220
  reviewSummaries: /* @__PURE__ */ new WeakMap()
3770
4221
  };
3771
4222
  ctx.effect(() => registerReviewPruner(this), "contextCompressionSelector.reviewRegistry()");
@@ -3821,6 +4272,7 @@ var ToolResultPruner = class extends Service {
3821
4272
  ctx.logger.warn("context-compression review turn-boundary pass failed open: %o", error);
3822
4273
  }
3823
4274
  this.postflightEstimatorPass(agent.session, signal).catch(() => void 0);
4275
+ this.postflightAdvisorPass(agent.session, turn, signal).catch(() => void 0);
3824
4276
  });
3825
4277
  }
3826
4278
  /**
@@ -4068,6 +4520,88 @@ var ToolResultPruner = class extends Service {
4068
4520
  });
4069
4521
  }
4070
4522
  /**
4523
+ * Advisory advisor pass at the turn boundary, strictly fire-and-forget.
4524
+ * Produces todolist-bound tail-task summaries, incremental relevance
4525
+ * scores, and a prefix-decay figure — all observational. Every short
4526
+ * circuit below (mode off, re-entry, cooldown, no task semantics, no
4527
+ * direct endpoint) returns without touching any state the pruning chain
4528
+ * reads, so the default configuration adds exactly zero behavior.
4529
+ */
4530
+ async postflightAdvisorPass(session, turn, signal) {
4531
+ const policy = this.activePolicy(session);
4532
+ const presetOptions = policy?.presetOptions;
4533
+ const advisor = presetOptions?.advisor;
4534
+ if (policy === void 0 || presetOptions === void 0 || advisor === void 0 || advisor.mode === "") return;
4535
+ const advisorState = getAdvisorState(session);
4536
+ if (advisorState.inFlight) return;
4537
+ if (isCoolingDown(advisorState.failures, Date.now())) return;
4538
+ const events = sessionEvents(session);
4539
+ const task = collectTaskSemantics(events);
4540
+ if (task === void 0) return;
4541
+ const settings = this.activeSettings(session).presetOptions ?? {};
4542
+ if (advisor.mode === "direct" && (settings.estimatorBaseUrl === void 0 || settings.estimatorBaseUrl.length === 0 || settings.estimatorModel === void 0 || settings.estimatorModel.length === 0)) {
4543
+ emitCompressionAudit(this.ctx.logger, {
4544
+ schemaVersion: 1,
4545
+ kind: "advisor-outcome",
4546
+ sessionId: String(session.id),
4547
+ phase: "summary",
4548
+ channel: "direct",
4549
+ ok: false,
4550
+ turnIndex: turn,
4551
+ reason: "no-direct-endpoint",
4552
+ latencyMs: 0
4553
+ });
4554
+ advisorState.failures = {
4555
+ failures: (advisorState.failures?.failures ?? 0) + 1,
4556
+ cooldownUntil: Date.now() + backoffCooldownMs((advisorState.failures?.failures ?? 0) + 1)
4557
+ };
4558
+ return;
4559
+ }
4560
+ let channel = this.state.advisorChannels.get(session);
4561
+ if (channel === void 0) {
4562
+ channel = new SideChannel(this.ctx, settings, {
4563
+ mode: advisor.mode,
4564
+ timeoutMs: advisor.timeoutMs,
4565
+ maxTokens: 512
4566
+ });
4567
+ this.state.advisorChannels.set(session, channel);
4568
+ }
4569
+ const view = measureForCompaction(this.ctx, session);
4570
+ const candidates = this.snapshot(session, view).filter((candidate) => !this.isRecoveryExempt(session, candidate)).map((candidate) => ({
4571
+ seq: candidate.seq,
4572
+ characterPressure: candidate.characterPressure,
4573
+ preview: advisorCandidatePreview(candidate.call.name, candidate.event.data.message.content)
4574
+ }));
4575
+ let sawFailure = false;
4576
+ const outcome = await runSessionAdvisorPass(session, channel, (record) => {
4577
+ if (record.ok === false) sawFailure = true;
4578
+ emitCompressionAudit(this.ctx.logger, record);
4579
+ }, {
4580
+ profile: policy.profile,
4581
+ sessionId: String(session.id),
4582
+ turn,
4583
+ candidates,
4584
+ task: {
4585
+ source: task.source,
4586
+ todoVersion: task.todoVersion,
4587
+ taskText: task.taskText
4588
+ },
4589
+ advisor: {
4590
+ refreshTurns: advisor.refreshTurns,
4591
+ scoreThreshold: advisor.scoreThreshold,
4592
+ sampleLimit: advisor.sampleLimit,
4593
+ minTokens: advisor.minTokens
4594
+ },
4595
+ tailText: collectTailText(events),
4596
+ signal
4597
+ });
4598
+ if (outcome === void 0 && sawFailure && signal.aborted === false) advisorState.failures = {
4599
+ failures: (advisorState.failures?.failures ?? 0) + 1,
4600
+ cooldownUntil: Date.now() + backoffCooldownMs((advisorState.failures?.failures ?? 0) + 1)
4601
+ };
4602
+ else if (outcome !== void 0) advisorState.failures = void 0;
4603
+ }
4604
+ /**
4071
4605
  * The per-session review queue, or `undefined` while review mode is off
4072
4606
  * (every review path must then behave exactly like before).
4073
4607
  */
@@ -25,6 +25,8 @@ const PRESET_OPTION_KEYS = [
25
25
  'dedupeToolResults', 'summaryLocator', 'prefixStabilizer', 'readState', 'estimatorMode',
26
26
  'estimatorProvider', 'estimatorModel', 'estimatorBaseUrl', 'estimatorApiKey', 'estimatorTimeoutMs',
27
27
  'reviewMode', 'reviewTimeoutTurns', 'cacheHitDiscountAlpha', 'reviewHighImpactTokens',
28
+ 'advisorMode', 'advisorTimeoutMs', 'advisorRefreshTurns', 'advisorScoreThreshold', 'advisorSampleLimit',
29
+ 'advisorMinTokens',
28
30
  ] as const
29
31
 
30
32
  /** One partial edit of `presetOptions`; `undefined` clears the named field. */