@miller-tech/uap 1.75.0 → 1.76.2

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.
@@ -1 +1 @@
1
- {"version":3,"file":"model-slot-lease.d.ts","sourceRoot":"","sources":["../../src/utils/model-slot-lease.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAIjE,MAAM,WAAW,gBAAgB;IAC/B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,wDAAwD;IACxD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uEAAuE;IACvE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,oFAAoF;IACpF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6DAA6D;IAC7D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6EAA6E;IAC7E,OAAO,CAAC,EAAE,mBAAmB,CAAC;CAC/B;AA0BD,4EAA4E;AAC5E,wBAAsB,qBAAqB,CAAC,IAAI,GAAE,gBAAqB,GAAG,OAAO,CAAC,MAAM,CAAC,CAGxF;AAED,4DAA4D;AAC5D,wBAAsB,kBAAkB,CAAC,IAAI,GAAE,gBAAqB,GAAG,OAAO,CAAC,MAAM,CAAC,CAGrF;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,MAAM,EACd,IAAI,GAAE,gBAAqB,GAC1B,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,OAAO,EAAE,mBAAmB,CAAA;CAAE,CAAC,CAcnE;AAED,oEAAoE;AACpE,wBAAsB,aAAa,CAAC,CAAC,EACnC,MAAM,EAAE,MAAM,EACd,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,IAAI,GAAE,gBAAqB,GAC1B,OAAO,CAAC,CAAC,CAAC,CAaZ"}
1
+ {"version":3,"file":"model-slot-lease.d.ts","sourceRoot":"","sources":["../../src/utils/model-slot-lease.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAIjE,MAAM,WAAW,gBAAgB;IAC/B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,wDAAwD;IACxD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uEAAuE;IACvE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,oFAAoF;IACpF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6DAA6D;IAC7D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6EAA6E;IAC7E,OAAO,CAAC,EAAE,mBAAmB,CAAC;CAC/B;AAID,8EAA8E;AAC9E,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAGvD;AAgCD,4EAA4E;AAC5E,wBAAsB,qBAAqB,CAAC,IAAI,GAAE,gBAAqB,GAAG,OAAO,CAAC,MAAM,CAAC,CAGxF;AAED,4DAA4D;AAC5D,wBAAsB,kBAAkB,CAAC,IAAI,GAAE,gBAAqB,GAAG,OAAO,CAAC,MAAM,CAAC,CAGrF;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,MAAM,EACd,IAAI,GAAE,gBAAqB,GAC1B,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,OAAO,EAAE,mBAAmB,CAAA;CAAE,CAAC,CAcnE;AAED,oEAAoE;AACpE,wBAAsB,aAAa,CAAC,CAAC,EACnC,MAAM,EAAE,MAAM,EACd,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,IAAI,GAAE,gBAAqB,GAC1B,OAAO,CAAC,CAAC,CAAC,CAgCZ"}
@@ -6,10 +6,21 @@
6
6
  * `withModelSlot(holder, fn)` acquires a slot (waiting when the budget is full),
7
7
  * runs `fn`, and releases — the right wrapper around any model call / agent spawn.
8
8
  */
9
+ import { AsyncLocalStorage } from 'node:async_hooks';
9
10
  import { CoordinationService } from '../coordination/service.js';
10
11
  import { getModelSlotBudget } from './model-slots.js';
11
12
  import { loadUapConfigRaw } from './config-loader.js';
12
13
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
14
+ /** Heuristic: does this inference error indicate the backend is saturated? */
15
+ export function isExhaustionError(err) {
16
+ const m = err instanceof Error ? err.message : String(err);
17
+ return /\((?:429|408|502|503|504|529)\)|timed out|timeout|overloaded|too many requests|no slot|slots? (?:are )?full/i.test(m);
18
+ }
19
+ // Re-entrancy guard: nested withModelSlot calls within one async context reuse
20
+ // the outer lease (count as one slot) so layered wrappers can't deadlock or
21
+ // double-count. Cross-process callers (subprocesses) have a fresh store and
22
+ // lease independently, which is correct.
23
+ const holding = new AsyncLocalStorage();
13
24
  function adaptiveEnabled(cwd) {
14
25
  try {
15
26
  const raw = loadUapConfigRaw(cwd ?? process.cwd());
@@ -22,7 +33,9 @@ function adaptiveEnabled(cwd) {
22
33
  async function staticBudget(opts) {
23
34
  if (opts.budget && opts.budget > 0)
24
35
  return opts.budget;
25
- return (await getModelSlotBudget(opts.cwd)).budget;
36
+ // No probing on the per-call hot path — use env/config/cache/default. The
37
+ // cache is warmed by orchestrators (warmModelSlotBudget) and `uap coord slots`.
38
+ return (await getModelSlotBudget(opts.cwd, { probe: false })).budget;
26
39
  }
27
40
  /** Effective budget = min(static, adaptive). When backpressure is active the
28
41
  * adaptive limit is below the static ceiling, throttling the whole fleet. */
@@ -64,20 +77,40 @@ export async function acquireModelSlot(holder, opts = {}) {
64
77
  }
65
78
  /** Run `fn` while holding a model slot (acquire → fn → release). */
66
79
  export async function withModelSlot(holder, fn, opts = {}) {
67
- const { leaseId, service } = await acquireModelSlot(holder, opts);
80
+ // Already holding a slot in this async context → reuse it (re-entrant).
81
+ if (holding.getStore())
82
+ return fn();
83
+ // Fail-open: if the coordination layer is unavailable, run the call rather
84
+ // than block real work on infra problems.
85
+ let acquired;
86
+ try {
87
+ acquired = await acquireModelSlot(holder, opts);
88
+ }
89
+ catch {
90
+ return fn();
91
+ }
92
+ const { leaseId, service } = acquired;
68
93
  // Couldn't get a slot before the deadline → the backend is saturated.
69
94
  if (leaseId === null && adaptiveEnabled(opts.cwd)) {
70
- service.recordModelExhaustion(await staticBudget({ ...opts, service }));
95
+ try {
96
+ service.recordModelExhaustion(await staticBudget({ ...opts, service }));
97
+ }
98
+ catch {
99
+ /* ignore */
100
+ }
71
101
  }
72
102
  try {
73
- const result = await fn();
74
- if (adaptiveEnabled(opts.cwd))
75
- service.recordModelSuccess(await staticBudget({ ...opts, service }));
76
- return result;
103
+ return await holding.run(true, fn);
77
104
  }
78
105
  finally {
79
- if (leaseId !== null)
80
- service.releaseModelSlot(leaseId);
106
+ if (leaseId !== null) {
107
+ try {
108
+ service.releaseModelSlot(leaseId);
109
+ }
110
+ catch {
111
+ /* ignore */
112
+ }
113
+ }
81
114
  }
82
115
  }
83
116
  //# sourceMappingURL=model-slot-lease.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"model-slot-lease.js","sourceRoot":"","sources":["../../src/utils/model-slot-lease.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"model-slot-lease.js","sourceRoot":"","sources":["../../src/utils/model-slot-lease.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAgBtD,MAAM,KAAK,GAAG,CAAC,EAAU,EAAiB,EAAE,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;AAEnF,8EAA8E;AAC9E,MAAM,UAAU,iBAAiB,CAAC,GAAY;IAC5C,MAAM,CAAC,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAC3D,OAAO,8GAA8G,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAChI,CAAC;AAED,+EAA+E;AAC/E,4EAA4E;AAC5E,4EAA4E;AAC5E,yCAAyC;AACzC,MAAM,OAAO,GAAG,IAAI,iBAAiB,EAAW,CAAC;AAEjD,SAAS,eAAe,CAAC,GAAY;IACnC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,gBAAgB,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,EAAE,CAAyD,CAAC;QAC3G,OAAO,GAAG,EAAE,gBAAgB,EAAE,QAAQ,KAAK,KAAK,CAAC,CAAC,yCAAyC;IAC7F,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,KAAK,UAAU,YAAY,CAAC,IAAsB;IAChD,IAAI,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC,MAAM,CAAC;IACvD,0EAA0E;IAC1E,gFAAgF;IAChF,OAAO,CAAC,MAAM,kBAAkB,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC;AACvE,CAAC;AAED;8EAC8E;AAC9E,KAAK,UAAU,eAAe,CAAC,OAA4B,EAAE,IAAsB;IACjF,MAAM,OAAO,GAAG,MAAM,YAAY,CAAC,IAAI,CAAC,CAAC;IACzC,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,GAAG,CAAC;QAAE,OAAO,OAAO,CAAC;IAC/C,OAAO,OAAO,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC;AAC3C,CAAC;AAED,4EAA4E;AAC5E,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAAC,OAAyB,EAAE;IACrE,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,IAAI,mBAAmB,EAAE,CAAC;IAC1D,OAAO,OAAO,CAAC,qBAAqB,CAAC,MAAM,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC;AACjE,CAAC;AAED,4DAA4D;AAC5D,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,OAAyB,EAAE;IAClE,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,IAAI,mBAAmB,EAAE,CAAC;IAC1D,OAAO,OAAO,CAAC,kBAAkB,CAAC,MAAM,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC;AAC9D,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,MAAc,EACd,OAAyB,EAAE;IAE3B,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,IAAI,mBAAmB,EAAE,CAAC;IAC1D,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,OAAO,CAAC;IACpC,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,OAAO,CAAC;IAC5C,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,GAAG,CAAC;IAClC,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC;IAExC,SAAS,CAAC;QACR,MAAM,MAAM,GAAG,MAAM,eAAe,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;QACpD,MAAM,OAAO,GAAG,OAAO,CAAC,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC;QAChE,IAAI,OAAO,KAAK,IAAI;YAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;QAClD,IAAI,IAAI,CAAC,GAAG,EAAE,IAAI,QAAQ;YAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC,YAAY;QAC3E,MAAM,KAAK,CAAC,MAAM,CAAC,CAAC;IACtB,CAAC;AACH,CAAC;AAED,oEAAoE;AACpE,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,MAAc,EACd,EAAoB,EACpB,OAAyB,EAAE;IAE3B,wEAAwE;IACxE,IAAI,OAAO,CAAC,QAAQ,EAAE;QAAE,OAAO,EAAE,EAAE,CAAC;IAEpC,2EAA2E;IAC3E,0CAA0C;IAC1C,IAAI,QAAkE,CAAC;IACvE,IAAI,CAAC;QACH,QAAQ,GAAG,MAAM,gBAAgB,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IAClD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,EAAE,CAAC;IACd,CAAC;IACD,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,QAAQ,CAAC;IACtC,sEAAsE;IACtE,IAAI,OAAO,KAAK,IAAI,IAAI,eAAe,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QAClD,IAAI,CAAC;YACH,OAAO,CAAC,qBAAqB,CAAC,MAAM,YAAY,CAAC,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC;QAC1E,CAAC;QAAC,MAAM,CAAC;YACP,YAAY;QACd,CAAC;IACH,CAAC;IACD,IAAI,CAAC;QACH,OAAO,MAAM,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;IACrC,CAAC;YAAS,CAAC;QACT,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;YACrB,IAAI,CAAC;gBACH,OAAO,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC;YACpC,CAAC;YAAC,MAAM,CAAC;gBACP,YAAY;YACd,CAAC;QACH,CAAC;IACH,CAAC;AACH,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miller-tech/uap",
3
- "version": "1.75.0",
3
+ "version": "1.76.2",
4
4
  "description": "Autonomous AI agent memory system with CLAUDE.md protocol enforcement",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -6339,6 +6339,22 @@ def _looks_malformed_tool_payload(text: str) -> bool:
6339
6339
  if not text.strip():
6340
6340
  return False
6341
6341
 
6342
+ # 2026-06-27: Also strip a trailing UNCLOSED <think> (opener present, no
6343
+ # matching </think>). Under `--reasoning auto` the model frequently runs out
6344
+ # of its token budget mid-reasoning, emitting
6345
+ # "<think> ...let me write files via multiple sandbox calls... args="
6346
+ # with no </think>. That is TRUNCATED reasoning, not a malformed tool call —
6347
+ # but the balanced-only strip above left it intact, so the meta-tool talk
6348
+ # inside it tripped the structural-marker / apology branches below
6349
+ # (false-positive malformed_payload that stalled agentic builds, e.g. the
6350
+ # Octopus Invaders generation: ~11 false rejections in 40 min). Drop from the
6351
+ # first opener to end; KEEP any text before it so a genuine malformed payload
6352
+ # preceding the reasoning is still detected.
6353
+ if "<think>" in text and "</think>" not in text:
6354
+ text = text[: text.index("<think>")]
6355
+ if not text.strip():
6356
+ return False
6357
+
6342
6358
  # 2026-05-12: Strip orphan </parameter> and </function> closers that
6343
6359
  # have no matching opener. Qwen3.6 leaks these training residuals
6344
6360
  # after its visible answer when forced into tool_choice='required'
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env python3
2
+ """An UNCLOSED <think> block must NOT be classified as a malformed tool payload.
3
+
4
+ Under llama-server `--reasoning auto`, Qwen3.6 emits <think> reasoning. When it
5
+ runs out of its token budget mid-thought it produces an *unclosed* block, e.g.
6
+
7
+ "<think> The sandbox works. Now write the remaining files via multiple
8
+ sandbox calls... args="
9
+
10
+ with no </think>. That is TRUNCATED reasoning, not a malformed tool call — but
11
+ the prior detector only stripped *balanced* <think>...</think>, so the meta-tool
12
+ talk inside the unclosed block tripped the structural-marker branch and the proxy
13
+ rejected/retried the turn (observed: ~11 false rejections in 40 min stalling the
14
+ Octopus Invaders agentic build). `_looks_malformed_tool_payload` now also strips a
15
+ trailing unclosed <think> (keeping any text BEFORE the opener so a genuine
16
+ malformed payload preceding the reasoning is still caught).
17
+ """
18
+
19
+ import importlib.util
20
+ import unittest
21
+ from pathlib import Path
22
+
23
+
24
+ def _load_proxy_module():
25
+ proxy_path = Path(__file__).resolve().parents[1] / "scripts" / "anthropic_proxy.py"
26
+ spec = importlib.util.spec_from_file_location("anthropic_proxy", proxy_path)
27
+ module = importlib.util.module_from_spec(spec)
28
+ spec.loader.exec_module(module)
29
+ return module
30
+
31
+
32
+ proxy = _load_proxy_module()
33
+ _malformed = proxy._looks_malformed_tool_payload
34
+
35
+
36
+ class TestUnclosedThinkNotMalformed(unittest.TestCase):
37
+ def test_unclosed_think_with_meta_tool_talk_is_not_malformed(self):
38
+ # The exact failure shape that stalled the Octopus build.
39
+ text = (
40
+ "<think> The sandbox approach works. Now I need to write all the "
41
+ "remaining files. Let me write them efficiently using multiple "
42
+ "sandbox calls in parallel. I'll write the files in dependency "
43
+ "order: 1. config.js (done) 2. args="
44
+ )
45
+ self.assertFalse(_malformed(text))
46
+
47
+ def test_unclosed_think_with_function_words_is_not_malformed(self):
48
+ # meta-tool talk that mentions <function= / <parameter inside reasoning
49
+ text = "<think> I should call <function=Write> with <parameter=path>..."
50
+ self.assertFalse(_malformed(text))
51
+
52
+ def test_balanced_think_still_not_malformed(self):
53
+ text = "<think> planning... </think>"
54
+ self.assertFalse(_malformed(text))
55
+
56
+ def test_genuine_malformed_payload_without_think_still_detected(self):
57
+ # No <think> at all — a real leaked tool-call fragment must still trip.
58
+ self.assertTrue(_malformed("<function=Write><parameter=path>x</parameter>"))
59
+
60
+ def test_malformed_payload_BEFORE_unclosed_think_still_detected(self):
61
+ # Keep text before the opener: a real malformed payload preceding the
62
+ # reasoning must still be caught.
63
+ text = "<function=Write><parameter=p>v</parameter> <think> now reasoning..."
64
+ self.assertTrue(_malformed(text))
65
+
66
+ def test_clean_prose_with_no_tool_markup_is_not_malformed(self):
67
+ self.assertFalse(_malformed("Here is the summary of what I did."))
68
+
69
+
70
+ if __name__ == "__main__":
71
+ unittest.main()