@operatingtag/dsh-unclesam 1.3.3 → 1.3.5

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 (55) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.ar.md +1 -1
  3. package/README.az.md +1 -1
  4. package/README.bg.md +1 -1
  5. package/README.bn.md +1 -1
  6. package/README.cs.md +1 -1
  7. package/README.da.md +1 -1
  8. package/README.de.md +1 -1
  9. package/README.el.md +1 -1
  10. package/README.es.md +1 -1
  11. package/README.fa.md +1 -1
  12. package/README.fi.md +1 -1
  13. package/README.fr.md +1 -1
  14. package/README.he.md +1 -1
  15. package/README.hi.md +1 -1
  16. package/README.hr.md +1 -1
  17. package/README.hu.md +1 -1
  18. package/README.id.md +1 -1
  19. package/README.it.md +1 -1
  20. package/README.ja.md +1 -1
  21. package/README.ka.md +1 -1
  22. package/README.kk.md +1 -1
  23. package/README.km.md +1 -1
  24. package/README.ko.md +1 -1
  25. package/README.lt.md +1 -1
  26. package/README.md +1 -1
  27. package/README.mr.md +1 -1
  28. package/README.ms.md +1 -1
  29. package/README.my.md +1 -1
  30. package/README.ne.md +1 -1
  31. package/README.nl.md +1 -1
  32. package/README.no.md +1 -1
  33. package/README.pl.md +1 -1
  34. package/README.pt.md +1 -1
  35. package/README.ro.md +1 -1
  36. package/README.ru.md +1 -1
  37. package/README.si.md +1 -1
  38. package/README.sk.md +1 -1
  39. package/README.sl.md +1 -1
  40. package/README.sr.md +1 -1
  41. package/README.sv.md +1 -1
  42. package/README.sw.md +1 -1
  43. package/README.ta.md +1 -1
  44. package/README.te.md +1 -1
  45. package/README.th.md +1 -1
  46. package/README.tl.md +1 -1
  47. package/README.tr.md +1 -1
  48. package/README.uk.md +1 -1
  49. package/README.ur.md +1 -1
  50. package/README.uz.md +1 -1
  51. package/README.vi.md +1 -1
  52. package/README.zh.md +1 -1
  53. package/index.js +74 -8
  54. package/lib/prompt.mjs +289 -4
  55. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -8,6 +8,64 @@ fixes the patch.
8
8
 
9
9
  ---
10
10
 
11
+ ## 1.3.5
12
+
13
+ **A live web turn came apart into a loop, and the audit it prompted fixed three things.**
14
+
15
+ Asked to glob a workspace it had misread, the web model invented a tool format
16
+ (`<tool name="glob" string="true">{…}</tool>`), the parser did not accept it, nothing told the model so, and
17
+ it repeated the block until the transcript was full of copies.
18
+
19
+ - **The invented shape is now accepted.** `parseToolCall` recognises `<tool name="…">…</tool>` and
20
+ `<tool_call>…</tool_call>`, and `stripToolCall` removes them, so a model that drifts to tags still gets a
21
+ running call instead of a dead one.
22
+ - **A reply that loops is stopped, not displayed.** `looksDegenerate` fires on three `<tool` openings or
23
+ four identical substantial lines, and the turn ends with a plain `web_loop` error naming what happened.
24
+ Nothing was wrong with the page; the model was stuck, and the person no longer has to watch it write one
25
+ line for minutes.
26
+ - **Two audit findings, reproduced against the running code:**
27
+ - `findCallForm` treated an EMPTY tool list as "accept anything", so ordinary prose like `log(2)` and
28
+ `figure(2)` was silently deleted from every non-tool turn. The empty list now means "nothing is a call".
29
+ - A malformed call before a valid one (`read(bad) and pwsh({…})`) returned nothing, hiding the valid call
30
+ and leaking its text into the transcript. A bad candidate now skips to the next instead of aborting.
31
+
32
+ `dev/test-call-form.mjs` gained the loop, tag, empty-list and malformed-candidate cases; the suite is 15
33
+ suites, all green.
34
+
35
+ ---
36
+
37
+ ## 1.3.4
38
+
39
+ **A tool call is now written `name({...})` instead of a JSON envelope, so the answer can stream.**
40
+
41
+ MEASURED: a reply that is nothing but `{"tool":…}` cannot be shown while it is written — `streamSafeCut`
42
+ returns 0 for the whole generation — so the harness sits empty for the entire turn and looks stuck. And a
43
+ brace is the commonest punctuation in technical prose, so `{` is a poor delimiter: it both blocks streaming
44
+ and invites false positives.
45
+
46
+ `replace_note({"text":"…"})` needs no instruction, because it is the shape models already produce, and it is
47
+ unambiguous once anchored on a tool name the model was actually offered. Prose streams until a known name
48
+ opens a parenthesis, instead of stopping at the first brace anywhere.
49
+
50
+ - **`parseToolCall` walks characters, not a regex**, so an argument may contain a parenthesis, a brace, an
51
+ escaped quote or a newline. `read()` parses as empty arguments; `see figure(2)` does not parse at all,
52
+ because `figure` was never offered.
53
+ - **The JSON envelope is still accepted**, fenced or bare, as a fallback — a model may emit it unprompted
54
+ and refusing it would throw away a working call.
55
+ - **`stripToolCall` removes whichever shape was used**, so neither reaches the transcript.
56
+ - **`streamSafeCut` holds from a known name plus `(`, or from a trailing fragment of a known name of three
57
+ characters or more.** The first version held any fragment, so a sentence ending in `r` stalled one
58
+ character back — `r` prefixes `read`. Found by a test, fixed before shipping.
59
+ - **`dev/test-call-form.mjs`** pins ten shapes that are calls, five that are not, and the stripping and
60
+ streaming behaviour. Two of its own assertions were written from guesswork and failed; they now measure.
61
+ - **`dev/test-web-tool-write.mjs` asserts the shape**, not just the outcome: the raw reply is captured through
62
+ `UNCLE_SAM_RAW_REPLY` and must start with `name(`. Measured live: the page replied
63
+ `replace_note({"text":"…"})` and the file changed, **4,062 ms**.
64
+
65
+ The raw capture is off unless that variable names a file, so nothing is written during ordinary use.
66
+
67
+ ---
68
+
11
69
  ## 1.3.3
12
70
 
13
71
  **The tool names were not enough: a call with the wrong argument name cannot be executed.**
package/README.ar.md CHANGED
@@ -250,7 +250,7 @@ Copyright (c) 2026 Darren T. Weldon، منشور تحت اسم Operating Tag.
250
250
  Uncle Sam إضافة مستقلة. غير مرتبطة بـ DeepSeek، ولا تحظى بتأييدهم أو دعمهم.
251
251
 
252
252
  ```
253
- @operatingtag/dsh-unclesam v1.3.3
253
+ @operatingtag/dsh-unclesam v1.3.5
254
254
  المؤلف Operating Tag
255
255
  الترخيص CC BY-NC-ND 4.0 — مجاني للاستخدام، لا للتغيير أو المشاركة أو البيع
256
256
  ```
package/README.az.md CHANGED
@@ -256,7 +256,7 @@ soruşun — aydın cavab və aydın qiymət alacaqsınız.
256
256
  Uncle Sam müstəqil plagindir. DeepSeek ilə bağlı deyil, onlar tərəfindən təsdiqlənməyib və dəstəklənmir.
257
257
 
258
258
  ```
259
- @operatingtag/dsh-unclesam v1.3.3
259
+ @operatingtag/dsh-unclesam v1.3.5
260
260
  Müəllif Operating Tag
261
261
  Lisenziya CC BY-NC-ND 4.0 — istifadə üçün pulsuz, dəyişdirmək, paylaşmaq və ya satmaq üçün deyil
262
262
  ```
package/README.bg.md CHANGED
@@ -259,7 +259,7 @@ Copyright (c) 2026 Darren T. Weldon, публикувано под името Op
259
259
  Uncle Sam е независима приставка. Не е свързана с DeepSeek, не е одобрена или поддържана от тях.
260
260
 
