@r1z4x/tezgah 1.2.0 → 1.4.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.
- package/CHANGELOG.md +44 -1
- package/MANIFEST +3 -0
- package/bin/tezgah-doctor +75 -18
- package/bin/tezgah-setup +6 -17
- package/bin/tezgah-taste +38 -32
- package/docs/architecture.md +4 -4
- package/docs/clarity-baseline.json +4 -4
- package/docs/contract.md +1 -1
- package/docs/environment.md +1 -0
- package/docs/evidence.md +67 -31
- package/docs/gate.md +21 -21
- package/docs/glossary.md +4 -4
- package/docs/hosts.md +8 -8
- package/docs/judge.md +10 -5
- package/docs/operations.md +53 -17
- package/docs/skills.md +1 -1
- package/docs/status-line.md +7 -7
- package/hooks/tezgah_attest.py +15 -22
- package/hooks/tezgah_context.py +52 -118
- package/hooks/tezgah_gate.py +26 -23
- package/hooks/tezgah_guard.py +9 -11
- package/hooks/tezgah_integrity.py +77 -178
- package/hooks/tezgah_judge.py +25 -43
- package/hooks/tezgah_lessons.py +18 -63
- package/hooks/tezgah_paths.py +18 -28
- package/hooks/tezgah_replay.py +31 -18
- package/hooks/tezgah_shapes.py +4 -4
- package/hooks/tezgah_skill_pick.py +6 -35
- package/hooks/tezgah_snapshot.py +25 -46
- package/hooks/tezgah_store.py +1268 -0
- package/hooks/tezgah_taste.py +59 -6
- package/hooks/tezgah_taste_ledger.py +52 -86
- package/hooks/tezgah_update.py +6 -26
- package/hosts/cursor/hook.py +19 -31
- package/hosts/opencode/plugins/tezgah.js +309 -108
- package/package.json +1 -1
package/hooks/tezgah_taste.py
CHANGED
|
@@ -6,8 +6,8 @@ Opt-in and off by default. A row is written only when the `taste-on` marker is
|
|
|
6
6
|
armed (`tezgah_paths.armed`), the repository carries no `.no-taste` mark, its
|
|
7
7
|
`.tezgah/` already exists and is the user's own (`workspace_from_repo`: a
|
|
8
8
|
workspace that came with the clone is data, and nothing of the user's is
|
|
9
|
-
written into it). The store is
|
|
10
|
-
|
|
9
|
+
written into it). The store is the `signals` table of
|
|
10
|
+
`<git root>/.tezgah/taste/taste.db` (`tezgah_store.taste`), owner-only.
|
|
11
11
|
|
|
12
12
|
Every public function is total: it never raises and makes no network call.
|
|
13
13
|
The capture functions append at most one row each. They run on the prompt and
|
|
@@ -15,15 +15,24 @@ PostToolUse hot paths, so the switch is read before anything else is.
|
|
|
15
15
|
|
|
16
16
|
Learning lives in `bin/tezgah-taste learn` and `tezgah_taste_ledger`; the one
|
|
17
17
|
thing fed back from here is `write_note`, the learnings in scope for a write.
|
|
18
|
+
`learn_later` starts that `learn` in the background at session start.
|
|
18
19
|
"""
|
|
19
20
|
import datetime
|
|
20
21
|
import json
|
|
21
22
|
import os
|
|
23
|
+
import re
|
|
24
|
+
import subprocess
|
|
25
|
+
import sys
|
|
26
|
+
import time
|
|
22
27
|
|
|
23
28
|
import tezgah_integrity as ti
|
|
29
|
+
import tezgah_paths
|
|
24
30
|
from tezgah_paths import _toplevel, armed, root_for, workspace_from_repo
|
|
25
31
|
|
|
26
32
|
ARM = "taste-on"
|
|
33
|
+
LEARN_EVERY = 3600 # seconds between two background `learn` starts per repo
|
|
34
|
+
BIN = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))),
|
|
35
|
+
"bin", "tezgah-taste")
|
|
27
36
|
MARK = ".no-taste"
|
|
28
37
|
VERSION = 1
|
|
29
38
|
PROMPT_MAX = 2000
|
|
@@ -77,9 +86,10 @@ def _write(root, kind, session_id, host, **fields):
|
|
|
77
86
|
.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
78
87
|
"kind": kind, "session": str(session_id or ""), "host": host or _host()}
|
|
79
88
|
row.update(fields)
|
|
80
|
-
#
|
|
81
|
-
|
|
82
|
-
|
|
89
|
+
# imported here, not at the top: the off path never loads sqlite3
|
|
90
|
+
import tezgah_store as store
|
|
91
|
+
with store.taste(os.path.join(root, ".tezgah", "taste")) as db:
|
|
92
|
+
store.append(db, "signals", row)
|
|
83
93
|
|
|
84
94
|
|
|
85
95
|
def _host():
|
|
@@ -203,6 +213,50 @@ def write_note(session_id, inp, cwd, tool=None):
|
|
|
203
213
|
return ""
|
|
204
214
|
|
|
205
215
|
|
|
216
|
+
def learn_later(root):
|
|
217
|
+
"""Start `tezgah-taste learn --repo <root> --no-fallback` detached when the
|
|
218
|
+
signals of `root` (the git root `enabled` returned) grew since the last
|
|
219
|
+
start and an hour has passed; the stamp (a `taste_learn` document of the
|
|
220
|
+
cache database) keeps the newest signal's append number. Only when
|
|
221
|
+
TypeSafe resolves: the automatic run never pays a
|
|
222
|
+
generative provider for a decision it may not apply. Not on a
|
|
223
|
+
machine without flock (Windows), where two background runs in two
|
|
224
|
+
repositories would race on the user ledger. ponytail: no Windows lock; add
|
|
225
|
+
one (msvcrt.locking) before enabling it there. Total; the stamp is written
|
|
226
|
+
before the spawn, so a failed start waits an hour."""
|
|
227
|
+
try:
|
|
228
|
+
if not root:
|
|
229
|
+
return
|
|
230
|
+
import tezgah_store as store
|
|
231
|
+
with store.taste(os.path.join(root, ".tezgah", "taste"), create=False) as db:
|
|
232
|
+
if db is None:
|
|
233
|
+
return
|
|
234
|
+
count = store.last(db, "signals")
|
|
235
|
+
base = os.path.join(tezgah_paths.cache_dir(), "taste-learn")
|
|
236
|
+
name = re.sub(r"[^A-Za-z0-9]+", "-", root).strip("-")
|
|
237
|
+
last = store.doc("taste_learn", name)
|
|
238
|
+
last = last if isinstance(last, dict) else {}
|
|
239
|
+
now = time.time()
|
|
240
|
+
if count <= last.get("rows", 0) or now - last.get("at", 0) < LEARN_EVERY:
|
|
241
|
+
return
|
|
242
|
+
# the costlier checks run only once a start is due: at most hourly
|
|
243
|
+
import tezgah_judge
|
|
244
|
+
import tezgah_taste_ledger
|
|
245
|
+
if tezgah_taste_ledger.fcntl is None or tezgah_paths.off("judge-off") \
|
|
246
|
+
or not tezgah_judge.named(("typesafe",)):
|
|
247
|
+
return
|
|
248
|
+
if not store.put_doc("taste_learn", name, {"rows": count, "at": now}):
|
|
249
|
+
return
|
|
250
|
+
os.makedirs(base, exist_ok=True)
|
|
251
|
+
with open(os.path.join(base, name + ".log"), "ab") as log:
|
|
252
|
+
subprocess.Popen([sys.executable, BIN, "learn", "--repo", root,
|
|
253
|
+
"--no-fallback"],
|
|
254
|
+
cwd=root, stdin=subprocess.DEVNULL, stdout=log,
|
|
255
|
+
stderr=log, start_new_session=True)
|
|
256
|
+
except Exception:
|
|
257
|
+
pass
|
|
258
|
+
|
|
259
|
+
|
|
206
260
|
def main(argv):
|
|
207
261
|
"""`tezgah_taste.py '<json>'`: `note_write`, then `write_note`, for a host
|
|
208
262
|
that cannot call them in process (opencode's plugin is JavaScript). The one
|
|
@@ -223,5 +277,4 @@ def main(argv):
|
|
|
223
277
|
|
|
224
278
|
|
|
225
279
|
if __name__ == "__main__":
|
|
226
|
-
import sys
|
|
227
280
|
sys.exit(main(sys.argv[1:]))
|
|
@@ -21,20 +21,20 @@ answers and keeps the ledger.
|
|
|
21
21
|
- Apply: a rule needs confidence >= `RULE_AT` and a calibration bound >=
|
|
22
22
|
`BOUND_AT` (Clopper-Pearson on an independently labelled sample); a hint needs
|
|
23
23
|
`HINT_AT`; a learning whose evidence paths are all gone from the tree is
|
|
24
|
-
suspended. The benefit gate's
|
|
24
|
+
suspended. The benefit gate's stopped state turns injection off.
|
|
25
25
|
|
|
26
|
-
Store: `<repo>/.tezgah/taste/
|
|
27
|
-
learnings
|
|
26
|
+
Store: `tezgah_store.taste` - `<repo>/.tezgah/taste/taste.db` for path,
|
|
27
|
+
language and repository learnings and every taste row, and
|
|
28
|
+
`~/.config/tezgah/taste/taste.db` for user learnings.
|
|
28
29
|
"""
|
|
29
30
|
import datetime
|
|
30
31
|
import errno
|
|
31
32
|
import fnmatch
|
|
32
|
-
import json
|
|
33
33
|
import math
|
|
34
34
|
import os
|
|
35
35
|
|
|
36
|
-
import
|
|
37
|
-
from tezgah_paths import CONFIG_DIR
|
|
36
|
+
import tezgah_store as store
|
|
37
|
+
from tezgah_paths import CONFIG_DIR
|
|
38
38
|
|
|
39
39
|
try:
|
|
40
40
|
import fcntl
|
|
@@ -66,35 +66,21 @@ def user_dir():
|
|
|
66
66
|
return os.path.join(CONFIG_DIR, "taste")
|
|
67
67
|
|
|
68
68
|
|
|
69
|
-
def _load(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
data = json.load(fh)
|
|
73
|
-
if isinstance(data, dict) and isinstance(data.get("learnings"), dict):
|
|
74
|
-
data.setdefault("meta", {})
|
|
75
|
-
return data
|
|
76
|
-
except (OSError, ValueError):
|
|
77
|
-
pass
|
|
78
|
-
return {"v": 1, "learnings": {}, "meta": {}}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
def _save(path, data):
|
|
82
|
-
os.makedirs(os.path.dirname(path), mode=0o700, exist_ok=True)
|
|
83
|
-
tmp = "%s.%d.tmp" % (path, os.getpid())
|
|
84
|
-
with open(tmp, "w", encoding="utf-8") as fh:
|
|
85
|
-
json.dump(data, fh, ensure_ascii=False, indent=1, sort_keys=True)
|
|
86
|
-
os.replace(tmp, path)
|
|
69
|
+
def _load(dirpath, user=False):
|
|
70
|
+
with store.taste(dirpath, user, create=False) as db:
|
|
71
|
+
return store.load_ledger(db) if db else {"v": 1, "learnings": {}, "meta": {}}
|
|
87
72
|
|
|
88
73
|
|
|
89
74
|
def load(root):
|
|
90
75
|
"""{"repo": ledger, "user": ledger}."""
|
|
91
|
-
return {"repo": _load(
|
|
92
|
-
"user": _load(os.path.join(user_dir(), "ledger.json"))}
|
|
76
|
+
return {"repo": _load(store_dir(root)), "user": _load(user_dir(), user=True)}
|
|
93
77
|
|
|
94
78
|
|
|
95
79
|
def save(root, ledgers):
|
|
96
|
-
|
|
97
|
-
|
|
80
|
+
with store.taste(store_dir(root)) as db:
|
|
81
|
+
store.save_ledger(db, ledgers["repo"])
|
|
82
|
+
with store.taste(user_dir(), user=True) as db:
|
|
83
|
+
store.save_ledger(db, ledgers["user"])
|
|
98
84
|
|
|
99
85
|
|
|
100
86
|
def every(ledgers):
|
|
@@ -142,34 +128,24 @@ def cp_lower(x, n, alpha=0.05):
|
|
|
142
128
|
return lo
|
|
143
129
|
|
|
144
130
|
|
|
145
|
-
def rows(
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
row = json.loads(line)
|
|
151
|
-
except ValueError:
|
|
152
|
-
continue
|
|
153
|
-
if isinstance(row, dict):
|
|
154
|
-
yield row
|
|
155
|
-
except OSError:
|
|
156
|
-
return
|
|
131
|
+
def rows(root, table, where="", args=()):
|
|
132
|
+
"""One taste table's rows in append order (`tezgah_store.TASTE_ROWS`); []
|
|
133
|
+
when the repository has no store yet."""
|
|
134
|
+
with store.taste(store_dir(root), create=False) as db:
|
|
135
|
+
return store.rows(db, table, where, args) if db else []
|
|
157
136
|
|
|
158
137
|
|
|
159
|
-
def append(
|
|
160
|
-
"""One row
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
fh.seek(max(0, os.path.getsize(path) - len(data)))
|
|
171
|
-
if fh.read() != data:
|
|
172
|
-
raise OSError("taste row not written to %s" % path)
|
|
138
|
+
def append(root, table, row, strict=False):
|
|
139
|
+
"""One row into a taste table. A hook drops a row it cannot write; `strict`
|
|
140
|
+
(the CLI's decision, defect and label rows) raises instead, so `learn` stops
|
|
141
|
+
before it saves a ledger whose decision was never recorded and would be
|
|
142
|
+
re-applied."""
|
|
143
|
+
try:
|
|
144
|
+
with store.taste(store_dir(root)) as db:
|
|
145
|
+
store.append(db, table, row)
|
|
146
|
+
except store.ERRORS:
|
|
147
|
+
if strict:
|
|
148
|
+
raise
|
|
173
149
|
|
|
174
150
|
|
|
175
151
|
class Busy(Exception):
|
|
@@ -220,8 +196,8 @@ def calibration(root):
|
|
|
220
196
|
bound on that precision. Only typed-provider decisions count; the labels are
|
|
221
197
|
a sample drawn independently of what was injected (`tezgah-taste label`)."""
|
|
222
198
|
x = n = 0
|
|
223
|
-
for row in rows(
|
|
224
|
-
if row.get("
|
|
199
|
+
for row in rows(root, "labels", "WHERE provider = ?", ("typesafe",)):
|
|
200
|
+
if row.get("decided") != "preference":
|
|
225
201
|
continue
|
|
226
202
|
n += 1
|
|
227
203
|
x += row.get("label") == "preference"
|
|
@@ -428,11 +404,8 @@ def suspended(learning, root):
|
|
|
428
404
|
|
|
429
405
|
def stopped(root):
|
|
430
406
|
"""True when the benefit gate stopped injection for this repository."""
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
return bool(json.load(fh).get("stopped"))
|
|
434
|
-
except (OSError, ValueError, AttributeError):
|
|
435
|
-
return False
|
|
407
|
+
with store.taste(store_dir(root), create=False) as db:
|
|
408
|
+
return bool(db) and store.gate_stopped(db)
|
|
436
409
|
|
|
437
410
|
|
|
438
411
|
def usable(root, day=None):
|
|
@@ -478,10 +451,10 @@ def block(root, day=None):
|
|
|
478
451
|
|
|
479
452
|
|
|
480
453
|
def record_injection(root, session_id, ids, day=None):
|
|
481
|
-
"""One `injected
|
|
454
|
+
"""One `injected` row: the meta loop counts a later correction in this
|
|
482
455
|
session against these ids, and the benefit gate splits its arms at the first."""
|
|
483
456
|
if ids:
|
|
484
|
-
append(
|
|
457
|
+
append(root, "injected",
|
|
485
458
|
{"session": str(session_id or ""), "day": now_day() if day is None else day,
|
|
486
459
|
"ids": list(ids)})
|
|
487
460
|
|
|
@@ -489,9 +462,8 @@ def record_injection(root, session_id, ids, day=None):
|
|
|
489
462
|
def injected(root, session_id):
|
|
490
463
|
"""The learning ids injected in one session."""
|
|
491
464
|
ids = set()
|
|
492
|
-
for row in rows(
|
|
493
|
-
|
|
494
|
-
ids.update(row.get("ids") or ())
|
|
465
|
+
for row in rows(root, "injected", "WHERE session = ?", (session_id,)):
|
|
466
|
+
ids.update(row.get("ids") or ())
|
|
495
467
|
return ids
|
|
496
468
|
|
|
497
469
|
|
|
@@ -502,24 +474,18 @@ def write_note(root, session_id, rel_path):
|
|
|
502
474
|
if not session_id or not rel_path or stopped(root):
|
|
503
475
|
return ""
|
|
504
476
|
kind = os.path.splitext(rel_path)[1] or os.path.basename(rel_path)
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
with open(seen_path, encoding="utf-8") as fh:
|
|
509
|
-
seen = set(json.load(fh))
|
|
510
|
-
except (OSError, ValueError, TypeError):
|
|
511
|
-
seen = set()
|
|
477
|
+
session_id = str(session_id)
|
|
478
|
+
with store.taste(store_dir(root), create=False) as db:
|
|
479
|
+
seen = store.seen(db, session_id) if db else set()
|
|
512
480
|
hits = [(mode, learning) for mode, learning in usable(root)
|
|
513
481
|
if learning["scope"] in ("path", "language") and learning["id"] not in seen
|
|
514
482
|
and fnmatch.fnmatch(rel_path, learning["where"])]
|
|
515
483
|
if not hits:
|
|
516
484
|
return ""
|
|
517
|
-
seen.update(m[1]["id"] for m in hits)
|
|
518
485
|
try:
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
except OSError:
|
|
486
|
+
with store.taste(store_dir(root)) as db:
|
|
487
|
+
store.mark_seen(db, session_id, [m[1]["id"] for m in hits])
|
|
488
|
+
except store.ERRORS:
|
|
523
489
|
return ""
|
|
524
490
|
record_injection(root, session_id, [m[1]["id"] for m in hits])
|
|
525
491
|
return ("Taste for %s files (narrower scope wins):\n" % kind
|
|
@@ -529,8 +495,8 @@ def write_note(root, session_id, rel_path):
|
|
|
529
495
|
# --- markdown and export ----------------------------------------------------------------
|
|
530
496
|
|
|
531
497
|
def markdown(root, day=None):
|
|
532
|
-
"""Rewrite `<store>/<category>/taste.md` from the active learnings; the
|
|
533
|
-
|
|
498
|
+
"""Rewrite `<store>/<category>/taste.md` from the active learnings; the
|
|
499
|
+
database stays the source of truth. Returns the files written."""
|
|
534
500
|
day = now_day() if day is None else day
|
|
535
501
|
by_cat = {}
|
|
536
502
|
for learning in every(load(root)).values():
|
|
@@ -590,7 +556,7 @@ def export_agents(root):
|
|
|
590
556
|
|
|
591
557
|
def first_injection(root):
|
|
592
558
|
"""The day of the first injection, or None: where the gate's arms split."""
|
|
593
|
-
days = [r["day"] for r in rows(
|
|
559
|
+
days = [r["day"] for r in rows(root, "injected")
|
|
594
560
|
if isinstance(r.get("day"), (int, float))]
|
|
595
561
|
return min(days) if days else None
|
|
596
562
|
|
|
@@ -601,13 +567,13 @@ def gate(root, need=980, alpha=0.1):
|
|
|
601
567
|
Every decided signal is one prompt after a writing turn. The arms split at
|
|
602
568
|
the first injection, by when the turn happened (`at`), not by when `learn`
|
|
603
569
|
decided it, so a backfill of old sessions stays in the before arm. Once both
|
|
604
|
-
arms hold `need` turns,
|
|
570
|
+
arms hold `need` turns, the gate's state says stopped unless the after-rate is
|
|
605
571
|
lower at one-sided p < `alpha`. Returns the report."""
|
|
606
572
|
first = first_injection(root)
|
|
607
573
|
arms = {"before": [0, 0], "after": [0, 0]}
|
|
608
|
-
for row in rows(
|
|
574
|
+
for row in rows(root, "decisions", "WHERE provider = ?", ("typesafe",)):
|
|
609
575
|
at = row.get("at")
|
|
610
|
-
if
|
|
576
|
+
if not isinstance(at, (int, float)):
|
|
611
577
|
continue
|
|
612
578
|
arm = arms["after" if first is not None and at >= first else "before"]
|
|
613
579
|
arm[0] += 1
|
|
@@ -623,6 +589,6 @@ def gate(root, need=980, alpha=0.1):
|
|
|
623
589
|
report["p"] = round(0.5 * math.erfc(z / math.sqrt(2)), 4)
|
|
624
590
|
if n1 >= need and n2 >= need:
|
|
625
591
|
report["stopped"] = not (report["p"] is not None and report["p"] < alpha)
|
|
626
|
-
|
|
627
|
-
|
|
592
|
+
with store.taste(store_dir(root)) as db:
|
|
593
|
+
store.set_gate(db, report["stopped"], report)
|
|
628
594
|
return report
|
package/hooks/tezgah_update.py
CHANGED
|
@@ -22,6 +22,7 @@ import sys
|
|
|
22
22
|
import time
|
|
23
23
|
|
|
24
24
|
import tezgah_paths as tp
|
|
25
|
+
import tezgah_store
|
|
25
26
|
|
|
26
27
|
REPO = os.environ.get("TEZGAH_REPO") or "r1z4x/tezgah"
|
|
27
28
|
CHECK_EVERY = 24 * 3600
|
|
@@ -30,10 +31,6 @@ SEMVER = re.compile(r"^v?(\d+)\.(\d+)\.(\d+)$")
|
|
|
30
31
|
ARROW = "\u2191" # ↑: East Asian width Ambiguous, one cell, never an emoji
|
|
31
32
|
|
|
32
33
|
|
|
33
|
-
def cache_path():
|
|
34
|
-
return os.path.join(tp.cache_dir(), "update.json")
|
|
35
|
-
|
|
36
|
-
|
|
37
34
|
def endpoint():
|
|
38
35
|
"""Where the newest release is read, repointed by TEZGAH_UPDATE_URL (tests).
|
|
39
36
|
|
|
@@ -73,32 +70,15 @@ def disabled():
|
|
|
73
70
|
|
|
74
71
|
|
|
75
72
|
def read_cache():
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
return data if isinstance(data, dict) else {}
|
|
80
|
-
except Exception:
|
|
81
|
-
return {}
|
|
73
|
+
"""The `update_check` store's one document, `{checked, latest}`, or {}."""
|
|
74
|
+
data = tezgah_store.doc("update_check", "")
|
|
75
|
+
return data if isinstance(data, dict) else {}
|
|
82
76
|
|
|
83
77
|
|
|
84
78
|
def write_cache(data):
|
|
85
|
-
"""Write the cache whole,
|
|
79
|
+
"""Write the cache whole, in one statement, so a redraw reading it
|
|
86
80
|
mid-write sees the old answer or the new one and never half of either."""
|
|
87
|
-
|
|
88
|
-
tmp = "%s.%d.tmp" % (path, os.getpid())
|
|
89
|
-
try:
|
|
90
|
-
os.makedirs(os.path.dirname(path), mode=0o700, exist_ok=True)
|
|
91
|
-
fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
|
92
|
-
with os.fdopen(fd, "w", encoding="utf-8") as fh:
|
|
93
|
-
json.dump(data, fh)
|
|
94
|
-
os.replace(tmp, path)
|
|
95
|
-
return True
|
|
96
|
-
except OSError:
|
|
97
|
-
try:
|
|
98
|
-
os.remove(tmp)
|
|
99
|
-
except OSError:
|
|
100
|
-
pass
|
|
101
|
-
return False
|
|
81
|
+
return tezgah_store.put_doc("update_check", "", data)
|
|
102
82
|
|
|
103
83
|
|
|
104
84
|
# Every 0.x ever published, retired by 1.0.0: npm served 0.17.0 through 0.32.0
|
package/hosts/cursor/hook.py
CHANGED
|
@@ -52,7 +52,8 @@ try:
|
|
|
52
52
|
from tezgah_integrity import ( # noqa: E402
|
|
53
53
|
SUBAGENT_CHANNEL, bind_session, note, note_tool, ran_nothing, report_bytes,
|
|
54
54
|
stop_reason)
|
|
55
|
-
from tezgah_paths import
|
|
55
|
+
from tezgah_paths import off # noqa: E402
|
|
56
|
+
import tezgah_store # noqa: E402
|
|
56
57
|
from tezgah_taste import write_note # noqa: E402
|
|
57
58
|
from tezgah_untrusted import marks # noqa: E402
|
|
58
59
|
except Exception as exc:
|
|
@@ -215,25 +216,21 @@ def first_time(session_id, tag):
|
|
|
215
216
|
"""True the first time a tag is seen for a session; fail-open when unwritable."""
|
|
216
217
|
if not session_id:
|
|
217
218
|
return True
|
|
218
|
-
mark = os.path.join(cache_dir(), "reinforced", slug(str(session_id)), tag)
|
|
219
|
-
if os.path.exists(mark):
|
|
220
|
-
return False
|
|
221
219
|
try:
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
pass
|
|
226
|
-
return True
|
|
220
|
+
return tezgah_store.mark("cursor_reinforced", "%s/%s" % (slug(str(session_id)), tag))
|
|
221
|
+
except tezgah_store.ERRORS:
|
|
222
|
+
return True
|
|
227
223
|
|
|
228
224
|
|
|
229
|
-
def
|
|
230
|
-
"""
|
|
225
|
+
def answer_key(session_id):
|
|
226
|
+
"""The key of the last assistant text of a conversation in the
|
|
227
|
+
`cursor_answer` store.
|
|
231
228
|
|
|
232
229
|
Cursor's `stop` payload carries only `status`/`loop_count`, so the reply the
|
|
233
230
|
Stop rule has to read arrives on the earlier `afterAgentResponse`, which
|
|
234
|
-
documents `{"text": "<assistant final text>"}`. One
|
|
235
|
-
overwritten."""
|
|
236
|
-
return
|
|
231
|
+
documents `{"text": "<assistant final text>"}`. One document per
|
|
232
|
+
conversation, overwritten."""
|
|
233
|
+
return slug(str(session_id or "nosession"))
|
|
237
234
|
|
|
238
235
|
|
|
239
236
|
# A long reply is kept by its two ends rather than by one slice: the Stop rule's
|
|
@@ -255,29 +252,20 @@ def remember_answer(session_id, text):
|
|
|
255
252
|
count of what was dropped, then `ANSWER_TAIL` characters of its tail - so
|
|
256
253
|
both the opener the Stop rule reads and the closing marker it must not lose
|
|
257
254
|
survive the round trip. A reply with no `conversation_id` is not stored; a
|
|
258
|
-
|
|
255
|
+
store that cannot be written is not fatal."""
|
|
259
256
|
if not session_id or not text:
|
|
260
257
|
return
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
text =
|
|
265
|
-
|
|
266
|
-
if dropped > 0:
|
|
267
|
-
text = text[:ANSWER_HEAD] + ANSWER_CUT % dropped + text[-ANSWER_TAIL:]
|
|
268
|
-
with open(path, "w", encoding="utf-8") as fh:
|
|
269
|
-
fh.write(text)
|
|
270
|
-
except OSError:
|
|
271
|
-
pass
|
|
258
|
+
text = str(text)
|
|
259
|
+
dropped = len(text) - ANSWER_HEAD - ANSWER_TAIL
|
|
260
|
+
if dropped > 0:
|
|
261
|
+
text = text[:ANSWER_HEAD] + ANSWER_CUT % dropped + text[-ANSWER_TAIL:]
|
|
262
|
+
tezgah_store.put_doc("cursor_answer", answer_key(session_id), text)
|
|
272
263
|
|
|
273
264
|
|
|
274
265
|
def last_answer(session_id):
|
|
275
266
|
"""The reply remembered for this conversation, or ""."""
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
return fh.read()
|
|
279
|
-
except OSError:
|
|
280
|
-
return ""
|
|
267
|
+
text = tezgah_store.doc("cursor_answer", answer_key(session_id))
|
|
268
|
+
return text if isinstance(text, str) else ""
|
|
281
269
|
|
|
282
270
|
|
|
283
271
|
def dispatch(payload):
|