openzoo 0.48.17 → 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.
- package/lib/ctxalias.js +127 -0
- package/lib/grokui.mjs +1687 -59
- package/lib/namespace.js +34 -8
- package/lib/podagent.mjs +326 -7
- package/lib/x402.js +16 -1
- package/package.json +1 -1
package/lib/ctxalias.js
ADDED
|
@@ -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 };
|