261
261
  ```
262
- @operatingtag/dsh-unclesam v1.3.3
262
+ @operatingtag/dsh-unclesam v1.3.5
263
263
  Автор Operating Tag
264
264
  Лиценз CC BY-NC-ND 4.0 — свободно за използване, не за промяна, споделяне или продажба
265
265
  ```
package/README.bn.md CHANGED
@@ -254,7 +254,7 @@ Copyright (c) 2026 Darren T. Weldon, Operating Tag নামে প্রকা
254
254
  Uncle Sam একটি স্বাধীন প্লাগইন। DeepSeek-এর সঙ্গে যুক্ত নয়, তাদের অনুমোদন বা সমর্থন নেই।
255
255
 
256
256
  ```
257
- @operatingtag/dsh-unclesam v1.3.3
257
+ @operatingtag/dsh-unclesam v1.3.5
258
258
  লেখক Operating Tag
259
259
  লাইসেন্স CC BY-NC-ND 4.0 — ব্যবহারে বিনামূল্যে, পরিবর্তন, শেয়ার বা বিক্রিতে নয়
260
260
  ```
package/README.cs.md CHANGED
@@ -256,7 +256,7 @@ Závazný je soubor [LICENSE](LICENSE) a začíná stejným shrnutím v jednoduc
256
256
  Uncle Sam je nezávislý plugin. Není spojený s DeepSeek, není jimi schválený ani podporovaný.
257
257
 
258
258
  ```
259
- @operatingtag/dsh-unclesam v1.3.3
259
+ @operatingtag/dsh-unclesam v1.3.5
260
260
  Autor Operating Tag
261
261
  Licence CC BY-NC-ND 4.0 — zdarma k použití, ne ke změně, sdílení nebo prodeji
262
262
  ```
package/README.da.md CHANGED
@@ -257,7 +257,7 @@ Filen [LICENSE](LICENSE) er den gældende, og den begynder med samme resumé på
257
257
  Uncle Sam er et uafhængigt plugin. Ikke knyttet til DeepSeek, ikke godkendt eller støttet af dem.
258
258
 
259
259
  ```
260
- @operatingtag/dsh-unclesam v1.3.3
260
+ @operatingtag/dsh-unclesam v1.3.5
261
261
  Forfatter Operating Tag
262
262
  Licens CC BY-NC-ND 4.0 — frit at bruge, ikke at ændre, dele eller sælge
263
263
  ```
package/README.de.md CHANGED
@@ -270,7 +270,7 @@ Englisch.
270
270
  Uncle Sam ist ein unabhängiges Plugin. Nicht mit DeepSeek verbunden, von ihnen weder unterstützt noch gefördert.
271
271
 
272
272
  ```
273
- @operatingtag/dsh-unclesam v1.3.3
273
+ @operatingtag/dsh-unclesam v1.3.5
274
274
  Autor Operating Tag
275
275
  Lizenz CC BY-NC-ND 4.0 — frei zur Nutzung, nicht zum Ändern, Weitergabe oder Verkauf
276
276
  ```
package/README.el.md CHANGED
@@ -266,7 +266,7 @@ Copyright (c) 2026 Darren T. Weldon, δημοσιευμένο με το όνομ
266
266
  Το Uncle Sam είναι ανεξάρτητο πρόσθετο. Δεν συνδέεται με τη DeepSeek, δεν εγκρίνεται ούτε υποστηρίζεται από αυτούς.
267
267
 
268
268
  ```
269
- @operatingtag/dsh-unclesam v1.3.3
269
+ @operatingtag/dsh-unclesam v1.3.5
270
270
  Δημιουργός Operating Tag
271
271
  Άδεια CC BY-NC-ND 4.0 — ελεύθερο για χρήση, όχι για αλλαγή, κοινή χρήση ή πώληση
272
272
  ```
package/README.es.md CHANGED
@@ -264,7 +264,7 @@ El archivo [LICENSE](LICENSE) es el que rige, y abre con el mismo resumen en ing
264
264
  Uncle Sam es un plugin independiente. No está afiliado, respaldado ni soportado por DeepSeek.
265
265
 
266
266
  ```
267
- @operatingtag/dsh-unclesam v1.3.3
267
+ @operatingtag/dsh-unclesam v1.3.5
268
268
  Autor Operating Tag
269
269
  Licencia CC BY-NC-ND 4.0 — gratis para usar, no para cambiar, compartir o vender
270
270
  ```
package/README.fa.md CHANGED
@@ -256,7 +256,7 @@ Copyright (c) 2026 Darren T. Weldon، منتشرشده با نام Operating Tag
256
256
  Uncle Sam یک افزونه مستقل است. وابسته به DeepSeek نیست، تأیید یا پشتیبانی نمی‌شود.
257
257
 
258
258
  ```
259
- @operatingtag/dsh-unclesam v1.3.3
259
+ @operatingtag/dsh-unclesam v1.3.5
260
260
  نویسنده Operating Tag
261
261
  پروانه CC BY-NC-ND 4.0 — آزاد برای استفاده، نه برای تغییر، اشتراک یا فروش
262
262
  ```
package/README.fi.md CHANGED
@@ -258,7 +258,7 @@ Tiedosto [LICENSE](LICENSE) on ratkaiseva, ja se alkaa samalla yhteenvedolla yks
258
258
  Uncle Sam on itsenäinen lisäosa. Ei sidoksissa DeepSeekiin, ei heidän hyväksymänsä tai tukemansa.
259
259
 
260
260
  ```
261
- @operatingtag/dsh-unclesam v1.3.3
261
+ @operatingtag/dsh-unclesam v1.3.5
262
262
  Tekijä Operating Tag
263
263
  Lisenssi CC BY-NC-ND 4.0 — vapaa käytettäväksi, ei muutettavaksi, jaettavaksi tai myytäväksi
264
264
  ```
package/README.fr.md CHANGED
@@ -268,7 +268,7 @@ Le fichier [LICENSE](LICENSE) fait foi, et il s'ouvre sur le même résumé en a
268
268
  Uncle Sam est un plugin indépendant. Non affilié à DeepSeek, ni approuvé ni soutenu par eux.
269
269
 
270
270
  ```
271
- @operatingtag/dsh-unclesam v1.3.3
271
+ @operatingtag/dsh-unclesam v1.3.5
272
272
  Auteur Operating Tag
273
273
  Licence CC BY-NC-ND 4.0 — gratuit à l'usage, pas au changement, au partage ou à la vente
274
274
  ```
package/README.he.md CHANGED
@@ -248,7 +248,7 @@ Copyright (c) 2026 Darren T. Weldon, פורסם תחת השם Operating Tag.
248
248
  Uncle Sam הוא תוסף עצמאי. לא קשור ל‑DeepSeek, לא מאושר ולא נתמך על ידם.
249
249
 
250
250
  ```
251
- @operatingtag/dsh-unclesam v1.3.3
251
+ @operatingtag/dsh-unclesam v1.3.5
252
252
  יוצר Operating Tag
253
253
  רישיון CC BY-NC-ND 4.0 — חופשי לשימוש, לא לשינוי, שיתוף או מכירה
254
254
  ```
package/README.hi.md CHANGED
@@ -259,7 +259,7 @@ Copyright (c) 2026 Darren T. Weldon, Operating Tag नाम से प्रक
259
259
  Uncle Sam एक स्वतंत्र प्लगइन है। DeepSeek से जुड़ा नहीं, उनका समर्थन या अनुमोदन नहीं।
260
260
 
261
261
  ```
262
- @operatingtag/dsh-unclesam v1.3.3
262
+ @operatingtag/dsh-unclesam v1.3.5
263
263
  लेखक Operating Tag
264
264
  लाइसेंस CC BY-NC-ND 4.0 — इस्तेमाल मुफ़्त, बदलना, बाँटना या बेचना नहीं
265
265
  ```
package/README.hr.md CHANGED
@@ -256,7 +256,7 @@ Datoteka [LICENSE](LICENSE) je obvezujuća i počinje istim sažetkom na jednost
256
256
  Uncle Sam je neovisni dodatak. Nije povezan s DeepSeekom, nije odobren ni podržan od njih.
257
257
 
