cortad 0.3.1-rc.9 → 0.3.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.
Files changed (57) hide show
  1. package/README.md +8 -5
  2. package/lib/app-port.mjs +9 -0
  3. package/lib/auto-reach.mjs +88 -0
  4. package/lib/backing.mjs +230 -0
  5. package/lib/calls.mjs +23 -0
  6. package/lib/cli.mjs +115 -30
  7. package/lib/contact-text.mjs +167 -0
  8. package/lib/data.mjs +16 -22
  9. package/lib/dissection-text.mjs +232 -0
  10. package/lib/document.mjs +75 -0
  11. package/lib/env-secrets.mjs +48 -0
  12. package/lib/failed-text.mjs +36 -0
  13. package/lib/fresh.mjs +33 -12
  14. package/lib/health.mjs +61 -0
  15. package/lib/held.mjs +67 -0
  16. package/lib/home.mjs +55 -17
  17. package/lib/hosted.mjs +201 -0
  18. package/lib/identities.mjs +1 -1
  19. package/lib/keeping.mjs +162 -0
  20. package/lib/ledger.mjs +20 -0
  21. package/lib/lock.mjs +6 -1
  22. package/lib/mcp.mjs +52 -18
  23. package/lib/mint.mjs +179 -38
  24. package/lib/mongo.mjs +74 -0
  25. package/lib/numbers-text.mjs +68 -0
  26. package/lib/pg.mjs +118 -0
  27. package/lib/proc.mjs +100 -0
  28. package/lib/proof.mjs +1014 -0
  29. package/lib/proxy.mjs +184 -0
  30. package/lib/pyhook/cortad_sites.py +218 -0
  31. package/lib/pyhook/cortad_tied.py +291 -0
  32. package/lib/pyhook/sitecustomize.py +1175 -166
  33. package/lib/qdrant.mjs +47 -0
  34. package/lib/reach.mjs +223 -0
  35. package/lib/read-text.mjs +78 -24
  36. package/lib/redis.mjs +130 -0
  37. package/lib/register.mjs +36 -27
  38. package/lib/replay.mjs +464 -34
  39. package/lib/runner.mjs +85 -0
  40. package/lib/service.mjs +25 -0
  41. package/lib/spec.mjs +7 -4
  42. package/lib/start.mjs +33 -2
  43. package/lib/stick.mjs +65 -25
  44. package/lib/stores.mjs +347 -0
  45. package/lib/switches.mjs +36 -0
  46. package/lib/tether.mjs +9 -0
  47. package/lib/text.mjs +314 -179
  48. package/lib/tied.cjs +217 -0
  49. package/lib/trace.cjs +386 -514
  50. package/lib/verbs.mjs +155 -38
  51. package/lib/wire.cjs +882 -0
  52. package/lib/words.mjs +2 -0
  53. package/lib/writes.mjs +2 -2
  54. package/local.mjs +661 -297
  55. package/package.json +31 -1
  56. package/skill/SKILL.md +49 -42
  57. package/skill/references/results.md +279 -102
