@rubytech/create-maxy-code 0.1.434 → 0.1.435

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.
Files changed (35) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/embed-client/dist/index.d.ts +1 -1
  3. package/payload/platform/lib/embed-client/dist/index.d.ts.map +1 -1
  4. package/payload/platform/lib/embed-client/dist/index.js +73 -25
  5. package/payload/platform/lib/embed-client/dist/index.js.map +1 -1
  6. package/payload/platform/lib/embed-client/src/index.ts +77 -25
  7. package/payload/platform/plugins/admin/PLUGIN.md +1 -0
  8. package/payload/platform/plugins/admin/hooks/__tests__/askuserquestion-channel-carrier-gate.test.sh +175 -0
  9. package/payload/platform/plugins/admin/hooks/askuserquestion-channel-carrier-gate.sh +137 -0
  10. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +2 -2
  11. package/payload/platform/plugins/docs/references/internals.md +1 -1
  12. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/embed-throughput.test.js +161 -19
  13. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/embed-throughput.test.js.map +1 -1
  14. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/embed-timeout.test.js +11 -9
  15. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/embed-timeout.test.js.map +1 -1
  16. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/embeddings-cap.test.js +9 -5
  17. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/embeddings-cap.test.js.map +1 -1
  18. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-update-own-preference-selfheal.test.d.ts +2 -0
  19. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-update-own-preference-selfheal.test.d.ts.map +1 -0
  20. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-update-own-preference-selfheal.test.js +113 -0
  21. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-update-own-preference-selfheal.test.js.map +1 -0
  22. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-update-reconcile.test.js +6 -0
  23. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-update-reconcile.test.js.map +1 -1
  24. package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.js +47 -4
  25. package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.js.map +1 -1
  26. package/payload/platform/scripts/lib/provision-account-dir.sh +2 -1
  27. package/payload/platform/services/claude-session-manager/dist/index.js +20 -0
  28. package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
  29. package/payload/platform/services/claude-session-manager/dist/interactive-wedge.d.ts +42 -0
  30. package/payload/platform/services/claude-session-manager/dist/interactive-wedge.d.ts.map +1 -0
  31. package/payload/platform/services/claude-session-manager/dist/interactive-wedge.js +149 -0
  32. package/payload/platform/services/claude-session-manager/dist/interactive-wedge.js.map +1 -0
  33. package/payload/server/{chunk-UUG6ZSO5.js → chunk-DAYMXQG2.js} +28 -0
  34. package/payload/server/maxy-edge.js +1 -1
  35. package/payload/server/server.js +6 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy-code",
3
- "version": "0.1.434",
3
+ "version": "0.1.435",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy-code": "./dist/index.js"
@@ -5,6 +5,6 @@ export declare const EMBED_MS_PER_INPUT: number;
5
5
  export declare const EMBED_REQUEST_TIMEOUT_MS: number;
6
6
  export declare const EMBED_KEEP_ALIVE: number | string;
7
7
  export declare function embed(text: string): Promise<number[]>;
8
- export declare function effectiveSubBatchSize(): number;
8
+ export declare function subBatchCharBudget(msPerChar: number): number;
9
9
  export declare function embedBatch(texts: string[]): Promise<number[][]>;
