dsh-context-compression-improved 0.3.0 → 0.4.0-beta.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 (41) hide show
  1. package/.githooks/pre-push +37 -0
  2. package/package.json +3 -2
  3. package/packages/selector/cordis.patch.yml +12 -5
  4. package/packages/selector/lib/client.d.ts +24 -0
  5. package/packages/selector/lib/client.js +506 -5
  6. package/packages/selector/lib/config.js +27 -4
  7. package/packages/selector/lib/index.d.ts +7 -0
  8. package/packages/selector/lib/index.js +229 -1
  9. package/packages/selector/lib/pruner.d.ts +254 -0
  10. package/packages/selector/lib/pruner.js +714 -25
  11. package/packages/selector/package.json +0 -1
  12. package/packages/selector/src/client/EstimatorControls.tsx +101 -0
  13. package/packages/selector/src/client/ReviewOverlay.tsx +320 -0
  14. package/packages/selector/src/client/index.ts +17 -0
  15. package/packages/selector/src/client/locales.ts +38 -0
  16. package/packages/selector/src/client/preset-options.ts +1 -0
  17. package/packages/selector/src/client/review-scope.ts +16 -0
  18. package/packages/selector/src/client/settings-section.tsx +17 -8
  19. package/packages/selector/src/index.ts +308 -0
  20. package/packages/selector/src/profiles.ts +28 -1
  21. package/packages/selector/src/pruner/state.ts +27 -0
  22. package/packages/selector/src/pruner.ts +430 -10
  23. package/packages/selector/src/runtime/audit.ts +27 -0
  24. package/packages/selector/src/runtime/config.ts +33 -1
  25. package/packages/selector/src/runtime/tokenpilot/estimator.ts +60 -13
  26. package/packages/selector/src/runtime/tokenpilot/proposal.ts +223 -0
  27. package/packages/selector/src/runtime/tokenpilot/review-queue.ts +231 -0
  28. package/packages/selector/src/runtime/tokenpilot/review-storage.ts +122 -0
  29. package/packages/selector/src/runtime/types.ts +17 -0
  30. package/packages/selector/tests/code-skeleton.client.spec.ts +3 -2
  31. package/packages/selector/tests/custom-contract.client.spec.ts +3 -2
  32. package/packages/selector/tests/preset-options-write.client.spec.ts +34 -1
  33. package/packages/selector/tests/review-overlay.client.spec.tsx +118 -0
  34. package/packages/selector/tests/review-routes.host.spec.ts +290 -0
  35. package/packages/selector/tests/runtime/audit.spec.ts +44 -0
  36. package/packages/selector/tests/runtime/tokenpilot/estimator.spec.ts +23 -0
  37. package/packages/selector/tests/runtime/tokenpilot/profile-baseline.spec.ts +5 -0
  38. package/packages/selector/tests/runtime/tokenpilot/proposal.spec.ts +199 -0
  39. package/packages/selector/tests/runtime/tokenpilot/pruner-review.spec.ts +313 -0
  40. package/packages/selector/tests/runtime/tokenpilot/review-queue.spec.ts +168 -0
  41. package/packages/selector/tests/settings-seat.client.spec.ts +5 -4
