anolisa-tokenless 0.7.14 → 0.8.0

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 (30) hide show
  1. package/README.md +206 -79
  2. package/adapters/tokenless/claude-code/.claude-plugin/plugin.json +1 -1
  3. package/adapters/tokenless/claude-code/hooks/run-hook.sh +62 -0
  4. package/adapters/tokenless/codex/.codex-plugin/plugin.json +1 -1
  5. package/adapters/tokenless/common/cosh-extension.json +4 -4
  6. package/adapters/tokenless/common/hooks/compress_response_hook.py +180 -96
  7. package/adapters/tokenless/common/hooks/compress_schema_hook.py +19 -31
  8. package/adapters/tokenless/common/hooks/hook_utils.py +240 -70
  9. package/adapters/tokenless/common/hooks/rewrite_hook.py +53 -169
  10. package/adapters/tokenless/dsh/dist/index.js +353 -264
  11. package/adapters/tokenless/dsh/package.json +2 -2
  12. package/adapters/tokenless/hermes/__init__.py +191 -357
  13. package/adapters/tokenless/hermes/plugin.yaml +2 -2
  14. package/adapters/tokenless/manifest.json +17 -1
  15. package/adapters/tokenless/openclaw/dist/index.d.ts +4 -16
  16. package/adapters/tokenless/openclaw/dist/index.js +291 -507
  17. package/adapters/tokenless/openclaw/index.ts +408 -628
  18. package/adapters/tokenless/openclaw/openclaw.plugin.json +4 -20
  19. package/adapters/tokenless/openclaw/package.json +6 -4
  20. package/adapters/tokenless/qoder/.qoder-plugin/plugin.json +1 -1
  21. package/adapters/tokenless/qwencode/hooks/run-hook.sh +62 -0
  22. package/adapters/tokenless/qwencode/qwen-extension.json +4 -4
  23. package/adapters/tokenless/qwenpaw/plugin.json +17 -0
  24. package/adapters/tokenless/qwenpaw/plugin.py +390 -0
  25. package/adapters/tokenless/qwenpaw/requirements.txt +6 -0
  26. package/adapters/tokenless/qwenpaw/scripts/detect.sh +131 -0
  27. package/adapters/tokenless/qwenpaw/scripts/install.sh +98 -0
  28. package/adapters/tokenless/qwenpaw/scripts/uninstall.sh +61 -0
  29. package/package.json +5 -5
  30. package/adapters/tokenless/common/hooks/compress_toon_hook.py +0 -174
@@ -2,15 +2,14 @@
2
2
  """Tokenless response compression hook for Cosh-NG, Claude Code, Qoder, and OpenCode.
3
3
 
4
4
  Reads a PostToolUse JSON from stdin, forwards the model-visible tool
5
- response to the unified ``tokenless compress`` entry point (protocol v1,
6
- roadmap §5.4), and translates the CompressionResponse into the host's
5
+ response to the unified ``tokenless compress`` Protocol v2 PostTool operation
6
+ and translates the result into the host's
7
7
  envelope. JSON detection, tool threshold selection, TOON selection, and
8
8
  final acceptance all live behind the entry point; this hook only parses the
9
9
  host object, declares capabilities, and builds envelopes (§4.5).
10
10
 
11
- One Tokenless subprocess per invocation (§5.6). Environment-error
12
- attribution stays hook-side: it is genuinely additive diagnostics, not a
13
- compression decision.
11
+ One Tokenless subprocess per invocation. Environment-error attribution is
12
+ owned by the Rust PostTool service.
14
13
 
15
14
  Hook point: **PostToolUse**
16
15
 
@@ -37,10 +36,10 @@ Output contract per agent:
37
36
  replacement remain passthrough (roadmap §7). Environment attribution is
38
37
  still injected: it is additive by design.
39
38
 
40
- The agent ID is read from the TOKENLESS_AGENT_ID environment variable
41
- (set by the install action script). When running under Cosh-NG, the
42
- agent ID is overridden to ``cosh-ng`` for correct stats attribution.
43
- Fallback paths follow the ANOLISA FHS spec: /usr/bin/tokenless.
39
+ The agent ID is resolved from the host runtime, ``--agent-id`` argument, or
40
+ TOKENLESS_AGENT_ID environment variable. When running under Cosh-NG, runtime
41
+ detection overrides the declared ID for correct stats attribution. Fallback
42
+ paths follow the ANOLISA FHS spec: /usr/bin/tokenless.
44
43
  """
