@miller-tech/uap 1.133.2 → 1.135.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 (66) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/bin/cli.js +4 -1
  3. package/dist/bin/cli.js.map +1 -1
  4. package/dist/cli/config-command.d.ts +49 -0
  5. package/dist/cli/config-command.d.ts.map +1 -0
  6. package/dist/cli/config-command.js +465 -0
  7. package/dist/cli/config-command.js.map +1 -0
  8. package/dist/cli/config-wizard.d.ts +13 -0
  9. package/dist/cli/config-wizard.d.ts.map +1 -0
  10. package/dist/cli/config-wizard.js +134 -0
  11. package/dist/cli/config-wizard.js.map +1 -0
  12. package/dist/cli/deliver.js +11 -2
  13. package/dist/cli/deliver.js.map +1 -1
  14. package/dist/cli/guided-setup.d.ts.map +1 -1
  15. package/dist/cli/guided-setup.js +11 -0
  16. package/dist/cli/guided-setup.js.map +1 -1
  17. package/dist/cli/policy.d.ts.map +1 -1
  18. package/dist/cli/policy.js +34 -0
  19. package/dist/cli/policy.js.map +1 -1
  20. package/dist/cli/setup.d.ts +1 -1
  21. package/dist/cli/setup.d.ts.map +1 -1
  22. package/dist/cli/setup.js +22 -0
  23. package/dist/cli/setup.js.map +1 -1
  24. package/dist/config/policy-recommendations.d.ts +29 -0
  25. package/dist/config/policy-recommendations.d.ts.map +1 -0
  26. package/dist/config/policy-recommendations.js +108 -0
  27. package/dist/config/policy-recommendations.js.map +1 -0
  28. package/dist/config/settings-registry.d.ts +65 -0
  29. package/dist/config/settings-registry.d.ts.map +1 -0
  30. package/dist/config/settings-registry.js +334 -0
  31. package/dist/config/settings-registry.js.map +1 -0
  32. package/dist/dashboard/controls.d.ts +66 -0
  33. package/dist/dashboard/controls.d.ts.map +1 -0
  34. package/dist/dashboard/controls.js +188 -0
  35. package/dist/dashboard/controls.js.map +1 -0
  36. package/dist/dashboard/data-service.d.ts +20 -0
  37. package/dist/dashboard/data-service.d.ts.map +1 -1
  38. package/dist/dashboard/data-service.js +47 -0
  39. package/dist/dashboard/data-service.js.map +1 -1
  40. package/dist/dashboard/server.d.ts.map +1 -1
  41. package/dist/dashboard/server.js +81 -5
  42. package/dist/dashboard/server.js.map +1 -1
  43. package/dist/delivery/convergence-loop.d.ts +5 -0
  44. package/dist/delivery/convergence-loop.d.ts.map +1 -1
  45. package/dist/delivery/convergence-loop.js +11 -0
  46. package/dist/delivery/convergence-loop.js.map +1 -1
  47. package/dist/delivery/run-state.d.ts +12 -0
  48. package/dist/delivery/run-state.d.ts.map +1 -1
  49. package/dist/delivery/run-state.js +58 -1
  50. package/dist/delivery/run-state.js.map +1 -1
  51. package/docs/DASHBOARD_UPLIFT_SPEC.md +171 -0
  52. package/docs/INDEX.md +2 -0
  53. package/docs/guides/POLICY_SELECTION.md +98 -0
  54. package/docs/reference/CONFIGURATION_REFERENCE.md +688 -0
  55. package/package.json +1 -1
  56. package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
  57. package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
  58. package/tools/agents/scripts/anthropic_proxy.py +164 -12
  59. package/web/dash/core.js +398 -0
  60. package/web/dash/styles.css +307 -0
  61. package/web/dash/tab-deliver.js +12 -0
  62. package/web/dash/tab-memory.js +12 -0
  63. package/web/dash/tab-models.js +12 -0
  64. package/web/dash/tab-policies.js +12 -0
  65. package/web/dash/tabs.js +581 -0
  66. package/web/dashboard.html +12 -1303
@@ -184,6 +184,27 @@ PROXY_CONTEXT_WINDOW = int(os.environ.get("PROXY_CONTEXT_WINDOW", "0"))
184
184
  PROXY_CONTEXT_PRUNE_THRESHOLD = float(
185
185
  os.environ.get("PROXY_CONTEXT_PRUNE_THRESHOLD", "0.85")
186
186
  )