258
258
  ```
259
- @operatingtag/dsh-unclesam v1.3.3
259
+ @operatingtag/dsh-unclesam v1.3.5
260
260
  Autor Operating Tag
261
261
  Licencija CC BY-NC-ND 4.0 — besplatno za korištenje, ne za izmjenu, dijeljenje ili prodaju
262
262
  ```
package/README.hu.md CHANGED
@@ -260,7 +260,7 @@ A [LICENSE](LICENSE) fájl a mérvadó, és ugyanazzal az egyszerű angol össze
260
260
  Az Uncle Sam független bővítmény. Nem kapcsolódik a DeepSeekhez, ő nem hagyja jóvá és nem támogatja.
261
261
 
262
262
  ```
263
- @operatingtag/dsh-unclesam v1.3.3
263
+ @operatingtag/dsh-unclesam v1.3.5
264
264
  Szerző Operating Tag
265
265
  Licenc CC BY-NC-ND 4.0 — szabadon használható, nem módosítható, osztható meg vagy adható el
266
266
  ```
package/README.id.md CHANGED
@@ -260,7 +260,7 @@ Berkas [LICENSE](LICENSE) yang mengikat, dan ia dibuka dengan ringkasan yang sam
260
260
  Uncle Sam adalah plugin independen. Tidak berafiliasi dengan DeepSeek, tidak didukung maupun disponsori oleh mereka.
261
261
 
262
262
  ```
263
- @operatingtag/dsh-unclesam v1.3.3
263
+ @operatingtag/dsh-unclesam v1.3.5
264
264
  Penulis Operating Tag
265
265
  Lisensi CC BY-NC-ND 4.0 — bebas dipakai, tidak untuk diubah, dibagikan, atau dijual
266
266
  ```
package/README.it.md CHANGED
@@ -264,7 +264,7 @@ Il file [LICENSE](LICENSE) è quello che fa fede, e si apre con lo stesso riassu
264
264
  Uncle Sam è un plugin indipendente. Non affiliato a DeepSeek, né approvato né supportato da loro.
265
265
 
266
266
  ```
267
- @operatingtag/dsh-unclesam v1.3.3
267
+ @operatingtag/dsh-unclesam v1.3.5
268
268
  Autore Operating Tag
269
269
  Licenza CC BY-NC-ND 4.0 — libero da usare, non da modificare, condividere o vendere
270
270
  ```
package/README.ja.md CHANGED
@@ -259,7 +259,7 @@ Copyright (c) 2026 Darren T. Weldon、Operating Tag 名義で公開。
259
259
  Uncle Sam は独立したプラグインです。DeepSeek とは関係がなく、承認も支援も受けていません。
260
260
 
261
261
  ```
262
- @operatingtag/dsh-unclesam v1.3.3
262
+ @operatingtag/dsh-unclesam v1.3.5
263
263
  作者 Operating Tag
264
264
  ライセンス CC BY-NC-ND 4.0 — 使用は自由、変更・共有・販売は不可
265
265
  ```
package/README.ka.md CHANGED
@@ -257,7 +257,7 @@ Copyright (c) 2026 Darren T. Weldon, გამოქვეყნებული
257
257
  Uncle Sam დამოუკიდებელი დანამატია. არ არის დაკავშირებული DeepSeek-თან, მათ მიერ არ არის დამტკიცებული ან მხარდაჭერილი.
258
258
 
259
259
  ```
260
- @operatingtag/dsh-unclesam v1.3.3
260
+ @operatingtag/dsh-unclesam v1.3.5
261
261
  ავტორი Operating Tag
262
262
  ლიცენზია CC BY-NC-ND 4.0 — თავისუფალი გამოსაყენებლად, არა შესაცვლელად, გასაზიარებლად ან გასაყიდად
263
263
  ```
package/README.kk.md CHANGED
@@ -258,7 +258,7 @@ Operating Tag-тан сұраңыз — нақты жауап және нақт
258
258
  Uncle Sam — тәуелсіз плагин. DeepSeek-пен байланысты емес, олар мақұлдамаған немесе қолдамаған.
259
259
 
260
260
  ```
261
- @operatingtag/dsh-unclesam v1.3.3
261
+ @operatingtag/dsh-unclesam v1.3.5
262
262
  Авторы Operating Tag
263
263
  Лицензия CC BY-NC-ND 4.0 — пайдалануға тегін, өзгертуге, бөлісуге немесе сатуға емес
264
264
  ```
package/README.km.md CHANGED
@@ -253,7 +253,7 @@ Tag — អ្នកនឹងទទួលបានចម្លើយច្បា
253
253
  Uncle Sam គឺជាកម្មវិធីបន្ថែមឯករាជ្យ។ មិនពាក់ព័ន្ធនឹង DeepSeek មិនត្រូវបានអនុម័ត ឬគាំទ្រដោយពួកគេ។
254
254
 
255
255
  ```
256
- @operatingtag/dsh-unclesam v1.3.3
256
+ @operatingtag/dsh-unclesam v1.3.5
257
257
  អ្នកនិពន្ធ Operating Tag
258
258
  អាជ្ញាបណ្ណ CC BY-NC-ND 4.0 — ប្រើឥតគិតថ្លៃ មិនមែនសម្រាប់កែប្រែ ចែករំលែក ឬលក់
259
259
  ```
package/README.ko.md CHANGED
@@ -254,7 +254,7 @@ Copyright (c) 2026 Darren T. Weldon, Operating Tag 이름으로 공개.
254
254
  Uncle Sam은 독립적인 플러그인입니다. DeepSeek과 무관하며, 그들의 보증이나 지원을 받지 않습니다.
255
255
 
256
256
  ```
257
- @operatingtag/dsh-unclesam v1.3.3
257
+ @operatingtag/dsh-unclesam v1.3.5
258
258
  저자 Operating Tag
259
259
  라이선스 CC BY-NC-ND 4.0 — 사용은 자유, 변경·공유·판매는 불가
260
260
  ```
package/README.lt.md CHANGED
@@ -257,7 +257,7 @@ Failas [LICENSE](LICENSE) yra privalomas, ir jis prasideda ta pačia santrauka p
257
257
  Uncle Sam yra nepriklausomas papildinys. Nesusijęs su DeepSeek, jų nepatvirtintas ir neremiamas.
258
258
 
259
259
  ```
260
- @operatingtag/dsh-unclesam v1.3.3
260
+ @operatingtag/dsh-unclesam v1.3.5
261
261
  Autorius Operating Tag
262
262
  Licencija CC BY-NC-ND 4.0 — nemokama naudoti, ne keisti, bendrinti ar parduoti
263
263
  ```
package/README.md CHANGED
@@ -257,7 +257,7 @@ The [LICENSE](LICENSE) file governs, and it opens with the same summary in plain
257
257
  Uncle Sam is an independent plugin. Not affiliated with, endorsed by, or supported by DeepSeek.
258
258
 
259
259
  ```
260
- @operatingtag/dsh-unclesam v1.3.3
260
+ @operatingtag/dsh-unclesam v1.3.5
261
261
  Author Operating Tag
262
262
  Licence CC BY-NC-ND 4.0 — free to use, not to change, share or sell
263
263
  ```
package/README.mr.md CHANGED
@@ -254,7 +254,7 @@ Copyright (c) 2026 Darren T. Weldon, Operating Tag नावाने प्र
254
254
  Uncle Sam एक स्वतंत्र प्लगइन आहे. DeepSeek शी संबंधित नाही, त्यांची मान्यता किंवा समर्थन नाही.
255
255
 
256
256
  ```
257
- @operatingtag/dsh-unclesam v1.3.3
257
+ @operatingtag/dsh-unclesam v1.3.5
258
258
  लेखक Operating Tag
259
259
  परवाना CC BY-NC-ND 4.0 — वापरण्यास मोफत, बदलण्यास, सामायिक करण्यास किंवा विकण्यास नाही
260
260
  ```
package/README.ms.md CHANGED
@@ -261,7 +261,7 @@ mudah.
261
261
  Uncle Sam ialah pemalam bebas. Tidak berkaitan dengan DeepSeek, tidak disokong atau ditaja oleh mereka.
262
262
 
