@rubytech/create-maxy-code 0.1.435 → 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 +1 -1
- package/payload/platform/lib/embed-client/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/embed-client/dist/index.js +42 -13
- package/payload/platform/lib/embed-client/dist/index.js.map +1 -1
- package/payload/platform/lib/embed-client/src/index.ts +44 -13
- 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 +30 -5
- 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__/embeddings-cap.test.js +9 -7
- 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-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__/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 +5 -3
- 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/server/{chunk-DAYMXQG2.js → chunk-LMLVOZ6G.js} +7 -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 +3 -3
- package/payload/server/public/assets/page-BIfOKi9I.js +0 -32
|
@@ -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,7 +44,20 @@ 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
62
|
// Drift baseline (Task 1553). This was Task 1533's sizing driver — the budgeted
|
|
49
63
|
// worst-case wall-time for one capped input — but sizing that from a single
|
|
@@ -171,12 +185,23 @@ function subBatchCharBudget(msPerChar) {
|
|
|
171
185
|
const perChar = Math.max(msPerChar, Number.MIN_VALUE); // guard div-by-zero
|
|
172
186
|
return Math.max(1, Math.floor((exports.EMBED_REQUEST_TIMEOUT_MS * EMBED_TIMEOUT_UTILISATION) / perChar));
|
|
173
187
|
}
|
|
174
|
-
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) {
|
|
175
197
|
const inputs = texts.map(capInput);
|
|
176
198
|
// 0 or 1 input is exactly one POST: the warm interactive query path is
|
|
177
|
-
// unaffected, and the empty-array behaviour is unchanged.
|
|
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.
|
|
178
201
|
if (inputs.length <= 1) {
|
|
179
|
-
|
|
202
|
+
const vectors = await postBatch(inputs);
|
|
203
|
+
onProgress?.(inputs.length, inputs.length);
|
|
204
|
+
return vectors;
|
|
180
205
|
}
|
|
181
206
|
// Measure-and-size. Probe with one input, then pack each subsequent POST with
|
|
182
207
|
// as many inputs as fit the character budget derived from the worst
|
|
@@ -213,6 +238,10 @@ async function embedBatch(texts) {
|
|
|
213
238
|
maxMsPerChar === null ? observedMsPerChar : Math.max(maxMsPerChar, observedMsPerChar);
|
|
214
239
|
out.push(...vectors);
|
|
215
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);
|
|
216
245
|
}
|
|
217
246
|
return out;
|
|
218
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,7 +49,20 @@ 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),
|
|
@@ -209,13 +223,26 @@ export function subBatchCharBudget(msPerChar: number): number {
|
|
|
209
223
|
);
|
|
210
224
|
}
|
|
211
225
|
|
|
212
|
-
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[][]> {
|
|
213
237
|
const inputs = texts.map(capInput);
|
|
214
238
|
|
|
215
239
|
// 0 or 1 input is exactly one POST: the warm interactive query path is
|
|
216
|
-
// unaffected, and the empty-array behaviour is unchanged.
|
|
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.
|
|
217
242
|
if (inputs.length <= 1) {
|
|
218
|
-
|
|
243
|
+
const vectors = await postBatch(inputs);
|
|
244
|
+
onProgress?.(inputs.length, inputs.length);
|
|
245
|
+
return vectors;
|
|
219
246
|
}
|
|
220
247
|
|
|
221
248
|
// Measure-and-size. Probe with one input, then pack each subsequent POST with
|
|
@@ -255,6 +282,10 @@ export async function embedBatch(texts: string[]): Promise<number[][]> {
|
|
|
255
282
|
maxMsPerChar === null ? observedMsPerChar : Math.max(maxMsPerChar, observedMsPerChar);
|
|
256
283
|
out.push(...vectors);
|
|
257
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);
|
|
258
289
|
}
|
|
259
290
|
return out;
|
|
260
291
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: platform-architecture
|
|
3
3
|
description: Use when grounding any documented-surface claim about what Maxy ships — plugins, skills, specialists, install/deploy flows, internals. This is the install catalogue, not evidence of what is enabled on the current account. For install state on this account, call `capabilities-here`; for documented surface, cite the `Source:` URL inline.
|
|
4
|
-
content-hash: sha256:
|
|
4
|
+
content-hash: sha256:bd08bc341313562b1dd2139010b1120b23d7aab130e1d32c3e800678ad559f7c
|
|
5
5
|
brand: maxy-code
|
|
6
6
|
product-name: Maxy
|
|
7
7
|
---
|
|
@@ -3566,7 +3566,7 @@ If Ollama is unavailable at write time, the node is stored without an embedding
|
|
|
3566
3566
|
|
|
3567
3567
|
The `memory-reindex` tool backfills missing embeddings by iterating nodes where `embedding IS NULL`. It uses the bounded representation (the `summary` for document/archive labels, never the raw body), embeds in batches with a per-node fallback on batch error, and **skips and continues** past a failing node — keeping skipped nodes out of the re-fetch via an exclude set — instead of aborting the whole run. `embed()`/`embedBatch()` cap oversized input (`EMBED_INPUT_MAX_CHARS`) before the Ollama POST, so a large body embeds truncated rather than throwing. Per-node `[reindex] op=embedded|skip-error` lines and a terminal `[reindex] op=done processed=N skipped=M`.
|
|
3568
3568
|
|
|
3569
|
-
`embedBatch()` also paces large batches. A document's summary plus every section body in one `/api/embed` POST pins ollama's `llama-server` across all cores for the whole batch with no idle gaps; on a passively-cooled Pi the SoC climbs past its soft-temp limit and the board thermally hard-resets. `embedBatch` splits a multi-input array into sub-batches, waiting `EMBED_SUBBATCH_COOLDOWN_MS` (default 1000) between them so the SoC gets cooling gaps; vectors are concatenated in input order. Each POST is sized from the device's *measured* throughput, not a fixed constant: the first POST of a multi-input batch is a single-input probe (the only size provably safe without a measurement), and every POST's observed per-character wall-time folds into a running maximum. Subsequent POSTs are packed with as many inputs as fit a character budget `floor(EMBED_REQUEST_TIMEOUT_MS * 0.8 / observedMsPerChar)`, capped by the `EMBED_SUBBATCH_SIZE` count ceiling. Sizing by characters rather than input count matters because an embed's wall-time scales with its characters — a POST sized by count from a short probe (a document summary) would over-provision when the later inputs are near-cap sections and abort. So a CPU-only device slower than the old budget gets character-bounded POSTs that complete inside `EMBED_REQUEST_TIMEOUT_MS`, instead of a fixed 16/32-input POST that exceeds it and aborts the atomic ingest write; the running maximum is monotonic, so a mid-batch slowdown shrinks the remainder and never re-expands into a timeout. `EMBED_SUBBATCH_SIZE` (default
|
|
3569
|
+
`embedBatch()` also paces large batches. A document's summary plus every section body in one `/api/embed` POST pins ollama's `llama-server` across all cores for the whole batch with no idle gaps; on a passively-cooled Pi the SoC climbs past its soft-temp limit and the board thermally hard-resets. `embedBatch` splits a multi-input array into sub-batches, waiting `EMBED_SUBBATCH_COOLDOWN_MS` (default 1000) between them so the SoC gets cooling gaps; vectors are concatenated in input order. Each POST is sized from the device's *measured* throughput, not a fixed constant: the first POST of a multi-input batch is a single-input probe (the only size provably safe without a measurement), and every POST's observed per-character wall-time folds into a running maximum. Subsequent POSTs are packed with as many inputs as fit a character budget `floor(EMBED_REQUEST_TIMEOUT_MS * 0.8 / observedMsPerChar)`, capped by the `EMBED_SUBBATCH_SIZE` count ceiling. Sizing by characters rather than input count matters because an embed's wall-time scales with its characters — a POST sized by count from a short probe (a document summary) would over-provision when the later inputs are near-cap sections and abort. So a CPU-only device slower than the old budget gets character-bounded POSTs that complete inside `EMBED_REQUEST_TIMEOUT_MS`, instead of a fixed 16/32-input POST that exceeds it and aborts the atomic ingest write; the running maximum is monotonic, so a mid-batch slowdown shrinks the remainder and never re-expands into a timeout. `EMBED_SUBBATCH_SIZE` (default 4) is the thermal count ceiling and the hard cap on any POST — kept low because the character budget is derived from the probe's observed rate, so a probe taken while the machine is briefly quiet would otherwise let the next POST grow to the ceiling and then blow `EMBED_REQUEST_TIMEOUT_MS` if desktop contention spikes mid-batch; a ceiling of 4 bounds that worst case (~4 near-cap cold-CPU embeds) under the timeout on a contended shared desktop, while a quiet appliance is governed by the character budget instead. `EMBED_MS_PER_INPUT` (default 6000 ms, env-tunable) no longer sizes the POST; it is the reconciliation baseline — after each POST an `[embed] op=drift` line warns when the observed per-input time exceeds the budget by more than 1.5×, so a device slower than budget is surfaced by a standing check rather than by a later ingest failure. Every `/api/embed` POST also carries `keep_alive` (`EMBED_KEEP_ALIVE`, default `-1` = keep the model resident forever) so an interactive graph query-embed never cold-loads and blows its 5 s timeout. All knobs are env-overridable for on-device tuning. The load-bearing thermal protection is the installer's `CPUQuota` cap on `ollama.service` (every embed caller is bounded there regardless of batch shape); this pacing is defence-in-depth. The UI's file-index path uses its own single-input embed client in `platform/ui/app/lib/neo4j-store.ts` (one embed per file, not a batch), so it is covered by the CPU cap, not by `embedBatch` pacing.
|
|
3570
3570
|
|
|
3571
3571
|
---
|
|
3572
3572
|
|
|
@@ -95,7 +95,7 @@ If Ollama is unavailable at write time, the node is stored without an embedding
|
|
|
95
95
|
|
|
96
96
|
The `memory-reindex` tool backfills missing embeddings by iterating nodes where `embedding IS NULL`. It uses the bounded representation (the `summary` for document/archive labels, never the raw body), embeds in batches with a per-node fallback on batch error, and **skips and continues** past a failing node — keeping skipped nodes out of the re-fetch via an exclude set — instead of aborting the whole run. `embed()`/`embedBatch()` cap oversized input (`EMBED_INPUT_MAX_CHARS`) before the Ollama POST, so a large body embeds truncated rather than throwing. Per-node `[reindex] op=embedded|skip-error` lines and a terminal `[reindex] op=done processed=N skipped=M`.
|
|
97
97
|
|
|
98
|
-
`embedBatch()` also paces large batches. A document's summary plus every section body in one `/api/embed` POST pins ollama's `llama-server` across all cores for the whole batch with no idle gaps; on a passively-cooled Pi the SoC climbs past its soft-temp limit and the board thermally hard-resets. `embedBatch` splits a multi-input array into sub-batches, waiting `EMBED_SUBBATCH_COOLDOWN_MS` (default 1000) between them so the SoC gets cooling gaps; vectors are concatenated in input order. Each POST is sized from the device's *measured* throughput, not a fixed constant: the first POST of a multi-input batch is a single-input probe (the only size provably safe without a measurement), and every POST's observed per-character wall-time folds into a running maximum. Subsequent POSTs are packed with as many inputs as fit a character budget `floor(EMBED_REQUEST_TIMEOUT_MS * 0.8 / observedMsPerChar)`, capped by the `EMBED_SUBBATCH_SIZE` count ceiling. Sizing by characters rather than input count matters because an embed's wall-time scales with its characters — a POST sized by count from a short probe (a document summary) would over-provision when the later inputs are near-cap sections and abort. So a CPU-only device slower than the old budget gets character-bounded POSTs that complete inside `EMBED_REQUEST_TIMEOUT_MS`, instead of a fixed 16/32-input POST that exceeds it and aborts the atomic ingest write; the running maximum is monotonic, so a mid-batch slowdown shrinks the remainder and never re-expands into a timeout. `EMBED_SUBBATCH_SIZE` (default
|
|
98
|
+
`embedBatch()` also paces large batches. A document's summary plus every section body in one `/api/embed` POST pins ollama's `llama-server` across all cores for the whole batch with no idle gaps; on a passively-cooled Pi the SoC climbs past its soft-temp limit and the board thermally hard-resets. `embedBatch` splits a multi-input array into sub-batches, waiting `EMBED_SUBBATCH_COOLDOWN_MS` (default 1000) between them so the SoC gets cooling gaps; vectors are concatenated in input order. Each POST is sized from the device's *measured* throughput, not a fixed constant: the first POST of a multi-input batch is a single-input probe (the only size provably safe without a measurement), and every POST's observed per-character wall-time folds into a running maximum. Subsequent POSTs are packed with as many inputs as fit a character budget `floor(EMBED_REQUEST_TIMEOUT_MS * 0.8 / observedMsPerChar)`, capped by the `EMBED_SUBBATCH_SIZE` count ceiling. Sizing by characters rather than input count matters because an embed's wall-time scales with its characters — a POST sized by count from a short probe (a document summary) would over-provision when the later inputs are near-cap sections and abort. So a CPU-only device slower than the old budget gets character-bounded POSTs that complete inside `EMBED_REQUEST_TIMEOUT_MS`, instead of a fixed 16/32-input POST that exceeds it and aborts the atomic ingest write; the running maximum is monotonic, so a mid-batch slowdown shrinks the remainder and never re-expands into a timeout. `EMBED_SUBBATCH_SIZE` (default 4) is the thermal count ceiling and the hard cap on any POST — kept low because the character budget is derived from the probe's observed rate, so a probe taken while the machine is briefly quiet would otherwise let the next POST grow to the ceiling and then blow `EMBED_REQUEST_TIMEOUT_MS` if desktop contention spikes mid-batch; a ceiling of 4 bounds that worst case (~4 near-cap cold-CPU embeds) under the timeout on a contended shared desktop, while a quiet appliance is governed by the character budget instead. `EMBED_MS_PER_INPUT` (default 6000 ms, env-tunable) no longer sizes the POST; it is the reconciliation baseline — after each POST an `[embed] op=drift` line warns when the observed per-input time exceeds the budget by more than 1.5×, so a device slower than budget is surfaced by a standing check rather than by a later ingest failure. Every `/api/embed` POST also carries `keep_alive` (`EMBED_KEEP_ALIVE`, default `-1` = keep the model resident forever) so an interactive graph query-embed never cold-loads and blows its 5 s timeout. All knobs are env-overridable for on-device tuning. The load-bearing thermal protection is the installer's `CPUQuota` cap on `ollama.service` (every embed caller is bounded there regardless of batch shape); this pacing is defence-in-depth. The UI's file-index path uses its own single-input embed client in `platform/ui/app/lib/neo4j-store.ts` (one embed per file, not a batch), so it is covered by the CPU cap, not by `embedBatch` pacing.
|
|
99
99
|
|
|
100
100
|
---
|
|
101
101
|
|
|
@@ -222,6 +222,8 @@ Read-only window into the extractive graph writes produced under this account. R
|
|
|
222
222
|
|
|
223
223
|
The owner's profile and preferences accumulate organically from conversation — never from questionnaires. Run `skill-load skillName=conversational-memory` for full guidance on when to observe preferences, how to handle remember/forget requests, and how to answer "what do you know about me?" transparently with confidence scores and evidence trail.
|
|
224
224
|
|
|
225
|
+
When a house operator operates a client's sub-account, the session is scoped into that account (`ACCOUNT_ID` is the sub-account, `USER_ID` is the operator, no `HOUSE_ADMIN_SCOPE`). In that session `profile-read`/`profile-update`/`profile-delete` resolve to the account's **owner** `AdminUser`, not the operator: the operator has no `AdminUser` node in the sub-account subgraph, so `resolveProfileIdentity` sees the caller is not an admin of the account it is scoped to and attributes to the seeded owner — the same owner identity the cross-account `targetAccountId` path resolves (above), reached without `targetAccountId` because the re-scoped session cannot set it. An account with no seeded owner rejects the write loudly (`… has no owner AdminUser …`) rather than attributing it to the operator, and emits `[xacct] op=identity tool=profile-* mode=operator-on-behalf target=<sub8> caller=<op8> resolved-userId=<owner8>` on the owner-attributed path. The operator's own preferences live on their own (house) session.
|
|
226
|
+
|
|
225
227
|
## UserProfile + Person Field Management
|
|
226
228
|
|
|
227
229
|
`profile-update` serves three purposes: managing Preference nodes (category/key/value), setting top-level UserProfile node properties via `profileFields`, and setting Person properties on the operator's personal-profile Person via `personFields`.
|
|
@@ -12,6 +12,7 @@ import { buildLiveSchemaSource, defaultSchemaCypherPath, } from "./lib/live-sche
|
|
|
12
12
|
import { memoryReindex } from "./tools/memory-reindex.js";
|
|
13
13
|
import { memoryIngestExtract } from "./tools/memory-ingest-extract.js";
|
|
14
14
|
import { memoryIngest } from "./tools/memory-ingest.js";
|
|
15
|
+
import { makeEmbedProgressReporter } from "./lib/embed-progress-reporter.js";
|
|
15
16
|
import { CONVERSATION_SOURCES } from "./lib/conversation-normalisers/types.js";
|
|
16
17
|
import { kdClassify } from "./tools/kd-classify.js";
|
|
17
18
|
import { memoryArchiveWrite } from "./tools/memory-archive-write.js";
|
|
@@ -42,6 +43,8 @@ import { memoryBrainCaptureRecent } from "./tools/memory-brain-capture-recent.js
|
|
|
42
43
|
import { profileUpdate } from "./tools/profile-update.js";
|
|
43
44
|
import { profileDelete } from "./tools/profile-delete.js";
|
|
44
45
|
import { resolveOwnerUserId } from "./lib/resolve-owner-userid.js";
|
|
46
|
+
import { resolveAdminHomeAccount } from "./lib/resolve-admin-home-account.js";
|
|
47
|
+
import { classifyProfileIdentity } from "./lib/profile-identity.js";
|
|
45
48
|
import { graphPruneDenylistAdd } from "./tools/graph-prune-denylist-add.js";
|
|
46
49
|
import { graphPruneDenylistList } from "./tools/graph-prune-denylist-list.js";
|
|
47
50
|
import { graphPruneDenylistRemove } from "./tools/graph-prune-denylist-remove.js";
|
|
@@ -113,31 +116,42 @@ const XACCT_DESC = "House-scoped admin only: operate on the named sub-account su
|
|
|
113
116
|
// call (the resolver only returns a target for a house-authorised session).
|
|
114
117
|
const BOOT_ACCOUNT_ID = accountId;
|
|
115
118
|
async function resolveProfileIdentity(toolName, crossAccount, resolvedAccountId, callerUserId) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
119
|
+
// Resolve the facts the pure classifier needs. On the own-account path the
|
|
120
|
+
// account's owner is resolved only when the caller is NOT operating their
|
|
121
|
+
// home account (a foreign house operator inside a client's sub-account), so
|
|
122
|
+
// the common owner-is-speaker path costs one AdminUser lookup, not two.
|
|
123
|
+
const callerHomeAccountId = !crossAccount && callerUserId ? await resolveAdminHomeAccount(callerUserId) : null;
|
|
124
|
+
const needOwner = crossAccount || (!!callerUserId && callerHomeAccountId !== resolvedAccountId);
|
|
125
|
+
const ownerUserId = needOwner ? await resolveOwnerUserId(resolvedAccountId) : null;
|
|
126
|
+
const decision = classifyProfileIdentity({
|
|
127
|
+
crossAccount,
|
|
128
|
+
resolvedAccountId,
|
|
129
|
+
callerUserId,
|
|
130
|
+
callerHomeAccountId,
|
|
131
|
+
ownerUserId,
|
|
132
|
+
});
|
|
133
|
+
if (decision.kind === "reject") {
|
|
134
|
+
const text = decision.reason === "no-owner"
|
|
135
|
+
? `${toolName} failed: account ${resolvedAccountId.slice(0, 8)}… has no owner AdminUser to attribute the profile to — its graph root is unprovisioned (see Task 1359 seedAccountGraphRoot).`
|
|
136
|
+
: `${toolName} requires an authenticated admin session with userId`;
|
|
133
137
|
return {
|
|
134
138
|
reject: {
|
|
135
|
-
content: [{ type: "text", text
|
|
139
|
+
content: [{ type: "text", text }],
|
|
136
140
|
isError: true,
|
|
137
141
|
},
|
|
138
142
|
};
|
|
139
143
|
}
|
|
140
|
-
|
|
144
|
+
if (decision.kind === "owner") {
|
|
145
|
+
if (crossAccount) {
|
|
146
|
+
process.stderr.write(`[xacct] op=identity tool=${toolName} target=${resolvedAccountId.slice(0, 8)} resolved-userId=${decision.userId.slice(0, 8)}\n`);
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
process.stderr.write(`[xacct] op=identity tool=${toolName} mode=operator-on-behalf target=${resolvedAccountId.slice(0, 8)} caller=${(callerUserId ?? "").slice(0, 8)} resolved-userId=${decision.userId.slice(0, 8)}\n`);
|
|
150
|
+
}
|
|
151
|
+
return { userId: decision.userId, bootstrap: true };
|
|
152
|
+
}
|
|
153
|
+
// decision.kind === "caller" — own-account owner-is-speaker / second admin.
|
|
154
|
+
return { userId: decision.userId, bootstrap: false };
|
|
141
155
|
}
|
|
142
156
|
// Load the markdown schema sidecar once at startup. Every memory-write call
|
|
143
157
|
// reads its required-property and synonym maps. If loading fails we throw —
|
|
@@ -1336,11 +1350,16 @@ if (!readOnly) {
|
|
|
1336
1350
|
forceRewrite: z.boolean().optional().describe("When inlineRewrite is true, bypass the 7-day per-entity cooldown on compiledTruthUpdatedAt. All threshold-qualifying entities are returned as inlineRewriteCandidates regardless of when their truth was last rewritten."),
|
|
1337
1351
|
inlineRewriteThreshold: z.number().int().min(1).optional().describe("Minimum MENTIONS/REFERENCES edge count from this source to an entity for it to qualify as an inline-rewrite candidate. Default: 2."),
|
|
1338
1352
|
targetAccountId: z.string().optional().describe(XACCT_DESC),
|
|
1339
|
-
}, async ({ attachmentId, conversationIdentity, source, archiveSha256, archiveSourceFile, participantElementIds, lastIngestedMessageHash, lastIngestedMessageAt, archiveTitle, cleanupPriorChunks, sessionIndex, documentSummary, anchorNodeId, anchorLabel, sections, documentEdges, orphanCandidates, scope, sourceUrl, sourceType, documentKeywords, userKeywords, sessionId: sessionIdOverride, inlineRewrite, forceRewrite, inlineRewriteThreshold, targetAccountId }) => {
|
|
1353
|
+
}, async ({ attachmentId, conversationIdentity, source, archiveSha256, archiveSourceFile, participantElementIds, lastIngestedMessageHash, lastIngestedMessageAt, archiveTitle, cleanupPriorChunks, sessionIndex, documentSummary, anchorNodeId, anchorLabel, sections, documentEdges, orphanCandidates, scope, sourceUrl, sourceType, documentKeywords, userKeywords, sessionId: sessionIdOverride, inlineRewrite, forceRewrite, inlineRewriteThreshold, targetAccountId }, extra) => {
|
|
1340
1354
|
const scoped = resolveToolAccount("memory-ingest", targetAccountId);
|
|
1341
1355
|
if ("reject" in scoped)
|
|
1342
1356
|
return scoped.reject;
|
|
1343
1357
|
const accountId = scoped.accountId;
|
|
1358
|
+
// Task 1555 — stream a progress notification per embed sub-batch POST so
|
|
1359
|
+
// a long embedding phase resets Claude Code's stdio idle timer and the
|
|
1360
|
+
// ingest commits instead of aborting mid-write. No-ops when the client
|
|
1361
|
+
// sent no progressToken.
|
|
1362
|
+
const reportProgress = makeEmbedProgressReporter(extra);
|
|
1344
1363
|
try {
|
|
1345
1364
|
const result = await memoryIngest({
|
|
1346
1365
|
accountId,
|
|
@@ -1370,6 +1389,7 @@ if (!readOnly) {
|
|
|
1370
1389
|
inlineRewrite,
|
|
1371
1390
|
forceRewrite,
|
|
1372
1391
|
inlineRewriteThreshold,
|
|
1392
|
+
reportProgress,
|
|
1373
1393
|
});
|
|
1374
1394
|
return {
|
|
1375
1395
|
content: [{
|
|
@@ -2228,8 +2248,8 @@ if (!readOnly) {
|
|
|
2228
2248
|
profileFields: profileFields,
|
|
2229
2249
|
personFields,
|
|
2230
2250
|
mergeSourceIds,
|
|
2231
|
-
bootstrapProfile:
|
|
2232
|
-
bootstrapPerson:
|
|
2251
|
+
bootstrapProfile: ident.bootstrap,
|
|
2252
|
+
bootstrapPerson: ident.bootstrap,
|
|
2233
2253
|
});
|
|
2234
2254
|
return {
|
|
2235
2255
|
content: [{
|