10
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA4BA,eAAO,MAAM,qBAAqB,QASjC,CAAC;AAaF,eAAO,MAAM,mBAAmB,QAA6D,CAAC;AAC9F,eAAO,MAAM,0BAA0B,QAGtC,CAAC;AAWF,eAAO,MAAM,kBAAkB,QAA8D,CAAC;AAQ9F,eAAO,MAAM,wBAAwB,QAGpC,CAAC;AAcF,eAAO,MAAM,gBAAgB,EAAE,MAAM,GAAG,MAEhB,CAAC;AA8EzB,wBAAsB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAG3D;AAKD,wBAAgB,qBAAqB,IAAI,MAAM,CAM9C;AAED,wBAAsB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAqBrE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA4BA,eAAO,MAAM,qBAAqB,QASjC,CAAC;AAaF,eAAO,MAAM,mBAAmB,QAA6D,CAAC;AAC9F,eAAO,MAAM,0BAA0B,QAGtC,CAAC;AAWF,eAAO,MAAM,kBAAkB,QAA8D,CAAC;AAY9F,eAAO,MAAM,wBAAwB,QAGpC,CAAC;AAcF,eAAO,MAAM,gBAAgB,EAAE,MAAM,GAAG,MAEhB,CAAC;AA0FzB,wBAAsB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAG3D;AAcD,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAM5D;AAED,wBAAsB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAgDrE"}
@@ -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.effectiveSubBatchSize = effectiveSubBatchSize;
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";
@@ -45,17 +45,21 @@ exports.EMBED_INPUT_MAX_CHARS = Math.max(1, Math.floor(envInt("EMBED_INPUT_MAX_C
45
45
  // measurement without a code change.
46
46
  exports.EMBED_SUBBATCH_SIZE = Math.max(1, Math.floor(envInt("EMBED_SUBBATCH_SIZE", 32)));
47
47
  exports.EMBED_SUBBATCH_COOLDOWN_MS = Math.max(0, envInt("EMBED_SUBBATCH_COOLDOWN_MS", 1000));
48
- // Throughput-aware sub-batch sizing (Task 1533). On a CPU-only embedder each
49
- // input takes ~EMBED_MS_PER_INPUT ms; a fixed 32-input POST at the char cap is
50
- // ~128 s, past the 120 s request timeout, so a full-document batch aborts and
51
- // the atomic ingest write lands nothing. Bound each POST to 80% of the timeout
52
- // so it provably completes: derive a duration ceiling and take the smaller of
53
- // it and the count ceiling (EMBED_SUBBATCH_SIZE, Task 960's thermal cap).
54
- // EMBED_MS_PER_INPUT is the budgeted worst-case wall-time for one input at the
55
- // EMBED_INPUT_MAX_CHARS cap on the measured device; env-tunable for other
56
- // hardware.
48
+ // Drift baseline (Task 1553). This was Task 1533's sizing driver — the budgeted
49
+ // worst-case wall-time for one capped input but sizing that from a single
50
+ // fixed reading is exactly what over-provisioned a POST on a device slower than
51
+ // the reading and aborted the ingest. Sizing is now measured per batch
52
+ // (nextSubBatchSize), so EMBED_MS_PER_INPUT no longer drives it. It survives as
53
+ // the reconciliation baseline: each POST compares its observed per-input time
54
+ // against this budget and warns (op=drift) when the device runs slower, so a
55
+ // device below budget is surfaced by a standing check rather than by a silent
56
+ // ingest failure. Env-tunable for other hardware.
57
57
  exports.EMBED_MS_PER_INPUT = Math.max(1, Math.floor(envInt("EMBED_MS_PER_INPUT", 6000)));
58
58
  const EMBED_TIMEOUT_UTILISATION = 0.8;
59
+ // Warn (op=drift) once a POST's observed per-input time exceeds the budget by
60
+ // this factor. 1.5 tolerates ordinary variance while catching a device that is
61
+ // materially slower than EMBED_MS_PER_INPUT.
62
+ const EMBED_DRIFT_WARN_RATIO = 1.5;
59
63
  // Explicit per-request timeout, well under undici's 300000ms headers default.
60
64
  // Without it a slow or wedged ollama returns no headers and the caller hangs a
61
65
  // silent 5 minutes before a bare `fetch failed`. With it the request aborts in
@@ -130,7 +134,17 @@ async function embedRequest(input) {
130
134
  // oversized or slow sub-batch is visible before it times out and the
131
135
  // throughput-aware sizing is verifiable from the log. inputs is the POST's
132
136
  // sub-batch size; maxChars its largest input.
133
- process.stderr.write(`[embed] op=post inputs=${inputs.length} maxChars=${maxChars} ms=${Date.now() - startedAt}\n`);
137
+ const ms = Date.now() - startedAt;
138
+ process.stderr.write(`[embed] op=post inputs=${inputs.length} maxChars=${maxChars} ms=${ms}\n`);
139
+ // Standing reconciliation (Task 1553). Compare this POST's observed per-input
140
+ // time against the EMBED_MS_PER_INPUT budget and warn on drift, so a device
141
+ // slower than budget is caught by a check rather than by a later ingest that
142
+ // aborts. Sizing already adapts to the true rate; this line makes the
143
+ // slowness legible.
144
+ const observedMsPerInput = ms / inputs.length;
145
+ if (inputs.length > 0 && observedMsPerInput > exports.EMBED_MS_PER_INPUT * EMBED_DRIFT_WARN_RATIO) {
146
+ 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`);
147
+ }
134
148
  return data.embeddings;
135
149
  }
136
150
  // POST one already-capped sub-batch of inputs to the embed endpoint.
@@ -141,30 +155,64 @@ async function embed(text) {
141
155
  const data = await embedRequest(capInput(text));
142
156
  return data[0];
143
157
  }
144
- // The POST size actually used by embedBatch: the smaller of the count ceiling
145
- // (thermal, Task 960) and the duration ceiling (throughput, Task 1533). At
146
- // defaults: min(32, floor(120000*0.8/6000)) = min(32, 16) = 16.
147
- function effectiveSubBatchSize() {
148
- const durationCap = Math.max(1, Math.floor((exports.EMBED_REQUEST_TIMEOUT_MS * EMBED_TIMEOUT_UTILISATION) / exports.EMBED_MS_PER_INPUT));
149
- return Math.min(exports.EMBED_SUBBATCH_SIZE, durationCap);
158
+ // The character budget for one POST, decided from the device's *measured*
159
+ // throughput rather than a fixed constant (Task 1553). Task 1533 sized every
160
+ // POST by input count against `EMBED_MS_PER_INPUT`, a single optimistic
161
+ // reading; on a device slower than that constant the derived POST was too large
162
+ // to finish inside `EMBED_REQUEST_TIMEOUT_MS`, so it aborted and the atomic
163
+ // ingest landed nothing. Sizing by *count* is also unsafe when inputs differ in
164
+ // size — an embed's wall-time scales with its characters, so a POST sized from
165
+ // a short probe (e.g. a document summary) over-provisions when the later inputs
166
+ // are near-cap sections. Bound each POST by total characters instead:
167
+ // `floor(EMBED_REQUEST_TIMEOUT_MS * 0.8 / msPerChar)`, floored at 1. A near-zero
168
+ // (very fast) rate yields an unbounded budget so the thermal count ceiling
169
+ // (`EMBED_SUBBATCH_SIZE`, Task 960) governs instead.
170
+ function subBatchCharBudget(msPerChar) {
171
+ const perChar = Math.max(msPerChar, Number.MIN_VALUE); // guard div-by-zero
172
+ return Math.max(1, Math.floor((exports.EMBED_REQUEST_TIMEOUT_MS * EMBED_TIMEOUT_UTILISATION) / perChar));
150
173
  }
151
174
  async function embedBatch(texts) {
152
175
  const inputs = texts.map(capInput);
153
- const subBatch = effectiveSubBatchSize();
154
- // Common case — an array within the sub-batch size is one POST, unchanged.
155
- if (inputs.length <= subBatch) {
176
+ // 0 or 1 input is exactly one POST: the warm interactive query path is
177
+ // unaffected, and the empty-array behaviour is unchanged.
178
+ if (inputs.length <= 1) {
156
179
  return postBatch(inputs);
157
180
  }
158
- // Oversized: split into sub-batches, cooling between each (not after the
159
- // last). Vectors are concatenated in input order so callers can keep their
181
+ // Measure-and-size. Probe with one input, then pack each subsequent POST with
182
+ // as many inputs as fit the character budget derived from the worst
183
+ // ms-per-char seen so far, capped by the thermal count ceiling. Using the
184
+ // running max makes the budget monotonic — a mid-batch slowdown (e.g. the
185
+ // ingest itself loading the CPU) shrinks the remainder and it never re-expands
186
+ // into a timeout. Cool between POSTs (not after the last) so the SoC gets gaps
187
+ // (Task 960). Vectors concatenate in input order so callers keep their
160
188
  // index-aligned `embeddings[i]` ↔ `texts[i]` contract.
161
189
  const out = [];
162
- for (let i = 0; i < inputs.length; i += subBatch) {
190
+ let maxMsPerChar = null;
191
+ for (let i = 0; i < inputs.length;) {
192
+ // Build the next chunk. With no measurement yet, probe with one input.
193
+ let end = i + 1;
194
+ if (maxMsPerChar !== null) {
195
+ const charBudget = subBatchCharBudget(maxMsPerChar);
196
+ let chars = inputs[i].length;
197
+ while (end < inputs.length &&
198
+ end - i < exports.EMBED_SUBBATCH_SIZE &&
199
+ chars + inputs[end].length <= charBudget) {
200
+ chars += inputs[end].length;
201
+ end += 1;
202
+ }
203
+ }
163
204
  if (i > 0 && exports.EMBED_SUBBATCH_COOLDOWN_MS > 0) {
164
205
  await sleep(exports.EMBED_SUBBATCH_COOLDOWN_MS);
165
206
  }
166
- const chunk = inputs.slice(i, i + subBatch);
167
- out.push(...(await postBatch(chunk)));
207
+ const chunk = inputs.slice(i, end);
208
+ const chunkChars = chunk.reduce((s, t) => s + t.length, 0);
209
+ const startedAt = Date.now();
210
+ const vectors = await postBatch(chunk);
211
+ const observedMsPerChar = (Date.now() - startedAt) / Math.max(chunkChars, 1);
212
+ maxMsPerChar =
213
+ maxMsPerChar === null ? observedMsPerChar : Math.max(maxMsPerChar, observedMsPerChar);
214
+ out.push(...vectors);
215
+ i = end;
168
216
  }
169
217
  return out;
170
218
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -54,17 +54,21 @@ export const EMBED_SUBBATCH_COOLDOWN_MS = Math.max(
54
54
  envInt("EMBED_SUBBATCH_COOLDOWN_MS", 1000),
55
55
  );
56
56
 
57
- // Throughput-aware sub-batch sizing (Task 1533). On a CPU-only embedder each
58
- // input takes ~EMBED_MS_PER_INPUT ms; a fixed 32-input POST at the char cap is
59
- // ~128 s, past the 120 s request timeout, so a full-document batch aborts and
60
- // the atomic ingest write lands nothing. Bound each POST to 80% of the timeout
61
- // so it provably completes: derive a duration ceiling and take the smaller of
62
- // it and the count ceiling (EMBED_SUBBATCH_SIZE, Task 960's thermal cap).
63
- // EMBED_MS_PER_INPUT is the budgeted worst-case wall-time for one input at the
64
- // EMBED_INPUT_MAX_CHARS cap on the measured device; env-tunable for other
65
- // hardware.
57
+ // Drift baseline (Task 1553). This was Task 1533's sizing driver — the budgeted
58
+ // worst-case wall-time for one capped input but sizing that from a single
59
+ // fixed reading is exactly what over-provisioned a POST on a device slower than
60
+ // the reading and aborted the ingest. Sizing is now measured per batch
61
+ // (nextSubBatchSize), so EMBED_MS_PER_INPUT no longer drives it. It survives as
62
+ // the reconciliation baseline: each POST compares its observed per-input time
63
+ // against this budget and warns (op=drift) when the device runs slower, so a
64
+ // device below budget is surfaced by a standing check rather than by a silent
65
+ // ingest failure. Env-tunable for other hardware.
66
66
  export const EMBED_MS_PER_INPUT = Math.max(1, Math.floor(envInt("EMBED_MS_PER_INPUT", 6000)));
67
67
  const EMBED_TIMEOUT_UTILISATION = 0.8;
68
+ // Warn (op=drift) once a POST's observed per-input time exceeds the budget by
69
+ // this factor. 1.5 tolerates ordinary variance while catching a device that is
70
+ // materially slower than EMBED_MS_PER_INPUT.
71
+ const EMBED_DRIFT_WARN_RATIO = 1.5;
68
72
 
69
73
  // Explicit per-request timeout, well under undici's 300000ms headers default.
70
74
  // Without it a slow or wedged ollama returns no headers and the caller hangs a
@@ -157,9 +161,21 @@ async function embedRequest(input: string | string[]): Promise<number[][]> {
157
161
  // oversized or slow sub-batch is visible before it times out and the
158
162
  // throughput-aware sizing is verifiable from the log. inputs is the POST's
159
163
  // sub-batch size; maxChars its largest input.
164
+ const ms = Date.now() - startedAt;
160
165
  process.stderr.write(
161
- `[embed] op=post inputs=${inputs.length} maxChars=${maxChars} ms=${Date.now() - startedAt}\n`,
166
+ `[embed] op=post inputs=${inputs.length} maxChars=${maxChars} ms=${ms}\n`,
162
167
  );
168
+ // Standing reconciliation (Task 1553). Compare this POST's observed per-input
169
+ // time against the EMBED_MS_PER_INPUT budget and warn on drift, so a device
170
+ // slower than budget is caught by a check rather than by a later ingest that
171
+ // aborts. Sizing already adapts to the true rate; this line makes the
172
+ // slowness legible.
173
+ const observedMsPerInput = ms / inputs.length;
174
+ if (inputs.length > 0 && observedMsPerInput > EMBED_MS_PER_INPUT * EMBED_DRIFT_WARN_RATIO) {
175
+ process.stderr.write(
176
+ `[embed] op=drift observedMsPerInput=${Math.round(observedMsPerInput)} budgetMsPerInput=${EMBED_MS_PER_INPUT} ratio=${(observedMsPerInput / EMBED_MS_PER_INPUT).toFixed(1)}\n`,
177
+ );
178
+ }
163
179
  return data.embeddings;
164
180
  }
165
181
 
@@ -173,36 +189,72 @@ export async function embed(text: string): Promise<number[]> {
173
189
  return data[0];
174
190
  }
175
191
 
176
- // The POST size actually used by embedBatch: the smaller of the count ceiling
177
- // (thermal, Task 960) and the duration ceiling (throughput, Task 1533). At
178
- // defaults: min(32, floor(120000*0.8/6000)) = min(32, 16) = 16.
179
- export function effectiveSubBatchSize(): number {
180
- const durationCap = Math.max(
192
+ // The character budget for one POST, decided from the device's *measured*
193
+ // throughput rather than a fixed constant (Task 1553). Task 1533 sized every
194
+ // POST by input count against `EMBED_MS_PER_INPUT`, a single optimistic
195
+ // reading; on a device slower than that constant the derived POST was too large
196
+ // to finish inside `EMBED_REQUEST_TIMEOUT_MS`, so it aborted and the atomic
197
+ // ingest landed nothing. Sizing by *count* is also unsafe when inputs differ in
198
+ // size — an embed's wall-time scales with its characters, so a POST sized from
199
+ // a short probe (e.g. a document summary) over-provisions when the later inputs
200
+ // are near-cap sections. Bound each POST by total characters instead:
201
+ // `floor(EMBED_REQUEST_TIMEOUT_MS * 0.8 / msPerChar)`, floored at 1. A near-zero
202
+ // (very fast) rate yields an unbounded budget so the thermal count ceiling
203
+ // (`EMBED_SUBBATCH_SIZE`, Task 960) governs instead.
204
+ export function subBatchCharBudget(msPerChar: number): number {
205
+ const perChar = Math.max(msPerChar, Number.MIN_VALUE); // guard div-by-zero
206
+ return Math.max(
181
207
  1,
182
- Math.floor((EMBED_REQUEST_TIMEOUT_MS * EMBED_TIMEOUT_UTILISATION) / EMBED_MS_PER_INPUT),
208
+ Math.floor((EMBED_REQUEST_TIMEOUT_MS * EMBED_TIMEOUT_UTILISATION) / perChar),
183
209
  );
184
- return Math.min(EMBED_SUBBATCH_SIZE, durationCap);
185
210
  }
186
211
 
187
212
  export async function embedBatch(texts: string[]): Promise<number[][]> {
188
213
  const inputs = texts.map(capInput);
189
- const subBatch = effectiveSubBatchSize();
190
214
 
191
- // Common case an array within the sub-batch size is one POST, unchanged.
192
- if (inputs.length <= subBatch) {
215
+ // 0 or 1 input is exactly one POST: the warm interactive query path is
216
+ // unaffected, and the empty-array behaviour is unchanged.
217
+ if (inputs.length <= 1) {
193
218
  return postBatch(inputs);
194
219
  }
195
220
 
196
- // Oversized: split into sub-batches, cooling between each (not after the
197
- // last). Vectors are concatenated in input order so callers can keep their
221
+ // Measure-and-size. Probe with one input, then pack each subsequent POST with
222
+ // as many inputs as fit the character budget derived from the worst
223
+ // ms-per-char seen so far, capped by the thermal count ceiling. Using the
224
+ // running max makes the budget monotonic — a mid-batch slowdown (e.g. the
225
+ // ingest itself loading the CPU) shrinks the remainder and it never re-expands
226
+ // into a timeout. Cool between POSTs (not after the last) so the SoC gets gaps
227
+ // (Task 960). Vectors concatenate in input order so callers keep their
198
228
  // index-aligned `embeddings[i]` ↔ `texts[i]` contract.
199
229
  const out: number[][] = [];
200
- for (let i = 0; i < inputs.length; i += subBatch) {
230
+ let maxMsPerChar: number | null = null;
231
+ for (let i = 0; i < inputs.length; ) {
232
+ // Build the next chunk. With no measurement yet, probe with one input.
233
+ let end = i + 1;
234
+ if (maxMsPerChar !== null) {
235
+ const charBudget = subBatchCharBudget(maxMsPerChar);
236
+ let chars = inputs[i].length;
237
+ while (
238
+ end < inputs.length &&
239
+ end - i < EMBED_SUBBATCH_SIZE &&
240
+ chars + inputs[end].length <= charBudget
241
+ ) {
242
+ chars += inputs[end].length;
243
+ end += 1;
244
+ }
245
+ }
201
246
  if (i > 0 && EMBED_SUBBATCH_COOLDOWN_MS > 0) {
202
247
  await sleep(EMBED_SUBBATCH_COOLDOWN_MS);
203
248
  }
204
- const chunk = inputs.slice(i, i + subBatch);
205
- out.push(...(await postBatch(chunk)));
249
+ const chunk = inputs.slice(i, end);
250
+ const chunkChars = chunk.reduce((s, t) => s + t.length, 0);
251
+ const startedAt = Date.now();
252
+ const vectors = await postBatch(chunk);
253
+ const observedMsPerChar = (Date.now() - startedAt) / Math.max(chunkChars, 1);
254
+ maxMsPerChar =
255
+ maxMsPerChar === null ? observedMsPerChar : Math.max(maxMsPerChar, observedMsPerChar);
256
+ out.push(...vectors);
257
+ i = end;
206
258
  }
207
259
  return out;
208
260
  }
@@ -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.
@@ -0,0 +1,175 @@
1
+ #!/usr/bin/env bash
2
+ # Regression test for askuserquestion-channel-carrier-gate.sh.
3
+ #
4
+ # The hook blocks AskUserQuestion when the session has a native channel
5
+ # attached — signalled by a channel MCP config file for this session id in
6
+ # tmpdir. A channel carries no elicitation card, so the turn would wedge; the
7
+ # hook steers the agent to ask in prose via its channel reply tool instead.
8
+ #
9
+ # Covers (per .tasks/1552):
10
+ # 1. Channel attached (webchat config file present) + AskUserQuestion → BLOCK channel-attached
11
+ # 2. No channel file + AskUserQuestion → ALLOW no-channel
12
+ # 3. Non-AskUserQuestion tool → exit 0 silent (no [ask-channel] line)
13
+ # 4. PostToolUse event on AskUserQuestion → exit 0 silent
14
+ # 5. Missing session_id → ALLOW fail-open-no-session
15
+ # 6. No stdin envelope → ALLOW fail-open-no-envelope
16
+ # 7. Each of the 4 channel filename shapes present → BLOCK channel-attached
17
+ # 8. Session id needing sanitization, sanitized file present → BLOCK channel-attached
18
+ #
19
+ # All decisive cases assert exit code AND the decision-log signature on stderr.
20
+
21
+ set -u
22
+
23
+ HOOK="$(cd "$(dirname "$0")/.." && pwd)/askuserquestion-channel-carrier-gate.sh"
24
+ if [ ! -x "$HOOK" ]; then
25
+ echo "FAIL: $HOOK not executable" >&2
26
+ exit 1
27
+ fi
28
+
29
+ PASS=0
30
+ FAIL=0
31
+
32
+ # Run one case. Args:
33
+ # $1 = name
34
+ # $2 = stdin payload ("" for no-envelope: closed stdin)
35
+ # $3 = tmpdir the hook should scan (ASK_CHANNEL_TMPDIR override), "" for default
36
+ # $4 = expected exit code
37
+ # $5 = expected log signature regex (matched against stderr; "" = assert stderr is empty)
38
+ run_case() {
39
+ local name="$1" stdin="$2" scan_dir="$3" expected_exit="$4" expected_log_re="$5"
40
+ local stderr_file actual_exit
41
+ stderr_file="$(mktemp)"
42
+ if [ -z "$stdin" ]; then
43
+ # No envelope: close stdin so the hook's `[ -t 0 ]` / empty-read path fires.
44
+ ASK_CHANNEL_TMPDIR="$scan_dir" bash "$HOOK" >/dev/null 2>"$stderr_file" </dev/null
45
+ else
46
+ printf '%s' "$stdin" | ASK_CHANNEL_TMPDIR="$scan_dir" bash "$HOOK" >/dev/null 2>"$stderr_file"
47
+ fi
48
+ actual_exit=$?
49
+ local stderr_content
50
+ stderr_content=$(cat "$stderr_file")
51
+ rm -f "$stderr_file"
52
+
53
+ local exit_ok=0 log_ok=0
54
+ if [ "$actual_exit" -eq "$expected_exit" ]; then exit_ok=1; fi
55
+ if [ -z "$expected_log_re" ]; then
56
+ # Empty regex means: assert the hook emitted NO [ask-channel] line.
57
+ if ! printf '%s' "$stderr_content" | grep -q '\[ask-channel\]'; then log_ok=1; fi
58
+ else
59
+ if printf '%s' "$stderr_content" | grep -Eq "$expected_log_re"; then log_ok=1; fi
60
+ fi
61
+
62
+ if [ $exit_ok -eq 1 ] && [ $log_ok -eq 1 ]; then
63
+ echo "PASS: $name (exit=$actual_exit)"
64
+ PASS=$((PASS + 1))
65
+ else
66
+ echo "FAIL: $name" >&2
67
+ [ $exit_ok -eq 0 ] && echo " expected exit=$expected_exit, got=$actual_exit" >&2
68
+ [ $log_ok -eq 0 ] && echo " expected log regex: ${expected_log_re:-<none>}" >&2 && \
69
+ echo " actual stderr: $stderr_content" >&2
70
+ FAIL=$((FAIL + 1))
71
+ fi
72
+ }
73
+
74
+ SID="1a085718-1111-2222-3333-444455556666"
75
+ SID8="1a085718"
76
+
77
+ # Stdin payloads.
78
+ PRE_ASK="{\"hook_event_name\":\"PreToolUse\",\"tool_name\":\"AskUserQuestion\",\"session_id\":\"$SID\",\"tool_input\":{\"questions\":[]}}"
79
+ PRE_OTHER="{\"hook_event_name\":\"PreToolUse\",\"tool_name\":\"Bash\",\"session_id\":\"$SID\",\"tool_input\":{\"command\":\"ls\"}}"
80
+ POST_ASK="{\"hook_event_name\":\"PostToolUse\",\"tool_name\":\"AskUserQuestion\",\"session_id\":\"$SID\",\"tool_input\":{\"questions\":[]}}"
81
+ PRE_ASK_NO_SID='{"hook_event_name":"PreToolUse","tool_name":"AskUserQuestion","tool_input":{"questions":[]}}'
82
+
83
+ # ---------- Cases ----------
84
+
85
+ # 1. Channel attached (webchat config present) → BLOCK.
86
+ DIR_WEBCHAT=$(mktemp -d)
87
+ : > "$DIR_WEBCHAT/maxy-webchat-channel-$SID.json"
88
+ run_case "Webchat channel attached → BLOCK channel-attached" \
89
+ "$PRE_ASK" "$DIR_WEBCHAT" 2 \
90
+ "\[ask-channel\] decision=block sessionId=$SID8 channel=attached reason=channel-attached"
91
+ rm -rf "$DIR_WEBCHAT"
92
+
93
+ # 2. No channel file → ALLOW no-channel.
94
+ DIR_EMPTY=$(mktemp -d)
95
+ run_case "No channel file → ALLOW no-channel" \
96
+ "$PRE_ASK" "$DIR_EMPTY" 0 \
97
+ "\[ask-channel\] decision=allow sessionId=$SID8 channel=none reason=no-channel"
98
+ rm -rf "$DIR_EMPTY"
99
+
100
+ # 3. Non-AskUserQuestion tool → silent exit 0.
101
+ DIR_WITH_FILE=$(mktemp -d)
102
+ : > "$DIR_WITH_FILE/maxy-webchat-channel-$SID.json"
103
+ run_case "Non-AskUserQuestion tool → exit 0 silent" \
104
+ "$PRE_OTHER" "$DIR_WITH_FILE" 0 ""
105
+ rm -rf "$DIR_WITH_FILE"
106
+
107
+ # 4. PostToolUse event → silent exit 0 (even with channel file present).
108
+ DIR_POST=$(mktemp -d)
109
+ : > "$DIR_POST/maxy-webchat-channel-$SID.json"
110
+ run_case "PostToolUse on AskUserQuestion → exit 0 silent" \
111
+ "$POST_ASK" "$DIR_POST" 0 ""
112
+ rm -rf "$DIR_POST"
113
+
114
+ # 5. Missing session_id → ALLOW fail-open-no-session.
115
+ DIR_NOSID=$(mktemp -d)
116
+ run_case "Missing session_id → ALLOW fail-open-no-session" \
117
+ "$PRE_ASK_NO_SID" "$DIR_NOSID" 0 \
118
+ '\[ask-channel\] decision=allow sessionId=- channel=none reason=fail-open-no-session'
119
+ rm -rf "$DIR_NOSID"
120
+
121
+ # 6. No stdin envelope → ALLOW fail-open-no-envelope.
122
+ run_case "No stdin envelope → ALLOW fail-open-no-envelope" \
123
+ "" "" 0 \
124
+ '\[ask-channel\] decision=allow sessionId=- channel=none reason=fail-open-no-envelope'
125
+
126
+ # 7. Each of the 4 channel filename shapes present → BLOCK.
127
+ for kind in channel webchat-channel wa-channel telegram-channel; do
128
+ DIR_KIND=$(mktemp -d)
129
+ : > "$DIR_KIND/maxy-$kind-$SID.json"
130
+ run_case "Channel file shape maxy-$kind-<sid>.json → BLOCK channel-attached" \
131
+ "$PRE_ASK" "$DIR_KIND" 2 \
132
+ "\[ask-channel\] decision=block sessionId=$SID8 channel=attached reason=channel-attached"
133
+ rm -rf "$DIR_KIND"
134
+ done
135
+
136
+ # 8. Session id needing sanitization: raw id has chars outside [A-Za-z0-9_-];
137
+ # the writer sanitizes to '_', so the on-disk file uses the sanitized id.
138
+ RAW_SID="acc/one:sess" # '/' and ':' are sanitized to '_'
139
+ SAN_SID="acc_one_sess"
140
+ DIR_SAN=$(mktemp -d)
141
+ : > "$DIR_SAN/maxy-webchat-channel-$SAN_SID.json"
142
+ PRE_ASK_RAW="{\"hook_event_name\":\"PreToolUse\",\"tool_name\":\"AskUserQuestion\",\"session_id\":\"$RAW_SID\",\"tool_input\":{\"questions\":[]}}"
143
+ run_case "Sanitized session id resolves the file → BLOCK channel-attached" \
144
+ "$PRE_ASK_RAW" "$DIR_SAN" 2 \
145
+ '\[ask-channel\] decision=block sessionId=acc/one: channel=attached reason=channel-attached'
146
+ rm -rf "$DIR_SAN"
147
+
148
+ # 9. Real tmpdir resolution fallback: no ASK_CHANNEL_TMPDIR override, TMPDIR
149
+ # unset, TMP set → the hook must resolve TMP (mirroring Node's os.tmpdir())
150
+ # and find the config file there. Proves the production resolution order, not
151
+ # just the test override.
152
+ DIR_TMP=$(mktemp -d)
153
+ : > "$DIR_TMP/maxy-webchat-channel-$SID.json"
154
+ tmp_stderr="$(mktemp)"
155
+ printf '%s' "$PRE_ASK" | env -u TMPDIR -u ASK_CHANNEL_TMPDIR -u TEMP TMP="$DIR_TMP" \
156
+ bash "$HOOK" >/dev/null 2>"$tmp_stderr"
157
+ tmp_exit=$?
158
+ if [ "$tmp_exit" -eq 2 ] && grep -Eq "\[ask-channel\] decision=block sessionId=$SID8 channel=attached reason=channel-attached" "$tmp_stderr"; then
159
+ echo "PASS: TMP-fallback resolution finds the config file → BLOCK (exit=$tmp_exit)"
160
+ PASS=$((PASS + 1))
161
+ else
162
+ echo "FAIL: TMP-fallback resolution" >&2
163
+ echo " expected exit=2 got=$tmp_exit; stderr: $(cat "$tmp_stderr")" >&2
164
+ FAIL=$((FAIL + 1))
165
+ fi
166
+ rm -f "$tmp_stderr"
167
+ rm -rf "$DIR_TMP"
168
+
169
+ echo
170
+ echo "──────── askuserquestion-channel-carrier-gate test summary ────────"
171
+ echo "PASS: $PASS"
172
+ echo "FAIL: $FAIL"
173
+
174
+ [ "$FAIL" -gt 0 ] && exit 1
175
+ exit 0