263
263
  ```
264
- @operatingtag/dsh-unclesam v1.3.3
264
+ @operatingtag/dsh-unclesam v1.3.5
265
265
  Penulis Operating Tag
266
266
  Lesen CC BY-NC-ND 4.0 — bebas digunakan, bukan untuk diubah, dikongsi atau dijual
267
267
  ```
package/README.my.md CHANGED
@@ -262,7 +262,7 @@ Copyright (c) 2026 Darren T. Weldon, Operating Tag အမည်ဖြင့်
262
262
  Uncle Sam သည် သီးခြားလွတ်လပ်သော plugin ဖြစ်သည်။ DeepSeek နှင့် မသက်ဆိုင်ပါ၊ ၎င်းတို့က အတည်မပြု၊ မပံ့ပိုးပါ။
263
263
 
264
264
  ```
265
- @operatingtag/dsh-unclesam v1.3.3
265
+ @operatingtag/dsh-unclesam v1.3.5
266
266
  စာရေးသူ Operating Tag
267
267
  လိုင်စင် CC BY-NC-ND 4.0 — အသုံးပြုရန် အခမဲ့၊ ပြောင်းလဲရန်၊ မျှဝေရန် သို့မဟုတ် ရောင်းရန် မဟုတ်
268
268
  ```
package/README.ne.md CHANGED
@@ -254,7 +254,7 @@ Copyright (c) 2026 Darren T. Weldon, Operating Tag नाममा प्रक
254
254
  Uncle Sam स्वतन्त्र प्लगइन हो। DeepSeek सँग सम्बन्धित छैन, उनीहरूको स्वीकृति वा समर्थन छैन।
255
255
 
256
256
  ```
257
- @operatingtag/dsh-unclesam v1.3.3
257
+ @operatingtag/dsh-unclesam v1.3.5
258
258
  लेखक Operating Tag
259
259
  इजाजतपत्र CC BY-NC-ND 4.0 — प्रयोग गर्न निःशुल्क, परिवर्तन, बाँड्न वा बेच्न होइन
260
260
  ```
package/README.nl.md CHANGED
@@ -262,7 +262,7 @@ Het bestand [LICENSE](LICENSE) is bindend, en het begint met dezelfde samenvatti
262
262
  Uncle Sam is een onafhankelijke plugin. Niet verbonden met DeepSeek, door hen niet goedgekeurd of ondersteund.
263
263
 
264
264
  ```
265
- @operatingtag/dsh-unclesam v1.3.3
265
+ @operatingtag/dsh-unclesam v1.3.5
266
266
  Auteur Operating Tag
267
267
  Licentie CC BY-NC-ND 4.0 — vrij om te gebruiken, niet om te wijzigen, delen of verkopen
268
268
  ```
package/README.no.md CHANGED
@@ -257,7 +257,7 @@ Filen [LICENSE](LICENSE) er den gjeldende, og den begynner med det samme sammend
257
257
  Uncle Sam er en uavhengig utvidelse. Ikke knyttet til DeepSeek, ikke godkjent eller støttet av dem.
258
258
 
259
259
  ```
260
- @operatingtag/dsh-unclesam v1.3.3
260
+ @operatingtag/dsh-unclesam v1.3.5
261
261
  Forfatter Operating Tag
262
262
  Lisens CC BY-NC-ND 4.0 — fritt å bruke, ikke å endre, dele eller selge
263
263
  ```
package/README.pl.md CHANGED
@@ -262,7 +262,7 @@ Wiążący jest plik [LICENSE](LICENSE), a otwiera się tym samym podsumowaniem
262
262
  Uncle Sam to niezależna wtyczka. Niepowiązana z DeepSeek, nie firmowana ani nie wspierana przez nich.
263
263
 
264
264
  ```
265
- @operatingtag/dsh-unclesam v1.3.3
265
+ @operatingtag/dsh-unclesam v1.3.5
266
266
  Autor Operating Tag
267
267
  Licencja CC BY-NC-ND 4.0 — wolno używać, nie wolno zmieniać, udostępniać ani sprzedawać
268
268
  ```
package/README.pt.md CHANGED
@@ -261,7 +261,7 @@ O arquivo [LICENSE](LICENSE) é o que vale, e ele abre com o mesmo resumo em ing
261
261
  O Uncle Sam é um plugin independente. Não afiliado ao DeepSeek, nem endossado nem apoiado por ele.
262
262
 
263
263
  ```
264
- @operatingtag/dsh-unclesam v1.3.3
264
+ @operatingtag/dsh-unclesam v1.3.5
265
265
  Autor Operating Tag
266
266
  Licença CC BY-NC-ND 4.0 — livre para usar, não para alterar, compartilhar ou vender
267
267
  ```
package/README.ro.md CHANGED
@@ -258,7 +258,7 @@ Fișierul [LICENSE](LICENSE) este cel care guvernează și începe cu același r
258
258
  Uncle Sam este un plugin independent. Neafiliat cu DeepSeek, neaprobat și nesprijinit de ei.
259
259
 
260
260
  ```
261
- @operatingtag/dsh-unclesam v1.3.3
261
+ @operatingtag/dsh-unclesam v1.3.5
262
262
  Autor Operating Tag
263
263
  Licență CC BY-NC-ND 4.0 — liber de folosit, nu de modificat, partajat sau vândut
264
264
  ```
package/README.ru.md CHANGED
@@ -265,7 +265,7 @@ International. Copyright (c) 2026 Darren T. Weldon, опубликовано п
265
265
  Uncle Sam — независимый плагин. Не связан с DeepSeek, не одобрен и не поддерживается им.
266
266
 
267
267
  ```
268
- @operatingtag/dsh-unclesam v1.3.3
268
+ @operatingtag/dsh-unclesam v1.3.5
269
269
  Автор Operating Tag
270
270
  Лицензия CC BY-NC-ND 4.0 — бесплатно для использования, не для изменения, передачи или продажи
271
271
  ```
package/README.si.md CHANGED
@@ -255,7 +255,7 @@ Copyright (c) 2026 Darren T. Weldon, Operating Tag නාමයෙන් ප්
255
255
  Uncle Sam ස්වාධීන ප්ලගීනයකි. DeepSeek සමඟ සම්බන්ධ නැත, ඔවුන් අනුමත හෝ අනුග්‍රහය නොදක්වයි.
256
256
 
257
257
  ```
258
- @operatingtag/dsh-unclesam v1.3.3
258
+ @operatingtag/dsh-unclesam v1.3.5
259
259
  කතෘ Operating Tag
260
260
  බලපත්‍රය CC BY-NC-ND 4.0 — භාවිතයට නොමිලේ, වෙනස් කිරීමට, බෙදා ගැනීමට හෝ විකිණීමට නොවේ
261
261
  ```
package/README.sk.md CHANGED
@@ -257,7 +257,7 @@ Záväzný je súbor [LICENSE](LICENSE) a začína rovnakým zhrnutím v jednodu
257
257
  Uncle Sam je nezávislý doplnok. Nie je spojený s DeepSeek, nie je nimi schválený ani podporovaný.
258
258
 
259
259
  ```
260
- @operatingtag/dsh-unclesam v1.3.3
260
+ @operatingtag/dsh-unclesam v1.3.5
261
261
  Autor Operating Tag
262
262
  Licencia CC BY-NC-ND 4.0 — zadarmo na používanie, nie na zmenu, zdieľanie alebo predaj
263
263
  ```
package/README.sl.md CHANGED
@@ -258,7 +258,7 @@ Zavezujoča je datoteka [LICENSE](LICENSE) in se začne z istim povzetkom v prep
258
258
  Uncle Sam je neodvisen vtičnik. Ni povezan z DeepSeek, ni odobren ali podprt z njihove strani.
259
259
 
260
260
  ```
261
- @operatingtag/dsh-unclesam v1.3.3
261
+ @operatingtag/dsh-unclesam v1.3.5
262
262
  Avtor Operating Tag
263
263
  Licenca CC BY-NC-ND 4.0 — brezplačno za uporabo, ne za spreminjanje, deljenje ali prodajo
264
264
  ```
