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 { n as resolveReviewPruner, o as CONTEXT_COMPRESSION_SETTINGS_NAMESPACE, s as ContextCompressionSettingsSchema } from "./review-registry.js";
1
+ import { d as ContextCompressionSettingsSchema, o as resolveReviewPruner, t as getAdvisorState, u as CONTEXT_COMPRESSION_SETTINGS_NAMESPACE } from "./advisor-state.js";
2
2
  import z from "@deepseek-ai/schemastery";
3
3
  import "@deepseek-ai/dsh-settings";
4
4
  import { AsyncLocalStorage } from "node:async_hooks";
@@ -118,6 +118,15 @@ function standingStampMsAtWindow(identity, windowIndex) {
118
118
  const subSecond = parseInt(identity.slice(start + 8, start + STANDING_MTIME_WINDOW_HEX).padEnd(3, "0"), 16) % 1e3;
119
119
  return seconds * 1e3 + subSecond;
120
120
  }
121
+ /**
122
+ * True for the filesystem errors a concurrent publish can raise on Windows:
123
+ * `MoveFileEx` reports a lost race — or a reader holding the destination open —
124
+ * as EPERM/EBUSY/EACCES, where POSIX `rename` simply replaces the destination.
125
+ */
126
+ function isPublishRace(error) {
127
+ const code = error?.code;
128
+ return code === "EPERM" || code === "EBUSY" || code === "EACCES" || code === "EEXIST";
129
+ }
121
130
  const COMPRESSION_IDS = /* @__PURE__ */ new Set([
122
131
  "compaction",
123
132
  "compaction-basic",
@@ -191,7 +200,7 @@ var PresetOverlayStore = class {
191
200
  });
192
201
  await chmod(staging, 384);
193
202
  await this.disambiguateStamp(staging, identity);
194
- await rename(staging, path);
203
+ await this.publish(staging, path);
195
204
  } catch (error) {
196
205
  try {
197
206
  await rm(staging, { force: true });
@@ -203,6 +212,45 @@ var PresetOverlayStore = class {
203
212
  path
204
213
  };
205
214
  }
215
+ /** In-flight publish chains, one per destination path. */
216
+ publishChains = /* @__PURE__ */ new Map();
217
+ /**
218
+ * Publish one stamped staging file to its final path, serialized per path.
219
+ *
220
+ * POSIX `rename` replaces an existing destination atomically, so concurrent
221
+ * composers of one identity are naturally idempotent there. Windows
222
+ * `MoveFileEx` instead fails the loser of that race with EPERM/EBUSY, which
223
+ * surfaced as `standingKeyFor()` throwing during concurrent composition.
224
+ * Every composer of one identity writes identical bytes and re-derives the
225
+ * same deterministic identity stamp, so a destination that already observes
226
+ * this staging file's {mtimeMs, size} key IS the intended end state: confirm
227
+ * it and treat the lost race as success. Every other outcome still throws, so
228
+ * a silently reused generation stays forbidden, and the caller still removes
229
+ * the staging file when this rejects.
230
+ */
231
+ publish(staging, path) {
232
+ const tracked = (this.publishChains.get(path) ?? Promise.resolve()).catch(() => void 0).then(async () => {
233
+ try {
234
+ await rename(staging, path);
235
+ return;
236
+ } catch (error) {
237
+ if (!isPublishRace(error)) throw error;
238
+ try {
239
+ const intended = await this.metadataIo.read(staging);
240
+ const published = await this.metadataIo.read(path);
241
+ if (published.size === intended.size && Math.abs(published.mtimeMs - intended.mtimeMs) < 2) {
242
+ await rm(staging, { force: true });
243
+ return;
244
+ }
245
+ } catch {}
246
+ throw error;
247
+ }
248
+ }).finally(() => {
249
+ if (this.publishChains.get(path) === tracked) this.publishChains.delete(path);
250
+ });
251
+ this.publishChains.set(path, tracked);
252
+ return tracked;
253
+ }
206
254
  /** Observed {mtimeMs,size} keys published by this store, per identity. */
207
255
  standingKeys = /* @__PURE__ */ new Map();
208
256
  /**
@@ -459,6 +507,7 @@ const CONTEXT_COMPRESSION_NAMESPACE = CONTEXT_COMPRESSION_SETTINGS_NAMESPACE;
459
507
  const ESTIMATOR_CATALOG_ROUTES = ["/endpoint/dsh-context-compression-improved/estimator-catalog", "/api/dsh-context-compression-improved/estimator-catalog"];
460
508
  const REVIEW_QUEUE_ROUTES = ["/endpoint/dsh-context-compression-improved/review-queue", "/api/dsh-context-compression-improved/review-queue"];
461
509
  const REVIEW_DECIDE_ROUTES = ["/endpoint/dsh-context-compression-improved/review-decide", "/api/dsh-context-compression-improved/review-decide"];
510
+ const ADVISOR_REPORT_ROUTES = ["/endpoint/dsh-context-compression-improved/advisor-report", "/api/dsh-context-compression-improved/advisor-report"];
462
511
  /**
463
512
  * Resolve the review pipeline for the top-level routes.
464
513
  *
@@ -694,6 +743,102 @@ function registerReviewQueueRoutes(ctx) {
694
743
  log("warn", "context-compression webServer not active yet — review routes pending: %s", REVIEW_QUEUE_ROUTES.join(", "));
695
744
  }
696
745
  /**
746
+ * Serve the advisory advisor's read-only report route (same registration
747
+ * skeleton as the review routes):
748
+ *
749
+ * `GET .../advisor-report?sessionId=…` → the session's prefix-decay figure,
750
+ * the todolist-bound task summary, and the score distribution. Content-free
751
+ * by construction: task semantics are LLM-derived summaries, never message
752
+ * text, and no score reason or candidate preview is ever returned.
753
+ * Unknown session → 404; no agents service → 503. A session whose advisor
754
+ * never ran reports nulls and empty arrays, not an error.
755
+ */
756
+ function registerAdvisorReportRoute(ctx) {
757
+ const readService = (name) => {
758
+ try {
759
+ return ctx.get(name);
760
+ } catch {
761
+ return;
762
+ }
763
+ };
764
+ const log = (level, message, ...args) => {
765
+ console[level](message, ...args);
766
+ };
767
+ const getHandler = (req, res) => {
768
+ if (typeof readService("agents")?.get !== "function") {
769
+ reviewJson(res, 503, {
770
+ ok: false,
771
+ error: "advisor report unavailable"
772
+ });
773
+ return;
774
+ }
775
+ let sessionId = "";
776
+ try {
777
+ sessionId = new URL(String(req.url ?? ""), "http://localhost").searchParams.get("sessionId") ?? "";
778
+ } catch {}
779
+ if (sessionId === "") {
780
+ reviewJson(res, 400, {
781
+ ok: false,
782
+ error: "sessionId is required"
783
+ });
784
+ return;
785
+ }
786
+ const session = sessionFor(readService, sessionId);
787
+ if (session === void 0) {
788
+ reviewJson(res, 404, {
789
+ ok: false,
790
+ error: "unknown session"
791
+ });
792
+ return;
793
+ }
794
+ const state = getAdvisorState(session);
795
+ reviewJson(res, 200, {
796
+ ok: true,
797
+ sessionId,
798
+ advisor: {
799
+ summary: state.summary ?? null,
800
+ decay: state.lastDecay?.decay ?? null,
801
+ weightedChars: state.lastDecay?.weightedChars ?? null,
802
+ decayTurn: state.lastDecay?.turn ?? null,
803
+ scores: [...state.scores].map(([seq, entry]) => ({
804
+ seq,
805
+ score: entry.score,
806
+ turn: entry.turn
807
+ })),
808
+ lowRelevanceSeqs: [...state.recertified.keys()]
809
+ }
810
+ });
811
+ };
812
+ const register = (webServer) => {
813
+ const disposers = [...ADVISOR_REPORT_ROUTES].map((path) => ({
814
+ path,
815
+ handler: getHandler
816
+ })).map((entry) => webServer.register({
817
+ kind: "exact",
818
+ path: entry.path,
819
+ handler: entry.handler
820
+ })).filter((off) => typeof off === "function");
821
+ ctx.effect(() => () => {
822
+ for (const off of disposers) off();
823
+ }, "contextCompressionSelector.advisor report route");
824
+ log("info", "context-compression advisor report route registered: %s", ADVISOR_REPORT_ROUTES.join(", "));
825
+ };
826
+ const active = asWebServer(readService("webServer"));
827
+ if (active !== void 0) {
828
+ register(active);
829
+ return;
830
+ }
831
+ ctx.inject(["webServer"], (injected) => {
832
+ const webServer = asWebServer(injected.webServer);
833
+ if (webServer === void 0) {
834
+ log("warn", "context-compression webServer exposes no register() — advisor report route not registered");
835
+ return;
836
+ }
837
+ register(webServer);
838
+ });
839
+ log("warn", "context-compression webServer not active yet — advisor report route pending: %s", ADVISOR_REPORT_ROUTES.join(", "));
840
+ }
841
+ /**
697
842
  * The one service the catalog route actually needs. `llm` and
698
843
  * `agentDefaultModel` are payload enrichment the handler resolves per request,
699
844
  * never reasons to withhold the route.
@@ -797,7 +942,8 @@ const SHARED_SETTINGS = Symbol.for("dsh-context-compression-improved/settings-re
797
942
  const Config = z.object({
798
943
  presetOverlay: z.boolean().default(false),
799
944
  estimatorCatalogRoute: z.boolean().default(false),
800
- reviewQueueRoute: z.boolean().default(false)
945
+ reviewQueueRoute: z.boolean().default(false),
946
+ advisorReportRoute: z.boolean().default(false)
801
947
  });
802
948
  /** Register the persisted default read by the currently mounted root pruner. */
803
949
  function apply(ctx, config = {}) {
@@ -807,6 +953,7 @@ function apply(ctx, config = {}) {
807
953
  });
808
954
  if (config.estimatorCatalogRoute === true) registerEstimatorCatalogRoute(ctx);
809
955
  if (config.reviewQueueRoute === true) registerReviewQueueRoutes(ctx);
956
+ if (config.advisorReportRoute === true) registerAdvisorReportRoute(ctx);
810
957
  if (config.presetOverlay !== true) return;
811
958
  ctx.inject(["agentPresets"], (presetsCtx) => {
812
959
  const installation = decorateAgentPresets(presetsCtx.agentPresets, {
@@ -82,6 +82,20 @@ interface PresetOptions {
82
82
  readonly estimator: {
83
83
  readonly mode: '' | 'host' | 'direct';
84
84
  };
85
+ /**
86
+ * Advisory relevance advisor: statistics and suggestions only — every output
87
+ * (summaries, scores, decay, recertification) is observational and must never
88
+ * suppress, delay, or rewrite any reduction that would land. `''` (the
89
+ * default) keeps the advisor fully off.
90
+ */
91
+ readonly advisor: {
92
+ readonly mode: '' | 'host' | 'direct';
93
+ readonly timeoutMs: number;
94
+ readonly refreshTurns: number;
95
+ readonly scoreThreshold: number;
96
+ readonly sampleLimit: number;
97
+ readonly minTokens: number;
98
+ };
85
99
  /**
86
100
  * Human-gated review pipeline (beta): edge/high-impact candidates queue for
87
101
  * manual approval and execute in one merged batch at the next turn boundary
@@ -158,6 +172,13 @@ interface PresetOptionsSettings {
158
172
  readonly estimatorBaseUrl?: string;
159
173
  readonly estimatorApiKey?: string;
160
174
  readonly estimatorTimeoutMs?: number;
175
+ /** Advisory advisor channel; `''` (the default) keeps the advisor off. */
176
+ readonly advisorMode?: '' | 'host' | 'direct';
177
+ readonly advisorTimeoutMs?: number;
178
+ readonly advisorRefreshTurns?: number;
179
+ readonly advisorScoreThreshold?: number;
180
+ readonly advisorSampleLimit?: number;
181
+ readonly advisorMinTokens?: number;
161
182
  }
162
183
  /** Durable global preference exposed through `ctx.settings`. */
163
184
  interface ContextCompressionSettings {
@@ -336,6 +357,56 @@ interface PruneResult {
336
357
  readonly tokensRemoved: number;
337
358
  }
338
359
  //#endregion
360
+ //#region src/runtime/tokenpilot/sidechannel.d.ts
361
+ /** Audit record of one side-channel call (Phase 13). All fields optional-safe. */
362
+ interface SideChannelAudit {
363
+ readonly ok: boolean;
364
+ readonly latencyMs: number;
365
+ /** host|direct plus the resolved provider/model identity. */
366
+ readonly channel?: string;
367
+ /** L2 coverage: content shown N of node content total M. */
368
+ readonly coverage?: {
369
+ readonly shown: number;
370
+ readonly total: number;
371
+ };
372
+ readonly reason?: string;
373
+ }
374
+ interface SideChannelRequest {
375
+ readonly system: string;
376
+ readonly user: string;
377
+ readonly signal: AbortSignal;
378
+ }
379
+ /** One bound side channel. `ask` resolves `undefined` on ANY failure. */
380
+ declare class SideChannel {
381
+ private readonly ctx;
382
+ private readonly options;
383
+ private readonly overrides?;
384
+ /**
385
+ * @param overrides - per-consumer overrides of the estimator-named options.
386
+ * The estimator itself never passes them (byte-identical behavior); the
387
+ * advisory advisor passes its own mode/timeout/output budget so both
388
+ * consumers share one transport without sharing one configuration.
389
+ */
390
+ constructor(ctx: Context, options: PresetOptionsSettings, overrides?: {
391
+ readonly mode?: "" | "host" | "direct";
392
+ readonly timeoutMs?: number;
393
+ readonly maxTokens?: number;
394
+ } | undefined);
395
+ private get mode();
396
+ get enabled(): boolean;
397
+ ask(request: SideChannelRequest): Promise<string | undefined>;
398
+ /** Failure-open wrapper that also records one audit record per call. */
399
+ askAudited(request: SideChannelRequest): Promise<{
400
+ text?: string;
401
+ audit: SideChannelAudit;
402
+ }>;
403
+ identity(): string | undefined;
404
+ /** Same host-route resolution as the estimator: explicit, then host default. */
405
+ private resolveHostRoute;
406
+ private askHost;
407
+ private askDirect;
408
+ }
409
+ //#endregion
339
410
  //#region src/runtime/tokenpilot/estimator.d.ts
340
411
  /** Per-session estimator failure bookkeeping for exponential backoff. */
341
412
  interface EstimatorFailures {
@@ -532,6 +603,8 @@ interface PrunerState {
532
603
  readonly reviewClocks: WeakMap<Session, number>;
533
604
  /** Estimator-reported remaining turns Ŝ per Session; advisory only. */
534
605
  readonly estimatorRemainingTurns: WeakMap<Session, number>;
606
+ /** Per-session advisor side channel, constructed once with the advisor overrides. */
607
+ readonly advisorChannels: WeakMap<Session, SideChannel>;
535
608
  /** Four-state outcome counters per Session (floating-window summary row). */
536
609
  readonly reviewSummaries: WeakMap<Session, ReviewSessionSummary>;
537
610
  }
@@ -847,6 +920,15 @@ declare class ToolResultPruner extends Service {
847
920
  * superseded classification.
848
921
  */
849
922
  private postflightEstimatorPass;
923
+ /**
924
+ * Advisory advisor pass at the turn boundary, strictly fire-and-forget.
925
+ * Produces todolist-bound tail-task summaries, incremental relevance
926
+ * scores, and a prefix-decay figure — all observational. Every short
927
+ * circuit below (mode off, re-entry, cooldown, no task semantics, no
928
+ * direct endpoint) returns without touching any state the pruning chain
929
+ * reads, so the default configuration adds exactly zero behavior.
930
+ */
931
+ private postflightAdvisorPass;
850
932
  /**
851
933
  * The per-session review queue, or `undefined` while review mode is off
852
934
  * (every review path must then behave exactly like before).