45
44
 
46
45
  from __future__ import annotations
@@ -56,11 +55,13 @@ from hook_utils import (
56
55
  _TOKENLESS_FALLBACK,
57
56
  _TOKENLESS_LOCAL_LIB,
58
57
  _TOKENLESS_LOCAL_SHARE,
58
+ SHELL_TOOLS,
59
59
  SKIP_TOOLS,
60
- build_compression_request,
61
- classify_env_error,
60
+ build_post_tool_request,
61
+ consume_output_optimization,
62
62
  detect_cosh_ng_runtime,
63
63
  is_skill_file,
64
+ is_tokenless_retrieve_command,
64
65
  parse_version,
65
66
  resolve_agent_id,
66
67
  resolve_binary,
@@ -68,17 +69,19 @@ from hook_utils import (
68
69
  run_compress,
69
70
  secure_write_text,
70
71
  skip,
72
+ tokenless_retrieve_command_available,
71
73
  try_parse_json,
72
74
  warn,
73
75
  )
74
76
 
75
77
  # -- constants ---------------------------------------------------------------
76
78
 
77
- # Spawn-avoidance mirror of the entry point's 200-char gate. The authority
78
- # lives in Rust; skipping here only saves the subprocess for content the
79
- # entry would pass through anyway (normalization never grows the char
80
- # count, so raw < 200 implies normalized < 200).
81
- _MIN_RESPONSE_CHARS = 200
79
+ # Shell tool envelopes carry the log in one dominant text field. Unwrapping
80
+ # is worth a rebuilt envelope only when that field is large enough for the
81
+ # build/log engine to bite (its own gates start at 30 lines / 200 chars;
82
+ # 2000 chars keeps the rewrap machinery out of trivial outputs).
83
+ _SHELL_TEXT_FIELDS = ("stdout", "stderr")
84
+ _SHELL_UNWRAP_MIN_CHARS = 2_000
82
85
 
83
86
  # Below the qwen/cosh extension manifests' 10 s host wrapper so a
84
87
  # pathological input is killed here (fail-open skip) before the host kills
@@ -96,9 +99,7 @@ _OPENCODE_AGENT_ID = "opencode"
96
99
  # Cache for `claude --version`, keyed on binary path+mtime+size so upgrades
97
100
  # invalidate it. Hooks run as a fresh process per tool call and spawning the
98
101
  # node CLI every time would add noticeable latency.
99
- _CLAUDE_VERSION_CACHE = os.path.join(
100
- os.path.expanduser("~"), ".tokenless", ".claude-version"
101
- )
102
+ _CLAUDE_VERSION_CACHE = os.path.join(os.path.expanduser("~"), ".tokenless", ".claude-version")
102
103
 
103
104
 
104
105
  # -- helpers -------------------------------------------------------------------
@@ -115,17 +116,44 @@ def _emit_attribution_or_skip(env_attribution: str) -> None:
115
116
  additive and safe on every agent), otherwise a plain skip. Never returns.