package/README.sr.md CHANGED
@@ -258,7 +258,7 @@ Copyright (c) 2026 Darren T. Weldon, објављено под именом Oper
258
258
  Uncle Sam је независни додатак. Није повезан са DeepSeek, није одобрен ни подржан од њих.
259
259
 
260
260
  ```
261
- @operatingtag/dsh-unclesam v1.3.3
261
+ @operatingtag/dsh-unclesam v1.3.5
262
262
  Аутор Operating Tag
263
263
  Лиценца CC BY-NC-ND 4.0 — бесплатно за употребу, не за измену, дељење или продају
264
264
  ```
package/README.sv.md CHANGED
@@ -259,7 +259,7 @@ Filen [LICENSE](LICENSE) är den som gäller, och den börjar med samma sammanfa
259
259
  Uncle Sam är ett fristående insticksprogram. Inte knutet till DeepSeek, inte godkänt eller stött av dem.
260
260
 
261
261
  ```
262
- @operatingtag/dsh-unclesam v1.3.3
262
+ @operatingtag/dsh-unclesam v1.3.5
263
263
  Författare Operating Tag
264
264
  Licens CC BY-NC-ND 4.0 — fritt att använda, inte att ändra, dela eller sälja
265
265
  ```
package/README.sw.md CHANGED
@@ -259,7 +259,7 @@ Faili [LICENSE](LICENSE) ndiyo inayotumika, na inaanza na muhtasari huo huo kwa
259
259
  Uncle Sam ni programu-jalizi huru. Haijahusishwa na DeepSeek, haijaidhinishwa wala kuungwa mkono nao.
260
260
 
261
261
  ```
262
- @operatingtag/dsh-unclesam v1.3.3
262
+ @operatingtag/dsh-unclesam v1.3.5
263
263
  Mwandishi Operating Tag
264
264
  Leseni CC BY-NC-ND 4.0 — huru kutumika, si kubadilisha, kushiriki au kuuza
265
265
  ```
package/README.ta.md CHANGED
@@ -262,7 +262,7 @@ Tag ஐக் கேளுங்கள் — தெளிவான பதில
262
262
  Uncle Sam ஒரு சுயாதீனச் செருகல். DeepSeek உடன் தொடர்பில்லை, அவர்களின் ஒப்புதல் அல்லது ஆதரவு இல்லை.
263
263
 
264
264
  ```
265
- @operatingtag/dsh-unclesam v1.3.3
265
+ @operatingtag/dsh-unclesam v1.3.5
266
266
  ஆசிரியர் Operating Tag
267
267
  உரிமம் CC BY-NC-ND 4.0 — பயன்படுத்த இலவசம், மாற்ற, பகிர அல்லது விற்க அல்ல
268
268
  ```
package/README.te.md CHANGED
@@ -256,7 +256,7 @@ Tag ను అడగండి — స్పష్టమైన సమాధాన
256
256
  Uncle Sam స్వతంత్ర ప్లగిన్. DeepSeek తో సంబంధం లేదు, వారి ఆమోదం లేదా మద్దతు లేదు.
257
257
 
258
258
  ```
259
- @operatingtag/dsh-unclesam v1.3.3
259
+ @operatingtag/dsh-unclesam v1.3.5
260
260
  రచయిత Operating Tag
261
261
  లైసెన్స్ CC BY-NC-ND 4.0 — వాడటానికి ఉచితం, మార్చడానికి, పంచుకోవడానికి లేదా అమ్మడానికి కాదు
262
262
  ```
package/README.th.md CHANGED
@@ -252,7 +252,7 @@ Operating Tag แล้วคุณจะได้คำตอบที่ชั
252
252
  Uncle Sam เป็นปลั๊กอินอิสระ ไม่เกี่ยวข้องกับ DeepSeek ไม่ได้รับการรับรองหรือสนับสนุนจากพวกเขา
253
253
 
254
254
  ```
255
- @operatingtag/dsh-unclesam v1.3.3
255
+ @operatingtag/dsh-unclesam v1.3.5
256
256
  ผู้เขียน Operating Tag
257
257
  สัญญาอนุญาต CC BY-NC-ND 4.0 — ใช้ฟรี ไม่ใช่เพื่อดัดแปลง แบ่งปัน หรือขาย
258
258
  ```
package/README.tl.md CHANGED
@@ -265,7 +265,7 @@ Ang file na [LICENSE](LICENSE) ang namamahala, at nagsisimula ito sa parehong bu
265
265
  Ang Uncle Sam ay isang malayang plugin. Hindi kaanib sa DeepSeek, hindi inendorso o sinusuportahan nila.
266
266
 
267
267
  ```
268
- @operatingtag/dsh-unclesam v1.3.3
268
+ @operatingtag/dsh-unclesam v1.3.5
269
269
  May-akda Operating Tag
270
270
  Lisensya CC BY-NC-ND 4.0 — libre gamitin, hindi para baguhin, ibahagi o ibenta
271
271
  ```
package/README.tr.md CHANGED
@@ -261,7 +261,7 @@ Uncle Sam bağımsız bir eklentidir. DeepSeek ile bağlantılı değildir, onla
261
261
  desteklenmemiştir.
262
262
 
263
263
  ```
264
- @operatingtag/dsh-unclesam v1.3.3
264
+ @operatingtag/dsh-unclesam v1.3.5
265
265
  Yazar Operating Tag
266
266
  Lisans CC BY-NC-ND 4.0 — kullanması serbest, değiştirmesi, paylaşması veya satması değil
267
267
  ```
package/README.uk.md CHANGED
@@ -261,7 +261,7 @@ International. Copyright (c) 2026 Darren T. Weldon, опубліковано п
261
261
  Uncle Sam — незалежний плагін. Не пов'язаний із DeepSeek, не схвалений і не підтриманий ними.
262
262
 
263
263
  ```
264
- @operatingtag/dsh-unclesam v1.3.3
264
+ @operatingtag/dsh-unclesam v1.3.5
265
265
  Автор Operating Tag
266
266
  Ліцензія CC BY-NC-ND 4.0 — вільно використовувати, не змінювати, передавати чи продавати
267
267
  ```
package/README.ur.md CHANGED
@@ -257,7 +257,7 @@ Copyright (c) 2026 Darren T. Weldon، Operating Tag نام سے شائع شدہ
257
257
  Uncle Sam ایک خودمختار پلگ ان ہے۔ DeepSeek سے وابستہ نہیں، ان کی توثیق یا معاونت حاصل نہیں۔
258
258
 
259
259
  ```
260
- @operatingtag/dsh-unclesam v1.3.3
260
+ @operatingtag/dsh-unclesam v1.3.5
261
261
  مصنف Operating Tag
262
262
  لائسنس CC BY-NC-ND 4.0 — استعمال کے لیے مفت، تبدیلی، اشتراک یا فروخت کے لیے نہیں
263
263
  ```
package/README.uz.md CHANGED
@@ -258,7 +258,7 @@ Tag dan so'rang — aniq javob va aniq narx olasiz.
258
258
  Uncle Sam — mustaqil plagin. DeepSeek bilan bog'liq emas, ular tomonidan ma'qullanmagan yoki qo'llab-quvvatlanmagan.
259
259
 
260
260
  ```
261
- @operatingtag/dsh-unclesam v1.3.3
261
+ @operatingtag/dsh-unclesam v1.3.5
262
262
  Muallif Operating Tag
263
263
  Litsenziya CC BY-NC-ND 4.0 — foydalanish uchun bepul, o'zgartirish, ulashish yoki sotish uchun emas
264
264
  ```
package/README.vi.md CHANGED
@@ -259,7 +259,7 @@ Tệp [LICENSE](LICENSE) là văn bản có hiệu lực, và nó mở đầu b
259
259
  Uncle Sam là một plugin độc lập. Không liên kết với DeepSeek, không được họ chứng thực hay hỗ trợ.
260
260
 
261
261
  ```
262
- @operatingtag/dsh-unclesam v1.3.3
262
+ @operatingtag/dsh-unclesam v1.3.5
263
263
  Tác giả Operating Tag
264
264
  Giấy phép CC BY-NC-ND 4.0 — miễn phí để dùng, không được sửa, chia sẻ hay bán
265
265
  ```