187
+ # Compaction forcing (Option A, 2026-07-10): Claude Code decides when to
188
+ # auto-compact against ITS believed model window (~200k) using
189
+ # /v1/messages/count_tokens, so an HONEST count on a smaller local rail lets
190
+ # every session grow into the "thrash band" — above the rail (per-request
191
+ # critical prunes, findings evaporate, read-only doom loops; observed live:
192
+ # 87 reads / 0 writes in 30 min at 122% utilization) but below the client's
193
+ # compact trigger (~92.5% of 200k). Scaling the reported count makes the
194
+ # client compact BEFORE the rail. "auto" (default) derives the scale from the
195
+ # LIVE rail at call time — assumed_window / (rail * target_fraction) — so it
196
+ # tracks rail resizes; a number forces that scale; "1"/"off" disables.
197
+ PROXY_COUNT_TOKENS_SCALE = os.environ.get("PROXY_COUNT_TOKENS_SCALE", "auto")
198
+ PROXY_CLIENT_ASSUMED_WINDOW = int(
199
+ os.environ.get("PROXY_CLIENT_ASSUMED_WINDOW", "200000")
200
+ )
201
+ # Where compaction should land sessions, as a fraction of the rail. Unset
202
+ # (0) = auto: just under the pruner threshold (x0.95), so the client's own
203
+ # compaction fires BEFORE the proxy ever needs to prune — the pruner stays on
204
+ # purely as a backstop. An explicit value overrides (clamped to <1).
205
+ PROXY_COMPACT_TARGET_FRACTION = float(
206
+ os.environ.get("PROXY_COMPACT_TARGET_FRACTION", "0")
207
+ )
187
208
  PROXY_CONTEXT_PRUNE_TARGET_FRACTION = float(
188
209
  os.environ.get("PROXY_CONTEXT_PRUNE_TARGET_FRACTION", "0.50")
189
210
  )
@@ -1329,6 +1350,7 @@ class SessionMonitor:
1329
1350
  deferral_streak: int = 0 # consecutive no-tool turns deferring the work (Fix A)
1330
1351
  deferral_break_fires: int = 0 # monotonic count of forced deferral-breaks (Fix A)
1331
1352
  tool_starvation_streak: int = 0 # Consecutive forced turns with no tool_calls produced
1353
+ last_request_msg_count: int = 0 # Message count of the previous request (compaction-boundary detection)
1332
1354
  malformed_tool_streak: int = 0 # consecutive malformed pseudo tool payloads
1333
1355
  invalid_tool_call_streak: int = 0 # consecutive invalid tool arg payloads
1334
1356
  required_tool_miss_streak: int = 0 # required tool turns with no tool call
