@rubytech/create-maxy-code 0.1.434 → 0.1.436
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/package.json +1 -1
- package/payload/platform/docs/superpowers/plans/2026-07-11-task-1557-operator-on-behalf-preference-attribution.md +542 -0
- package/payload/platform/docs/superpowers/specs/2026-07-11-task-1557-operator-on-behalf-preference-attribution-design.md +90 -0
- package/payload/platform/lib/embed-client/dist/index.d.ts +2 -2
- package/payload/platform/lib/embed-client/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/embed-client/dist/index.js +114 -37
- package/payload/platform/lib/embed-client/dist/index.js.map +1 -1
- package/payload/platform/lib/embed-client/src/index.ts +120 -37
- package/payload/platform/plugins/admin/PLUGIN.md +1 -0
- package/payload/platform/plugins/admin/hooks/__tests__/askuserquestion-channel-carrier-gate.test.sh +175 -0
- package/payload/platform/plugins/admin/hooks/askuserquestion-channel-carrier-gate.sh +137 -0
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +2 -2
- package/payload/platform/plugins/docs/references/internals.md +1 -1
- package/payload/platform/plugins/memory/PLUGIN.md +2 -0
- package/payload/platform/plugins/memory/mcp/dist/index.js +42 -22
- package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/embed-progress-reporter.d.ts +22 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/embed-progress-reporter.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/embed-progress-reporter.js +43 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/embed-progress-reporter.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/profile-identity.d.ts +34 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/profile-identity.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/profile-identity.js +22 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/profile-identity.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/resolve-admin-home-account.d.ts +13 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/resolve-admin-home-account.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/resolve-admin-home-account.js +26 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/resolve-admin-home-account.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/embed-progress-reporter.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/embed-progress-reporter.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/embed-progress-reporter.test.js +52 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/embed-progress-reporter.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/embed-progress.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/embed-progress.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/embed-progress.test.js +56 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/embed-progress.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/embed-throughput.test.js +186 -19
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/embed-throughput.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/embed-timeout.test.js +11 -9
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/embed-timeout.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/embeddings-cap.test.js +18 -12
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/embeddings-cap.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-identity.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-identity.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-identity.test.js +72 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-identity.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-update-own-preference-selfheal.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-update-own-preference-selfheal.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-update-own-preference-selfheal.test.js +113 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-update-own-preference-selfheal.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-update-personfields-bootstrap.test.js +2 -2
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-update-personfields-bootstrap.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-update-reconcile.test.js +6 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-update-reconcile.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/resolve-admin-home-account.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/resolve-admin-home-account.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/resolve-admin-home-account.test.js +38 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/resolve-admin-home-account.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.d.ts +9 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js +2 -2
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.js +52 -7
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.js.map +1 -1
- package/payload/platform/plugins/memory/skills/conversational-memory/SKILL.md +8 -0
- package/payload/platform/scripts/lib/provision-account-dir.sh +2 -1
- package/payload/platform/services/claude-session-manager/dist/index.js +20 -0
- package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/interactive-wedge.d.ts +42 -0
- package/payload/platform/services/claude-session-manager/dist/interactive-wedge.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/interactive-wedge.js +149 -0
- package/payload/platform/services/claude-session-manager/dist/interactive-wedge.js.map +1 -0
- package/payload/server/{chunk-UUG6ZSO5.js → chunk-LMLVOZ6G.js} +35 -2
- package/payload/server/maxy-edge.js +1 -1
- package/payload/server/public/assets/{chat-C56bmC6C.js → chat-DUc7bt5W.js} +1 -1
- package/payload/server/public/assets/{operator-B4wN2c53.js → operator-B3Mi3TW0.js} +1 -1
- package/payload/server/public/assets/page-DbUaFfk6.js +32 -0
- package/payload/server/public/assets/{public-DmBKnBkr.js → public-CYhDdDJd.js} +1 -1
- package/payload/server/public/chat.html +2 -2
- package/payload/server/public/operator.html +2 -2
- package/payload/server/public/public.html +2 -2
- package/payload/server/server.js +8 -7
- package/payload/server/public/assets/page-BIfOKi9I.js +0 -32
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
9
|
exports.EMBED_KEEP_ALIVE = exports.EMBED_REQUEST_TIMEOUT_MS = exports.EMBED_MS_PER_INPUT = exports.EMBED_SUBBATCH_COOLDOWN_MS = exports.EMBED_SUBBATCH_SIZE = exports.EMBED_INPUT_MAX_CHARS = void 0;
|
|
10
10
|
exports.embed = embed;
|
|
11
|
-
exports.
|
|
11
|
+
exports.subBatchCharBudget = subBatchCharBudget;
|
|
12
12
|
exports.embedBatch = embedBatch;
|
|
13
13
|
const OLLAMA_URL = process.env.OLLAMA_URL ?? "http://localhost:11434";
|
|
14
14
|
const EMBED_MODEL = process.env.EMBED_MODEL ?? "nomic-embed-text";
|
|
@@ -19,17 +19,18 @@ function envInt(name, def) {
|
|
|
19
19
|
const n = Number(raw);
|
|
20
20
|
return Number.isFinite(n) ? n : def;
|
|
21
21
|
}
|
|
22
|
-
// nomic-embed-text's usable embed context is 2048 tokens. The
|
|
23
|
-
// assumed ~4 chars/token
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
22
|
+
// nomic-embed-text's usable embed context is 2048 tokens. The cap has been
|
|
23
|
+
// tightened twice against real 400s: 8000 (assumed ~4 chars/token) still
|
|
24
|
+
// overflowed on dense emoji/markup, then 6000 (~3 chars/token) still 400'd on
|
|
25
|
+
// the densest sections with "input length exceeds the context length" because
|
|
26
|
+
// that content runs closer to ~2 chars/token. Size the default against a
|
|
27
|
+
// conservative 2 chars/token worst case, reserving a small token margin for the
|
|
28
|
+
// tokenizer's specials, so a capped input is provably under context:
|
|
29
|
+
// (2048 - 48) * 2 = 4000 (Task 1558). Context is env-overridable so raising it
|
|
30
|
+
// raises the cap; EMBED_INPUT_MAX_CHARS is also directly overridable.
|
|
30
31
|
const EMBED_MODEL_CONTEXT_TOKENS = Math.max(1, Math.floor(envInt("EMBED_MODEL_CONTEXT_TOKENS", 2048)));
|
|
31
32
|
const EMBED_CONTEXT_MARGIN_TOKENS = 48;
|
|
32
|
-
const EMBED_WORST_CASE_CHARS_PER_TOKEN =
|
|
33
|
+
const EMBED_WORST_CASE_CHARS_PER_TOKEN = 2;
|
|
33
34
|
exports.EMBED_INPUT_MAX_CHARS = Math.max(1, Math.floor(envInt("EMBED_INPUT_MAX_CHARS", (EMBED_MODEL_CONTEXT_TOKENS - EMBED_CONTEXT_MARGIN_TOKENS) *
|
|
34
35
|
EMBED_WORST_CASE_CHARS_PER_TOKEN)));
|
|
35
36
|
// Batch pacing (Task 960). On a passively-cooled Pi, embedding a whole
|
|
@@ -43,19 +44,36 @@ exports.EMBED_INPUT_MAX_CHARS = Math.max(1, Math.floor(envInt("EMBED_INPUT_MAX_C
|
|
|
43
44
|
// this pacing is defence-in-depth and faster recovery. Both knobs are
|
|
44
45
|
// env-overridable so an operator can tune them from on-device thermal
|
|
45
46
|
// measurement without a code change.
|
|
46
|
-
|
|
47
|
+
//
|
|
48
|
+
// Count ceiling after adaptive sizing (Task 1558). Since Task 1553 each POST is
|
|
49
|
+
// sized by a character budget derived from the device's *measured* ms-per-char,
|
|
50
|
+
// so EMBED_SUBBATCH_SIZE is no longer the primary timeout guard — it is the hard
|
|
51
|
+
// count cap layered on top. It still matters for the one case the char budget
|
|
52
|
+
// cannot catch: the budget is derived from the probe's observed rate, so if the
|
|
53
|
+
// probe runs while the machine is briefly quiet the budget is large and the next
|
|
54
|
+
// POST packs up to this ceiling; if desktop contention then spikes mid-batch,
|
|
55
|
+
// that POST runs far slower than measured and can blow EMBED_REQUEST_TIMEOUT_MS.
|
|
56
|
+
// A ceiling of 4 bounds that worst case (~4 x 17s cold-CPU embeds on a contended
|
|
57
|
+
// shared desktop ~= 68s, under the 120s timeout) so a fast-probe-then-contention
|
|
58
|
+
// batch still fits. On a quiet dedicated appliance (a Pi) the char budget, not
|
|
59
|
+
// this count, is the binding constraint.
|
|
60
|
+
exports.EMBED_SUBBATCH_SIZE = Math.max(1, Math.floor(envInt("EMBED_SUBBATCH_SIZE", 4)));
|
|
47
61
|
exports.EMBED_SUBBATCH_COOLDOWN_MS = Math.max(0, envInt("EMBED_SUBBATCH_COOLDOWN_MS", 1000));
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
// the
|
|
52
|
-
// so
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
// hardware.
|
|
62
|
+
// Drift baseline (Task 1553). This was Task 1533's sizing driver — the budgeted
|
|
63
|
+
// worst-case wall-time for one capped input — but sizing that from a single
|
|
64
|
+
// fixed reading is exactly what over-provisioned a POST on a device slower than
|
|
65
|
+
// the reading and aborted the ingest. Sizing is now measured per batch
|
|
66
|
+
// (nextSubBatchSize), so EMBED_MS_PER_INPUT no longer drives it. It survives as
|
|
67
|
+
// the reconciliation baseline: each POST compares its observed per-input time
|
|
68
|
+
// against this budget and warns (op=drift) when the device runs slower, so a
|
|
69
|
+
// device below budget is surfaced by a standing check rather than by a silent
|
|
70
|
+
// ingest failure. Env-tunable for other hardware.
|
|
57
71
|
exports.EMBED_MS_PER_INPUT = Math.max(1, Math.floor(envInt("EMBED_MS_PER_INPUT", 6000)));
|
|
58
72
|
const EMBED_TIMEOUT_UTILISATION = 0.8;
|
|
73
|
+
// Warn (op=drift) once a POST's observed per-input time exceeds the budget by
|
|
74
|
+
// this factor. 1.5 tolerates ordinary variance while catching a device that is
|
|
75
|
+
// materially slower than EMBED_MS_PER_INPUT.
|
|
76
|
+
const EMBED_DRIFT_WARN_RATIO = 1.5;
|
|
59
77
|
// Explicit per-request timeout, well under undici's 300000ms headers default.
|
|
60
78
|
// Without it a slow or wedged ollama returns no headers and the caller hangs a
|
|
61
79
|
// silent 5 minutes before a bare `fetch failed`. With it the request aborts in
|
|
@@ -130,7 +148,17 @@ async function embedRequest(input) {
|
|
|
130
148
|
// oversized or slow sub-batch is visible before it times out and the
|
|
131
149
|
// throughput-aware sizing is verifiable from the log. inputs is the POST's
|
|
132
150
|
// sub-batch size; maxChars its largest input.
|
|
133
|
-
|
|
151
|
+
const ms = Date.now() - startedAt;
|
|
152
|
+
process.stderr.write(`[embed] op=post inputs=${inputs.length} maxChars=${maxChars} ms=${ms}\n`);
|
|
153
|
+
// Standing reconciliation (Task 1553). Compare this POST's observed per-input
|
|
154
|
+
// time against the EMBED_MS_PER_INPUT budget and warn on drift, so a device
|
|
155
|
+
// slower than budget is caught by a check rather than by a later ingest that
|
|
156
|
+
// aborts. Sizing already adapts to the true rate; this line makes the
|
|
157
|
+
// slowness legible.
|
|
158
|
+
const observedMsPerInput = ms / inputs.length;
|
|
159
|
+
if (inputs.length > 0 && observedMsPerInput > exports.EMBED_MS_PER_INPUT * EMBED_DRIFT_WARN_RATIO) {
|
|
160
|
+
process.stderr.write(`[embed] op=drift observedMsPerInput=${Math.round(observedMsPerInput)} budgetMsPerInput=${exports.EMBED_MS_PER_INPUT} ratio=${(observedMsPerInput / exports.EMBED_MS_PER_INPUT).toFixed(1)}\n`);
|
|
161
|
+
}
|
|
134
162
|
return data.embeddings;
|
|
135
163
|
}
|
|
136
164
|
// POST one already-capped sub-batch of inputs to the embed endpoint.
|
|
@@ -141,30 +169,79 @@ async function embed(text) {
|
|
|
141
169
|
const data = await embedRequest(capInput(text));
|
|
142
170
|
return data[0];
|
|
143
171
|
}
|
|
144
|
-
// The
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
172
|
+
// The character budget for one POST, decided from the device's *measured*
|
|
173
|
+
// throughput rather than a fixed constant (Task 1553). Task 1533 sized every
|
|
174
|
+
// POST by input count against `EMBED_MS_PER_INPUT`, a single optimistic
|
|
175
|
+
// reading; on a device slower than that constant the derived POST was too large
|
|
176
|
+
// to finish inside `EMBED_REQUEST_TIMEOUT_MS`, so it aborted and the atomic
|
|
177
|
+
// ingest landed nothing. Sizing by *count* is also unsafe when inputs differ in
|
|
178
|
+
// size — an embed's wall-time scales with its characters, so a POST sized from
|
|
179
|
+
// a short probe (e.g. a document summary) over-provisions when the later inputs
|
|
180
|
+
// are near-cap sections. Bound each POST by total characters instead:
|
|
181
|
+
// `floor(EMBED_REQUEST_TIMEOUT_MS * 0.8 / msPerChar)`, floored at 1. A near-zero
|
|
182
|
+
// (very fast) rate yields an unbounded budget so the thermal count ceiling
|
|
183
|
+
// (`EMBED_SUBBATCH_SIZE`, Task 960) governs instead.
|
|
184
|
+
function subBatchCharBudget(msPerChar) {
|
|
185
|
+
const perChar = Math.max(msPerChar, Number.MIN_VALUE); // guard div-by-zero
|
|
186
|
+
return Math.max(1, Math.floor((exports.EMBED_REQUEST_TIMEOUT_MS * EMBED_TIMEOUT_UTILISATION) / perChar));
|
|
150
187
|
}
|
|
151
|
-
async function embedBatch(texts
|
|
188
|
+
async function embedBatch(texts,
|
|
189
|
+
// Progress callback (Task 1555). Fired after each sub-batch POST resolves
|
|
190
|
+
// with (done, total): done = cumulative inputs embedded so far, total =
|
|
191
|
+
// inputs.length. memory-ingest passes a callback that emits an MCP progress
|
|
192
|
+
// notification per POST, which resets Claude Code's ~30-minute stdio idle
|
|
193
|
+
// timer so a long single ingest is not aborted mid-write. Kept MCP-agnostic
|
|
194
|
+
// — a plain (done, total) callback — so embed-client carries no MCP concepts
|
|
195
|
+
// and every existing one-arg caller is unchanged.
|
|
196
|
+
onProgress) {
|
|
152
197
|
const inputs = texts.map(capInput);
|
|
153
|
-
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
|
|
198
|
+
// 0 or 1 input is exactly one POST: the warm interactive query path is
|
|
199
|
+
// unaffected, and the empty-array behaviour is unchanged. Report once so the
|
|
200
|
+
// callback contract ("fires after every POST") holds on this path too.
|
|
201
|
+
if (inputs.length <= 1) {
|
|
202
|
+
const vectors = await postBatch(inputs);
|
|
203
|
+
onProgress?.(inputs.length, inputs.length);
|
|
204
|
+
return vectors;
|
|
157
205
|
}
|
|
158
|
-
//
|
|
159
|
-
//
|
|
206
|
+
// Measure-and-size. Probe with one input, then pack each subsequent POST with
|
|
207
|
+
// as many inputs as fit the character budget derived from the worst
|
|
208
|
+
// ms-per-char seen so far, capped by the thermal count ceiling. Using the
|
|
209
|
+
// running max makes the budget monotonic — a mid-batch slowdown (e.g. the
|
|
210
|
+
// ingest itself loading the CPU) shrinks the remainder and it never re-expands
|
|
211
|
+
// into a timeout. Cool between POSTs (not after the last) so the SoC gets gaps
|
|
212
|
+
// (Task 960). Vectors concatenate in input order so callers keep their
|
|
160
213
|
// index-aligned `embeddings[i]` ↔ `texts[i]` contract.
|
|
161
214
|
const out = [];
|
|
162
|
-
|
|
215
|
+
let maxMsPerChar = null;
|
|
216
|
+
for (let i = 0; i < inputs.length;) {
|
|
217
|
+
// Build the next chunk. With no measurement yet, probe with one input.
|
|
218
|
+
let end = i + 1;
|
|
219
|
+
if (maxMsPerChar !== null) {
|
|
220
|
+
const charBudget = subBatchCharBudget(maxMsPerChar);
|
|
221
|
+
let chars = inputs[i].length;
|
|
222
|
+
while (end < inputs.length &&
|
|
223
|
+
end - i < exports.EMBED_SUBBATCH_SIZE &&
|
|
224
|
+
chars + inputs[end].length <= charBudget) {
|
|
225
|
+
chars += inputs[end].length;
|
|
226
|
+
end += 1;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
163
229
|
if (i > 0 && exports.EMBED_SUBBATCH_COOLDOWN_MS > 0) {
|
|
164
230
|
await sleep(exports.EMBED_SUBBATCH_COOLDOWN_MS);
|
|
165
231
|
}
|
|
166
|
-
const chunk = inputs.slice(i,
|
|
167
|
-
|
|
232
|
+
const chunk = inputs.slice(i, end);
|
|
233
|
+
const chunkChars = chunk.reduce((s, t) => s + t.length, 0);
|
|
234
|
+
const startedAt = Date.now();
|
|
235
|
+
const vectors = await postBatch(chunk);
|
|
236
|
+
const observedMsPerChar = (Date.now() - startedAt) / Math.max(chunkChars, 1);
|
|
237
|
+
maxMsPerChar =
|
|
238
|
+
maxMsPerChar === null ? observedMsPerChar : Math.max(maxMsPerChar, observedMsPerChar);
|
|
239
|
+
out.push(...vectors);
|
|
240
|
+
i = end;
|
|
241
|
+
// Report progress after each POST resolves. out.length is the cumulative
|
|
242
|
+
// inputs embedded so far; the final iteration reports out.length ===
|
|
243
|
+
// inputs.length.
|
|
244
|
+
onProgress?.(out.length, inputs.length);
|
|
168
245
|
}
|
|
169
246
|
return out;
|
|
170
247
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA,yEAAyE;AACzE,6EAA6E;AAC7E,2EAA2E;AAC3E,6EAA6E;AAC7E,+EAA+E;AAC/E,gDAAgD;;;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -15,17 +15,18 @@ function envInt(name: string, def: number): number {
|
|
|
15
15
|
return Number.isFinite(n) ? n : def;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
// nomic-embed-text's usable embed context is 2048 tokens. The
|
|
19
|
-
// assumed ~4 chars/token
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
18
|
+
// nomic-embed-text's usable embed context is 2048 tokens. The cap has been
|
|
19
|
+
// tightened twice against real 400s: 8000 (assumed ~4 chars/token) still
|
|
20
|
+
// overflowed on dense emoji/markup, then 6000 (~3 chars/token) still 400'd on
|
|
21
|
+
// the densest sections with "input length exceeds the context length" because
|
|
22
|
+
// that content runs closer to ~2 chars/token. Size the default against a
|
|
23
|
+
// conservative 2 chars/token worst case, reserving a small token margin for the
|
|
24
|
+
// tokenizer's specials, so a capped input is provably under context:
|
|
25
|
+
// (2048 - 48) * 2 = 4000 (Task 1558). Context is env-overridable so raising it
|
|
26
|
+
// raises the cap; EMBED_INPUT_MAX_CHARS is also directly overridable.
|
|
26
27
|
const EMBED_MODEL_CONTEXT_TOKENS = Math.max(1, Math.floor(envInt("EMBED_MODEL_CONTEXT_TOKENS", 2048)));
|
|
27
28
|
const EMBED_CONTEXT_MARGIN_TOKENS = 48;
|
|
28
|
-
const EMBED_WORST_CASE_CHARS_PER_TOKEN =
|
|
29
|
+
const EMBED_WORST_CASE_CHARS_PER_TOKEN = 2;
|
|
29
30
|
export const EMBED_INPUT_MAX_CHARS = Math.max(
|
|
30
31
|
1,
|
|
31
32
|
Math.floor(
|
|
@@ -48,23 +49,40 @@ export const EMBED_INPUT_MAX_CHARS = Math.max(
|
|
|
48
49
|
// this pacing is defence-in-depth and faster recovery. Both knobs are
|
|
49
50
|
// env-overridable so an operator can tune them from on-device thermal
|
|
50
51
|
// measurement without a code change.
|
|
51
|
-
|
|
52
|
+
//
|
|
53
|
+
// Count ceiling after adaptive sizing (Task 1558). Since Task 1553 each POST is
|
|
54
|
+
// sized by a character budget derived from the device's *measured* ms-per-char,
|
|
55
|
+
// so EMBED_SUBBATCH_SIZE is no longer the primary timeout guard — it is the hard
|
|
56
|
+
// count cap layered on top. It still matters for the one case the char budget
|
|
57
|
+
// cannot catch: the budget is derived from the probe's observed rate, so if the
|
|
58
|
+
// probe runs while the machine is briefly quiet the budget is large and the next
|
|
59
|
+
// POST packs up to this ceiling; if desktop contention then spikes mid-batch,
|
|
60
|
+
// that POST runs far slower than measured and can blow EMBED_REQUEST_TIMEOUT_MS.
|
|
61
|
+
// A ceiling of 4 bounds that worst case (~4 x 17s cold-CPU embeds on a contended
|
|
62
|
+
// shared desktop ~= 68s, under the 120s timeout) so a fast-probe-then-contention
|
|
63
|
+
// batch still fits. On a quiet dedicated appliance (a Pi) the char budget, not
|
|
64
|
+
// this count, is the binding constraint.
|
|
65
|
+
export const EMBED_SUBBATCH_SIZE = Math.max(1, Math.floor(envInt("EMBED_SUBBATCH_SIZE", 4)));
|
|
52
66
|
export const EMBED_SUBBATCH_COOLDOWN_MS = Math.max(
|
|
53
67
|
0,
|
|
54
68
|
envInt("EMBED_SUBBATCH_COOLDOWN_MS", 1000),
|
|
55
69
|
);
|
|
56
70
|
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
// the
|
|
61
|
-
// so
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
// hardware.
|
|
71
|
+
// Drift baseline (Task 1553). This was Task 1533's sizing driver — the budgeted
|
|
72
|
+
// worst-case wall-time for one capped input — but sizing that from a single
|
|
73
|
+
// fixed reading is exactly what over-provisioned a POST on a device slower than
|
|
74
|
+
// the reading and aborted the ingest. Sizing is now measured per batch
|
|
75
|
+
// (nextSubBatchSize), so EMBED_MS_PER_INPUT no longer drives it. It survives as
|
|
76
|
+
// the reconciliation baseline: each POST compares its observed per-input time
|
|
77
|
+
// against this budget and warns (op=drift) when the device runs slower, so a
|
|
78
|
+
// device below budget is surfaced by a standing check rather than by a silent
|
|
79
|
+
// ingest failure. Env-tunable for other hardware.
|
|
66
80
|
export const EMBED_MS_PER_INPUT = Math.max(1, Math.floor(envInt("EMBED_MS_PER_INPUT", 6000)));
|
|
67
81
|
const EMBED_TIMEOUT_UTILISATION = 0.8;
|
|
82
|
+
// Warn (op=drift) once a POST's observed per-input time exceeds the budget by
|
|
83
|
+
// this factor. 1.5 tolerates ordinary variance while catching a device that is
|
|
84
|
+
// materially slower than EMBED_MS_PER_INPUT.
|
|
85
|
+
const EMBED_DRIFT_WARN_RATIO = 1.5;
|
|
68
86
|
|
|
69
87
|
// Explicit per-request timeout, well under undici's 300000ms headers default.
|
|
70
88
|
// Without it a slow or wedged ollama returns no headers and the caller hangs a
|
|
@@ -157,9 +175,21 @@ async function embedRequest(input: string | string[]): Promise<number[][]> {
|
|
|
157
175
|
// oversized or slow sub-batch is visible before it times out and the
|
|
158
176
|
// throughput-aware sizing is verifiable from the log. inputs is the POST's
|
|
159
177
|
// sub-batch size; maxChars its largest input.
|
|
178
|
+
const ms = Date.now() - startedAt;
|
|
160
179
|
process.stderr.write(
|
|
161
|
-
`[embed] op=post inputs=${inputs.length} maxChars=${maxChars} ms=${
|
|
180
|
+
`[embed] op=post inputs=${inputs.length} maxChars=${maxChars} ms=${ms}\n`,
|
|
162
181
|
);
|
|
182
|
+
// Standing reconciliation (Task 1553). Compare this POST's observed per-input
|
|
183
|
+
// time against the EMBED_MS_PER_INPUT budget and warn on drift, so a device
|
|
184
|
+
// slower than budget is caught by a check rather than by a later ingest that
|
|
185
|
+
// aborts. Sizing already adapts to the true rate; this line makes the
|
|
186
|
+
// slowness legible.
|
|
187
|
+
const observedMsPerInput = ms / inputs.length;
|
|
188
|
+
if (inputs.length > 0 && observedMsPerInput > EMBED_MS_PER_INPUT * EMBED_DRIFT_WARN_RATIO) {
|
|
189
|
+
process.stderr.write(
|
|
190
|
+
`[embed] op=drift observedMsPerInput=${Math.round(observedMsPerInput)} budgetMsPerInput=${EMBED_MS_PER_INPUT} ratio=${(observedMsPerInput / EMBED_MS_PER_INPUT).toFixed(1)}\n`,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
163
193
|
return data.embeddings;
|
|
164
194
|
}
|
|
165
195
|
|
|
@@ -173,36 +203,89 @@ export async function embed(text: string): Promise<number[]> {
|
|
|
173
203
|
return data[0];
|
|
174
204
|
}
|
|
175
205
|
|
|
176
|
-
// The
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
|
|
180
|
-
|
|
206
|
+
// The character budget for one POST, decided from the device's *measured*
|
|
207
|
+
// throughput rather than a fixed constant (Task 1553). Task 1533 sized every
|
|
208
|
+
// POST by input count against `EMBED_MS_PER_INPUT`, a single optimistic
|
|
209
|
+
// reading; on a device slower than that constant the derived POST was too large
|
|
210
|
+
// to finish inside `EMBED_REQUEST_TIMEOUT_MS`, so it aborted and the atomic
|
|
211
|
+
// ingest landed nothing. Sizing by *count* is also unsafe when inputs differ in
|
|
212
|
+
// size — an embed's wall-time scales with its characters, so a POST sized from
|
|
213
|
+
// a short probe (e.g. a document summary) over-provisions when the later inputs
|
|
214
|
+
// are near-cap sections. Bound each POST by total characters instead:
|
|
215
|
+
// `floor(EMBED_REQUEST_TIMEOUT_MS * 0.8 / msPerChar)`, floored at 1. A near-zero
|
|
216
|
+
// (very fast) rate yields an unbounded budget so the thermal count ceiling
|
|
217
|
+
// (`EMBED_SUBBATCH_SIZE`, Task 960) governs instead.
|
|
218
|
+
export function subBatchCharBudget(msPerChar: number): number {
|
|
219
|
+
const perChar = Math.max(msPerChar, Number.MIN_VALUE); // guard div-by-zero
|
|
220
|
+
return Math.max(
|
|
181
221
|
1,
|
|
182
|
-
Math.floor((EMBED_REQUEST_TIMEOUT_MS * EMBED_TIMEOUT_UTILISATION) /
|
|
222
|
+
Math.floor((EMBED_REQUEST_TIMEOUT_MS * EMBED_TIMEOUT_UTILISATION) / perChar),
|
|
183
223
|
);
|
|
184
|
-
return Math.min(EMBED_SUBBATCH_SIZE, durationCap);
|
|
185
224
|
}
|
|
186
225
|
|
|
187
|
-
export async function embedBatch(
|
|
226
|
+
export async function embedBatch(
|
|
227
|
+
texts: string[],
|
|
228
|
+
// Progress callback (Task 1555). Fired after each sub-batch POST resolves
|
|
229
|
+
// with (done, total): done = cumulative inputs embedded so far, total =
|
|
230
|
+
// inputs.length. memory-ingest passes a callback that emits an MCP progress
|
|
231
|
+
// notification per POST, which resets Claude Code's ~30-minute stdio idle
|
|
232
|
+
// timer so a long single ingest is not aborted mid-write. Kept MCP-agnostic
|
|
233
|
+
// — a plain (done, total) callback — so embed-client carries no MCP concepts
|
|
234
|
+
// and every existing one-arg caller is unchanged.
|
|
235
|
+
onProgress?: (done: number, total: number) => void,
|
|
236
|
+
): Promise<number[][]> {
|
|
188
237
|
const inputs = texts.map(capInput);
|
|
189
|
-
const subBatch = effectiveSubBatchSize();
|
|
190
238
|
|
|
191
|
-
//
|
|
192
|
-
|
|
193
|
-
|
|
239
|
+
// 0 or 1 input is exactly one POST: the warm interactive query path is
|
|
240
|
+
// unaffected, and the empty-array behaviour is unchanged. Report once so the
|
|
241
|
+
// callback contract ("fires after every POST") holds on this path too.
|
|
242
|
+
if (inputs.length <= 1) {
|
|
243
|
+
const vectors = await postBatch(inputs);
|
|
244
|
+
onProgress?.(inputs.length, inputs.length);
|
|
245
|
+
return vectors;
|
|
194
246
|
}
|
|
195
247
|
|
|
196
|
-
//
|
|
197
|
-
//
|
|
248
|
+
// Measure-and-size. Probe with one input, then pack each subsequent POST with
|
|
249
|
+
// as many inputs as fit the character budget derived from the worst
|
|
250
|
+
// ms-per-char seen so far, capped by the thermal count ceiling. Using the
|
|
251
|
+
// running max makes the budget monotonic — a mid-batch slowdown (e.g. the
|
|
252
|
+
// ingest itself loading the CPU) shrinks the remainder and it never re-expands
|
|
253
|
+
// into a timeout. Cool between POSTs (not after the last) so the SoC gets gaps
|
|
254
|
+
// (Task 960). Vectors concatenate in input order so callers keep their
|
|
198
255
|
// index-aligned `embeddings[i]` ↔ `texts[i]` contract.
|
|
199
256
|
const out: number[][] = [];
|
|
200
|
-
|
|
257
|
+
let maxMsPerChar: number | null = null;
|
|
258
|
+
for (let i = 0; i < inputs.length; ) {
|
|
259
|
+
// Build the next chunk. With no measurement yet, probe with one input.
|
|
260
|
+
let end = i + 1;
|
|
261
|
+
if (maxMsPerChar !== null) {
|
|
262
|
+
const charBudget = subBatchCharBudget(maxMsPerChar);
|
|
263
|
+
let chars = inputs[i].length;
|
|
264
|
+
while (
|
|
265
|
+
end < inputs.length &&
|
|
266
|
+
end - i < EMBED_SUBBATCH_SIZE &&
|
|
267
|
+
chars + inputs[end].length <= charBudget
|
|
268
|
+
) {
|
|
269
|
+
chars += inputs[end].length;
|
|
270
|
+
end += 1;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
201
273
|
if (i > 0 && EMBED_SUBBATCH_COOLDOWN_MS > 0) {
|
|
202
274
|
await sleep(EMBED_SUBBATCH_COOLDOWN_MS);
|
|
203
275
|
}
|
|
204
|
-
const chunk = inputs.slice(i,
|
|
205
|
-
|
|
276
|
+
const chunk = inputs.slice(i, end);
|
|
277
|
+
const chunkChars = chunk.reduce((s, t) => s + t.length, 0);
|
|
278
|
+
const startedAt = Date.now();
|
|
279
|
+
const vectors = await postBatch(chunk);
|
|
280
|
+
const observedMsPerChar = (Date.now() - startedAt) / Math.max(chunkChars, 1);
|
|
281
|
+
maxMsPerChar =
|
|
282
|
+
maxMsPerChar === null ? observedMsPerChar : Math.max(maxMsPerChar, observedMsPerChar);
|
|
283
|
+
out.push(...vectors);
|
|
284
|
+
i = end;
|
|
285
|
+
// Report progress after each POST resolves. out.length is the cumulative
|
|
286
|
+
// inputs embedded so far; the final iteration reports out.length ===
|
|
287
|
+
// inputs.length.
|
|
288
|
+
onProgress?.(out.length, inputs.length);
|
|
206
289
|
}
|
|
207
290
|
return out;
|
|
208
291
|
}
|
|
@@ -174,6 +174,7 @@ Tools are available via the `admin` MCP server.
|
|
|
174
174
|
|
|
175
175
|
- `hooks/webfetch-preflight.mjs` — short-circuits WebFetch on JS-SPA shells with a structured `WEBFETCH_CANNOT_READ_JS_SPA` error so the agent surfaces a loud failure to the owner instead of paying the 60s extraction timeout. Fail-open on any internal error.
|
|
176
176
|
- `hooks/askuserquestion-investigate-gate.sh` — PreToolUse matcher=`AskUserQuestion`. Blocks the question (exit 2) when no read-only investigation tool has fired since the latest real user turn in the session JSONL. The structural fix for the failure class where the agent fabricates a menu before evidence-gathering (session `c085ec2c-46fb-4b73-8865-68cf85866ea8` 2026-05-22 — "change remote access password" → invented options "Admin PIN / Cloudflare tunnel / WiFi password" with zero prior tool_use; post-correction the agent immediately fired `remote-auth-status` → `ToolSearch` → `remote-auth-set-password`, proving it knew the moves). **Allowlist** (exact, with trailing `__<tool>` suffix-match for namespaced `mcp__plugin_<plugin>_<server>__<tool>` aliases): `ToolSearch`, `Grep`, `Glob`, `Read`, `LS`, `NotebookRead`, `Bash`, `WebFetch`, `WebSearch`, plus the read-only admin / memory MCP tools (`*-status`, `*-list`, `*-read`, `skill-find`, `memory-find-candidates`, `profile-read`, `conversation-list`, `memory-list-attachments`, `memory-read-attachment`). **Block message:** `Blocked: AskUserQuestion requires at least one investigation tool (ToolSearch, Grep, Read, *-list, *-status, *-read, skill-find, ...) earlier in this turn. Search the operator's literal phrase first.` **Log line** (stderr, one per call): `[ask-gate] decision=<allow|block> sessionId=<id8> seen=<csv|-> reason=<allowlist-hit|no-investigation|fail-open-no-transcript|fail-open-parse-error>`. **Fail-open** on missing transcript or parse error — nudges, never bricks the UI.
|
|
177
|
+
- `hooks/askuserquestion-channel-carrier-gate.sh` — PreToolUse matcher=`AskUserQuestion`, registered as a **second command on the same matcher** as the investigate-gate (either exit 2 blocks; the two guard different conditions). Blocks the question (exit 2) when the session has a native channel attached, because the Claude Code channel protocol carries only the two permission methods — there is no carrier for the `AskUserQuestion` elicitation, so on an rc-spawn channel (webchat/WhatsApp/Telegram) session the harness would block the turn awaiting a selection that can never arrive and the reader renders only delivered replies, wedging the turn (live SiteDesk incident session `1a085718` 2026-07-11: `/chat` stalled on the delivered "On it…" ack with no card). **Signal:** the rc-spawn writes the channel MCP registration as a config file in `os.tmpdir()` keyed by session id (`maxy-channel-<sid>.json` / `maxy-webchat-channel-<sid>.json` / `maxy-wa-channel-<sid>.json` / `maxy-telegram-channel-<sid>.json`, `sid` sanitized `[^A-Za-z0-9_-]→_` exactly as the writers do); its presence at tool-call time is the authoritative "channel attached to this session" marker. The hook resolves the tmpdir the way Node's `os.tmpdir()` does (`${TMPDIR:-/tmp}`, trailing slash stripped). Correct in both readings of the claude.ai/code scoping question: a Pi channel process sees the tmpfile (block); a claude.ai/code process on another host never sees the Pi tmpfile (fail-open → allow, that surface can answer the card). **Block message:** `Blocked: this is a native-channel session and AskUserQuestion cannot be delivered over a channel (the channel protocol carries no elicitation card, so the turn would wedge). Ask the same question as prose in your channel reply tool and wait for the operator's next message.` **Log line** (stderr, one per decisive call): `[ask-channel] decision=<allow|block> sessionId=<id8> channel=<attached|none> reason=<channel-attached|no-channel|fail-open-no-envelope|fail-open-no-session>`. **Fail-open** on no envelope, blank session id, or absent channel file — nudges, never bricks the UI. The complementary standing check (`interactive-wedge` in the session manager) catches any un-carried interactive tool_use that slips past this hook.
|
|
177
178
|
- `hooks/mcp-tool-missing.sh` — **PostToolUse hook on `mcp__.*` (directive 3).** Defence-in-depth for the `No such tool available: mcp__…` failure class that the name-binding is built to eliminate. Fires on any MCP tool call; no-op unless the `tool_response` carries `No such tool available` AND the qualified name resolves to a maxy plugin (read from the generated `hooks/lib/maxy-mcp-plugins.txt`). On a maxy match it logs one deterministic `[mcp-tool-missing] server=<server> tool=<tool>` line and exits 2 with a fixed envelope on stderr, so the agent relays a named server-unavailable failure instead of narrating "warming up" or blind-retrying. A missing non-maxy bridge tool (Playwright etc., upstream-owned) passes through (exit 0). The maxy-plugin list is regenerated and gate-diffed by `platform/scripts/check-canonical-tool-names.mjs`.
|
|
178
179
|
- `hooks/post-tool-use-agent.sh` — **PostToolUse hook on `Agent`.** Drains any subagent hook-decision buffers under `~/.maxy-code/logs/hook-decisions/` modified since this parent's previous PostToolUse-Agent fire (cursor file keyed by parent session id), prints one `[hook-propagate]` line per record to stdout — Claude Code attaches the stdout as a `hook_success` attachment on the parent JSONL, making the records grep-queryable from the parent session alone. Rotates consumed buffers to `consumed/`. Emits one `[hook-propagate-census] parentSession=<…> subagentHooksObserved=<N> attachmentsEmitted=<M>` line per fire to stdout and server.log; `N != M` is the propagation regression signal. The companion emitter library `hooks/lib/hook-emit.sh` is sourced by `post-tool-use-agent.sh` and any other hook that records a block decision (4 KB stderr truncation, `truncated=true` set on the record).
|
|
179
180
|
- `hooks/admin-authoring-observer.sh` — **PostToolUse hook on Write and Edit.** Observation only — never blocks; exits 0 on every path. Fires when the admin agent (not a specialist subagent — gated by `MAXY_SPECIALIST` env) writes or edits a file under `<accountDir>/output/`. Walks the session transcript from the latest real-user turn forward to detect any prior `Task` `tool_use` whose `subagent_type` starts with `specialists:`. Emits one stderr line `[admin-authoring] inline-write path=<rel> priorSpecialistSpawnInTurn=<true|false|unknown>`. A `false` value on a long-form prose file is the regression signal this hook was designed to make visible — the BioSymm proposal session (admin authored a customer-facing proposal inline despite content-producer being installed) is the failure mode this surfaces mechanically. Mechanical enforcement (refuse the write, force a re-spawn) is deferred per the task spec.
|