openzoo 0.48.18 → 0.48.22

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.
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Well-known context NAMES for the one shared stacc tenant.
3
+ *
4
+ * THE PROBLEM. Every stacc app now resolves to the same tenant
5
+ * (sha256(chain:signer:"stacc") — see namespace.js), so a corpus bound by
6
+ * `npx openzoo` is finally REACHABLE from openzoo brain or open-webui. But
7
+ * reachable is not findable: recall takes ONE context_id
8
+ * ({tenant_id, context_id, query, top_k}), and the gateway mints that id
9
+ * itself — `{"context_id":"stacc:notes"}` is rejected outright with
10
+ * "invalid context_id format". So the apps could reach each other's memory
11
+ * and still had no way to NAME it.
12
+ *
13
+ * THE FIX. A name -> id registry that lives where every app and every device
14
+ * can already see it: the tenant's own leCore memory. `/v1/memory/write` and
15
+ * `/v1/memory/search` are tenant-scoped via the same tenantFor() the rest of
16
+ * the gateway uses, and they are FREE (logged status "free"), so the registry
17
+ * costs nothing to read and needs no new endpoint, table, or sync.
18
+ *
19
+ * WHY NAMED CONTEXTS RATHER THAN ONE BIG POOL. The obvious alternative is
20
+ * tenant-wide recall: search every context and merge. That is worse, and
21
+ * measurably so. `top_k` is FIXED (32 by default), so pooling makes your
22
+ * actual question compete for retrieval slots against every unrelated corpus
23
+ * you have ever bound — retrieval breadth is already the ceiling here, and
24
+ * widening the haystack lowers the hit rate rather than raising it. A handful
25
+ * of purposeful contexts you can name beats one undifferentiated blob.
26
+ *
27
+ * USAGE
28
+ * import { setAlias, resolveAlias, listAliases } from './ctxalias.js';
29
+ * await setAlias('notes', ctxId); // after a bind
30
+ * const id = await resolveAlias('notes'); // from any app, any device
31
+ */
32
+
33
+ import { withNamespace } from './namespace.js';
34
+
35
+ const GATEWAY = process.env.OPENZOO_API_BASE || 'https://x402-tokens.fly.dev';
36
+
37
+ // A tag, not a prefix match on free text: memory/search takes tags, and a tag
38
+ // keeps registry rows from ever colliding with a user's real notes.
39
+ const TAG = 'stacc-ctx-alias';
40
+
41
+ // Deliberately strict. These names are meant to be typed by a human in
42
+ // another app a week later, so ambiguity (case, spaces, unicode) is the enemy.
43
+ const NAME_RE = /^[a-z0-9][a-z0-9._-]{0,62}$/;
44
+
45
+ function assertName(name) {
46
+ if (!NAME_RE.test(String(name || ''))) {
47
+ throw new Error(
48
+ `invalid alias name "${name}" — use lowercase a-z 0-9 . _ - (max 63)`);
49
+ }
50
+ }
51
+
52
+ function line(name, contextId) {
53
+ return `${TAG} ${name} = ${contextId}`;
54
+ }
55
+
56
+ async function call(path, body) {
57
+ const res = await fetch(`${GATEWAY}${path}`, {
58
+ method: 'POST',
59
+ headers: withNamespace({ 'content-type': 'application/json' }),
60
+ body: JSON.stringify(body),
61
+ signal: AbortSignal.timeout(20_000),
62
+ });
63
+ if (res.status === 401) {
64
+ // The gateway refuses an expired namespace signature on money paths now;
65
+ // say so plainly rather than surfacing a bare 401 from a "free" route.
66
+ throw new Error('namespace signature rejected — is the wallet present?');
67
+ }
68
+ if (!res.ok) throw new Error(`${path} -> ${res.status}`);
69
+ return res.json();
70
+ }
71
+
72
+ /** Point a name at a context id. Last write wins. */
73
+ export async function setAlias(name, contextId) {
74
+ assertName(name);
75
+ if (!contextId) throw new Error('setAlias needs a context id');
76
+ await call('/v1/memory/write', { text: line(name, contextId), tags: [TAG, name] });
77
+ return { name, contextId };
78
+ }
79
+
80
+ /**
81
+ * Resolve a name to a context id, or null.
82
+ *
83
+ * Reads the NEWEST matching row rather than the first: setAlias appends, so a
84
+ * re-pointed name leaves the old row behind and a naive "first hit" would
85
+ * silently keep resolving to the stale context.
86
+ */
87
+ export async function resolveAlias(name) {
88
+ assertName(name);
89
+ const rows = hitsOf(await call('/v1/memory/search',
90
+ { query: `${TAG} ${name}`, tags: [TAG, name], top: 200 }));
91
+ let best = null;
92
+ for (const r of rows) {
93
+ const m = String(r?.text ?? '').match(new RegExp(`^${TAG}\\s+${name}\\s*=\\s*(\\S+)$`));
94
+ if (m) best = m[1]; // rows arrive oldest-first, so the last wins
95
+ }
96
+ return best;
97
+ }
98
+
99
+ /** Every name currently registered in this tenant. */
100
+ export async function listAliases() {
101
+ const rows = hitsOf(await call('/v1/memory/search', { query: TAG, tags: [TAG], top: 500 }));
102
+ const map = new Map();
103
+ for (const r of rows) {
104
+ const m = String(r?.text ?? '').match(new RegExp(`^${TAG}\\s+(\\S+)\\s*=\\s*(\\S+)$`));
105
+ if (m) map.set(m[1], m[2]); // later row replaces earlier — re-pointing works
106
+ }
107
+ return Object.fromEntries(map);
108
+ }
109
+
110
+ /**
111
+ * Rows out of a memory_search response, oldest first.
112
+ *
113
+ * The payload key is `hits` (object "ouroboros.memory_search"), NOT items or
114
+ * results — reading the wrong key returns [] and every lookup silently
115
+ * resolves to null, which looks exactly like "the alias was never set".
116
+ *
117
+ * Sorting by id matters as much. Ids are sequential (`note-0000`,
118
+ * `note-0001`, ...) but search returns them by SCORE, and two rows for the
119
+ * same name score identically — so without an explicit order, re-pointing an
120
+ * alias would resolve to whichever row the ranker happened to emit last.
121
+ */
122
+ function hitsOf(out) {
123
+ const rows = out?.hits ?? [];
124
+ return rows.slice().sort((a, b) => String(a?.id ?? '').localeCompare(String(b?.id ?? '')));
125
+ }
126
+
127
+ export { TAG as ALIAS_TAG };