116
117
  """
117
118
  if env_attribution:
118
- _emit({
119
- "suppressOutput": True,
120
- "hookSpecificOutput": {
121
- "hookEventName": "PostToolUse",
122
- "additionalContext": env_attribution,
123
- },
124
- })
119
+ _emit(
120
+ {
121
+ "suppressOutput": True,
122
+ "hookSpecificOutput": {
123
+ "hookEventName": "PostToolUse",
124
+ "additionalContext": env_attribution,
125
+ },
126
+ }
127
+ )
125
128
  sys.exit(0)
126
129
  skip()
127
130
 
128
131
 
132
+ def _shell_text_field(tool_name: str, envelope) -> tuple | None:
133
+ """The dominant text field of a shell tool's envelope, or ``None``.
134
+
135
+ Shell envelopes (``{"stdout": …, "stderr": …}``) are JSON to the entry
136
+ point, which would compress them log-blind. Unwrapping the largest text
137
+ field sends the log itself through the text slot; step 13 re-injects the
138
+ compressed text into a same-shaped envelope, so the host's tool protocol
139
+ is untouched (adapters own envelope knowledge, §4.5). Only the single
140
+ largest field is compressed — one Tokenless subprocess per invocation
141
+ (§5.6) — the other field stays byte-identical.
142
+ """
143
+ if tool_name not in SHELL_TOOLS or not isinstance(envelope, dict):
144
+ return None
145
+ best = None
146
+ for name in _SHELL_TEXT_FIELDS:
147
+ value = envelope.get(name)
148
+ if (
149
+ isinstance(value, str)
150
+ and len(value) >= _SHELL_UNWRAP_MIN_CHARS
151
+ and (best is None or len(value) > len(best[1]))
152
+ ):
153
+ best = (name, value)
154
+ return best
155
+
156
+
129
157
  def _cached_claude_version(claude_bin: str) -> tuple | None:
130
158
  """Return the Claude Code version tuple, caching `claude --version`."""
131
159
  try:
@@ -145,7 +173,9 @@ def _cached_claude_version(claude_bin: str) -> tuple | None:
145
173
  try:
146
174
  proc = subprocess.run(
147
175
  [claude_bin, "--version"],
148
- capture_output=True, text=True, timeout=5,
176
+ capture_output=True,
177
+ text=True,
178
+ timeout=5,
149
179
  )
150
180
  except Exception as e:
151
181
  warn(f"claude --version failed: {e}")
@@ -157,9 +187,7 @@ def _cached_claude_version(claude_bin: str) -> tuple | None:
157
187
  try:
158
188
  # Same hardened write as other ~/.tokenless state files (0o600,
159
189
  # symlink-safe) so the cache stays private on shared HOMEs.
160
- secure_write_text(
161
- _CLAUDE_VERSION_CACHE, f"{cache_key}\n{proc.stdout.strip()}"
162
- )
190
+ secure_write_text(_CLAUDE_VERSION_CACHE, f"{cache_key}\n{proc.stdout.strip()}")
163
191
  except OSError:
164
192
  pass
165
193
  return ver
@@ -187,15 +215,29 @@ def main() -> None:
187
215
  cosh_ng_version = detect_cosh_ng_runtime()
188
216
  cosh_ng_detected = cosh_ng_version is not None
189
217
 
190
- # If Cosh-NG is detected but unsupported version, fail open
218
+ # 2. Resolve agent ID based on runtime
219
+ agent_id = resolve_agent_id()
220
+
221
+ # 3. Read stdin JSON and consume any matching PreTool state.
222
+ try:
223
+ input_data = json.load(sys.stdin)
224
+ except (json.JSONDecodeError, EOFError, ValueError):
225
+ warn("failed to read PostToolUse payload. Passing through unchanged.")
226
+ skip()
227
+
228
+ session_id = input_data.get("session_id", "")
229
+ tool_use_id = resolve_tool_call_id(agent_id, input_data)
230
+ try:
231
+ output_optimization = consume_output_optimization(agent_id, session_id, tool_use_id)
232
+ except OSError as error:
233
+ warn(f"failed to consume PreTool optimization state: {error}")
234
+ output_optimization = "none"
235
+
191
236
  if cosh_ng_detected and cosh_ng_version == (0, 0, 0):
192
237
  warn("Unsupported Cosh-NG version. Response compression disabled (fail open).")
193
238
  skip()
194
239
 
195
- # 2. Resolve agent ID based on runtime
196
- agent_id = resolve_agent_id()
197
-
198
- # 3. Resolve binaries
240
+ # 4. Resolve the single Core entry point after consuming per-call state.
199
241
  tokenless_bin = resolve_binary(
200
242
  "tokenless", _TOKENLESS_FALLBACK, _TOKENLESS_LOCAL_SHARE, _TOKENLESS_LOCAL_LIB
201
243
  )
@@ -203,13 +245,6 @@ def main() -> None:
203
245
  warn("tokenless is not installed. Response compression hook disabled.")
204
246
  skip()
205
247
 
206
- # 4. Read stdin JSON
207
- try:
208
- input_data = json.load(sys.stdin)
209
- except (json.JSONDecodeError, EOFError, ValueError):
210
- warn("failed to read PostToolUse payload. Passing through unchanged.")
211
- skip()
212
-
213
248
  tool_name = input_data.get("tool_name", "unknown")
214
249
  tool_response_raw = input_data.get("tool_response", "")
215
250
  if not tool_response_raw or tool_response_raw == "{}":
@@ -235,51 +270,36 @@ def main() -> None:
235
270
  if isinstance(model_visible_before, str) and is_skill_file(model_visible_before):
236
271
  skip()
237
272
 
238
- # 7. Copy the model-visible value into the request content (§4.5).
239
- # ensure_ascii=False matches the entry point's normalization, so size
240
- # gates measure Unicode characters on both sides.
241
- if isinstance(model_visible_before, str):
273
+ # 7. Copy the model-visible value into the request content (§4.5). A
274
+ # shell envelope's dominant text field goes through the text slot
275
+ # instead of log-blind JSON; ensure_ascii=False matches the entry
276
+ # point's normalization, so size gates measure Unicode characters on
277
+ # both sides.
278
+ shell_field = _shell_text_field(tool_name, model_visible_before)
279
+ if shell_field is not None:
280
+ content = shell_field[1]
281
+ elif isinstance(model_visible_before, str):
242
282
  content = model_visible_before
243
283
  elif isinstance(model_visible_before, (dict, list)):
244
- content = json.dumps(
245
- model_visible_before, separators=(",", ":"), ensure_ascii=False
246
- )
284
+ content = json.dumps(model_visible_before, separators=(",", ":"), ensure_ascii=False)
247
285
  else:
248
286
  skip()
249
287
 
250
- # 8. Extract caller context
251
- session_id = input_data.get("session_id", "")
252
- tool_use_id = resolve_tool_call_id(agent_id, input_data)
253
-
254
- # 9. Environment attribution analysis — additive diagnostics, computed
255
- # hook-side. Only structured payloads are classified (with the same
256
- # string-unwrap the entry point applies): plain text never reached
257
- # attribution in the two-subprocess hook and still does not.
258
- if isinstance(model_visible_before, dict):
259
- attr_subject = model_visible_before
260
- else:
261
- parsed = try_parse_json(content)
262
- if isinstance(parsed, str):
263
- parsed = try_parse_json(parsed)
264
- attr_subject = parsed if isinstance(parsed, (dict, list)) else None
265
- env_attribution = ""
266
- attr_category, attr_fix_hint = classify_env_error(attr_subject)
267
- if attr_category:
268
- env_attribution = (
269
- f"[tokenless:env] {tool_name} failed: "
270
- f"{attr_category} ({attr_fix_hint}). Skip retry."
271
- )
272
-
273
- # 10. Capability declaration (§4.5): what can this host actually do?
288
+ # 8. Capability declaration: what can this host actually do?
274
289
  if cosh_ng_detected:
275
290
  can_replace = True
276
291
  replace_with_text = True # updatedToolResponse accepts any text
277
292
  elif agent_id in {_QODER_AGENT_ID, _OPENCODE_AGENT_ID}:
278
293
  can_replace = True
279
- replace_with_text = not isinstance(tool_response_raw, (dict, list))
294
+ # An unwrapped shell field is plain text regardless of its envelope.
295
+ replace_with_text = shell_field is not None or not isinstance(
296
+ tool_response_raw, (dict, list)
297
+ )
280
298
  elif agent_id == _CLAUDE_AGENT_ID:
281
299
  can_replace = _claude_supports_replacement()
282
- replace_with_text = not isinstance(tool_response_raw, (dict, list))
300
+ replace_with_text = shell_field is not None or not isinstance(
301
+ tool_response_raw, (dict, list)
302
+ )
283
303
  if not can_replace:
284
304
  warn(
285
305
  "Claude Code < 2.1.121 (or version unknown): "
@@ -291,30 +311,81 @@ def main() -> None:
291
311
  can_replace = False
292
312
  replace_with_text = True
293
313
 
294
- # 11. Spawn-avoidance prefilters. The entry point re-checks all three
295
- # authoritatively; skipping here just saves the exec. SKIP_TOOLS reads
296
- # the same tool_categories.json the entry point embeds, so content
297
- # retrieval — the hottest PostToolUse traffic — never pays a spawn.
298
- if not can_replace:
299
- _emit_attribution_or_skip(env_attribution)
314
+ # 9. Map host facts into the required lifecycle fields.
300
315
  if tool_name in SKIP_TOOLS:
301
- _emit_attribution_or_skip(env_attribution)
302
- if len(content) < _MIN_RESPONSE_CHARS:
303
- _emit_attribution_or_skip(env_attribution)
316
+ content_origin = "file_content"
317
+ elif tool_name in SHELL_TOOLS:
318
+ content_origin = "command_output"
319
+ else:
320
+ content_origin = "api_response"
321
+ raw_status = str(input_data.get("status", "")).lower()
322
+ shell_process_result = model_visible_before if isinstance(model_visible_before, dict) else None
323
+ shell_process_error = (
324
+ tool_name in SHELL_TOOLS
325
+ and shell_process_result is not None
326
+ and (
327
+ shell_process_result.get("error") is not None
328
+ or (
329
+ shell_process_result.get("exit_code") is not None
330
+ and shell_process_result.get("exit_code") != 0
331
+ )
332
+ or (
333
+ shell_process_result.get("exitCode") is not None
334
+ and shell_process_result.get("exitCode") != 0
335
+ )
336
+ )
337
+ )
338
+ if raw_status in {"interrupted", "denied"}:
339
+ status = raw_status
340
+ elif input_data.get("is_error") is True or (
341
+ isinstance(tool_response_raw, dict) and tool_response_raw.get("isError") is True
342
+ ):
343
+ status = "error"
344
+ elif shell_process_error:
345
+ status = "error"
346
+ else:
347
+ status = "success"
348
+
349
+ # Shell envelopes often carry a large stdout alongside the actual failure
350
+ # in a short stderr. Error results are never replaced, so send the error
351
+ # stream to Core for diagnosis while the host keeps the original envelope.
352
+ if status == "error" and tool_name in SHELL_TOOLS and isinstance(model_visible_before, dict):
353
+ error_parts = []
354
+ for field in ("stderr", "error"):
355
+ value = model_visible_before.get(field)
356
+ if isinstance(value, str) and value.strip():
357
+ error_parts.append(value)
358
+ if error_parts:
359
+ content = "\n".join(error_parts)
360
+
361
+ retrieve_result = status == "success" and is_tokenless_retrieve_command(
362
+ tool_name, input_data.get("tool_input")
363
+ )
364
+ retrieval_available = (
365
+ can_replace
366
+ and status == "success"
367
+ and output_optimization == "none"
368
+ and not retrieve_result
369
+ and tokenless_retrieve_command_available()
370
+ )
304
371
 
305
- # 12. The one Tokenless subprocess: the unified entry point decides.
306
- request = build_compression_request(
372
+ # 10. The one Tokenless subprocess: Core owns all PostTool policy.
373
+ request = build_post_tool_request(
307
374
  content,
308
375
  agent_id,
309
- "post_tool",
376
+ tool_name,
377
+ status,
378
+ content_origin,
379
+ output_optimization,
380
+ result_kind="retrieve" if retrieve_result else "tool",
381
+ recovery={"kind": "shell" if retrieval_available else "none"},
310
382
  session_id=session_id,
311
383
  tool_use_id=tool_use_id,
312
- tool_name=tool_name,
313
- replace_output=True,
314
- publish_retrieve_tool=True,
384
+ replace_output=can_replace,
315
385
  replace_with_text=replace_with_text,
316
386
  )
317
- response = run_compress(tokenless_bin, request, _COMPRESS_TIMEOUT)
387
+ response = run_compress(tokenless_bin, request, _COMPRESS_TIMEOUT, "post_tool")
388
+ env_attribution = response.get("additional_context", "") if response is not None else ""
318
389
  if response is None or response.get("disposition") != "applied":
319
390
  _emit_attribution_or_skip(env_attribution)
320
391
 
@@ -323,18 +394,28 @@ def main() -> None:
323
394
  warn("tokenless compress returned no output. Passing through unchanged.")
324
395
  _emit_attribution_or_skip(env_attribution)
325
396
 
326
- # 13. Envelope construction — dispatch by agent runtime.
397
+ # 11. Envelope construction — dispatch by agent runtime. An unwrapped
398
+ # shell field is re-injected into a same-shaped envelope: the compressed
399
+ # text replaces exactly the field that was sent, every other field stays
400
+ # byte-identical.
401
+ rewrapped = None
402
+ if shell_field is not None:
403
+ rewrapped = dict(model_visible_before)
404
+ rewrapped[shell_field[0]] = output_text
405
+
327
406
  if cosh_ng_detected:
328
407
  hook_specific = {
329
408
  "hookEventName": "PostToolUse",
330
- "updatedToolResponse": output_text,
409
+ "updatedToolResponse": rewrapped if rewrapped is not None else output_text,
331
410
  }
332
411
  if env_attribution:
333
412
  hook_specific["additionalContext"] = env_attribution
334
413
  _emit({"suppressOutput": True, "hookSpecificOutput": hook_specific})
335
414
  return
336
415
 
337
- if replace_with_text:
416
+ if rewrapped is not None:
417
+ updated_output = rewrapped
418
+ elif replace_with_text:
338
419
  updated_output = output_text
339
420
  else:
340
421
  # Structured slot: the entry point guarantees schema-stable JSON for
@@ -347,9 +428,12 @@ def main() -> None:
347
428
 
348
429
  # Qoder validates updatedToolOutput as a string even when the original
349
430
  # tool response is structured. The entry point's compact serialization
350
- # is exactly that string.
431
+ # is exactly that string; a rewrapped shell envelope serializes here.
351
432
  if agent_id == _QODER_AGENT_ID and not isinstance(updated_output, str):
352
- updated_output = output_text
433
+ if rewrapped is not None:
434
+ updated_output = json.dumps(rewrapped, separators=(",", ":"), ensure_ascii=False)
435
+ else:
436
+ updated_output = output_text
353
437
 
354
438
  hook_output = {
355
439
  "hookEventName": "PostToolUse",
@@ -2,8 +2,8 @@
2
2
  """Tokenless schema compression hook.
