@dreamforgestudiollc/emet 0.0.0-stage → 0.2.4
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/.env.example +136 -0
- package/CHANGELOG.md +296 -0
- package/CHARTER.md +125 -0
- package/LICENSE +22 -0
- package/README.md +783 -2
- package/docs/AUTHORSHIP.md +26 -0
- package/docs/BENCHMARKS.md +261 -0
- package/docs/BOOTSTRAP.md +127 -0
- package/docs/BOTS.md +79 -0
- package/docs/COVERAGE.md +64 -0
- package/docs/DEPLOY.md +246 -0
- package/docs/DISCIPLINES.md +29 -0
- package/docs/GUARDRAILS-AND-MONORAILS.md +107 -0
- package/docs/GUIDED-SETUP.md +296 -0
- package/docs/PERMISSIONS.md +118 -0
- package/docs/ROADMAP.md +59 -0
- package/docs/SETUP.md +259 -0
- package/guides/NEURODIVERGENT_SUPPORT.md +278 -0
- package/package.json +70 -4
- package/server.json +39 -0
- package/src/aleph.js +395 -0
- package/src/annotations.js +135 -0
- package/src/approve-throttle.js +54 -0
- package/src/atlas-role.js +58 -0
- package/src/conformance.js +688 -0
- package/src/content_analyzer.js +450 -0
- package/src/database.js +811 -0
- package/src/disciplines.js +26 -0
- package/src/dispatch.js +279 -0
- package/src/env.js +52 -0
- package/src/floor.js +187 -0
- package/src/gaps.js +476 -0
- package/src/guides.js +48 -0
- package/src/http-remote.js +269 -0
- package/src/index.js +390 -0
- package/src/invites.js +167 -0
- package/src/layers.js +449 -0
- package/src/lessons-to-checks.js +366 -0
- package/src/logger.js +299 -0
- package/src/metadata.js +61 -0
- package/src/module-path.js +32 -0
- package/src/mongo-connect.js +48 -0
- package/src/named-items.js +208 -0
- package/src/oauth-provider.js +838 -0
- package/src/persons.js +116 -0
- package/src/provenance.js +257 -0
- package/src/redact.js +231 -0
- package/src/request-log.js +124 -0
- package/src/scopes.js +97 -0
- package/src/security-headers.js +26 -0
- package/src/session-graph.js +945 -0
- package/src/session.js +1097 -0
- package/src/setup.js +1313 -0
- package/src/startup-pages.js +354 -0
- package/src/startup-payload.js +126 -0
- package/src/tools.js +4271 -0
- package/src/transcript-session.js +106 -0
- package/src/validation.js +1163 -0
- package/src/version.js +2 -0
- package/templates/BOARD.md +72 -0
- package/templates/BOTHANDOFF.md +57 -0
- package/templates/CHARACTER.md +30 -0
- package/templates/HANDOFF.md +63 -0
- package/templates/IDENTITY.md +29 -0
- package/templates/INDEX.md +82 -0
- package/templates/INSTALL.md +29 -0
- package/templates/PROCEDURE.md +39 -0
- package/templates/ROSTER.md +43 -0
- package/templates/SEED.md +65 -0
- package/templates/STAGING.md +37 -0
- package/templates/STANDARD.md +99 -0
- package/templates/THREAD.md +43 -0
- package/templates/USER.md +29 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paged startup (the host-aware startup design, paging; approved by the owner
|
|
3
|
+
* 2026-10-03). The connector gateway cuts a tool result at 20,000 UTF-8
|
|
4
|
+
* bytes, and the full startup is about 64K, so on those hosts the floor,
|
|
5
|
+
* install notes, source tags, seeds_due, record gaps and lessons never
|
|
6
|
+
* arrived. Paging delivers the SAME full payload in pages, binding text first.
|
|
7
|
+
*
|
|
8
|
+
* The rules this file keeps:
|
|
9
|
+
* - Every page, envelope included, is at most the page budget
|
|
10
|
+
* (EMET_INIT_PAGE_BYTES, default 18,000 bytes: 2,000 under the cut;
|
|
11
|
+
* clamped to 12,000-19,000).
|
|
12
|
+
* - Keys go out in one fixed order, binding first (PAGE_ORDER), never
|
|
13
|
+
* reordered by size. A key splits only when it cannot fit a page on its
|
|
14
|
+
* own; its chunks run consecutively and join back exactly.
|
|
15
|
+
* - Page 1 carries the plan: which keys are on which page, and which of them
|
|
16
|
+
* are binding. Nothing binding can drop silently: a host that stops early
|
|
17
|
+
* has been told, in page 1, what it has not yet read.
|
|
18
|
+
* - Every page but the last says, first, to call again with the next page
|
|
19
|
+
* before replying. The pages of one load come from one build (load_id);
|
|
20
|
+
* a page asked for after the startup changed says to start over.
|
|
21
|
+
* - The pages joined back are the full payload byte for byte (assemblePages).
|
|
22
|
+
* - Full payload only. This file never builds or serves the abridged form.
|
|
23
|
+
* - `page: "all"` returns the whole payload in one result, unchanged, for a
|
|
24
|
+
* host with no 20,000-byte limit; a payload that fits one page is returned
|
|
25
|
+
* unchanged with no paging fields at all.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import crypto from 'crypto';
|
|
29
|
+
import { resultSize } from './startup-payload.js';
|
|
30
|
+
|
|
31
|
+
export const DEFAULT_PAGE_BYTES = 18000;
|
|
32
|
+
export const MIN_PAGE_BYTES = 12000;
|
|
33
|
+
export const MAX_PAGE_BYTES = 19000;
|
|
34
|
+
|
|
35
|
+
/** The page budget in UTF-8 bytes of the whole result, from EMET_INIT_PAGE_BYTES, clamped. */
|
|
36
|
+
export function pageBudget(raw = process.env.EMET_INIT_PAGE_BYTES) {
|
|
37
|
+
const n = Number(raw);
|
|
38
|
+
if (!Number.isFinite(n) || n <= 0) return DEFAULT_PAGE_BYTES;
|
|
39
|
+
return Math.min(MAX_PAGE_BYTES, Math.max(MIN_PAGE_BYTES, Math.floor(n)));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Binding first: the order keys are paged in. Keys not listed follow, in payload order. */
|
|
43
|
+
export const PAGE_ORDER = Object.freeze([
|
|
44
|
+
'state', 'database', 'source_tag', 'person_id', 'unhandled_drops', 'unhandled_drops_more', 'retired_drops', 'emet_line', 'emet_line_if_unanswered',
|
|
45
|
+
'tools_in_force', 'instructions_short', 'session_close_protocol', 'operating_discipline', 'charter',
|
|
46
|
+
'floor', 'floor_precedence', 'floor_reassertion',
|
|
47
|
+
'seeds_due', 'seeds_evaluated_on', 'install_notes_doc_id',
|
|
48
|
+
'identity', 'character', 'character_doc_id', 'character_sha256', 'user',
|
|
49
|
+
'record_gaps', 'named_items', 'lessons_pending', 'layer_health',
|
|
50
|
+
'install_notes', 'instructions', 'layer_charter',
|
|
51
|
+
'seeds', 'threads', 'recent_context', 'layer_counts', 'layer_counts_last_30d'
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
/** Keys that bind the session (the startup rules): never optional, never dropped. */
|
|
55
|
+
export const BINDING_KEYS = Object.freeze([
|
|
56
|
+
'tools_in_force', 'instructions_short', 'session_close_protocol',
|
|
57
|
+
'floor', 'floor_precedence', 'floor_reassertion', 'identity', 'character', 'user'
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
/** Page fields that are never payload keys. */
|
|
61
|
+
export const PAGE_FIELDS = Object.freeze(['startup_page', 'page_notice', 'page_plan']);
|
|
62
|
+
|
|
63
|
+
export const loadIdOf = (full) => crypto.createHash('sha256').update(JSON.stringify(full), 'utf8').digest('hex').slice(0, 16);
|
|
64
|
+
|
|
65
|
+
function orderedKeys(full) {
|
|
66
|
+
const keys = PAGE_ORDER.filter((k) => k in full);
|
|
67
|
+
for (const k of Object.keys(full)) if (!keys.includes(k)) keys.push(k);
|
|
68
|
+
return keys;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const bytes = (s) => Buffer.byteLength(s, 'utf8');
|
|
72
|
+
const entryBytes = (key, value) => bytes(JSON.stringify({ [key]: value })) - 1; // `"key":value` plus a comma
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Pack the payload into pages of entries. budgets(k) = content bytes allowed
|
|
76
|
+
* on page k (0-based). An entry is {key, value} or {key, value, chunk, of, json}.
|
|
77
|
+
*/
|
|
78
|
+
function pack(full, budgets) {
|
|
79
|
+
const pages = [[]]; const used = [0];
|
|
80
|
+
const room = () => budgets(pages.length - 1) - used[pages.length - 1];
|
|
81
|
+
const newPage = () => { pages.push([]); used.push(0); };
|
|
82
|
+
for (const key of orderedKeys(full)) {
|
|
83
|
+
const value = full[key];
|
|
84
|
+
const size = entryBytes(key, value);
|
|
85
|
+
if (size <= room()) { pages[pages.length - 1].push({ key, value }); used[pages.length - 1] += size; continue; }
|
|
86
|
+
if (size <= budgets(pages.length)) { newPage(); pages[pages.length - 1].push({ key, value }); used[pages.length - 1] += size; continue; }
|
|
87
|
+
// Too big for any one page: chunk it. Strings split as text; anything else
|
|
88
|
+
// as its JSON text. Code points are never split; chunks run consecutively.
|
|
89
|
+
const json = typeof value !== 'string';
|
|
90
|
+
const cps = Array.from(json ? JSON.stringify(value) : value);
|
|
91
|
+
const mine = [];
|
|
92
|
+
let i = 0;
|
|
93
|
+
while (i < cps.length) {
|
|
94
|
+
if (room() < 1000 && pages[pages.length - 1].length) newPage();
|
|
95
|
+
const r = room() - 60; // the chunk marker in startup_page
|
|
96
|
+
let lo = 1, hi = cps.length - i, best = 0;
|
|
97
|
+
while (lo <= hi) {
|
|
98
|
+
const mid = (lo + hi) >> 1;
|
|
99
|
+
if (entryBytes(key, cps.slice(i, i + mid).join('')) <= r) { best = mid; lo = mid + 1; } else hi = mid - 1;
|
|
100
|
+
}
|
|
101
|
+
if (best === 0) best = 1;
|
|
102
|
+
const piece = cps.slice(i, i + best).join('');
|
|
103
|
+
const e = { key, value: piece, chunk: mine.length + 1, of: 0, json };
|
|
104
|
+
mine.push(e);
|
|
105
|
+
pages[pages.length - 1].push(e);
|
|
106
|
+
used[pages.length - 1] += entryBytes(key, piece) + 60;
|
|
107
|
+
i += best;
|
|
108
|
+
if (i < cps.length) newPage();
|
|
109
|
+
}
|
|
110
|
+
for (const e of mine) e.of = mine.length;
|
|
111
|
+
}
|
|
112
|
+
return pages.filter((p, i) => p.length || i === 0);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function planOf(pages) {
|
|
116
|
+
return pages.map((entries, i) => {
|
|
117
|
+
const keys = entries.map((e) => (e.chunk ? `${e.key} (part ${e.chunk} of ${e.of})` : e.key));
|
|
118
|
+
const binding = [...new Set(entries.filter((e) => BINDING_KEYS.includes(e.key)).map((e) => e.key))];
|
|
119
|
+
return { page: i + 1, keys, ...(binding.length ? { binding } : {}) };
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function nextCall(k, loadId, recentLimit) {
|
|
124
|
+
const args = { page: k, load_id: loadId };
|
|
125
|
+
if (recentLimit !== undefined && recentLimit !== null && recentLimit !== '') args.recent_limit = Number(recentLimit);
|
|
126
|
+
return `emet_initialize ${JSON.stringify(args)}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** The last page's line (2026-10-03): the open is tied to this startup, so it names the load_id to pass. */
|
|
130
|
+
export function sessionOpenLine(loadId) {
|
|
131
|
+
return `Pass "load_id": "${loadId}" to emet_session_open; the startup page check runs on that load.`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* A startup served whole - page "all", a startup that fits one result, or a
|
|
136
|
+
* store that is not ready (setup_required, unreachable, misconfigured) -
|
|
137
|
+
* carries its load_id too (2026-10-03), in the same `startup_page` block a
|
|
138
|
+
* page carries, with a notice to pass it to emet_session_open. The payload
|
|
139
|
+
* follows unchanged, and the load_id is the payload's own (loadIdOf), the
|
|
140
|
+
* same id its pages carry. The caller records it with every page fetched.
|
|
141
|
+
*/
|
|
142
|
+
export function wholeStartup(full, index = 1) {
|
|
143
|
+
const loadId = loadIdOf(full);
|
|
144
|
+
return {
|
|
145
|
+
startup_page: { index, of: 1, load_id: loadId, next: null },
|
|
146
|
+
page_notice: `The whole startup in one result (load ${loadId}). ${sessionOpenLine(loadId)}`,
|
|
147
|
+
...full
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** The line page 1 and every next-call notice carry (2026-10-03): open checks, so fetch first. */
|
|
152
|
+
export function fetchAllLine(n) {
|
|
153
|
+
return `Fetch all ${n} pages before emet_session_open; an open with any page not fetched is refused.`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function noticeFor(k, n, plan, loadId, recentLimit) {
|
|
157
|
+
if (k === n) {
|
|
158
|
+
return `Startup page ${k} of ${n}, the last. You now hold the whole startup (load ${loadId}). ` +
|
|
159
|
+
'Follow `instructions_short` from page 1 and `instructions`, now, in that order. ' +
|
|
160
|
+
`${sessionOpenLine(loadId)}`;
|
|
161
|
+
}
|
|
162
|
+
const later = plan.slice(k).filter((p) => p.binding).map((p) => `page ${p.page}: ${p.binding.join(', ')}`);
|
|
163
|
+
// "Fetch all N pages" says how far to keep calling, so it replaced "keep
|
|
164
|
+
// calling until a page says it is the last" (2026-10-03): the notice did not
|
|
165
|
+
// grow, and the binding text keeps its place on the page it was on.
|
|
166
|
+
return `Startup page ${k} of ${n}. Do not reply to the user yet. Call ${nextCall(k + 1, loadId, recentLimit)} now. ` +
|
|
167
|
+
`${fetchAllLine(n)} None is optional` +
|
|
168
|
+
(later.length ? `; binding text is still to come (${later.join('; ')}).` : '.') +
|
|
169
|
+
(k === 1 ? ' `page_plan` lists what each page carries.' : '');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function pageObject(pages, plan, k, loadId, recentLimit, payloadOrder) {
|
|
173
|
+
const entries = pages[k - 1];
|
|
174
|
+
const n = pages.length;
|
|
175
|
+
const chunks = {};
|
|
176
|
+
const content = {};
|
|
177
|
+
for (const e of entries) {
|
|
178
|
+
content[e.key] = e.value;
|
|
179
|
+
if (e.chunk) chunks[e.key] = { part: e.chunk, of: e.of, ...(e.json ? { json_text: true } : {}) };
|
|
180
|
+
}
|
|
181
|
+
const startup_page = {
|
|
182
|
+
index: k, of: n, load_id: loadId,
|
|
183
|
+
keys: plan[k - 1].keys,
|
|
184
|
+
...(Object.keys(chunks).length ? { chunks } : {}),
|
|
185
|
+
next: k < n ? nextCall(k + 1, loadId, recentLimit) : null,
|
|
186
|
+
// The last page names the payload's own key order, so the pages join back
|
|
187
|
+
// to the exact bytes of the one-result payload (assemblePages).
|
|
188
|
+
...(k === n ? { payload_order: payloadOrder } : {})
|
|
189
|
+
};
|
|
190
|
+
return {
|
|
191
|
+
startup_page,
|
|
192
|
+
page_notice: noticeFor(k, n, plan, loadId, recentLimit),
|
|
193
|
+
...(k === 1 ? { page_plan: plan } : {}),
|
|
194
|
+
...content
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Lay the full payload out in pages that each fit the budget. Returns
|
|
200
|
+
* {pages: [pageObject...], plan, load_id}. Pure: the same payload and budget
|
|
201
|
+
* always give the same pages.
|
|
202
|
+
*/
|
|
203
|
+
export function paginate(full, { budget = pageBudget(), recentLimit } = {}) {
|
|
204
|
+
const loadId = loadIdOf(full);
|
|
205
|
+
const order = Object.keys(full);
|
|
206
|
+
// The page fields (notice, plan, next call) cost bytes that depend on the
|
|
207
|
+
// layout, so the layout is found by iterating: pack with the overheads last
|
|
208
|
+
// measured, build, measure again. Stops at the first layout that fits and
|
|
209
|
+
// has stopped changing.
|
|
210
|
+
let overhead = [2400]; let other = 900;
|
|
211
|
+
let best = null; let lastShape = null;
|
|
212
|
+
for (let attempt = 0; attempt < 16; attempt++) {
|
|
213
|
+
const pages = pack(full, (i) => budget - (overhead[i] ?? other) - 48);
|
|
214
|
+
const plan = planOf(pages);
|
|
215
|
+
const objs = pages.map((_, i) => pageObject(pages, plan, i + 1, loadId, recentLimit, order));
|
|
216
|
+
const sizes = objs.map((o) => resultSize(o).bytes);
|
|
217
|
+
const fits = sizes.every((sz) => sz <= budget);
|
|
218
|
+
const shape = JSON.stringify(plan);
|
|
219
|
+
if (fits) {
|
|
220
|
+
best = { pages: objs, plan, load_id: loadId, sizes };
|
|
221
|
+
if (shape === lastShape) break;
|
|
222
|
+
}
|
|
223
|
+
lastShape = shape;
|
|
224
|
+
const measured = pages.map((entries, i) => sizes[i] - entries.reduce((n, e) => n + entryBytes(e.key, e.value) + (e.chunk ? 60 : 0), 0));
|
|
225
|
+
overhead = measured.map((m, i) => (fits ? m : Math.max(m, overhead[i] ?? other)));
|
|
226
|
+
other = Math.max(...measured.slice(1), 600);
|
|
227
|
+
if (!fits) sizes.forEach((sz, i) => { if (sz > budget) overhead[i] += sz - budget + 64; });
|
|
228
|
+
}
|
|
229
|
+
if (!best) throw new Error('startup paging: could not fit the pages to the budget');
|
|
230
|
+
return best;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Join pages back into the payload, exactly, in the payload's own key order (for hosts and tests). */
|
|
234
|
+
export function assemblePages(pages) {
|
|
235
|
+
const got = {}; const text = {}; const done = {};
|
|
236
|
+
const ordered = [...pages].sort((a, b) => a.startup_page.index - b.startup_page.index);
|
|
237
|
+
for (const p of ordered) {
|
|
238
|
+
const chunks = p.startup_page.chunks || {};
|
|
239
|
+
for (const [k, v] of Object.entries(p)) {
|
|
240
|
+
if (PAGE_FIELDS.includes(k)) continue;
|
|
241
|
+
if (!chunks[k]) { got[k] = v; continue; }
|
|
242
|
+
text[k] = (text[k] || '') + v;
|
|
243
|
+
if (chunks[k].part === chunks[k].of) { got[k] = chunks[k].json_text ? JSON.parse(text[k]) : text[k]; done[k] = true; }
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const last = ordered[ordered.length - 1];
|
|
247
|
+
const order = (last && last.startup_page.payload_order) || Object.keys(got);
|
|
248
|
+
const out = {};
|
|
249
|
+
for (const k of order) if (k in got) out[k] = got[k];
|
|
250
|
+
for (const k of Object.keys(got)) if (!(k in out)) out[k] = got[k];
|
|
251
|
+
return out;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// One load's pages are served from the build that made page 1, so a write
|
|
255
|
+
// between two calls never moves a key across pages. In memory, per process.
|
|
256
|
+
const CACHE_MS = 10 * 60 * 1000;
|
|
257
|
+
const CACHE_MAX = 32;
|
|
258
|
+
// The fetched-page record outlives the payload cache: the startup_pages gate
|
|
259
|
+
// refuses an open whose load has no record, so a host that reads its startup
|
|
260
|
+
// and opens a while later (up to 60 minutes after its last page) still opens.
|
|
261
|
+
// Its own lifetime, never the payload cache's.
|
|
262
|
+
export const SERVED_MS = 60 * 60 * 1000;
|
|
263
|
+
// The fetched-page record is a few numbers per load, so it keeps far more
|
|
264
|
+
// loads than the payload cache: an open with an evicted load is refused (the
|
|
265
|
+
// startup_pages gate cannot check it), so many hosts starting at once must not
|
|
266
|
+
// push a load out before its host opens.
|
|
267
|
+
export const SERVED_MAX = 2000;
|
|
268
|
+
const cache = new Map();
|
|
269
|
+
export function cacheLoad(full, now = Date.now()) {
|
|
270
|
+
const id = loadIdOf(full);
|
|
271
|
+
cache.set(id, { full, at: now });
|
|
272
|
+
for (const [k, v] of cache) if (now - v.at > CACHE_MS) cache.delete(k);
|
|
273
|
+
while (cache.size > CACHE_MAX) cache.delete(cache.keys().next().value);
|
|
274
|
+
return id;
|
|
275
|
+
}
|
|
276
|
+
export function cachedLoad(id, now = Date.now()) {
|
|
277
|
+
const hit = id && cache.get(String(id));
|
|
278
|
+
if (!hit) return null;
|
|
279
|
+
if (now - hit.at > CACHE_MS) { cache.delete(String(id)); return null; }
|
|
280
|
+
return hit.full;
|
|
281
|
+
}
|
|
282
|
+
export function clearPageCache() { cache.clear(); served.clear(); }
|
|
283
|
+
|
|
284
|
+
// Which pages of each load a host was given (2026-10-03): read by the
|
|
285
|
+
// startup_pages gate, which refuses an open with any page never fetched.
|
|
286
|
+
// In memory, per process, same lifetime as the cache. Keyed by load_id, a hash
|
|
287
|
+
// of the content, so two hosts that load the same startup share one record.
|
|
288
|
+
const served = new Map();
|
|
289
|
+
/**
|
|
290
|
+
* Note that page `index` of `of` was served for a load; index 'all' = the
|
|
291
|
+
* whole startup at once. Returns the LATE pages this serve settles
|
|
292
|
+
* (2026-10-03): for each session opened while this page was missing,
|
|
293
|
+
* {load_id, session, page, of}. A page fetched after the open is late, not
|
|
294
|
+
* still missing, so the lab does not count it as missing. [] when none.
|
|
295
|
+
*/
|
|
296
|
+
export function notePageServed(loadId, index, of, now = Date.now()) {
|
|
297
|
+
if (!loadId) return [];
|
|
298
|
+
const id = String(loadId);
|
|
299
|
+
const rec = served.get(id) || { of: null, pages: new Set(), all: false, at: now, opens: new Map() };
|
|
300
|
+
if (index === 'all') rec.all = true;
|
|
301
|
+
else { rec.pages.add(index); rec.of = of; }
|
|
302
|
+
rec.at = now;
|
|
303
|
+
const late = [];
|
|
304
|
+
for (const [session, missing] of rec.opens || []) {
|
|
305
|
+
for (const p of [...missing]) {
|
|
306
|
+
if (index === 'all' || p === index) { missing.delete(p); late.push({ load_id: id, session, page: p, of: rec.of }); }
|
|
307
|
+
}
|
|
308
|
+
if (!missing.size) rec.opens.delete(session);
|
|
309
|
+
}
|
|
310
|
+
served.delete(id); served.set(id, rec); // keep insertion order = recency
|
|
311
|
+
for (const [k, v] of served) if (now - v.at > SERVED_MS) served.delete(k);
|
|
312
|
+
while (served.size > SERVED_MAX) served.delete(served.keys().next().value);
|
|
313
|
+
return late;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Note that a session opened while pages of a load were missing (report
|
|
317
|
+
* mode), so a page fetched afterwards is recorded as late. `session` is the
|
|
318
|
+
* label the open's lab row carries. Same lifetime as the served record.
|
|
319
|
+
*/
|
|
320
|
+
export function noteOpenMissing(loadId, session, missing = []) {
|
|
321
|
+
const rec = loadId ? served.get(String(loadId)) : null;
|
|
322
|
+
if (!rec || !session || !missing.length) return;
|
|
323
|
+
if (!rec.opens) rec.opens = new Map();
|
|
324
|
+
const set = rec.opens.get(session) || new Set();
|
|
325
|
+
for (const p of missing) set.add(p);
|
|
326
|
+
rec.opens.set(session, set);
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Which pages of the named load were never served. null when no load is
|
|
330
|
+
* named, or there is no record of it (no page of it served in the last 60
|
|
331
|
+
* minutes, SERVED_MS, or the server restarted). Never another load in its place. Never throws, never blocks.
|
|
332
|
+
*/
|
|
333
|
+
export function pageCompletion(loadId = null, now = Date.now()) {
|
|
334
|
+
const id = loadId ? String(loadId) : null;
|
|
335
|
+
const rec = id ? served.get(id) : null;
|
|
336
|
+
if (!rec || now - rec.at > SERVED_MS) return null;
|
|
337
|
+
if (rec.all || !rec.of) return { load_id: id, complete: true, missing: [] };
|
|
338
|
+
const missing = [];
|
|
339
|
+
for (let i = 1; i <= rec.of; i++) if (!rec.pages.has(i)) missing.push(i);
|
|
340
|
+
return { load_id: id, of: rec.of, served: [...rec.pages].sort((a, b) => a - b), missing, complete: missing.length === 0 };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** The reply to a page asked for after the startup changed (or the server restarted). */
|
|
344
|
+
export function restartPage(page, loadId) {
|
|
345
|
+
return {
|
|
346
|
+
state: 'ready',
|
|
347
|
+
startup_page: { index: null, requested: page, load_id: loadId || null, restart: true, next: 'emet_initialize {}' },
|
|
348
|
+
page_notice: 'The startup changed after page 1 was built (new memory was written, or the server restarted), so ' +
|
|
349
|
+
`page ${page} of that load can no longer be served. Call emet_initialize {} now to start again from page 1, and ` +
|
|
350
|
+
'use the new pages in place of the earlier ones. Do not reply to the user until you have every page.'
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export default { fetchAllLine, sessionOpenLine, wholeStartup, pageBudget, PAGE_ORDER, BINDING_KEYS, PAGE_FIELDS, paginate, assemblePages, loadIdOf, cacheLoad, cachedLoad, clearPageCache, restartPage, notePageServed, noteOpenMissing, pageCompletion };
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The startup payload's size and its abridged form (the host-aware startup
|
|
3
|
+
* design, conservative variant chosen by the owner 2026-10-02).
|
|
4
|
+
*
|
|
5
|
+
* WHAT THIS FILE DOES AND DOES NOT DO (2026-10-03):
|
|
6
|
+
* - payloadForLayerCharter: the layer charter as the payload serves it, with
|
|
7
|
+
* the one exact duplicate removed. `importance_range` is the machine twin
|
|
8
|
+
* of the `importance` band and says the same thing (tests/layers.test.js
|
|
9
|
+
* pins them consistent), so the payload carries the band once. Every word
|
|
10
|
+
* of the charter prose stays; LAYER_CHARTER itself is unchanged.
|
|
11
|
+
* - abridgeReadyPayload: builds the conservative abridged payload from a
|
|
12
|
+
* full ready payload. It is NOT reachable from any host. No tool argument
|
|
13
|
+
* selects it: the amendment says the server decides the mode and the model
|
|
14
|
+
* does not, and a host can never ask for less than the server allows. The
|
|
15
|
+
* server can only prove a returning host with the startup receipts of
|
|
16
|
+
* stage 2 of the design, which do not exist yet, so emet_initialize serves
|
|
17
|
+
* the full payload to every caller. This function is the tested builder
|
|
18
|
+
* stage 2's mode check will call, and the measure of what abridging saves.
|
|
19
|
+
* - resultSize: the size of a payload as the host receives it, in characters
|
|
20
|
+
* and UTF-8 bytes (the connector gateway cuts at 20,000 bytes).
|
|
21
|
+
*
|
|
22
|
+
* FIDELITY RULE (the owner's startup rules): the floor, identity, character, user, the close
|
|
23
|
+
* protocol and tools in force are never pointers and never shortened here.
|
|
24
|
+
* Only install notes, the layer charter and the long instructions become
|
|
25
|
+
* hash-pinned pointers in the abridged form, and the seed and thread indexes
|
|
26
|
+
* and the layer counts become compact indexes.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import crypto from 'crypto';
|
|
30
|
+
import { LAYER_CHARTER } from './layers.js';
|
|
31
|
+
|
|
32
|
+
const sha256 = (s) => crypto.createHash('sha256').update(String(s), 'utf8').digest('hex');
|
|
33
|
+
|
|
34
|
+
/** The layer charter as the startup payload serves it: every field except the duplicate importance_range. */
|
|
35
|
+
export function payloadForLayerCharter(charter = LAYER_CHARTER) {
|
|
36
|
+
const out = {};
|
|
37
|
+
for (const [layer, entry] of Object.entries(charter)) {
|
|
38
|
+
const { importance_range, ...rest } = entry; // eslint-disable-line no-unused-vars
|
|
39
|
+
out[layer] = rest;
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Characters and UTF-8 bytes of the result text a host receives (createSuccessResponse's compact envelope). */
|
|
45
|
+
export function resultSize(data, tool = 'emet_initialize') {
|
|
46
|
+
const text = JSON.stringify({ success: true, tool, timestamp: 1790000000000, data });
|
|
47
|
+
return { chars: text.length, bytes: Buffer.byteLength(text, 'utf8') };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Per-key JSON size of a payload, largest first. */
|
|
51
|
+
export function keySizes(payload) {
|
|
52
|
+
return Object.entries(payload)
|
|
53
|
+
.map(([key, v]) => ({ key, chars: JSON.stringify(v === undefined ? null : v).length }))
|
|
54
|
+
.sort((a, b) => b.chars - a.chars);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The keys every abridged load embeds exactly as the full load has them (the design's keep set; never dropped). */
|
|
58
|
+
export const ABRIDGED_KEEP = Object.freeze([
|
|
59
|
+
'state', 'database', 'source_tag', 'person_id', 'unhandled_drops', 'unhandled_drops_more', 'retired_drops',
|
|
60
|
+
'emet_line', 'emet_line_if_unanswered',
|
|
61
|
+
'tools_in_force', 'instructions_short', 'session_close_protocol', 'operating_discipline', 'charter',
|
|
62
|
+
'identity', 'character', 'character_doc_id', 'character_sha256', 'user',
|
|
63
|
+
'install_notes_doc_id',
|
|
64
|
+
'seeds_due', 'seeds_evaluated_on',
|
|
65
|
+
'record_gaps', 'named_items', 'lessons_pending',
|
|
66
|
+
'floor', 'floor_precedence', 'floor_reassertion'
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
/** Keys that become hash-pinned pointers: text the host already echoed at a full load, unchanged. */
|
|
70
|
+
export const ABRIDGED_PINNED = Object.freeze(['install_notes', 'layer_charter', 'instructions']);
|
|
71
|
+
|
|
72
|
+
/** Keys that become compact indexes (not hash-pinned; the seed index is watermarked in stage 2). */
|
|
73
|
+
export const ABRIDGED_INDEXED = Object.freeze(['seeds', 'threads', 'layer_counts', 'layer_counts_last_30d']);
|
|
74
|
+
|
|
75
|
+
function layerHealthy(h) {
|
|
76
|
+
return !!(h && h.assessed === true && Array.isArray(h.underused) && h.underused.length === 0);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The conservative abridged payload, built from a full ready payload.
|
|
81
|
+
* @param {object} full the object initialize() returns in the ready state
|
|
82
|
+
* @param {object} [opts]
|
|
83
|
+
* @param {string} [opts.since] ISO time of the host's last full-load receipt;
|
|
84
|
+
* recent_context then keeps only entries newer than it (at most 5). Without
|
|
85
|
+
* it recent_context is kept as served (the safe direction).
|
|
86
|
+
* @param {string} [opts.reason] startup_reason to report
|
|
87
|
+
*/
|
|
88
|
+
export function abridgeReadyPayload(full, opts = {}) {
|
|
89
|
+
if (!full || full.state !== 'ready') throw new Error('abridgeReadyPayload: only a ready payload can be abridged');
|
|
90
|
+
const out = { startup_mode: 'abridged', startup_reason: opts.reason || 'returning configured bot (proven by receipt)' };
|
|
91
|
+
for (const k of ABRIDGED_KEEP) if (k in full) out[k] = full[k];
|
|
92
|
+
|
|
93
|
+
const pointers = [];
|
|
94
|
+
if (full.install_notes) {
|
|
95
|
+
pointers.push({
|
|
96
|
+
key: 'install_notes', kind: 'hash_pinned', doc_id: full.install_notes.doc_id,
|
|
97
|
+
version: full.install_notes.version ?? null, sha256: full.install_notes.sha256 || sha256(full.install_notes.content),
|
|
98
|
+
read_via: `read_doc ${full.install_notes.doc_id}`
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
for (const k of ['layer_charter', 'instructions']) {
|
|
102
|
+
if (full[k] != null) {
|
|
103
|
+
pointers.push({ key: k, kind: 'hash_pinned', sha256: sha256(JSON.stringify(full[k])), read_via: 'emet_initialize (a full load; the server serves the whole text again whenever this sha256 differs from the copy you hold)' });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const seeds = Array.isArray(full.seeds) ? full.seeds : [];
|
|
107
|
+
pointers.push({ key: 'seeds', kind: 'index', count: seeds.length, read_via: 'list_docs {prefix: "seeds/"}, then read_doc' });
|
|
108
|
+
const threads = Array.isArray(full.threads) ? full.threads : [];
|
|
109
|
+
pointers.push({
|
|
110
|
+
key: 'threads', kind: 'index', count: threads.length,
|
|
111
|
+
latest_updated_at: threads.map((t) => t.updated_at).filter(Boolean).sort().pop() || null,
|
|
112
|
+
read_via: 'list_docs {prefix: "threads/"}'
|
|
113
|
+
});
|
|
114
|
+
pointers.push({ key: 'layer_counts', kind: 'index', read_via: 'emet_status' });
|
|
115
|
+
|
|
116
|
+
// layer_health only when it reports something; recent_context as a delta.
|
|
117
|
+
if (!layerHealthy(full.layer_health)) out.layer_health = full.layer_health;
|
|
118
|
+
else pointers.push({ key: 'layer_health', kind: 'index', status: 'healthy', read_via: 'emet_status' });
|
|
119
|
+
const recent = Array.isArray(full.recent_context) ? full.recent_context : [];
|
|
120
|
+
out.recent_context = (opts.since ? recent.filter((r) => r.when && r.when > opts.since) : recent).slice(0, 5);
|
|
121
|
+
|
|
122
|
+
out.pointers = pointers;
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export default { payloadForLayerCharter, resultSize, keySizes, abridgeReadyPayload, ABRIDGED_KEEP, ABRIDGED_PINNED, ABRIDGED_INDEXED };
|