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.
- {mnemonic_proxy-0.3.8/mnemonic_proxy.egg-info → mnemonic_proxy-0.3.9}/PKG-INFO +17 -2
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/README.md +16 -1
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/ctxproxy/__init__.py +1 -1
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/ctxproxy/core.py +25 -5
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/ctxproxy/server.py +3 -1
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9/mnemonic_proxy.egg-info}/PKG-INFO +17 -2
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/mnemonic_proxy.egg-info/SOURCES.txt +2 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/pyproject.toml +1 -1
- mnemonic_proxy-0.3.9/tests/test_dashboard.py +25 -0
- mnemonic_proxy-0.3.9/tests/test_response_limit.py +89 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/LICENSE +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/ctxproxy/answerfix.py +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/ctxproxy/api_anthropic.py +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/ctxproxy/api_responses.py +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/ctxproxy/autosave.py +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/ctxproxy/check.py +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/ctxproxy/compat.py +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/ctxproxy/demo.py +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/ctxproxy/enginectx.py +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/ctxproxy/engines.py +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/ctxproxy/fake_engine.py +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/ctxproxy/kvarchive.py +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/ctxproxy/lang.py +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/ctxproxy/leafsel.py +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/ctxproxy/mcp_server.py +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/ctxproxy/packtok.py +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/ctxproxy/paging.py +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/ctxproxy/recall2.py +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/ctxproxy/render.py +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/ctxproxy/router.py +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/ctxproxy/tooldefs.py +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/ctxproxy/upstream.py +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/ctxproxy/window.py +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/mnemonic_proxy.egg-info/dependency_links.txt +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/mnemonic_proxy.egg-info/entry_points.txt +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/mnemonic_proxy.egg-info/requires.txt +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/mnemonic_proxy.egg-info/top_level.txt +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/setup.cfg +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_answerfix.py +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_args_copy.py +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_autosave.py +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_check_kv_archive.py +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_clients.py +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_compat.py +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_default_identity.py +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_ds4.py +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_engine_window.py +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_engines.py +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_final_recall_retry.py +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_kvarchive.py +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_lang.py +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_leaf_select.py +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_leaf_select_formats.py +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_leaf_select_open_points.py +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_live.py +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_measure_restore.py +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_min_window.py +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_names.py +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_onboarding.py +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_paging.py +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_postprova.py +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_proxy.py +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_recall2.py +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_recall_paging.py +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_recall_small_window.py +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_router.py +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_search_keeps_evidence.py +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_setup_scripts.py +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_strata_mock.py +0 -0
- {mnemonic_proxy-0.3.8 → mnemonic_proxy-0.3.9}/tests/test_tooldefs.py +0 -0
- {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.
|
|
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
|
|
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
|
|
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 |
|
|
@@ -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
|
-
|
|
1310
|
-
if
|
|
1311
|
-
|
|
1312
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|