package/README.zh.md CHANGED
@@ -244,7 +244,7 @@ Copyright (c) 2026 Darren T. Weldon,以 Operating Tag 名义发布。
244
244
  Uncle Sam 是一个独立插件。与 DeepSeek 无关联,未获其背书,也未获其支持。
245
245
 
246
246
  ```
247
- @operatingtag/dsh-unclesam v1.3.3
247
+ @operatingtag/dsh-unclesam v1.3.5
248
248
  作者 Operating Tag
249
249
  许可 CC BY-NC-ND 4.0 —— 可免费使用,不可修改、共享或出售
250
250
  ```
package/index.js CHANGED
@@ -27,8 +27,8 @@
27
27
  * prose instead of a call, and the harness sees no tool use that turn.
28
28
  */
29
29
 
30
- import { ChatSession, WebChatError } from './lib/engine.mjs';
31
- import { buildPrompt, messageText, parseToolCall, stripToolCall, streamSafeCut } from './lib/prompt.mjs';
30
+ import { writeFileSync } from 'node:fs';
31
+ import { ChatSession, WebChatError } from './lib/engine.mjs';import { buildPrompt, looksDegenerate, messageText, parseToolCall, stripToolCall, streamSafeCut } from './lib/prompt.mjs';
32
32
  import { SavingsMeter, estimateTokens as estimateTokensForSavings } from './lib/savings.mjs';
33
33
  import { Locales } from './lib/i18n.mjs';
34
34
  import { Prefs, prefsPath, profileDir } from './lib/prefs.mjs';
@@ -457,6 +457,16 @@ export function apply(ctx, config = {}) {
457
457
  }
458
458
 
459
459
  const wantTools = Array.isArray(options.tools) && options.tools.length > 0;
460
+ /*
461
+ * The names this turn offered, exactly as the page was told them.
462
+ *
463
+ * `name({...})` is only a call when `name` is one of these - that is what makes the form unambiguous.
464
+ * Without the list, prose like `see figure(2)` would look like a call.
465
+ */
466
+ const toolNames = wantTools ? options.tools.map((t) => t?.name).filter(Boolean) : [];
467
+
468
+ /** Set when the reply collapsed into repetition; the turn then ends with an error, not garbage. */
469
+ let stoppedAsLoop = false;
460
470
  const switches = switchesFor(model, settings);
461
471
  // A search answer is delivered as two blocks: what was searched and where it looked, then the
462
472
  // answer. That means the text cannot stream while search is on - the sources are only readable