3
3
 
4
4
  Reads a BeforeModel JSON from stdin, extracts the tools array, forwards it
5
- to the unified ``tokenless compress`` entry point (protocol v1, seam
6
- ``before_model``, roadmap §5.4), and writes a HookOutput JSON to stdout.
5
+ to the unified ``tokenless compress`` Protocol v2 BeforeModel operation and
6
+ writes a HookOutput JSON to stdout.
7
7
  The entry point returns the original array on no-savings, which this hook
8
8
  wraps exactly like a compressed one — the historical behavior of the
9
9
  ``compress-schema`` flow it replaces.
@@ -32,7 +32,7 @@ from hook_utils import (
32
32
  _TOKENLESS_FALLBACK,
33
33
  _TOKENLESS_LOCAL_LIB,
34
34
  _TOKENLESS_LOCAL_SHARE,
35
- build_compression_request,
35
+ build_before_model_request,
36
36
  resolve_agent_id,
37
37
  resolve_binary,
38
38
  resolve_tool_call_id,
@@ -112,14 +112,6 @@ def _marker_lock():
112
112
  # -- helpers -----------------------------------------------------------------
113
113
 
114
114
 
115
- def _is_json_array(data: str) -> bool:
116
- try:
117
- obj = json.loads(data)
118
- return isinstance(obj, list)
119
- except (json.JSONDecodeError, ValueError):
120
- return False
121
-
122
-
123
115
  def _session_warn_key(session_id: str) -> str:
124
116
  """Normalize the session ID into the dedup key for the no-tools warning.
125
117
 
@@ -275,37 +267,33 @@ def main() -> None:
275
267
  )
276
268
  skip()
277
269
 
278
- tools_json = json.dumps(tools, separators=(",", ":"))
279
-
280
270
  # 4. Extract caller context
281
271
  session_id = input_data.get("session_id", "")
282
272
  tool_use_id = resolve_tool_call_id(_AGENT_ID, input_data)
283
273
 
284
274
  # 5. Compress schemas via the unified entry point (one subprocess).
285
- request = build_compression_request(
286
- tools_json,
275
+ llm_request = input_data.get("llm_request", {})
276
+ visible_context = dict(llm_request) if isinstance(llm_request, dict) else {}
277
+ visible_context.pop("tools", None)
278
+ config = visible_context.get("config")
279
+ if isinstance(config, dict):
280
+ visible_context["config"] = dict(config)
281
+ visible_context["config"].pop("tools", None)
282
+ request = build_before_model_request(
283
+ tools,
284
+ visible_context,
287
285
  _AGENT_ID,
288
- "before_model",
289
286
  session_id=session_id,
290
287
  tool_use_id=tool_use_id,
291
- replace_output=True,
292
- publish_retrieve_tool=True,
293
288
  )
294
- response = run_compress(tokenless_bin, request, _COMPRESS_TIMEOUT)
289
+ response = run_compress(
290
+ tokenless_bin, request, _COMPRESS_TIMEOUT, "before_model"
291
+ )
295
292
  if response is None:
296
293
  warn("Schema compression subprocess failed. Passing through unchanged.")
297
294
  skip()
298
- if response.get("disposition") in {"error", "timeout"}:
299
- warn("Schema compression failed. Passing through unchanged.")
300
- skip()
301
- if response.get("disposition") == "reversibility_unavailable":
302
- # Savings existed but the stash could not record the originals;
303
- # the entry point returned the uncompressed schemas. Surface the
304
- # distinction from "nothing to compress" (envelope unchanged).
305
- warn("Schema stash unavailable; truncated descriptions would be lossy.")
306
-
307
- compressed = response.get("output")
308
- if not isinstance(compressed, str) or not _is_json_array(compressed):
295
+ compressed = response.get("tools")
296
+ if not isinstance(compressed, list):
309
297
  warn(
310
298
  "Schema compression returned invalid JSON. Passing through unchanged."
311
299
  )
@@ -317,7 +305,7 @@ def main() -> None:
317
305
  "hookEventName": "BeforeModel",
318
306
  "llm_request": {
319
307
  "config": {
320
- "tools": json.loads(compressed),
308
+ "tools": compressed,
321
309
  },
322
310
  },
323
311
  },