@@ -8936,7 +8958,7 @@ async def _maybe_apply_recipe(anthropic_resp, anthropic_body, openai_body, clien
8936
8958
  return anthropic_resp
8937
8959
 
8938
8960
 
8939
- async def _heartbeat_then_buffered(produce_coro, model: str):
8961
+ async def _heartbeat_then_buffered(produce_coro, model: str, input_tokens: int = 0):
8940
8962
  """SSE generator: keep-alive heartbeat wrapper for the guarded-non-stream path.
8941
8963
 
8942
8964
  Emits an immediate ``message_start`` so the client registers an active
@@ -8953,7 +8975,7 @@ async def _heartbeat_then_buffered(produce_coro, model: str):
8953
8975
  msg_id = f"msg_{uuid.uuid4().hex[:24]}"
8954
8976
  yield (
8955
8977
  f"event: message_start\n"
8956
- f"data: {json.dumps({'type': 'message_start', 'message': {'id': msg_id, 'type': 'message', 'role': 'assistant', 'content': [], 'model': model, 'stop_reason': None, 'stop_sequence': None, 'usage': {'input_tokens': 0, 'output_tokens': 0}}})}\n\n"
8978
+ f"data: {json.dumps({'type': 'message_start', 'message': {'id': msg_id, 'type': 'message', 'role': 'assistant', 'content': [], 'model': model, 'stop_reason': None, 'stop_sequence': None, 'usage': {'input_tokens': input_tokens, 'output_tokens': 0}}})}\n\n"
8957
8979
  )
8958
8980
  interval = PROXY_STREAM_HEARTBEAT_SECS if PROXY_STREAM_HEARTBEAT_SECS > 0 else 15.0
8959
8981
  task = asyncio.ensure_future(produce_coro)
@@ -9011,7 +9033,13 @@ async def stream_anthropic_message(anthropic_resp: dict, emit_message_start: boo
9011
9033
  "model": anthropic_resp.get("model", "unknown"),
9012
9034
  "stop_reason": None,
9013
9035
  "stop_sequence": None,
9014
- "usage": {"input_tokens": 0, "output_tokens": 0},
9036
+ "usage": {
9037
+ "input_tokens": int(
9038
+ (anthropic_resp.get("usage", {}) or {}).get("input_tokens", 0)
9039
+ * _count_tokens_scale()
9040
+ ),
9041
+ "output_tokens": 0,
9042
+ },
9015
9043
  }
9016
9044
  yield f"event: message_start\ndata: {json.dumps({'type': 'message_start', 'message': message})}\n\n"
9017
9045
 
@@ -9053,11 +9081,13 @@ async def stream_anthropic_message(anthropic_resp: dict, emit_message_start: boo
9053
9081
  )
9054
9082
  block_index += 1
9055
9083
 
9056
- output_tokens = anthropic_resp.get("usage", {}).get("output_tokens", 0)
9084
+ resp_usage = anthropic_resp.get("usage", {}) or {}
9085
+ output_tokens = resp_usage.get("output_tokens", 0)
9086
+ scaled_input = int(resp_usage.get("input_tokens", 0) * _count_tokens_scale())
9057
9087
  stop_reason = anthropic_resp.get("stop_reason", "end_turn")
9058
9088
  yield (
9059
9089
  "event: message_delta\n"
9060
- f"data: {json.dumps({'type': 'message_delta', 'delta': {'stop_reason': stop_reason, 'stop_sequence': None}, 'usage': {'output_tokens': output_tokens}})}\n\n"
9090
+ f"data: {json.dumps({'type': 'message_delta', 'delta': {'stop_reason': stop_reason, 'stop_sequence': None}, 'usage': {'input_tokens': scaled_input, 'output_tokens': output_tokens}})}\n\n"
9061
9091
  )
9062
9092
  yield f"event: message_stop\ndata: {json.dumps({'type': 'message_stop'})}\n\n"
9063
9093
 
@@ -9084,10 +9114,11 @@ async def stream_anthropic_response(
9084
9114
  msg_id = f"msg_{uuid.uuid4().hex[:24]}"
9085
9115
  stream_started = time.monotonic()
9086
9116
 
9087
- # message_start
9117
+ # message_start — carries the client's (scaled) input size; a hardcoded 0
9118
+ # here blinded clients to their own context usage (compaction never fired).
9088
9119
  yield (
9089
9120
  f"event: message_start\n"
9090
- f"data: {json.dumps({'type': 'message_start', 'message': {'id': msg_id, 'type': 'message', 'role': 'assistant', 'content': [], 'model': model, 'stop_reason': None, 'stop_sequence': None, 'usage': {'input_tokens': 0, 'output_tokens': 0}}})}\n\n"
9121
+ f"data: {json.dumps({'type': 'message_start', 'message': {'id': msg_id, 'type': 'message', 'role': 'assistant', 'content': [], 'model': model, 'stop_reason': None, 'stop_sequence': None, 'usage': {'input_tokens': _client_input_tokens(monitor), 'output_tokens': 0}}})}\n\n"
9091
9122
  )
9092
9123
 
9093
9124
  # content_block_start for text (index 0)
@@ -9392,10 +9423,12 @@ async def stream_anthropic_response(
9392
9423
  if _is_unexpected_end_turn(synthetic_openai_resp, anthropic_body):
9393
9424
  monitor.unexpected_end_turn_count += 1
9394
9425
 
9395
- # message_delta with final stop reason
9426
+ # message_delta with final stop reason. Repeats the (scaled) input size so
9427
+ # clients that read usage from the delta rather than message_start still
9428
+ # see their context usage — the compaction-forcing signal.
9396
9429
  yield (
9397
9430
  f"event: message_delta\n"
9398
- f"data: {json.dumps({'type': 'message_delta', 'delta': {'stop_reason': finish_reason, 'stop_sequence': None}, 'usage': {'output_tokens': output_tokens}})}\n\n"
9431
+ f"data: {json.dumps({'type': 'message_delta', 'delta': {'stop_reason': finish_reason, 'stop_sequence': None}, 'usage': {'input_tokens': _client_input_tokens(monitor), 'output_tokens': output_tokens}})}\n\n"
9399
9432
  )
9400
9433
 
9401
9434
  # message_stop
@@ -9563,7 +9596,98 @@ async def count_tokens(request: Request):
9563
9596
  status_code=400,
9564
9597
  media_type="application/json",
9565
9598
  )
9566
- return {"input_tokens": estimate_total_tokens(body)}
9599
+ est = estimate_total_tokens(body)
9600
+ scale = _count_tokens_scale()
9601
+ if scale > 1.0:
9602
+ scaled = int(est * scale)
9603
+ # Once per scale value, explain the discrepancy an operator would
9604
+ # otherwise chase between this endpoint and the pruner's numbers.
9605
+ global _count_scale_logged
9606
+ if _count_scale_logged != scale:
9607
+ _count_scale_logged = scale
9608
+ logger.info(
9609
+ "COMPACT FORCING: scaling count_tokens by %.2f "
9610
+ "(client assumed window %d, rail %d) — client auto-compact "
9611
+ "fires at ~%d real tokens, before the pruner",
9612
+ scale,
9613
+ PROXY_CLIENT_ASSUMED_WINDOW,
9614
+ default_context_window if default_context_window > 0 else PROXY_CONTEXT_WINDOW,
9615
+ int(PROXY_CLIENT_ASSUMED_WINDOW * 0.925 / scale),
9616
+ )
9617
+ return {"input_tokens": scaled}
9618
+ return {"input_tokens": est}
9619
+
9620
+
9621
+ _count_scale_logged: float = 0.0
9622
+
9623
+
9624
+ def _count_tokens_scale() -> float:
9625
+ """Resolve the count_tokens compaction-forcing scale (>= 1.0; 1.0 = off).
9626
+
9627
+ "auto" derives it from the LIVE rail each call so a rail resize (server
9628
+ restart with a different --ctx-size) re-tunes the client's compact point
9629
+ without a proxy restart.
9630
+ """
9631
+ raw = PROXY_COUNT_TOKENS_SCALE.strip().lower()
9632
+ if raw in ("", "off", "none", "0", "1", "1.0"):
9633
+ return 1.0
9634
+ if raw != "auto":
9635
+ try:
9636
+ return max(1.0, float(raw))
9637
+ except ValueError:
9638
+ return 1.0
9639
+ window = default_context_window if default_context_window > 0 else PROXY_CONTEXT_WINDOW
9640
+ if window <= 0:
9641
+ return 1.0
9642
+ frac = (
9643
+ PROXY_COMPACT_TARGET_FRACTION
9644
+ if 0 < PROXY_COMPACT_TARGET_FRACTION < 1
9645
+ # Auto: land compaction just under the pruner trigger so the client
9646
+ # compacts before the proxy ever prunes (pruner = backstop only).
9647
+ else min(0.9, PROXY_CONTEXT_PRUNE_THRESHOLD * 0.95)
9648
+ )
9649
+ target = window * frac
9650
+ if target <= 0 or PROXY_CLIENT_ASSUMED_WINDOW <= target:
9651
+ # Rail already exceeds the client's own compact point — honest counts
9652
+ # are fine, the client compacts before the rail unaided.
9653
+ return 1.0
9654
+ return PROXY_CLIENT_ASSUMED_WINDOW / target
9655
+
9656
+
9657
+ def _client_input_tokens(monitor) -> int:
9658
+ """Input size to REPORT to the client, scaled for compaction forcing.
9659
+
9660
+ Streaming responses historically hardcoded ``input_tokens: 0`` in
9661
+ message_start — the client could never see its own context usage, so its
9662
+ auto-compaction NEVER fired and sessions ground against the pruner forever
9663
+ (the actual root cause of the interactive thrash band; count_tokens
9664
+ scaling alone was insufficient because the dominant clients don't call
9665
+ it). Reports the PRE-prune estimate — the client's own conversation size,
9666
+ which is what its compaction decision is about — times the forcing scale.
9667
+ """
9668
+ est = (
9669
+ getattr(monitor, "pre_prune_input_tokens", 0)
9670
+ or getattr(monitor, "last_input_tokens", 0)
9671
+ or 0
9672
+ )
9673
+ return int(est * _count_tokens_scale())
9674
+
9675
+
9676
+ def _scale_client_usage(anthropic_resp: dict) -> dict:
9677
+ """Shallow-copy a finalized response with usage.input_tokens scaled for the
9678
+ CLIENT. Internal consumers (pruner accounting, telemetry) read the original
9679
+ dict's honest numbers — only the wire copy lies, and it lies consistently
9680
+ with count_tokens and the streaming frames."""
9681
+ scale = _count_tokens_scale()
9682
+ usage = anthropic_resp.get("usage")
9683
+ if scale <= 1.0 or not isinstance(usage, dict):
9684
+ return anthropic_resp
9685
+ out = dict(anthropic_resp)
9686
+ out["usage"] = {
9687
+ **usage,
9688
+ "input_tokens": int(usage.get("input_tokens", 0) * scale),
9689
+ }
9690
+ return out
9567
9691
 
9568
9692
 
9569
9693
  @app.post("/v1/messages")
@@ -9692,6 +9816,32 @@ async def messages(request: Request):
9692
9816
  last_text,
9693
9817
  )
9694
9818
 
9819
+ # --- Compaction-boundary detection: a client-side auto-compact collapses
9820
+ # the conversation (observed live 2026-07-10: msgs 61 -> 3) into a fresh
9821
+ # epoch whose last assistant message is the text-only SUMMARY. Anti-spin
9822
+ # counters accumulated before the boundary (consecutive_forced_count,
9823
+ # starvation/no-write streaks, tool-state machine) then misfire on the
9824
+ # very first post-compact turn — the starvation breaker stripped tools
9825
+ # from a brand-new epoch because the summary "looked like" a text-only
9826
+ # stall. A halving-plus collapse of the message count is the boundary
9827
+ # signal; reset the per-conversation spin state so the new epoch starts
9828
+ # clean. (Token accounting is NOT reset — record_request below re-measures
9829
+ # the compacted size naturally.)
9830
+ prev_msg_count = getattr(monitor, "last_request_msg_count", 0)
9831
+ if prev_msg_count >= 8 and n_messages <= prev_msg_count // 2:
9832
+ monitor.reset_tool_turn_state(reason="compaction_boundary")
9833
+ monitor.consecutive_forced_count = 0
9834
+ monitor.no_progress_streak = 0
9835
+ monitor.tool_starvation_streak = 0
9836
+ monitor.consecutive_no_write_turns = 0
9837
+ logger.info(
9838
+ "COMPACTION BOUNDARY: message count collapsed %d -> %d; reset "
9839
+ "tool-turn/anti-spin state for the fresh epoch",
9840
+ prev_msg_count,
9841
+ n_messages,
9842
+ )
9843
+ monitor.last_request_msg_count = n_messages
9844
+
9695
9845
  # --- Option F: Estimate tokens and record in session monitor ---
9696
9846
  estimated_tokens = estimate_total_tokens(body)
9697
9847
  monitor.record_request(estimated_tokens)
@@ -10037,7 +10187,7 @@ async def messages(request: Request):
10037
10187
 
10038
10188
  if PROXY_STREAM_HEARTBEAT_SECS > 0:
10039
10189
  return StreamingResponse(
10040
- _heartbeat_then_buffered(_produce_guarded(), model),
10190
+ _heartbeat_then_buffered(_produce_guarded(), model, _client_input_tokens(monitor)),
10041
10191
  media_type="text/event-stream",
10042
10192
  headers={
10043
10193
  "Cache-Control": "no-cache",
@@ -10466,7 +10616,9 @@ async def messages(request: Request):
10466
10616
  await asyncio.to_thread(
10467
10617
  _record_project_telemetry, body, model, anthropic_resp.get("usage", {})
10468
10618
  )
10469
- return anthropic_resp
10619
+ # Wire copy only: telemetry above and monitor accounting keep the
10620
+ # honest numbers; the client sees the compaction-forcing scale.
10621
+ return _scale_client_usage(anthropic_resp)
10470
10622
 
10471
10623
 
10472
10624
  @app.post("/anthropic/v1/messages")