@@ -556,11 +566,44 @@ export function apply(ctx, config = {}) {
556
566
  if (chunk.done) {
557
567
  assembled = chunk.text;
558
568
  if (chunk.sources) sources = chunk.sources;
569
+ /*
570
+ * Optional raw capture, for tests that need to see the reply AS THE PAGE WROTE IT.
571
+ *
572
+ * A recovered call proves the parser worked, not which shape the model chose - and the shape is
573
+ * the thing being changed when the call format changes. Writing the raw reply to a path the test
574
+ * names turns "the call parsed" into "the model emitted this, character for character".
575
+ *
576
+ * Off unless UNCLE_SAM_RAW_REPLY names a file, so nothing is written during ordinary use.
577
+ */
578
+ if (process.env.UNCLE_SAM_RAW_REPLY) {
579
+ try {
580
+ writeFileSync(process.env.UNCLE_SAM_RAW_REPLY, chunk.text ?? '', 'utf8');
581
+ } catch {
582
+ /* a diagnostic must never break the turn */
583
+ }
584
+ }
559
585
  continue;
560
586
  }
561
587
  if (!chunk.text) continue;
562
588
  assembled += chunk.text;
563
589
 
590
+ /*
591
+ * Stop a reply that has come apart into a loop.
592
+ *
593
+ * MEASURED, live: a web turn emitted the same malformed `<tool …>` block hundreds of times and the
594
+ * adapter streamed every copy, so the person watched a page write one line for minutes and
595
+ * reasonably concluded the plugin was broken. Nothing was wrong with the page — the model had
596
+ * invented a call format, the call never ran, and nothing told it so.
597
+ *
598
+ * The loop is now cut at the source: the turn ends with a plain error that says what happened,
599
+ * instead of displaying the wreckage. Only checked when the new text could contribute, so ordinary
600
+ * turns pay nothing for it.
601
+ */
602
+ if (/<tool/i.test(chunk.text) && looksDegenerate(assembled)) {
603
+ stoppedAsLoop = true;
604
+ break;
605
+ }
606
+
564
607
  if (wantSearch) {
565
608
  // held for ordering; nothing is emitted until the sources are known
566
609
  continue;
@@ -573,7 +616,7 @@ export function apply(ctx, config = {}) {
573
616
  * grow into {"tool":...}. Ordinary prose passes straight through, braces included, so the stream
574
617
  * stays live; it simply stops one brace early when a call might be starting.
575
618
  */
576
- const safe = streamSafeCut(assembled);
619
+ const safe = streamSafeCut(assembled, toolNames);
577
620
  if (safe > shown) {
578
621
  const out = assembled.slice(shown, safe);
579
622
  shown = safe;
@@ -589,6 +632,29 @@ export function apply(ctx, config = {}) {
589
632
 
590
633
  // Search answers are emitted as two visible blocks so the transcript shows BOTH what was asked
591
634
  // of the web and what came back, before the answer that used it.
635
+ /*
636
+ * A reply that collapsed into repetition ends here, with an error that says so.
637
+ *
638
+ * The alternative - what used to happen - is that the loop is streamed to the transcript in full,
639
+ * which reads as the plugin having broken when in fact the model is stuck. Naming it is the whole
640
+ * point: the person can see that the page looped, not that their harness failed.
641
+ */
642
+ if (stoppedAsLoop) {
643
+ yield {
644
+ type: 'finish',
645
+ reason: {
646
+ kind: 'error',
647
+ failure: new WebChatError(
648
+ 'The web reply collapsed into repetition - the same block written over and over - so the ' +
649
+ 'turn was stopped. Nothing was lost: send the message again, or switch to an API model ' +
650
+ 'for this one.',
651
+ 'web_loop',
652
+ ),
653
+ },
654
+ };
655
+ return;
656
+ }
657
+
592
658
  if (wantSearch) {
593
659
  /*
594
660
  * The query is the person's newest message - and ONLY the person's.
@@ -634,7 +700,7 @@ export function apply(ctx, config = {}) {
634
700
  yield { type: 'text-delta', index: 0, text: summaryWithLabel };
635
701
  yield { type: 'block-end', index: 0, block: { type: 'text', text: summaryWithLabel } };
636
702
 
637
- const answerText = stripToolCall(assembled);
703
+ const answerText = stripToolCall(assembled, toolNames);
638
704
  yield { type: 'block-start', index: 1, blockType: 'text' };
639
705
  if (answerText) yield { type: 'text-delta', index: 1, text: answerText };
640
706
  yield { type: 'block-end', index: 1, block: { type: 'text', text: answerText } };
@@ -645,7 +711,7 @@ export function apply(ctx, config = {}) {
645
711
  * block is emitted after the answer, exactly as the plain path does.
646
712
  */
647
713
  if (wantTools) {
648
- const call = parseToolCall(assembled);
714
+ const call = parseToolCall(assembled, toolNames);
649
715
  if (call) {
650
716
  const callId = `webcall_${Date.now().toString(36)}`;
651
717
  yield { type: 'block-start', index: 2, blockType: 'tool-call' };
@@ -689,7 +755,7 @@ export function apply(ctx, config = {}) {
689
755
  }
690
756
 
691
757
  if (wantTools) {
692
- const call = parseToolCall(assembled);
758
+ const call = parseToolCall(assembled, toolNames);
693
759
  if (call) {
694
760
  // A tool call was made. Any prose the model wrapped around it belongs in the text block;
695
761
  // the JSON itself must not reach the transcript.
@@ -732,7 +798,7 @@ export function apply(ctx, config = {}) {
732
798
  emitted += WEB_LABEL;
733
799
  yield { type: 'text-delta', index: 0, text: WEB_LABEL };
734
800
  }
735
- const tail = stripToolCall(assembled.slice(shown));
801
+ const tail = stripToolCall(assembled.slice(shown), toolNames);
736
802
  if (tail) {
737
803
  emitted += tail;
738
804
  yield { type: 'text-delta', index: 0, text: tail };
@@ -774,7 +840,7 @@ export function apply(ctx, config = {}) {
774
840
  * scanner stopped one brace short of any. Scrubbing the tail and appending it keeps the committed
775
841
  * block equal to the deltas that were yielded, which the harness checks.
776
842
  */
777
- const held = stripToolCall(assembled.slice(shown));
843
+ const held = stripToolCall(assembled.slice(shown), toolNames);
778
844
  if (held) {
779
845
  if (!emitted) {
780
846
  emitted += WEB_LABEL;
package/lib/prompt.mjs CHANGED
@@ -161,7 +161,7 @@ export function toolPreamble(tools) {
161
161
  * emulation exists to work around.
162
162
  */
163
163
  const HEAD = 'Tools: ';
164
- const TAIL = ' · to call one reply only with {"tool":"name","arguments":{}}';
164
+ const TAIL = ' · to call one reply only with name({"arg":"value"})';
165
165
 
166
166
  /**
167
167
  * Room for the list.
@@ -393,10 +393,214 @@ function finish(text, expectsToolCall, continuing) {
393
393
  * that merely CONTAINS the word tool but no valid object is treated as prose, because treating it as a
394
394
  * malformed call would throw away a perfectly good answer.
395
395
  */
396
- export function parseToolCall(reply) {
396
+ /**
397
+ * Detect a reply that has come apart into repetition.
398
+ *
399
+ * MEASURED, live: asked to glob a workspace it had misread, the web model emitted the same malformed block
400
+ * — `<tool name="glob" string="true">{…}</tool>` — over and over, filling the transcript with hundreds of
401
+ * copies. The adapter streamed every one of them, because nothing was looking for that shape of failure.
402
+ *
403
+ * A format mismatch is recoverable: the call does not run and the model is told. A LOOP is not, because the
404
+ * person sits watching a page write the same line for minutes and concludes the plugin is broken.
405
+ *
406
+ * WHAT COUNTS AS DEGENERATE
407
+ * Four identical substantial lines, or three `<tool` openings. Both are far beyond anything a real answer
408
+ * does; an answer that repeats a sentence twice is fine and stays fine.
409
+ */
410
+ export function looksDegenerate(reply) {
411
+ const s = String(reply ?? '');
412
+
413
+ /*
414
+ * Tag stacking first, and without a length floor.
415
+ *
416
+ * The floor exists so a short answer cannot be judged on a handful of lines. It must not gate this check:
417
+ * three `<tool` openings is three calls attempted in one reply, which is wrong however short the text is.
418
+ * Measured: the first version returned false for a three-tag reply because it was under 200 characters -
419
+ * the guard missed exactly the shape it was written for.
420
+ */
421
+ const tags = s.match(/<tool\b/gi);
422
+ if (tags && tags.length >= 3) return true;
423
+
424
+ /*
425
+ * No length floor on the line check either.
426
+ *
427
+ * It was there to stop a short answer being judged on a few lines, and it was wrong twice: a five-line
428
+ * loop sat under the floor and went undetected, which is precisely the case the guard exists for. Four
429
+ * identical lines of twenty characters or more is already far outside ordinary prose on its own.
430
+ */
431
+ const lines = s.split('\n').map((l) => l.trim()).filter((l) => l.length >= 20);
432
+ const counts = new Map();
433
+ for (const line of lines) counts.set(line, (counts.get(line) || 0) + 1);
434
+ for (const n of counts.values()) if (n >= 4) return true;
435
+
436
+ return false;
437
+ }
438
+
439
+ /**
440
+ * Find `name({...})` in a reply, where `name` is one of the tools that was offered.
441
+ *
442
+ * WHY THIS FORM IS PREFERRED OVER JSON
443
+ * MEASURED: a reply that is nothing but `{"tool":…}` cannot be shown while it is written, so the harness
444
+ * sits empty for the whole generation and looks stuck. And a brace is the commonest punctuation in
445
+ * technical prose, so `{` is a poor delimiter - it both blocks streaming and invites false positives.
446
+ *
447
+ * `read({"path":"x"})` needs no instruction, because it is the shape models already produce, and it is
448
+ * unambiguous once it is anchored on a tool name the model was actually given. `read(` when `read` is in
449
+ * the offered list is a far stronger signal than `{`.
450
+ *
451
+ * WHAT IT DOES NOT DO
452
+ * It does not JSON.parse inside strings or respect nesting by regex - it walks the characters, because an
453
+ * argument may contain a parenthesis or a brace inside a string and a regex would stop there.
454
+ */
455
+ function findCallForm(text, names) {
456
+ /*
457
+ * `hasList` distinguishes "no list supplied" from "an empty list supplied", and that distinction is the
458
+ * whole of two bugs.
459
+ *
460
+ * An EMPTY list means "no tools are offered", so nothing can be a call - and a word followed by parentheses
461
+ * in ordinary prose, like `log(2)` or `figure(2)`, must be left alone. The old code collapsed both cases
462
+ * into `null` ("accept anything"), so a turn with no tools deleted such prose from the answer.
463
+ */
464
+ const hasList = Array.isArray(names);
465
+
466
+ for (let i = 0; i < text.length; i += 1) {
467
+ if (!/[A-Za-z_]/.test(text[i])) continue;
468
+
469
+ // Read the identifier at this position.
470
+ let j = i;
471
+ let word = '';
472
+ while (j < text.length && /[A-Za-z0-9_.-]/.test(text[j])) {
473
+ word += text[j];
474
+ j += 1;
475
+ }
476
+ if (!word) continue;
477
+ if (hasList && !names.includes(word)) {
478
+ i = j - 1;
479
+ continue;
480
+ }
481
+
482
+ // Optional whitespace, then a parenthesis.
483
+ let k = j;
484
+ while (k < text.length && (text[k] === ' ' || text[k] === '\t' || text[k] === '\n')) k += 1;
485
+ if (text[k] !== '(') {
486
+ i = j - 1;
487
+ continue;
488
+ }
489
+
490
+ // Walk to the matching parenthesis, respecting strings and escapes.
491
+ let depth = 0;
492
+ let inString = false;
493
+ let escaped = false;
494
+ let end = -1;
495
+ for (let p = k; p < text.length; p += 1) {
496
+ const ch = text[p];
497
+ if (inString) {
498
+ if (escaped) escaped = false;
499
+ else if (ch === '\\') escaped = true;
500
+ else if (ch === '"') inString = false;
501
+ continue;
502
+ }
503
+ if (ch === '"') inString = true;
504
+ else if (ch === '(') depth += 1;
505
+ else if (ch === ')') {
506
+ depth -= 1;
507
+ if (depth === 0) {
508
+ end = p;
509
+ break;
510
+ }
511
+ }
512
+ }
513
+ if (end === -1) return null; // still being written - no complete call yet
514
+
515
+ const inside = text.slice(k + 1, end).trim();
516
+ let args = {};
517
+ if (inside) {
518
+ try {
519
+ args = JSON.parse(inside);
520
+ } catch {
521
+ /*
522
+ * Skip this candidate and keep scanning, instead of aborting.
523
+ *
524
+ * MEASURED: `read(bad) and pwsh({"command":"dir"})` returned nothing, because the first malformed
525
+ * candidate returned null for the whole reply and the valid `pwsh(...)` behind it was never reached.
526
+ * A bad call earlier in a reply must not hide a good one later. Resume just past this closing paren.
527
+ */
528
+ i = end;
529
+ continue;
530
+ }
531
+ }
532
+
533
+ return {
534
+ name: word,
535
+ arguments: JSON.stringify(args),
536
+ start: i,
537
+ end: end + 1,
538
+ };
539
+ }
540
+
541
+ return null;
542
+ }
543
+
544
+ /**
545
+ * Recover a tool call from a plain-text reply, if the model made one.
546
+ *
547
+ * TWO SHAPES, IN ORDER
548
+ * 1 `name({...})` — preferred: no instruction needed, and it streams
549
+ * 2 `{"tool":"name","arguments":{…}}` — kept because models emit it unprompted, and because a fallback
550
+ * costs nothing when the primary misses
551
+ *
552
+ * Fenced blocks are searched last-first because a model that explains itself tends to put the final answer at
553
+ * the end. A reply that merely CONTAINS the word tool but no valid object is treated as prose, because
554
+ * treating it as a malformed call would throw away a perfectly good answer.
555
+ */
556
+ export function parseToolCall(reply, names) {
397
557
  const text = String(reply ?? '').trim();
398
558
  if (!text) return null;
399
559
 
560
+ // Shape 1: name({...}).
561
+ const direct = findCallForm(text, names);
562
+ if (direct) return { name: direct.name, arguments: direct.arguments };
563
+
564
+ /*
565
+ * Shape 2: the tags a model reaches for when it invents its own protocol.
566
+ *
567
+ * MEASURED, live: asked to run a glob, the web model answered
568
+ * `<tool name="glob" string="true">{"pattern":"…"}</tool>` — a shape nothing here had ever taught it, and
569
+ * one the parser did not accept. The call never ran, nothing said so, and the model repeated the block
570
+ * until the turn was full of copies. Accepting the shape ends the loop at its cause instead of only
571
+ * masking the symptom.
572
+ */
573
+ const xmlTag = /<tool\s+name\s*=\s*"([^"]+)"[^>]*>([\s\S]*?)<\/tool>/i.exec(text);
574
+ if (xmlTag) {
575
+ const name = xmlTag[1].trim();
576
+ if (!names?.length || names.includes(name)) {
577
+ let args = {};
578
+ const body = xmlTag[2].trim();
579
+ if (body) {
580
+ try {
581
+ args = JSON.parse(body);
582
+ } catch {
583
+ args = {};
584
+ }
585
+ }
586
+ return { name, arguments: JSON.stringify(args) };
587
+ }
588
+ }
589
+
590
+ const xmlBlock = /<tool_call>\s*([\s\S]*?)\s*<\/tool_call>/i.exec(text);
591
+ if (xmlBlock) {
592
+ try {
593
+ const parsed = JSON.parse(xmlBlock[1]);
594
+ const name = parsed?.name ?? parsed?.tool;
595
+ if (typeof name === 'string' && name && (!names?.length || names.includes(name))) {
596
+ const raw = parsed.arguments ?? {};
597
+ return { name, arguments: typeof raw === 'string' ? raw : JSON.stringify(raw) };
598
+ }
599
+ } catch {
600
+ /* fall through to the JSON scan below */
601
+ }
602
+ }
603
+
400
604
  const candidates = [];
401
605
  // fenced blocks first
402
606
  const fence = /```(?:json)?\s*([\s\S]*?)```/gi;
@@ -464,9 +668,74 @@ export function parseToolCall(reply) {
464
668
  * It does not decide whether a call exists - `parseToolCall` does that at the end. It only answers
465
669
  * "how far can I safely show right now", which is the question a live stream needs.
466
670
  */
467
- export function streamSafeCut(text) {
671
+ export function streamSafeCut(text, names) {
468
672
  const s = String(text ?? '');
469
673
 
674
+ /**
675
+ * Hold from a known tool name that has opened, or is about to open, a parenthesis.
676
+ *
677
+ * This is the call-syntax half of the same question. `read(` is a call; `read` alone might be the first
678
+ * letters of `readable`, so it is not held - but a trailing fragment that could still become a known name
679
+ * (`repl` at the very end of the text) IS held, because the next characters may complete it.
680
+ */
681
+ const wanted = Array.isArray(names) && names.length ? names : null;
682
+ if (wanted) {
683
+ for (let i = 0; i < s.length; i += 1) {
684
+ if (!/[A-Za-z_]/.test(s[i])) continue;
685
+ let j = i;
686
+ let word = '';
687
+ while (j < s.length && /[A-Za-z0-9_.-]/.test(s[j])) {
688
+ word += s[j];
689
+ j += 1;
690
+ }
691
+ if (wanted.includes(word)) {
692
+ let k = j;
693
+ while (k < s.length && /\s/.test(s[k])) k += 1;
694
+ // A complete `name (` is a call being written; nothing after it may stream.
695
+ if (k >= s.length || s[k] === '(') return i;
696
+ }
697
+ i = j - 1;
698
+ }
699
+
700
+ /**
701
+ * A name that is still arriving.
702
+ *
703
+ * The reply may end mid-identifier — `… now call replace_not` — and one more character would make it a
704
+ * call. Holding the fragment costs a few visible characters and stops a call's opening words from
705
+ * appearing in the transcript before the call does.
706
+ *
707
+ * THREE CHARACTERS MINIMUM, measured. The first version held any trailing fragment that prefixed a known
708
+ * name, so an ordinary sentence ending in `r` stalled one character back - `r` prefixes `read` and
709
+ * `replace_note`. On screen that is a reply that appears to stop mid-word and then finishes a keystroke
710
+ * later. A three-letter fragment is worth holding; one letter is not.
711
+ */
712
+ const tailStart = Math.max(0, s.length - 40);
713
+ for (let i = tailStart; i < s.length; i += 1) {
714
+ const fragment = s.slice(i);
715
+ if (fragment.length < 3) break;
716
+ if (!/^[A-Za-z_][A-Za-z0-9_.-]*$/.test(fragment)) continue;
717
+ if (wanted.some((n) => n.startsWith(fragment))) return i;
718
+ }
719
+ }
720
+
721
+ /**
722
+ * Hold at a tag that may be turning into `<tool …>` or `<tool_call>`.
723
+ *
724
+ * The same reasoning as the name fragment: a reply that is writing one of these must not show its opening
725
+ * characters first, or the transcript gains a partial tag that no parser will ever match.
726
+ */
727
+ for (let i = s.indexOf('<'); i !== -1; i = s.indexOf('<', i + 1)) {
728
+ const frag = s.slice(i + 1);
729
+ if (frag.length === 0) return i;
730
+ const letters = /^[A-Za-z_]*/.exec(frag)[0];
731
+ if (letters.length < frag.length) {
732
+ // The tag name is complete; only an exact match to one we hold is interesting.
733
+ if (letters === 'tool' || letters === 'tool_call') return i;
734
+ continue;
735
+ }
736
+ if ('tool'.startsWith(letters) || 'tool_call'.startsWith(letters)) return i;
737
+ }
738
+
470
739
  /**
471
740
  * Could the brace at `i` still turn out to be the start of a tool object?
472
741
  *
@@ -497,9 +766,25 @@ export function streamSafeCut(text) {
497
766
  return s.length;
498
767
  }
499
768
 
500
- export function stripToolCall(reply) {
769
+ export function stripToolCall(reply, names) {
501
770
  let text = String(reply ?? '');
502
771
 
772
+ /**
773
+ * Shape 1: `name({...})`, wherever it appears, prose either side.
774
+ *
775
+ * Removed by the same character walk that found it, so a call whose arguments contain a parenthesis or a
776
+ * brace inside a string is removed whole rather than up to the first `)`.
777
+ */
778
+ for (let guard = 0; guard < 20; guard += 1) {
779
+ const found = findCallForm(text, names);
780
+ if (!found) break;
781
+ text = text.slice(0, found.start) + ' ' + text.slice(found.end);
782
+ }
783
+
784
+ // The tagged shapes, before anything else, so their inner JSON is never treated as prose.
785
+ text = text.replace(/<tool\s+name\s*=\s*"[^"]+"[^>]*>[\s\S]*?<\/tool>/gi, ' ');
786
+ text = text.replace(/<tool_call>[\s\S]*?<\/tool_call>/gi, ' ');
787
+
503
788
  // Fenced calls first: the fence marks the whole block, so no brace counting is needed.
504
789
  text = text.replace(/```(?:json)?\s*\{\s*"tool"[\s\S]*?\}\s*```/gi, '');
505
790
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@operatingtag/dsh-unclesam",
3
- "version": "1.3.3",
3
+ "version": "1.3.5",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Uncle Sam — answers through chat.deepseek.com in a real browser tab instead of the metered API. Switch between the API and the web account from the composer; web replies are signed so you always know which route answered.",