mnemonic-proxy 0.3.8__tar.gz → 0.3.9__tar.gz

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 (71) hide show
  1. {mnemonic_proxy-0.3.8/mnemonic_proxy.egg-info → mnemonic_proxy-0.3.9}/PKG-INFO +17 -2
  2. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/README.md +16 -1
  3. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/ctxproxy/__init__.py +1 -1
  4. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/ctxproxy/core.py +25 -5
  5. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/ctxproxy/server.py +3 -1
  6. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9/mnemonic_proxy.egg-info}/PKG-INFO +17 -2
  7. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/mnemonic_proxy.egg-info/SOURCES.txt +2 -0
  8. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/pyproject.toml +1 -1
  9. mnemonic_proxy-0.3.9/tests/test_dashboard.py +25 -0
  10. mnemonic_proxy-0.3.9/tests/test_response_limit.py +89 -0
  11. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/LICENSE +0 -0
  12. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/ctxproxy/answerfix.py +0 -0
  13. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/ctxproxy/api_anthropic.py +0 -0
  14. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/ctxproxy/api_responses.py +0 -0
  15. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/ctxproxy/autosave.py +0 -0
  16. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/ctxproxy/check.py +0 -0
  17. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/ctxproxy/compat.py +0 -0
  18. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/ctxproxy/demo.py +0 -0
  19. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/ctxproxy/enginectx.py +0 -0
  20. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/ctxproxy/engines.py +0 -0
  21. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/ctxproxy/fake_engine.py +0 -0
  22. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/ctxproxy/kvarchive.py +0 -0
  23. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/ctxproxy/lang.py +0 -0
  24. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/ctxproxy/leafsel.py +0 -0
  25. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/ctxproxy/mcp_server.py +0 -0
  26. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/ctxproxy/packtok.py +0 -0
  27. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/ctxproxy/paging.py +0 -0
  28. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/ctxproxy/recall2.py +0 -0
  29. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/ctxproxy/render.py +0 -0
  30. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/ctxproxy/router.py +0 -0
  31. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/ctxproxy/tooldefs.py +0 -0
  32. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/ctxproxy/upstream.py +0 -0
  33. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/ctxproxy/window.py +0 -0
  34. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/mnemonic_proxy.egg-info/dependency_links.txt +0 -0
  35. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/mnemonic_proxy.egg-info/entry_points.txt +0 -0
  36. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/mnemonic_proxy.egg-info/requires.txt +0 -0
  37. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/mnemonic_proxy.egg-info/top_level.txt +0 -0
  38. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/setup.cfg +0 -0
  39. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_answerfix.py +0 -0
  40. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_args_copy.py +0 -0
  41. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_autosave.py +0 -0
  42. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_check_kv_archive.py +0 -0
  43. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_clients.py +0 -0
  44. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_compat.py +0 -0
  45. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_default_identity.py +0 -0
  46. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_ds4.py +0 -0
  47. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_engine_window.py +0 -0
  48. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_engines.py +0 -0
  49. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_final_recall_retry.py +0 -0
  50. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_kvarchive.py +0 -0
  51. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_lang.py +0 -0
  52. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_leaf_select.py +0 -0
  53. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_leaf_select_formats.py +0 -0
  54. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_leaf_select_open_points.py +0 -0
  55. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_live.py +0 -0
  56. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_measure_restore.py +0 -0
  57. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_min_window.py +0 -0
  58. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_names.py +0 -0
  59. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_onboarding.py +0 -0
  60. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_paging.py +0 -0
  61. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_postprova.py +0 -0
  62. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_proxy.py +0 -0
  63. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_recall2.py +0 -0
  64. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_recall_paging.py +0 -0
  65. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_recall_small_window.py +0 -0
  66. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_router.py +0 -0
  67. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_search_keeps_evidence.py +0 -0
  68. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_setup_scripts.py +0 -0
  69. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_strata_mock.py +0 -0
  70. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_tooldefs.py +0 -0
  71. {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_window_guard.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mnemonic-proxy
3
- Version: 0.3.8
3
+ Version: 0.3.9
4
4
  Summary: Transparent proxy that lets local coding agents work for hours: verbatim archive, structured recall, segments with handoff notes, saved engine state.
5
5
  Author: Maurizio Verde
6
6
  License-Expression: MIT
@@ -1057,6 +1057,21 @@ Reports (Italian): [DS4-LONG-RESULT.md](https://github.com/lastreload/mnemonic-p
1057
1057
 
1058
1058
  ## Configuration
1059
1059
 
1060
+ Output limits in managed mode: `max_completion_tokens` takes precedence over `max_tokens` when both are supplied;
1061
+ without either field, `default_response` is used (16384 by default). These values are ceilings, not a reason to
1062
+ switch segments. After masking and any necessary switch, the proxy caps the response to
1063
+ `window - estimated_prompt - reserve - 8` and sends the reduced limit to the engine (both fields are synchronized
1064
+ when present). The final window guard may reduce it further for its margin and estimate slack; it refuses a
1065
+ prompt with no space for even one generated token. Exact guarantees require `--tokenizer`; otherwise counts are
1066
+ estimates, not proof of the engine's actual token usage.
1067
+
1068
+ A genuinely large prompt still triggers a segment switch when there is not enough room for a useful response:
1069
+ `max(response_floor, min(requested_output, default_response))`, bounded to at least one token and at most a quarter
1070
+ of the window. A floor never raises the actual output ceiling requested by the client. Every reduction produces
1071
+ `response_clamped` in `journal.jsonl` (requested and sent limits, prompt estimate, window and reserve), plus one
1072
+ warning per conversation in the process log, remembered across restarts. No new option is required; for example,
1073
+ a client ceiling of 120000 in a 131072 window no longer causes a switch on every short turn.
1074
+
1060
1075
  `--config` takes a JSON object with any field of `ctxproxy.core.Config`. Defaults below are the real ones in the
1061
1076
  code. Most features are off by default so that 0.1 behaviour is unchanged; the last column is what we recommend
1062
1077
  with an engine that has saved state (`examples/config.example.json` plus the options the benchmarks above support).
@@ -1073,7 +1088,7 @@ with an engine that has saved state (`examples/config.example.json` plus the opt
1073
1088
  | `slot_dir` | "" | the engine's `--slot-save-path` folder, same host (anchor cleanup, autosave, `kv_archive`) | set |
1074
1089
  | `segments_enabled`, `tail_max`, `notes_max_tokens` | true, 24000, 8192 | segment switch with handoff notes | true, 24000, **12288** |
1075
1090
  | `slot_save` / `seal_experimental` | false / false | save the old segment to disk before switching (saved state) | **true / true** |
1076
- | `response_floor` | 0 | minimum response size assumed in the window check | **16384** |
1091
+ | `response_floor` | 0 | minimum useful response budget for segment switching (bounded to a quarter of the window) | **16384** |
1077
1092
  | `inject_recall`, `max_recall_rounds`, `recall_max_tokens` | true, 4, 6000 | the `recall` tool | default |
1078
1093
  | `recall_tool_name` / `tools_tool_name` | "recall" / "tools" | names of the proxy's tools for new conversations | default |
1079
1094
  | `prompt_language` | "en" | language of the texts the proxy writes into the prompt, for NEW conversations: `"en"` or `"it"`; stored per conversation at first use; conversations from before 0.3.1 stay Italian | default |
@@ -1023,6 +1023,21 @@ Reports (Italian): [DS4-LONG-RESULT.md](https://github.com/lastreload/mnemonic-p
1023
1023
 
1024
1024
  ## Configuration
1025
1025
 
1026
+ Output limits in managed mode: `max_completion_tokens` takes precedence over `max_tokens` when both are supplied;
1027
+ without either field, `default_response` is used (16384 by default). These values are ceilings, not a reason to
1028
+ switch segments. After masking and any necessary switch, the proxy caps the response to
1029
+ `window - estimated_prompt - reserve - 8` and sends the reduced limit to the engine (both fields are synchronized
1030
+ when present). The final window guard may reduce it further for its margin and estimate slack; it refuses a
1031
+ prompt with no space for even one generated token. Exact guarantees require `--tokenizer`; otherwise counts are
1032
+ estimates, not proof of the engine's actual token usage.
1033
+
1034
+ A genuinely large prompt still triggers a segment switch when there is not enough room for a useful response:
1035
+ `max(response_floor, min(requested_output, default_response))`, bounded to at least one token and at most a quarter
1036
+ of the window. A floor never raises the actual output ceiling requested by the client. Every reduction produces
1037
+ `response_clamped` in `journal.jsonl` (requested and sent limits, prompt estimate, window and reserve), plus one
1038
+ warning per conversation in the process log, remembered across restarts. No new option is required; for example,
1039
+ a client ceiling of 120000 in a 131072 window no longer causes a switch on every short turn.
1040
+
1026
1041
  `--config` takes a JSON object with any field of `ctxproxy.core.Config`. Defaults below are the real ones in the
1027
1042
  code. Most features are off by default so that 0.1 behaviour is unchanged; the last column is what we recommend
1028
1043
  with an engine that has saved state (`examples/config.example.json` plus the options the benchmarks above support).
@@ -1039,7 +1054,7 @@ with an engine that has saved state (`examples/config.example.json` plus the opt
1039
1054
  | `slot_dir` | "" | the engine's `--slot-save-path` folder, same host (anchor cleanup, autosave, `kv_archive`) | set |
1040
1055
  | `segments_enabled`, `tail_max`, `notes_max_tokens` | true, 24000, 8192 | segment switch with handoff notes | true, 24000, **12288** |
1041
1056
  | `slot_save` / `seal_experimental` | false / false | save the old segment to disk before switching (saved state) | **true / true** |
1042
- | `response_floor` | 0 | minimum response size assumed in the window check | **16384** |
1057
+ | `response_floor` | 0 | minimum useful response budget for segment switching (bounded to a quarter of the window) | **16384** |
1043
1058
  | `inject_recall`, `max_recall_rounds`, `recall_max_tokens` | true, 4, 6000 | the `recall` tool | default |
1044
1059
  | `recall_tool_name` / `tools_tool_name` | "recall" / "tools" | names of the proxy's tools for new conversations | default |
1045
1060
  | `prompt_language` | "en" | language of the texts the proxy writes into the prompt, for NEW conversations: `"en"` or `"it"`; stored per conversation at first use; conversations from before 0.3.1 stay Italian | default |
@@ -5,4 +5,4 @@ Verbatim archive, structured recall, segments with handoff notes, saved engine s
5
5
  (llama-server), ds4-server or any OpenAI-compatible engine; speaks Chat Completions, Anthropic Messages and OpenAI Responses.
6
6
  """
7
7
 
8
- __version__ = "0.3.8"
8
+ __version__ = "0.3.9"
@@ -16,6 +16,7 @@ from __future__ import annotations
16
16
  import dataclasses
17
17
  import hashlib
18
18
  import json
19
+ import logging
19
20
  import os
20
21
  import re
21
22
  import sqlite3
@@ -985,6 +986,7 @@ class Prepared:
985
986
  masked: int
986
987
  masked_tokens: int # token ARCHIVIATI dei pezzi nascosti (testo originale)
987
988
  masked_saved: int = 0 # riduzione fisica misurata (somma dei Δ dei pacchetti)
989
+ response_limit: int | None = None # generation ceiling after prepare; final window guard may lower it further
988
990
  catalog: dict = dataclasses.field(default_factory=dict) # strumenti caricabili con lo strumento tools
989
991
  invalidated: dict = dataclasses.field(default_factory=dict) # token di cache invalidati e causa
990
992
  recall_name: str = RECALL_NAME # nomi degli strumenti del proxy in questa conversazione
@@ -1306,10 +1308,13 @@ class Manager:
1306
1308
  events.extend(self._anchor(conv, seg or 0, req, phys, origin, per, tools, ev, upstream))
1307
1309
 
1308
1310
  # ---- livello 2: segmenti ----
1309
- resp = int(req.get("max_tokens") or req.get("max_completion_tokens") or cfg.default_response)
1310
- if resp <= 0:
1311
- resp = cfg.default_response
1312
- resp = max(resp, cfg.response_floor)
1311
+ requested = int(req.get("max_completion_tokens") or req.get("max_tokens") or cfg.default_response)
1312
+ if requested <= 0:
1313
+ requested = cfg.default_response
1314
+ # A client's output ceiling is not evidence that the prompt needs a new segment.
1315
+ # Keep a useful generation budget, not the entire requested maximum, for switching.
1316
+ floor = max(1, min(max(cfg.response_floor, min(requested, cfg.default_response)), cfg.window // 4))
1317
+ resp = max(floor, min(requested, cfg.window - est - cfg.reserve - 8))
1313
1318
  if cfg.segments_enabled and est + resp + cfg.reserve + 8 > cfg.window and upstream is not None:
1314
1319
  ev = self._switch(conv, req, msgs, hs, phys, origin, per, tools, seg or 0, upstream, est, resp)
1315
1320
  events.extend(ev)
@@ -1327,6 +1332,21 @@ class Manager:
1327
1332
  phys, origin = self.build(msgs, hs, start, notes_msg, pins_msg)
1328
1333
  est, per = self.estimate(phys, tools)
1329
1334
 
1335
+ # Recompute after masking, segmentation and auto-recall. Never raise a client's ceiling.
1336
+ room = cfg.window - est - cfg.reserve - 8
1337
+ response_limit = max(1, min(requested, room))
1338
+ if response_limit < requested:
1339
+ events.append(self.journal.log("response_clamped", conv=conv, requested=requested,
1340
+ max_tokens=response_limit, prompt=est, window=cfg.window,
1341
+ reserve=cfg.reserve, floor=floor,
1342
+ message="Output limit reduced to available context; no output-only switch"))
1343
+ warning_key = "response_clamp_warned:" + conv
1344
+ if not self.store.kv_get(warning_key):
1345
+ logging.getLogger(__name__).warning(
1346
+ "mnemonic-proxy conversation %s: requested output %d reduced to %d tokens to fit window %d",
1347
+ conv, requested, response_limit, cfg.window)
1348
+ self.store.kv_set(warning_key, "1")
1349
+
1330
1350
  masks = self.store.masks_for(hs + [h + THINK_SUFFIX for h in hs] + [h + ARGS_SUFFIX for h in hs]
1331
1351
  + [h + DROP_SUFFIX for h in hs])
1332
1352
  inv = self._invalidation(conv, phys, tools, per, est, events)
@@ -1344,7 +1364,7 @@ class Manager:
1344
1364
  else rec["ratio_planned"]
1345
1365
  self.journal.log("mask_cost", **rec)
1346
1366
  return Prepared(conv=conv, messages=phys, tools=tools, hs=hs, seg=seg or 0, est_tokens=est,
1347
- virtual_tokens=virtual, events=events, masked=len(masks),
1367
+ virtual_tokens=virtual, events=events, masked=len(masks), response_limit=response_limit,
1348
1368
  masked_tokens=sum(n for _, n in masks.values()),
1349
1369
  masked_saved=self.store.masks_saved(list(masks)), catalog=catalog,
1350
1370
  invalidated=inv, recall_name=self.rn, tools_name=self.tn, lang=self.lang,
@@ -32,7 +32,7 @@ from .upstream import Upstream, UpstreamError
32
32
  from .window import (ALL_BANDS, CALL_ALLOWANCE, FINAL_NOTE_EN, FINAL_NOTE_IT, LIMIT_NOTE_EN, LIMIT_NOTE_IT,
33
33
  NO_ROOM_EN, NO_ROOM_IT, NOTICE_MIN, RESULT_MIN, RESULT_R, SEARCH_BANDS, SearchState, WindowGuard,
34
34
  _text as _txt, clean, conclusion, idents, pieces, remove_piece, removal_entry, removal_order,
35
- sources_from_meta)
35
+ set_mt, sources_from_meta)
36
36
 
37
37
  LIMIT_MSG = ("{rn}: limite di ricerche raggiunto per questa risposta. Non chiamare più "
38
38
  "{rn}: rispondi ora con le informazioni già recuperate (o di' che non le hai).")
@@ -212,6 +212,8 @@ class Proxy:
212
212
  recalls, recall_logs = [], []
213
213
  strip_c, strip_r = [], []
214
214
  body = {k: v for k, v in req.items() if k not in ("messages", "tools", "stream", "stream_options")}
215
+ if p.response_limit is not None:
216
+ body = set_mt(body, p.response_limit)
215
217
  resp = None
216
218
  msgs_c = req.get("messages") or []
217
219
  last_user = max((i for i, m in enumerate(msgs_c) if is_human_user(m)), default=None)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mnemonic-proxy
3
- Version: 0.3.8
3
+ Version: 0.3.9
4
4
  Summary: Transparent proxy that lets local coding agents work for hours: verbatim archive, structured recall, segments with handoff notes, saved engine state.
5
5
  Author: Maurizio Verde
6
6
  License-Expression: MIT
@@ -1057,6 +1057,21 @@ Reports (Italian): [DS4-LONG-RESULT.md](https://github.com/lastreload/mnemonic-p
1057
1057
 
1058
1058
  ## Configuration
1059
1059
 
1060
+ Output limits in managed mode: `max_completion_tokens` takes precedence over `max_tokens` when both are supplied;
1061
+ without either field, `default_response` is used (16384 by default). These values are ceilings, not a reason to
1062
+ switch segments. After masking and any necessary switch, the proxy caps the response to
1063
+ `window - estimated_prompt - reserve - 8` and sends the reduced limit to the engine (both fields are synchronized
1064
+ when present). The final window guard may reduce it further for its margin and estimate slack; it refuses a
1065
+ prompt with no space for even one generated token. Exact guarantees require `--tokenizer`; otherwise counts are
1066
+ estimates, not proof of the engine's actual token usage.
1067
+
1068
+ A genuinely large prompt still triggers a segment switch when there is not enough room for a useful response:
1069
+ `max(response_floor, min(requested_output, default_response))`, bounded to at least one token and at most a quarter
1070
+ of the window. A floor never raises the actual output ceiling requested by the client. Every reduction produces
1071
+ `response_clamped` in `journal.jsonl` (requested and sent limits, prompt estimate, window and reserve), plus one
1072
+ warning per conversation in the process log, remembered across restarts. No new option is required; for example,
1073
+ a client ceiling of 120000 in a 131072 window no longer causes a switch on every short turn.
1074
+
1060
1075
  `--config` takes a JSON object with any field of `ctxproxy.core.Config`. Defaults below are the real ones in the
1061
1076
  code. Most features are off by default so that 0.1 behaviour is unchanged; the last column is what we recommend
1062
1077
  with an engine that has saved state (`examples/config.example.json` plus the options the benchmarks above support).
@@ -1073,7 +1088,7 @@ with an engine that has saved state (`examples/config.example.json` plus the opt
1073
1088
  | `slot_dir` | "" | the engine's `--slot-save-path` folder, same host (anchor cleanup, autosave, `kv_archive`) | set |
1074
1089
  | `segments_enabled`, `tail_max`, `notes_max_tokens` | true, 24000, 8192 | segment switch with handoff notes | true, 24000, **12288** |
1075
1090
  | `slot_save` / `seal_experimental` | false / false | save the old segment to disk before switching (saved state) | **true / true** |
1076
- | `response_floor` | 0 | minimum response size assumed in the window check | **16384** |
1091
+ | `response_floor` | 0 | minimum useful response budget for segment switching (bounded to a quarter of the window) | **16384** |
1077
1092
  | `inject_recall`, `max_recall_rounds`, `recall_max_tokens` | true, 4, 6000 | the `recall` tool | default |
1078
1093
  | `recall_tool_name` / `tools_tool_name` | "recall" / "tools" | names of the proxy's tools for new conversations | default |
1079
1094
  | `prompt_language` | "en" | language of the texts the proxy writes into the prompt, for NEW conversations: `"en"` or `"it"`; stored per conversation at first use; conversations from before 0.3.1 stay Italian | default |
@@ -38,6 +38,7 @@ tests/test_autosave.py
38
38
  tests/test_check_kv_archive.py
39
39
  tests/test_clients.py
40
40
  tests/test_compat.py
41
+ tests/test_dashboard.py
41
42
  tests/test_default_identity.py
42
43
  tests/test_ds4.py
43
44
  tests/test_engine_window.py
@@ -59,6 +60,7 @@ tests/test_proxy.py
59
60
  tests/test_recall2.py
60
61
  tests/test_recall_paging.py
61
62
  tests/test_recall_small_window.py
63
+ tests/test_response_limit.py
62
64
  tests/test_router.py
63
65
  tests/test_search_keeps_evidence.py
64
66
  tests/test_setup_scripts.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mnemonic-proxy"
7
- version = "0.3.8"
7
+ version = "0.3.9"
8
8
  description = "Transparent proxy that lets local coding agents work for hours: verbatim archive, structured recall, segments with handoff notes, saved engine state."
9
9
  readme = { file = "README.md", content-type = "text/markdown" }
10
10
  license = "MIT"
@@ -0,0 +1,25 @@
1
+ """Static dashboard contracts (no browser dependency in the unit suite)."""
2
+ import re
3
+ import unittest
4
+ from html.parser import HTMLParser
5
+ from pathlib import Path
6
+
7
+
8
+ class IdParser(HTMLParser):
9
+ def __init__(self):
10
+ super().__init__()
11
+ self.ids = []
12
+
13
+ def handle_starttag(self, tag, attrs):
14
+ self.ids.extend(value for key, value in attrs if key == "id")
15
+
16
+
17
+ class DashboardTests(unittest.TestCase):
18
+ def test_literal_id_references_exist(self):
19
+ html = (Path(__file__).resolve().parents[1] / "dashboard" / "index.html").read_text(encoding="utf-8")
20
+ parser = IdParser()
21
+ parser.feed(html)
22
+ refs = set(re.findall(r'\$\([\"\']([^\"\']+)[\"\']\)', html))
23
+ self.assertTrue(refs)
24
+ self.assertEqual(refs - set(parser.ids), set())
25
+ self.assertEqual(len(parser.ids), len(set(parser.ids)), "duplicate HTML IDs")
@@ -0,0 +1,89 @@
1
+ """Output ceilings must not cause repeated segment switches (DarkJezter)."""
2
+ import copy
3
+ from unittest.mock import patch
4
+
5
+ from tests.test_proxy import Base, TOOLS
6
+
7
+
8
+ class TestResponseLimit(Base):
9
+ def history(self):
10
+ return [{'role': 'system', 'content': 's' * 27000},
11
+ {'role': 'user', 'content': 'Continue.'}]
12
+
13
+ def test_high_ceiling_25_turns_and_one_warning(self):
14
+ msgs = self.history()
15
+ with patch('ctxproxy.core.logging.Logger.warning') as warn:
16
+ for i in range(25):
17
+ response, _ = self.send(msgs, max_tokens=120000)
18
+ msgs += [response['choices'][0]['message'], {'role': 'user', 'content': str(i)}]
19
+ self.assertEqual(warn.call_count, 1)
20
+ self.assertFalse(self.events('switch'))
21
+ self.assertFalse(self.events('mask'))
22
+ self.assertEqual(len(self.events('response_clamped')), 25)
23
+ self.assertEqual(len(self.events('new_conversation')), 1)
24
+ for body in self.eng.requests:
25
+ prompt = self.proxy.wguard.count_body(body)
26
+ self.assertLess(body['max_tokens'], 120000)
27
+ self.assertLessEqual(prompt + body['max_tokens'] + self.proxy.wguard.margin,
28
+ self.proxy.cfg.window)
29
+
30
+ def test_healthy_limit_unchanged(self):
31
+ self.send(self.history(), max_tokens=4096)
32
+ self.assertEqual(self.eng.requests[-1]['max_tokens'], 4096)
33
+ self.assertFalse(self.events('response_clamped'))
34
+ self.assertFalse(self.events('switch'))
35
+
36
+ def test_both_fields_follow_engine_precedence_and_input_is_unchanged(self):
37
+ req = {'messages': self.history(), 'tools': TOOLS, 'max_tokens': 4096,
38
+ 'max_completion_tokens': 120000}
39
+ original = copy.deepcopy(req)
40
+ status, _, _ = self.proxy.chat(req)
41
+ self.assertEqual(status, 200)
42
+ self.assertEqual(req, original)
43
+ sent = self.eng.requests[-1]
44
+ self.assertEqual(sent['max_tokens'], sent['max_completion_tokens'])
45
+ self.assertLess(sent['max_tokens'], 120000)
46
+ self.assertFalse(self.events('switch'))
47
+
48
+ def test_small_completion_limit_wins_over_large_max_tokens(self):
49
+ self.send(self.history(), max_tokens=120000, max_completion_tokens=512)
50
+ self.assertEqual(self.eng.requests[-1]['max_completion_tokens'], 512)
51
+ self.assertFalse(self.events('switch'))
52
+
53
+ def test_default_budget_is_explicit(self):
54
+ self.proxy.chat({'messages': self.history(), 'tools': TOOLS})
55
+ self.assertEqual(self.eng.requests[-1]['max_tokens'], self.proxy.cfg.default_response)
56
+
57
+ def test_large_prompt_still_attempts_segment_switch(self):
58
+ self.proxy.cfg.mask_enabled = False
59
+ msgs = self.history() + [{'role': 'assistant', 'content': 'a' * 390000},
60
+ {'role': 'user', 'content': 'Continue.'}]
61
+ with patch.object(self.proxy.mgr, '_switch', return_value=[]) as switch:
62
+ self.proxy.mgr.prepare({'messages': msgs, 'max_tokens': 120000}, upstream=self.proxy.up)
63
+ switch.assert_called_once()
64
+
65
+ def test_floor_never_raises_client_ceiling(self):
66
+ self.proxy.cfg.response_floor = 16384
67
+ self.send(self.history(), max_tokens=128)
68
+ self.assertEqual(self.eng.requests[-1]['max_tokens'], 128)
69
+
70
+ def test_clamping_without_segments_and_warning_survives_manager_restart(self):
71
+ from ctxproxy.core import Manager
72
+ self.proxy.cfg.segments_enabled = False
73
+ req = {'messages': self.history(), 'max_completion_tokens': 120000}
74
+ with patch('ctxproxy.core.logging.Logger.warning') as warn:
75
+ prepared = self.proxy.mgr.prepare(req, upstream=self.proxy.up)
76
+ replacement = Manager(self.proxy.cfg, self.proxy.store, self.proxy.tc, self.proxy.journal)
77
+ again = replacement.prepare(req, upstream=self.proxy.up)
78
+ self.assertEqual(warn.call_count, 1)
79
+ self.assertEqual(prepared.conv, again.conv)
80
+ self.assertLess(again.response_limit, 120000)
81
+ self.assertFalse(self.events('switch'))
82
+
83
+ def test_overfull_prompt_is_never_sent_without_segments(self):
84
+ self.proxy.cfg.mask_enabled = False
85
+ self.proxy.cfg.segments_enabled = False
86
+ status, _, _ = self.proxy.chat({'messages': [{'role': 'user', 'content': 'x' * 600000}],
87
+ 'max_tokens': 120000})
88
+ self.assertEqual(status, 400)
89
+ self.assertFalse(self.eng.requests)
File without changes
File without changes