@@ -457,6 +457,232 @@ function restoreMethod(presets, snapshot) {
457
457
  //#region src/index.ts
458
458
  const CONTEXT_COMPRESSION_NAMESPACE = CONTEXT_COMPRESSION_SETTINGS_NAMESPACE;
459
459
  const ESTIMATOR_CATALOG_ROUTES = ["/endpoint/dsh-context-compression-improved/estimator-catalog", "/api/dsh-context-compression-improved/estimator-catalog"];
460
+ const REVIEW_QUEUE_ROUTES = ["/endpoint/dsh-context-compression-improved/review-queue", "/api/dsh-context-compression-improved/review-queue"];
461
+ const REVIEW_DECIDE_ROUTES = ["/endpoint/dsh-context-compression-improved/review-decide", "/api/dsh-context-compression-improved/review-decide"];
462
+ function reviewPrunerOf(readService) {
463
+ const candidate = readService("toolResultPruner");
464
+ return typeof candidate?.listReviewProposals === "function" && typeof candidate?.decideReviewProposal === "function" ? candidate : void 0;
465
+ }
466
+ function sessionFor(readService, sessionId) {
467
+ const agents = readService("agents");
468
+ return typeof agents?.get === "function" ? agents.get(sessionId)?.session : void 0;
469
+ }
470
+ function reviewJson(res, status, body) {
471
+ const resTyped = res;
472
+ resTyped.writeHead(status, {
473
+ "content-type": "application/json; charset=utf-8",
474
+ "cache-control": "no-cache"
475
+ });
476
+ resTyped.end(JSON.stringify(body));
477
+ }
478
+ function readRequestBody(req) {
479
+ return new Promise((resolve, reject) => {
480
+ const typed = req;
481
+ let data = "";
482
+ try {
483
+ typed.on?.("data", (chunk) => {
484
+ data += String(chunk ?? "");
485
+ if (data.length > 65536) {
486
+ data = "";
487
+ resolve("");
488
+ }
489
+ });
490
+ typed.on?.("end", () => resolve(data));
491
+ typed.on?.("error", reject);
492
+ } catch (error) {
493
+ reject(error instanceof Error ? error : new Error(String(error)));
494
+ }
495
+ });
496
+ }
497
+ /**
498
+ * Serve the review pipeline's two HTTP routes (best effort, mirroring the
499
+ * estimator-catalog registration):
500
+ *
501
+ * - `GET .../review-queue?sessionId=…` → the session's pending proposals with
502
+ * their benefit numbers. Sanitized by construction: the queue never holds
503
+ * message content, and the response carries ids/seqs/counts only (digests
504
+ * stay in the runtime — the client cannot need them).
505
+ * - `POST .../review-decide` `{sessionId, proposalId, decision}` → one human
506
+ * decision. Invalid body → 400; unknown/not-pending proposal → 404; review
507
+ * mode off for the session → 503.
508
+ */
509
+ function registerReviewQueueRoutes(ctx) {
510
+ const readService = (name) => {
511
+ try {
512
+ return ctx.get(name);
513
+ } catch {
514
+ return;
515
+ }
516
+ };
517
+ const log = (level, message, ...args) => {
518
+ console[level](message, ...args);
519
+ };
520
+ const registered = () => {
521
+ log("info", "context-compression review queue routes registered: %s / %s", REVIEW_QUEUE_ROUTES.join(", "), REVIEW_DECIDE_ROUTES.join(", "));
522
+ };
523
+ const getHandler = (req, res) => {
524
+ const pruner = reviewPrunerOf(readService);
525
+ if (pruner === void 0 || pruner.listAllReviewProposals === void 0) {
526
+ reviewJson(res, 503, {
527
+ ok: false,
528
+ error: "review pipeline unavailable"
529
+ });
530
+ return;
531
+ }
532
+ let sessionId = "";
533
+ try {
534
+ sessionId = new URL(String(req.url ?? ""), "http://localhost").searchParams.get("sessionId") ?? "";
535
+ } catch {
536
+ sessionId = "";
537
+ }
538
+ const sanitize = (sid, proposal) => ({
539
+ sessionId: sid,
540
+ id: proposal.id,
541
+ kind: proposal.kind,
542
+ items: proposal.items.map((item) => ({
543
+ seq: item.seq,
544
+ kind: item.kind,
545
+ component: item.component,
546
+ tokensBefore: item.tokensBefore,
547
+ tokensAfter: item.tokensAfter
548
+ })),
549
+ benefit: {
550
+ recoveredTokens: proposal.benefit.recoveredTokens,
551
+ penaltyTokens: proposal.benefit.penaltyTokens,
552
+ ...proposal.benefit.paybackTurns === void 0 ? {} : { paybackTurns: proposal.benefit.paybackTurns },
553
+ ...proposal.benefit.expectedSaving === void 0 ? {} : { expectedSaving: proposal.benefit.expectedSaving }
554
+ },
555
+ enqueuedTurn: proposal.enqueuedTurn,
556
+ lastTurnIndex: proposal.lastTurnIndex
557
+ });
558
+ if (sessionId === "") {
559
+ const pending = pruner.listAllReviewProposals().flatMap((entry) => entry.proposals.map((proposal) => sanitize(entry.sessionId, proposal)));
560
+ reviewJson(res, 200, {
561
+ ok: true,
562
+ total: pending.length,
563
+ pending
564
+ });
565
+ return;
566
+ }
567
+ const session = sessionFor(readService, sessionId);
568
+ if (session === void 0) {
569
+ reviewJson(res, 404, {
570
+ ok: false,
571
+ error: "unknown session"
572
+ });
573
+ return;
574
+ }
575
+ const pending = pruner.listReviewProposals(session).map((proposal) => sanitize(sessionId, proposal));
576
+ const summary = pruner.reviewSummary?.(session);
577
+ reviewJson(res, 200, {
578
+ ok: true,
579
+ sessionId,
580
+ total: pending.length,
581
+ pending,
582
+ ...summary === void 0 ? {} : { summary }
583
+ });
584
+ };
585
+ const decideHandler = async (req, res) => {
586
+ const pruner = reviewPrunerOf(readService);
587
+ if (pruner === void 0) {
588
+ reviewJson(res, 503, {
589
+ ok: false,
590
+ error: "review pipeline unavailable"
591
+ });
592
+ return;
593
+ }
594
+ let body;
595
+ try {
596
+ body = JSON.parse(await readRequestBody(req));
597
+ } catch {
598
+ body = void 0;
599
+ }
600
+ if (typeof body !== "object" || body === null) {
601
+ reviewJson(res, 400, {
602
+ ok: false,
603
+ error: "invalid JSON body"
604
+ });
605
+ return;
606
+ }
607
+ const record = body;
608
+ if (typeof record.sessionId !== "string" || record.sessionId === "" || typeof record.proposalId !== "string" || record.proposalId === "") {
609
+ reviewJson(res, 400, {
610
+ ok: false,
611
+ error: "sessionId and proposalId are required"
612
+ });
613
+ return;
614
+ }
615
+ if (record.decision !== "approved" && record.decision !== "rejected" && record.decision !== "ignored") {
616
+ reviewJson(res, 400, {
617
+ ok: false,
618
+ error: "decision must be approved, rejected, or ignored"
619
+ });
620
+ return;
621
+ }
622
+ const session = sessionFor(readService, record.sessionId);
623
+ if (session === void 0) {
624
+ reviewJson(res, 404, {
625
+ ok: false,
626
+ error: "unknown session"
627
+ });
628
+ return;
629
+ }
630
+ const outcome = pruner.decideReviewProposal(session, record.proposalId, record.decision);
631
+ if (outcome === void 0) {
632
+ reviewJson(res, 503, {
633
+ ok: false,
634
+ error: "review mode is off for this session"
635
+ });
636
+ return;
637
+ }
638
+ if (!outcome.ok) {
639
+ reviewJson(res, 404, {
640
+ ok: false,
641
+ error: outcome.reason
642
+ });
643
+ return;
644
+ }
645
+ reviewJson(res, 200, {
646
+ ok: true,
647
+ sessionId: record.sessionId,
648
+ proposalId: record.proposalId,
649
+ decision: record.decision
650
+ });
651
+ };
652
+ const register = (webServer) => {
653
+ const disposers = [...[...REVIEW_QUEUE_ROUTES].map((path) => ({
654
+ path,
655
+ handler: getHandler
656
+ })), ...[...REVIEW_DECIDE_ROUTES].map((path) => ({
657
+ path,
658
+ handler: (req, res) => {
659
+ decideHandler(req, res);
660
+ }
661
+ }))].map((entry) => webServer.register({
662
+ kind: "exact",
663
+ path: entry.path,
664
+ handler: entry.handler
665
+ })).filter((off) => typeof off === "function");
666
+ ctx.effect(() => () => {
667
+ for (const off of disposers) off();
668
+ }, "contextCompressionSelector.review routes");
669
+ registered();
670
+ };
671
+ const active = asWebServer(readService("webServer"));
672
+ if (active !== void 0) {
673
+ register(active);
674
+ return;
675
+ }
676
+ ctx.inject(["webServer"], (injected) => {
677
+ const webServer = asWebServer(injected.webServer);
678
+ if (webServer === void 0) {
679
+ log("warn", "context-compression webServer exposes no register() — review routes not registered");
680
+ return;
681
+ }
682
+ register(webServer);
683
+ });
684
+ log("warn", "context-compression webServer not active yet — review routes pending: %s", REVIEW_QUEUE_ROUTES.join(", "));
685
+ }
460
686
  /**
461
687
  * The one service the catalog route actually needs. `llm` and
462
688
  * `agentDefaultModel` are payload enrichment the handler resolves per request,
@@ -560,7 +786,8 @@ const SHARED_SETTINGS = Symbol.for("dsh-context-compression-improved/settings-re
560
786
  /** Loader validation for the standalone Bundle opt-in. */
561
787
  const Config = z.object({
562
788
  presetOverlay: z.boolean().default(false),
563
- estimatorCatalogRoute: z.boolean().default(false)
789
+ estimatorCatalogRoute: z.boolean().default(false),
790
+ reviewQueueRoute: z.boolean().default(false)
564
791
  });
565
792
  /** Register the persisted default read by the currently mounted root pruner. */
566
793
  function apply(ctx, config = {}) {
@@ -569,6 +796,7 @@ function apply(ctx, config = {}) {
569
796
  acquireSettingsRegistration(settingsCtx);
570
797
  });
571
798
  if (config.estimatorCatalogRoute === true) registerEstimatorCatalogRoute(ctx);
799
+ if (config.reviewQueueRoute === true) registerReviewQueueRoutes(ctx);
572
800
  if (config.presetOverlay !== true) return;
573
801
  ctx.inject(["agentPresets"], (presetsCtx) => {
574
802
  const installation = decorateAgentPresets(presetsCtx.agentPresets, {
@@ -82,6 +82,18 @@ interface PresetOptions {
82
82
  readonly estimator: {
83
83
  readonly mode: '' | 'host' | 'direct';
84
84
  };
85
+ /**
86
+ * Human-gated review pipeline (beta): edge/high-impact candidates queue for
87
+ * manual approval and execute in one merged batch at the next turn boundary
88
+ * instead of the automatic path (R4).
89
+ */
90
+ readonly reviewMode: boolean;
91
+ /** Turn-boundary patience: pending review proposals older than this many turns auto-expire (R4). */
92
+ readonly reviewTimeoutTurns: number;
93
+ /** Cache-hit discount rate α in the benefit model; expectedSaving = α·R·Ŝ − (1−α)·tail. */
94
+ readonly cacheHitDiscountAlpha: number;
95
+ /** Candidates whose tokenBefore reaches this threshold bypass payback triage and always enter review (R4). */
96
+ readonly reviewHighImpactTokens: number;
85
97
  }
86
98
  /** Common user-authored Custom stages shared by persisted policy versions. */
87
99
  interface CustomCompressionPolicyFields<HistoryPolicy> {
@@ -131,6 +143,11 @@ interface PresetOptionsSettings {
131
143
  readonly prefixStabilizer?: boolean;
132
144
  readonly readState?: boolean;
133
145
  readonly estimatorMode?: '' | 'host' | 'direct';
146
+ /** Review-mode overrides (beta); see PresetOptions.reviewMode. */
147
+ readonly reviewMode?: boolean;
148
+ readonly reviewTimeoutTurns?: number;
149
+ readonly cacheHitDiscountAlpha?: number;
150
+ readonly reviewHighImpactTokens?: number;
134
151
  /**
135
152
  * Estimator endpoint fields. Persisted-settings only: they never enter the
136
153
  * frozen CompressionPolicy, which is emitted verbatim by policy-resolved
@@ -310,7 +327,157 @@ interface EstimatorFailures {
310
327
  cooldownUntil: number;
311
328
  }
312
329
  //#endregion
330
+ //#region src/runtime/tokenpilot/proposal.d.ts
331
+ interface BenefitEstimate {
332
+ /** Net reclaimed tokens across the batch; may be ≤ 0 when a batch is not worth it. */
333
+ readonly recoveredTokens: number;
334
+ /** The one-time cache-refill penalty the merged mutation pays: (1−α)·tailTokens. */
335
+ readonly penaltyTokens: number;
336
+ /** Turns of discounted recovery needed to recoup the penalty; `undefined` when α·R ≤ 0. */
337
+ readonly paybackTurns?: number;
338
+ /** Discounted net benefit over the remaining session; omitted when Ŝ is unknown. */
339
+ readonly expectedSaving?: number;
340
+ }
341
+ /** Human-facing reduction kind carried by every review proposal. */
342
+ type ProposalKind = 'estimator' | 'dedup' | 'read-state';
343
+ /** One frozen item inside a review proposal: metadata and digest, never content. */
344
+ interface ProposalItem {
345
+ readonly seq: number;
346
+ readonly component: string;
347
+ readonly kind: ProposalKind;
348
+ readonly tokensBefore: number;
349
+ readonly tokensAfter: number;
350
+ /** Content sha-256 frozen at enqueue time and re-checked at the apply point. */
351
+ readonly digest: string;
352
+ }
353
+ /** Proposal fields derivable at classification time; queue fields attach at enqueue. */
354
+ interface ProposalSkeleton {
355
+ readonly id: string;
356
+ readonly kind: ProposalKind;
357
+ readonly items: readonly ProposalItem[];
358
+ readonly benefit: BenefitEstimate;
359
+ }
360
+ //#endregion
361
+ //#region src/runtime/tokenpilot/review-queue.d.ts
362
+ /** Human-side proposal status. */
363
+ type ReviewProposalStatus = 'pending' | 'approved' | 'rejected' | 'ignored' | 'expired';
364
+ /** Execution-side receipt built only from real mutation evidence (never estimated). */
365
+ interface ReviewReceipt {
366
+ readonly status: 'applied' | 'deferred';
367
+ /** Reason code for deferred receipts, aligned with the upstream naming. */
368
+ readonly reasonCode?: string;
369
+ /** Predicted recovery from the benefit model at enqueue time. */
370
+ readonly estimatedTokens: number;
371
+ /** Measured recovery of the landed mutation; present only on applied. */
372
+ readonly appliedTokens?: number;
373
+ /** Canonical ISO timestamp of the execution evidence. */
374
+ readonly updatedAt: string;
375
+ }
376
+ /** One queued proposal: persisted metadata, never content. */
377
+ interface ReviewProposalRecord {
378
+ readonly id: string;
379
+ readonly sessionId: string;
380
+ readonly kind: ProposalKind;
381
+ readonly items: readonly {
382
+ readonly seq: number;
383
+ readonly component: string;
384
+ readonly kind: ProposalKind;
385
+ readonly tokensBefore: number;
386
+ readonly tokensAfter: number;
387
+ readonly digest: string;
388
+ }[];
389
+ readonly benefit: {
390
+ readonly recoveredTokens: number;
391
+ readonly penaltyTokens: number;
392
+ readonly paybackTurns?: number;
393
+ readonly expectedSaving?: number;
394
+ };
395
+ status: ReviewProposalStatus;
396
+ readonly enqueuedTurn: number;
397
+ lastTurnIndex: number;
398
+ }
399
+ /** Whole-session record: one durable KV value per session. */
400
+ interface ReviewSessionRecord {
401
+ readonly version: 1;
402
+ readonly proposals: readonly ReviewProposalRecord[];
403
+ }
404
+ /** A settled proposal: the live record plus its execution receipt. */
405
+ interface ReviewReceiptRecord extends ReviewProposalRecord {
406
+ readonly receipt: ReviewReceipt;
407
+ }
408
+ /** Minimal KV face the queue persists through. */
409
+ interface ReviewQueueStore {
410
+ load(sessionId: string): ReviewSessionRecord | undefined;
411
+ save(sessionId: string, record: ReviewSessionRecord): void;
412
+ /** Session ids with live records; optional (aggregate reads degrade to none). */
413
+ ids?(): readonly string[];
414
+ }
415
+ interface ReviewQueueOptions {
416
+ /** Pending proposals older than this many turns (since lastTurnIndex) expire. */
417
+ readonly timeoutTurns: number;
418
+ }
419
+ /** Decision outcomes for one decide call. */
420
+ type DecideOutcome = {
421
+ readonly ok: true;
422
+ } | {
423
+ readonly ok: false;
424
+ readonly reason: 'unknown-proposal' | 'not-pending';
425
+ };
426
+ declare class ReviewQueue {
427
+ private readonly store;
428
+ private readonly options;
429
+ constructor(store: ReviewQueueStore, options: ReviewQueueOptions);
430
+ /**
431
+ * Fail-open store access: a throwing seam must never break the compression
432
+ * pipeline. Reads degrade to "no stored record"; writes degrade to losing
433
+ * durability for that call (the store itself is expected to warn).
434
+ */
435
+ private safeLoad;
436
+ private safeSave;
437
+ private sessionRecord;
438
+ /**
439
+ * Queue one classified proposal. A repeated classification of the same
440
+ * content re-does nothing but refresh the patience clock, so re-enqueue
441
+ * cannot duplicate a live proposal.
442
+ * @returns `false` when an identical live proposal already exists.
443
+ */
444
+ enqueue(sessionId: string, skeleton: ProposalSkeleton, turnIndex: number): boolean;
445
+ /** Live pending proposals of one session, oldest enqueue first. */
446
+ listPending(sessionId: string): readonly ReviewProposalRecord[];
447
+ /** Approved proposals waiting for the next turn-boundary batch. */
448
+ listApproved(sessionId: string): readonly ReviewProposalRecord[];
449
+ /**
450
+ * Transition one pending proposal. Idempotent: deciding an unknown id or a
451
+ * non-pending proposal changes nothing and reports the miss.
452
+ */
453
+ decide(sessionId: string, id: string, decision: 'approved' | 'rejected' | 'ignored'): DecideOutcome;
454
+ /**
455
+ * Expire every pending proposal whose patience has run out at this turn
456
+ * boundary. Expired proposals are removed from the store (the summary view
457
+ * aggregates them from the audit log instead).
458
+ * @returns the expired proposals, for the caller's audit emission.
459
+ */
460
+ expireTurn(sessionId: string, turnIndex: number): readonly ReviewProposalRecord[];
461
+ /**
462
+ * Settle an approved proposal with its execution receipt and retire it from
463
+ * the live store. The caller is responsible for auditing the receipt; the
464
+ * queue only records which proposal left and why.
465
+ */
466
+ recordReceipt(sessionId: string, id: string, receipt: ReviewReceipt): ReviewReceiptRecord | undefined;
467
+ }
468
+ //#endregion
313
469
  //#region src/pruner/state.d.ts
470
+ /** Four-state per-session outcome counters behind the floating-window summary row. */
471
+ interface ReviewSessionSummary {
472
+ /** Rewrites that landed through the automatic path while review mode served this session. */
473
+ autoApplied: number;
474
+ /** Approved proposals whose merged batch executed with an applied receipt. */
475
+ reviewApplied: number;
476
+ /** Pending proposals that expired unhandled at a turn boundary. */
477
+ expired: number;
478
+ /** Approved proposals voided at the apply point (digest mismatch et al). */
479
+ voided: number;
480
+ }
314
481
  /** Mutable per-session state bag used inside {@link ToolResultPruner}. */
315
482
  interface PrunerState {
316
483
  /** Resolved immutable deployment configuration. */
@@ -337,6 +504,20 @@ interface PrunerState {
337
504
  readonly tailTrimBoundaryAttempts: WeakMap<Session, object>;
338
505
  /** Last effective policy audit key emitted for each Session. */
339
506
  readonly policyResolutionAudits: WeakMap<Session, string>;
507
+ /**
508
+ * TokenPilot-inspired R4: shared review-queue store. Starts as the in-memory
509
+ * fail-open fallback; swapped to the storageDomain-backed adapter when (and
510
+ * if) that seam opens successfully.
511
+ */
512
+ reviewStore: ReviewQueueStore;
513
+ /** Per-session review queue carrying the frozen timeout policy. */
514
+ readonly reviewQueues: WeakMap<Session, ReviewQueue>;
515
+ /** Last observed turn index per Session: the monotonic clock for review expiries. */
516
+ readonly reviewClocks: WeakMap<Session, number>;
517
+ /** Estimator-reported remaining turns Ŝ per Session; advisory only. */
518
+ readonly estimatorRemainingTurns: WeakMap<Session, number>;
519
+ /** Four-state outcome counters per Session (floating-window summary row). */
520
+ readonly reviewSummaries: WeakMap<Session, ReviewSessionSummary>;
340
521
  }
341
522
  //#endregion
342
523
  //#region src/runtime/custom-policy.d.ts
@@ -621,6 +802,79 @@ declare class ToolResultPruner extends Service {
621
802
  * superseded classification.
622
803
  */
623
804
  private postflightEstimatorPass;
805
+ /**
806
+ * The per-session review queue, or `undefined` while review mode is off
807
+ * (every review path must then behave exactly like before).
808
+ */
809
+ private reviewQueueFor;
810
+ /**
811
+ * Monotonic per-session turn clock for review patience and expiry. Bumped by
812
+ * the agent loop payloads (`pre-step` / `turn-stopping`); passes without a
813
+ * turn coordinate reuse the last observed value.
814
+ */
815
+ private reviewClock;
816
+ private auditReviewOutcome;
817
+ /**
818
+ * Review-mode triage hook: classify one pass's planned replacements and
819
+ * withhold the review bucket from landing, enqueuing it for human approval
820
+ * instead. With review mode off (or nothing planned) this is the identity.
821
+ *
822
+ * The digest freezes each candidate's ORIGINAL surface content, so the apply
823
+ * point can prove "what is removed now is what was approved then".
824
+ */
825
+ private triageForReview;
826
+ /**
827
+ * Execute every approved proposal of one session as ONE merged replacement
828
+ * batch at the current turn boundary, following the upstream applied-receipt
829
+ * discipline: applied receipts are built only from real mutation evidence —
830
+ * estimates never cross into applied savings.
831
+ *
832
+ * Per proposal: every item's frozen digest is re-checked against the current
833
+ * surface content; any mismatch voids the whole proposal (deferred with a
834
+ * reason code) instead of deleting something the user never approved.
835
+ * Fail-open: any unexpected error only logs and leaves the queue intact.
836
+ */
837
+ applyApprovedProposals(session: Session): void;
838
+ /**
839
+ * Current surface content at one seq: the newest covering replacement's
840
+ * blocks when the seq was rewritten, otherwise the original event's blocks.
841
+ */
842
+ private surfaceContentAt;
843
+ /**
844
+ * The execution-point digest check: `undefined` when every item's frozen
845
+ * digest still matches the current surface content, otherwise the aligned
846
+ * reason code explaining the void.
847
+ */
848
+ private reviewProposalVoided;
849
+ private reviewSummaryFor;
850
+ /** Live pending review proposals of one session; empty when review mode is off. */
851
+ listReviewProposals(session: Session): readonly ReviewProposalRecord[];
852
+ /**
853
+ * Every session's live pending proposals, for the floating window's
854
+ * aggregate badge (the client carries no session id of its own).
855
+ */
856
+ listAllReviewProposals(): readonly {
857
+ readonly sessionId: string;
858
+ readonly proposals: readonly ReviewProposalRecord[];
859
+ }[];
860
+ /** Four-state outcome counters of one session (floating-window summary row). */
861
+ reviewSummary(session: Session): ReviewSessionSummary;
862
+ /**
863
+ * Record one human decision. Returns the outcome, or `undefined` when
864
+ * review mode is off for this session (the route maps that to 503).
865
+ */
866
+ decideReviewProposal(session: Session, proposalId: string, decision: 'approved' | 'rejected' | 'ignored'): {
867
+ ok: true;
868
+ } | {
869
+ ok: false;
870
+ reason: 'unknown-proposal' | 'not-pending';
871
+ } | undefined;
872
+ /**
873
+ * Expire stale pending proposals at one turn boundary and audit each.
874
+ * Public because tests drive it directly; the turn-stopping handler calls
875
+ * it with the loop's own turn index.
876
+ */
877
+ expireReviewProposals(session: Session, turnIndex?: number): readonly ReviewProposalRecord[];
624
878
  private activePolicy;
625
879
  private contextWindowForRequest;
626
880
  private runRequestBoundary;