package/lib/tied.cjs ADDED
@@ -0,0 +1,217 @@
1
+ // Which request a model call belongs to when it is made outside every request: an in-process queue
2
+ // or worker started with the app, a job whose result is fetched on a second request. The same rule as
3
+ // lib/pyhook/cortad_tied.py.
4
+ //
5
+ // The call is pinned to the request that asked: the one still open whose words are in the prompt, the
6
+ // one an open request is tied to by an id it sent or got back (the answer is being fetched), or, when
7
+ // the call starts before that fetch, the newest request whose words are in the prompt, if every such
8
+ // request shares its id (one client's turn). Anything less pins nothing: never a guess.
9
+ //
10
+ // The request that asked answered only when it ended with none of its calls still running; otherwise
11
+ // the next request tied to it by an id carries the answer, and completes the exchange when it ends.
12
+ // A request whose reply went out before any model call was made for it (a webhook that queues the
13
+ // message and answers at once) and that no request comes to fetch is answered later, by its model:
14
+ // the exchange is written once its calls have gone quiet.
15
+ "use strict";
16
+
17
+ // A value made to name one thing: a session hash, an event id, a job number.
18
+ const ID = /^(?=.*\d)[\w.:-]{6,200}$/;
19
+ const RECENT_KEPT = 32, RECENT_MS = 300_000;
20
+ // The requests a client sends just before the one that asked, carrying the same id, are part of its
21
+ // turn (a chat UI shows the message with one event, then answers it with the next).
22
+ const BEFORE_MS = 60_000;
23
+
24
+ const scalars = (v, out = [], depth = 0) => {
25
+ if (depth > 12 || out.length > 2000) return out;
26
+ if (typeof v === "string" || (typeof v === "number" && Number.isInteger(v))) out.push(String(v));
27
+ else if (Array.isArray(v)) for (const x of v) scalars(x, out, depth + 1);
28
+ else if (v && typeof v === "object") for (const x of Object.values(v)) scalars(x, out, depth + 1);
29
+ return out;
30
+ };
31
+ const json = (raw) => { try { return JSON.parse(raw); } catch { return undefined; } };
32
+ const addressIds = (req) => {
33
+ const [path, query = ""] = req.path.split("?");
34
+ return new Set([...path.split("/"), ...new URLSearchParams(query).values()].filter((v) => ID.test(v)));
35
+ };
36
+ const meets = (a, b) => [...a].some((v) => b.has(v));
37
+ // The person's words: a value with a space or a letter outside ASCII, long enough to be a sentence.
38
+ const sentence = (v) => v.length >= 8 && /\s|[^\x00-\x7F]/.test(v);
39
+ // The sentences a body or a reply carries, read as the person's words are: an "ok" or an id says nothing.
40
+ const sentencesIn = (text) => { const v = json(text); return (v === undefined ? [String(text || "")] : scalars(v)).filter(sentence); };
41
+ const PROMPTS_KEPT = 64, PROMPT_HEAD = 500;
42
+
43
+ // `bodyOf(req)` and `replyOf(req)` read what a request carried and what the app answered.
44
+ function makeTied({ norm, bodyOf, replyOf }) {
45
+ const open = new Set();
46
+ const recent = [];
47
+ // The heads of the prompts this process sent: one arriving at a server in the same process (an
48
+ // app that serves its own model) is a model call, never a person asking.
49
+ const prompts = [];
50
+ const idsOf = (req) => (req.ids ??= new Set([...addressIds(req), ...scalars(json(bodyOf(req))), ...scalars(json(replyOf(req)))].filter((v) => ID.test(v))));
51
+ const wordsOf = (req) => scalars(json(bodyOf(req))).filter(sentence);
52
+ const says = (req, prompt) => wordsOf(req).some((v) => prompt.includes(norm(v)));
53
+ const speaks = (text) => sentencesIn(text).length > 0;
54
+ // Among one client's requests whose words the prompt carries, the one being answered: the one whose
55
+ // calls have begun and that has not settled (a later step of its work, while the next message sits
56
+ // in the prompt as history the app saved as it arrived), else the one whose own words sit last in
57
+ // the prompt. Requests that all carry the same words are the steps of one turn, the newest the one
58
+ // answered (a chat UI shows the message with one event and answers it with the next). Null when
59
+ // that still leaves more than one: a missing answer, never a wrong one.
60
+ const answering = (sayers, prompt) => {
61
+ if (sayers.length === 1) return sayers[0];
62
+ const begun = sayers.filter((t) => t.pinned);
63
+ if (begun.length === 1) return begun[0];
64
+ const pool = begun.length ? begun : sayers;
65
+ const own = (t) => wordsOf(t).filter((v) => prompt.includes(norm(v)) && !pool.some((o) => o !== t && wordsOf(o).includes(v)));
66
+ if (pool.every((t) => !own(t).length)) return pool.at(-1);
67
+ const [first, second] = pool.map((t) => [t, Math.max(-1, ...own(t).map((v) => prompt.lastIndexOf(norm(v))))]).sort((a, b) => b[1] - a[1]);
68
+ return first[1] >= 0 && first[1] !== second[1] ? first[0] : null;
69
+ };
70
+ const drop = (req) => { const i = recent.indexOf(req); if (i >= 0) recent.splice(i, 1); };
71
+ // Asks reached through one session are one client's turns, the newest the one being answered: a
72
+ // client holds a second stream open on its session (a heartbeat) that points at an older ask.
73
+ const newestOf = (found) => {
74
+ const ties = [...found.values()].map((f) => f.tie);
75
+ if (!ties.every((t) => t.size) || ![...ties[0]].some((v) => ties.every((t) => t.has(v)))) return null;
76
+ return [...found.keys()].reduce((a, b) => (b.at > a.at ? b : a));
77
+ };
78
+ // The ask each id's last answered turn was made by: no request up to it is a step of a later turn,
79
+ // even one that never became an exchange.
80
+ const answered = new Map();
81
+ const answeredAt = (tie, at) => {
82
+ for (const i of tie) { answered.delete(i); answered.set(i, at); }
83
+ while (answered.size > RECENT_KEPT * 8) answered.delete(answered.keys().next().value);
84
+ };
85
+ // The newest request that asked before `req` and shares an id with its address.
86
+ const askerOf = (req, pinnedOnly = false) => {
87
+ const mine = addressIds(req);
88
+ const now = Date.now();
89
+ for (let i = mine.size ? recent.length - 1 : -1; i >= 0; i--) {
90
+ const t = recent[i];
91
+ // A request answered later is fetched afterwards only by a read of it, never by a new message.
92
+ if (t.at > req.at || now - t.at > RECENT_MS || (pinnedOnly && !t.pinned) || (t.settled && req.method !== "GET")) continue;
93
+ const tie = [...mine].filter((v) => idsOf(t).has(v));
94
+ if (tie.length) return { asker: t, tie: new Set(tie) };
95
+ }
96
+ return null;
97
+ };
98
+ return {
99
+ opened(req) {
100
+ open.add(req);
101
+ const found = askerOf(req, true);
102
+ if (found) { Object.assign(req, found); found.asker.fetched = true; }
103
+ },
104
+ // The requests of the exchange this request completes, the one that asked last, or null. A
105
+ // request that asked and has not answered is kept to be answered on a later one. Once answered it
106
+ // leaves, written or not: a later turn's fetch tied by the same id is never pinned to it.
107
+ closed(req) {
108
+ open.delete(req);
109
+ const { asker } = req;
110
+ if (asker && !asker.written && !asker.callsOpen) {
111
+ const since = Math.max(0, ...[...req.tie].map((i) => answered.get(i) ?? asker.at).filter((at) => at < asker.at));
112
+ const before = recent.filter((p) => p !== asker && !p.settled && since < p.at && p.at < asker.at && asker.at - p.at <= BEFORE_MS && meets(idsOf(p), req.tie));
113
+ for (const s of [...before, asker]) drop(s);
114
+ answeredAt(req.tie, asker.at);
115
+ return [...before, asker];
116
+ }
117
+ if (req.method !== "GET" && !prompts.includes(bodyOf(req).slice(0, PROMPT_HEAD)) && (!req.noted || req.callsOpen || req.late)) { recent.push(req); if (recent.length > RECENT_KEPT) recent.shift(); }
118
+ return null;
119
+ },
120
+ sent(prompt) {
121
+ prompts.push(String(prompt).slice(0, PROMPT_HEAD));
122
+ if (prompts.length > PROMPTS_KEPT) prompts.shift();
123
+ },
124
+ pinned,
125
+ // A call made for the request has answered or failed. Each call is counted once, in `waiting` (its
126
+ // own, while it has not been answered later) or in `callsOpen`: the two together are its calls
127
+ // still running, and one ending takes one off.
128
+ done(req) {
129
+ if (!req) return;
130
+ if (req.waiting) req.waiting -= 1;
131
+ else req.callsOpen = Math.max(0, (req.callsOpen || 0) - 1);
132
+ },
133
+ // A call made in a request's own context: its own, unless the request's reply went out before any
134
+ // model call answered it. Then a loop the request started (a queue drained from its handler) can
135
+ // keep its context while it works through later messages, so the call is its own only when the
136
+ // prompt carries its words, and is otherwise pinned as a call made outside every request is, the
137
+ // context breaking a tie. A request that answered with its own model call keeps every later one
138
+ // (a title written after the reply). A request whose reply went out before its first call is
139
+ // answered later.
140
+ inside(req, sent) {
141
+ if (req.finished && (!req.noted || req.late) && !says(req, norm(sent))) return pinned(sent, req);
142
+ if (!req.noted && req.finished) req.late = true;
143
+ if (req.late) { req.pinned = true; req.callsOpen = (req.callsOpen || 0) + 1; }
144
+ else req.waiting = (req.waiting || 0) + 1;
145
+ return req;
146
+ },
147
+ // The request's reply has gone out, with `status`, and `text()` reads it. One whose reply only took
148
+ // the message (a 2xx that says nothing of its own) while its model calls are still running and
149
+ // none of them has answered (a task the handler started without waiting on it) is answered later,
150
+ // those calls with it. A reply that says something, an apology after a time limit or an error, is
151
+ // the answer the person got. `heard`: a model call made for it got its response, set as the
152
+ // response arrives, so a stream the app passes straight on is never taken for one.
153
+ // ponytail: a handler that awaits one model call (moderation, intent), then leaves the answer to a
154
+ // task and replies ok, is graded on its receipt; the calls after the reply carrying its words would
155
+ // say otherwise, and a data endpoint that replies with no sentence makes such calls too.
156
+ replied(req, status, text) {
157
+ req.finished = true;
158
+ if (req.heard || req.late || !(req.waiting || req.callsOpen)) return;
159
+ if (!(status >= 200 && status < 300) || speaks(text())) return;
160
+ req.late = true;
161
+ req.pinned = true;
162
+ req.callsOpen = (req.callsOpen || 0) + (req.waiting || 0);
163
+ req.waiting = 0;
164
+ },
165
+ // Whether a request answered later is done, once: it has ended, none of its calls is open, and no
166
+ // request tied to it came to fetch the answer. It stays where a fetch that comes later can still
167
+ // find it and complete the exchange on a second request, but no call is pinned to it by its words.
168
+ settled(req) {
169
+ if (!req || !req.late || !req.ended || req.callsOpen || req.waiting || req.written || req.settled || req.fetched) return false;
170
+ req.settled = true;
171
+ return true;
172
+ },
173
+ };
174
+ // The request that asked for the call made with `sent`, or undefined. `held`: the request whose
175
+ // context the call was made in after that request's reply went out, which only breaks a tie.
176
+ function pinned(sent, held) {
177
+ const prompt = norm(sent);
178
+ const found = new Map();
179
+ // Each ask with the open fetch to tie to it and the ids of the session it was reached through.
180
+ // A read of a request already answered later can complete that request's exchange, never be the
181
+ // one a new call answers. A read stands for a request still working, except for a call that carries
182
+ // another unsettled request's words and none of its own: that call is the other one's (a page
183
+ // polling the sender while the next message is worked on). A receipt's fetch carries no message.
184
+ const reads = (asker) => !asker.settled && (says(asker, prompt) || ![...recent, ...open].some((t) => t !== asker && t.method !== "GET" && !t.settled && says(t, prompt)));
185
+ for (const r of open) {
186
+ if (r.asker) { if (reads(r.asker)) found.set(r.asker, { fetch: null, tie: r.tie ?? new Set() }); continue; }
187
+ // A request whose own context made a model call is served there; a call outside it is not its.
188
+ if (r.method !== "GET" && (!r.noted || r.pinned) && says(r, prompt)) { found.set(r, { fetch: null, tie: new Set() }); continue; }
189
+ const tied = askerOf(r);
190
+ if (tied && reads(tied.asker)) found.set(tied.asker, { fetch: { r, tie: tied.tie }, tie: tied.tie });
191
+ }
192
+ let sayers = [];
193
+ if (!found.size) {
194
+ sayers = recent.filter((t) => !t.settled && says(t, prompt));
195
+ const last = sayers.at(-1);
196
+ const turn = last && sayers.every((t) => t === last || meets(idsOf(t), idsOf(last))) ? answering(sayers, prompt) : null;
197
+ if (turn) found.set(turn, { fetch: null, tie: new Set() });
198
+ }
199
+ if (found.size > 1 && !newestOf(found)) for (const t of [...found.keys()]) if (!says(t, prompt)) found.delete(t);
200
+ const newest = found.size > 1 && newestOf(found);
201
+ if (newest) for (const t of [...found.keys()]) if (t !== newest) found.delete(t);
202
+ if (found.size !== 1) {
203
+ const among = found.size ? [...found.keys()] : sayers;
204
+ if (!held || held.settled || (among.length && !among.includes(held))) return undefined;
205
+ found.clear();
206
+ found.set(held, { fetch: null, tie: new Set() });
207
+ }
208
+ const [[asker, { fetch }]] = found;
209
+ if (fetch) { Object.assign(fetch.r, { asker, tie: fetch.tie }); asker.fetched = true; }
210
+ if (!asker.noted && asker.finished) asker.late = true;
211
+ asker.pinned = true;
212
+ asker.callsOpen = (asker.callsOpen || 0) + 1;
213
+ return asker;
214
+ }
215
+ }
216
+
217
+ module.exports = { makeTied, sentencesIn };