prism-mcp-server 20.21.25 → 20.21.27
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/README.md +72 -0
- package/dist/tools/prismInferHandler.js +9 -20
- package/dist/utils/layer1.js +27 -35
- package/dist/utils/safetyGate.js +7 -1
- package/package.json +1 -1
- package/scripts/dev/browse.py +70 -19
package/README.md
CHANGED
|
@@ -170,6 +170,78 @@ or by re-enabling after each run.
|
|
|
170
170
|
<details>
|
|
171
171
|
<summary>Release history (optional)</summary>
|
|
172
172
|
|
|
173
|
+
## What's New in v20.21.27
|
|
174
|
+
|
|
175
|
+
**Why it matters:** a script that feeds Prism Browser through a pipe can
|
|
176
|
+
now pause between blocks of commands. Before, commands that had already
|
|
177
|
+
arrived waited for the next write, and never ran if the 600 s idle timeout
|
|
178
|
+
came first, so a script that checked a result before sending more stalled
|
|
179
|
+
and then failed.
|
|
180
|
+
|
|
181
|
+
- **Every piped command runs when it arrives.** The reader waited on
|
|
182
|
+
`select()` for new pipe data while `readline()` had already buffered the
|
|
183
|
+
commands sent so far. It now reads the descriptor itself and decodes it as
|
|
184
|
+
the text stream did (its encoding, universal newlines). Measured with two
|
|
185
|
+
commands written together and a third written only after the reader hands
|
|
186
|
+
over the second: the second came out only with the third before, and
|
|
187
|
+
before the third is written now.
|
|
188
|
+
- **Unchanged:** the idle timeout, `\` line continuation (CRLF endings
|
|
189
|
+
included), comment lines, and streams `select()` cannot watch, now read
|
|
190
|
+
blocking. At the idle timeout, a command still being written is dropped,
|
|
191
|
+
never run cut short.
|
|
192
|
+
|
|
193
|
+
## What's New in v20.21.26
|
|
194
|
+
|
|
195
|
+
**Why it matters:** routine wording no longer lets a mixed request skip
|
|
196
|
+
Layer 1, the safety screen that decides whether a request may run on your
|
|
197
|
+
device. A BCBA who asks in one message for an operational
|
|
198
|
+
definition and how many mL of a drug to give, or a developer who asks for a
|
|
199
|
+
code review and then authentication code, now has that request screened by
|
|
200
|
+
Layer 1's rules and, unless the request or conversation is already marked
|
|
201
|
+
reserved, a classifier read. Before, it could be answered by a local model
|
|
202
|
+
with no classifier read.
|
|
203
|
+
|
|
204
|
+
- **Rules no longer clear requests.** Before, one routine clause let Layer 1's
|
|
205
|
+
rules clear the whole request, so it skipped the classifier. "Write an
|
|
206
|
+
operational definition for tantrums and tell me how many mL of Benadryl to
|
|
207
|
+
give at bedtime" was answered locally with no classifier read, and so was
|
|
208
|
+
"review the dashboard component for hook order and then write the JWT
|
|
209
|
+
validation middleware". Now rules can only mark a request reserved. The
|
|
210
|
+
routine shortcut is gone for a single prompt and for the current prompt of a
|
|
211
|
+
follow-up; the prompt's classifier read is skipped only if the request or
|
|
212
|
+
conversation is already marked reserved. A long prompt that reaches the
|
|
213
|
+
classifier is read as excerpts, as before.
|
|
214
|
+
- **What happens to a flagged request.** A text request that a rule marks
|
|
215
|
+
reserved, or that the classifier flags, goes to a stronger cloud model when
|
|
216
|
+
your plan includes cloud and the call does not pass `cloud_fallback: false`.
|
|
217
|
+
Otherwise it is refused, and the refusal says why. Requests the classifier
|
|
218
|
+
clears remain eligible to run locally, so routine drafting (operational
|
|
219
|
+
definitions, data sheets, antecedent strategies) and routine code work still
|
|
220
|
+
run on your device when the local model can answer.
|
|
221
|
+
- **Seclusion by another name.** Placement phrases such as "put him in the
|
|
222
|
+
quiet room until he calms down" (also isolation, time-out, padded and
|
|
223
|
+
calm-down rooms) are now marked reserved by rule. This catches placement
|
|
224
|
+
requests of that shape (him, her, them, or the client, student or child
|
|
225
|
+
placed into the room) that never use the word "seclusion". Other wording,
|
|
226
|
+
and other mentions of these rooms, are left to the classifier.
|
|
227
|
+
- **Measured:** of 46 mixed requests built to test this bypass, which should
|
|
228
|
+
never be answered locally, 41 were cleared to run locally before this
|
|
229
|
+
release and 9 after: 32 fewer sensitive requests reaching a local model. Of
|
|
230
|
+
294 routine requests, 2 more than before are escalated (or refused, without
|
|
231
|
+
cloud).
|
|
232
|
+
- **Limits, unchanged in this release.** Keep them in mind when deciding
|
|
233
|
+
whether a local answer needs your review:
|
|
234
|
+
- The classifier can miss a request, and a missed request still runs
|
|
235
|
+
locally. The 9 above are such misses.
|
|
236
|
+
- Some classifier failures fall back to a keyword check, and text that
|
|
237
|
+
passes it can still run locally.
|
|
238
|
+
- A request with images is processed on this device even when flagged, and
|
|
239
|
+
is never sent to the cloud.
|
|
240
|
+
- **Cost:** requests that used the shortcut now need a classifier read, unless
|
|
241
|
+
screening has already marked them reserved. That added check is the price of
|
|
242
|
+
closing the bypass; in our tests the read took about 0.3 s at the 95th
|
|
243
|
+
percentile.
|
|
244
|
+
|
|
173
245
|
## What's New in v20.21.25
|
|
174
246
|
|
|
175
247
|
Semantic memory search now automatically applies ACT-R ranking, combining
|
|
@@ -318,7 +318,6 @@ export async function layer1HedgeSecondRead(o) {
|
|
|
318
318
|
const tier = MODEL_TIERS.find(t => t.tag === "prism-coder:9b");
|
|
319
319
|
if (!o.installed.has(big) || big === o.guardModel || tier === undefined)
|
|
320
320
|
return skip("no_9b");
|
|
321
|
-
const readPrompt = !o.promptFastPath;
|
|
322
321
|
const messages = o.args.messages ?? [];
|
|
323
322
|
const turnReads = messages.flatMap((m, index) => historyTurnWindows(m.content).filter(w => w.trim()).map(text => ({ text, index, role: m.role })));
|
|
324
323
|
const turnTexts = turnReads.map(t => t.text);
|
|
@@ -333,7 +332,7 @@ export async function layer1HedgeSecondRead(o) {
|
|
|
333
332
|
if (excluded)
|
|
334
333
|
return skip(excluded);
|
|
335
334
|
}
|
|
336
|
-
if (
|
|
335
|
+
if (o.args.prompt.length > MAX_CLASSIFIER_PROMPT_LENGTH)
|
|
337
336
|
return skip("prompt_oversize");
|
|
338
337
|
const reads = [];
|
|
339
338
|
const seen = new Set();
|
|
@@ -341,8 +340,7 @@ export async function layer1HedgeSecondRead(o) {
|
|
|
341
340
|
seen.add(k);
|
|
342
341
|
reads.push(r);
|
|
343
342
|
} };
|
|
344
|
-
|
|
345
|
-
add({ model: big, text: o.args.prompt, kind: "prompt", label: "9b:prompt" });
|
|
343
|
+
add({ model: big, text: o.args.prompt, kind: "prompt", label: "9b:prompt" });
|
|
346
344
|
for (let i = turnReads.length - 1; i >= 0; i--)
|
|
347
345
|
add({ model: big, text: turnReads[i].text, kind: "turn", label: `9b:turn:${turnReads[i].index}:${turnReads[i].role}` });
|
|
348
346
|
for (let i = ctxTexts.length - 1; i >= 0; i--)
|
|
@@ -388,7 +386,7 @@ export async function layer1HedgeSecondRead(o) {
|
|
|
388
386
|
if (!known(ctxBig) || !known(ctxSmall) || !known(overBig) || !known(overSmall))
|
|
389
387
|
return skip("unprobeable");
|
|
390
388
|
const fitsIn = (ctx, over) => (t) => Buffer.byteLength(layer1ClassifierContent(t), "utf8") + over + LAYER1_CLASSIFIER_OUTPUT_TOKENS <= ctx;
|
|
391
|
-
if (![...turnTexts,
|
|
389
|
+
if (![...turnTexts, o.args.prompt, ...ctxTexts].every(fitsIn(ctxBig, overBig)))
|
|
392
390
|
return skip("unfit");
|
|
393
391
|
if (!ctxTexts.every(fitsIn(ctxSmall, overSmall)))
|
|
394
392
|
return skip("unfit");
|
|
@@ -2242,27 +2240,18 @@ export async function runInfer(args, deps) {
|
|
|
2242
2240
|
// The current prompt is a request: its deterministic floor runs
|
|
2243
2241
|
// here explicitly (not only inside the classifier entry point, so
|
|
2244
2242
|
// an injected classifier cannot skip it), in the same proximity
|
|
2245
|
-
// slices as a turn; then
|
|
2246
|
-
//
|
|
2247
|
-
//
|
|
2248
|
-
let promptRoutine = true;
|
|
2243
|
+
// slices as a turn; then it is read alone with its images and that
|
|
2244
|
+
// verdict is kept like a turn's. The floor only refuses, so there is
|
|
2245
|
+
// no fast path: a prompt a rule does not refuse is always read.
|
|
2249
2246
|
for (const slice of windowsOf(args.prompt, DETERMINISTIC_FLOOR_WINDOW_CHARS, DETERMINISTIC_FLOOR_WINDOW_OVERLAP)) {
|
|
2250
2247
|
const promptDet = classifyDeterministicLayer1(slice);
|
|
2251
2248
|
if (promptDet)
|
|
2252
2249
|
l1 = raise(l1, promptDet, "rules");
|
|
2253
|
-
if (promptDet !== "OBVIOUS_NOT_RESERVED")
|
|
2254
|
-
promptRoutine = false;
|
|
2255
2250
|
}
|
|
2256
2251
|
// The classifier entry point's own whole-prompt deterministic pass
|
|
2257
2252
|
// is switched off here — it would undo the slicing above (words
|
|
2258
|
-
// 14k chars apart firing one rule; review round 18).
|
|
2259
|
-
|
|
2260
|
-
// prompt of at most 4,000 chars whose rules verdict is routine,
|
|
2261
|
-
// with no images, skips the model. Longer prompts always reach
|
|
2262
|
-
// the entry point, whose full-text keyword floor must run
|
|
2263
|
-
// (review round 19: skipping it there bypassed that floor).
|
|
2264
|
-
const promptFastPath = promptRoutine && args.prompt.length <= MAX_CLASSIFIER_PROMPT_LENGTH && (resolvedImages?.length ?? 0) === 0;
|
|
2265
|
-
if (l1 !== "OBVIOUS_RESERVED" && !promptFastPath) {
|
|
2253
|
+
// 14k chars apart firing one rule; review round 18).
|
|
2254
|
+
if (l1 !== "OBVIOUS_RESERVED") {
|
|
2266
2255
|
l1 = raise(l1, await l1fn(args.prompt, deps.ollamaUrl, l1Model, undefined, resolvedImages, { deterministic: false, ...(classifierInput ? { classifierInput } : {}) }), "prompt");
|
|
2267
2256
|
}
|
|
2268
2257
|
// 3. Context, raise only: one window per turn and one for the
|
|
@@ -2328,7 +2317,7 @@ export async function runInfer(args, deps) {
|
|
|
2328
2317
|
else if (modelHedgeOnly) {
|
|
2329
2318
|
const second = await layer1HedgeSecondRead({
|
|
2330
2319
|
l1fn, args, installed, listLoaded: deps.listLoaded, freeBytes: deps.freemem(), ollamaUrl: deps.ollamaUrl,
|
|
2331
|
-
guardModel: l1Model,
|
|
2320
|
+
guardModel: l1Model,
|
|
2332
2321
|
probeClassifierLimits: deps.probeClassifierLimits ?? probeClassifierLimits,
|
|
2333
2322
|
policy: await (deps.secondReadPolicy ?? getSecondReadPolicy)().catch(() => null),
|
|
2334
2323
|
});
|
package/dist/utils/layer1.js
CHANGED
|
@@ -150,7 +150,12 @@ const all = (...rules) => (prompt) => rules.every((rule) => rule(prompt));
|
|
|
150
150
|
* restraint, self-harm, suicide, medication or diagnosis. */
|
|
151
151
|
const CLINICAL_RESERVED_RULES = [
|
|
152
152
|
all(has(/\b(?:de[- ]?escalat\w*|meltdown\w*|rage\s+episode|violent\w*)\b/i), has(/\b(?:draft|write|plan|procedure|protocol|manag\w*|what\s+(?:do|should)\b|respond\w*)\b/i)),
|
|
153
|
-
|
|
153
|
+
(prompt) => (/\b(?:physical\s+intervention|containment|hold\s+procedure|restrain\w*|seclu\w*|physical\s+management)\b/i.test(prompt)
|
|
154
|
+
&& /\b(?:draft|write|document|procedure|protocol|use|implement|instruct\w*)\b/i.test(prompt)) || (
|
|
155
|
+
// Seclusion by another name: someone placed IN a room ("put him in the
|
|
156
|
+
// quiet room", "walk the student into the calm-down room"). A room
|
|
157
|
+
// named as a setting, a schedule, or a room used by choice is not one.
|
|
158
|
+
/\b(?:put|puts|putting|plac(?:e|es|ed|ing)|sen(?:d|ds|t|ding)|tak(?:e|es|en|ing)|took|walk\w*|mov(?:e|es|ed|ing)|bring\w*|brought|escort\w*|carr(?:y|ies|ied|ying)|drag\w*|lock\w*|confin\w*|keep\w*|kept)\s+(?:him|her|them|(?:the|a|an|this|that)\s+(?:client|student|child|kid|learner|resident|patient))\s+(?:back\s+)?(?:in|into|to|inside)\s+(?:the\s+|a\s+|an\s+)?(?:quiet|isolation|time[- ]?out|padded|calm[- ]?down|seclusion)\s+rooms?\b/i.test(prompt)),
|
|
154
159
|
all(has(/\b(?:self[- ]?(?:harm|injur\w*)|bites?\s+(?:him|her|them)self|bites?\s+(?:his|her|their)\s+own\b|scratches?\s+(?:him|her|them)self|scratches?\s+(?:his|her|their)\s+own\b|bangs?\s+(?:his|her|their)\s+head|harm\s+(?:himself|herself|themselves))\b/i), has(/\b(?:bleed\w*|blood|concrete|head\s+impact|medical\s+attention|risk(?:\s+of\s+injury)?|causes?\s+injury|actual\s+injury|assess\w*|screen\w*|want\w*)\b/i)),
|
|
155
160
|
(prompt) => (/\b(?:punch\w*|attack\w*|aggress\w*|hit\s+staff)\b/i.test(prompt)
|
|
156
161
|
&& /\b(?:last\s+(?:week|month|year)|prior|previous|history|injur\w*)\b/i.test(prompt)
|
|
@@ -173,17 +178,6 @@ const RESERVED_INTENT_RULES = [
|
|
|
173
178
|
...CLINICAL_RESERVED_RULES,
|
|
174
179
|
...OPERATIONAL_RESERVED_RULES,
|
|
175
180
|
];
|
|
176
|
-
const ROUTINE_BCBA_INTENT_RULES = [
|
|
177
|
-
has(/\boperational\s+definition\b/i),
|
|
178
|
-
all(has(/\bdefin\w*\b/i), has(/\b(?:onset|offset|observers?|score|measur\w*)\b/i)),
|
|
179
|
-
all(has(/\b(?:abc|fba|functional\s+behavior)\b/i), has(/\b(?:hypothes\w*|tentative|summari[sz]\w*)\b/i)),
|
|
180
|
-
all(has(/\b(?:what\s+data|data\s+(?:should|to)\s+(?:be\s+)?gather\w*|collect\s+data)\b/i), has(/\bbefore\s+(?:select\w*|choos\w*|design\w*)\s+(?:an?\s+)?intervention\b/i)),
|
|
181
|
-
all(has(/\b(?:aac|augmentative\s+communication)\b/i), has(/\b(?:replacement[- ]response|replacement\s+(?:skill|behavior)|function[- ]matched)\b/i), has(/\b(?:bcba|clinician)\s+review\b/i)),
|
|
182
|
-
all(has(/\b(?:explain|educat\w*|why)\b/i), has(/\b(?:dro|differential\s+reinforcement|function[- ]matched|maintain\w+\s+by|escape\s+from)\b/i)),
|
|
183
|
-
all(has(/\b(?:caregiver|staff|parent)\s+training\b/i), has(/\b(?:aac|replacement|break[- ]request|communication)\b/i)),
|
|
184
|
-
all(has(/\b(?:rewrite|rephrase)\b/i), has(/\b(?:stigmat\w*|dignity|objective|tentative|function[- ]based)\b/i)),
|
|
185
|
-
all(has(/\b(?:aac|communication\s+device)\b/i), has(/\b(?:remain|keep)\s+available\b/i), has(/\bno\s+injur(?:y|ies)\b/i), has(/\bnonrestrictive\b/i)),
|
|
186
|
-
];
|
|
187
181
|
function matchesAny(prompt, rules) {
|
|
188
182
|
return rules.some((rule) => rule(prompt));
|
|
189
183
|
}
|
|
@@ -235,11 +229,11 @@ export function reservedCategory(prompt) {
|
|
|
235
229
|
// The artifact exemption sits BETWEEN the two groups in
|
|
236
230
|
// classifyDeterministicLayer1, and omitting it here made the label disagree
|
|
237
231
|
// with the verdict: "add numeric validation fields to the auth token
|
|
238
|
-
// verification handler"
|
|
239
|
-
// "auth / session / token code". Harmless when the
|
|
240
|
-
// used directly, but the label is also read on
|
|
241
|
-
// path — prompts over MAX_CLASSIFIER_PROMPT_LENGTH,
|
|
242
|
-
// it could name a rule that did not fire.
|
|
232
|
+
// verification handler" is never refused by a rule (the classifier reads
|
|
233
|
+
// it), while this reported "auth / session / token code". Harmless when the
|
|
234
|
+
// deterministic verdict is used directly, but the label is also read on
|
|
235
|
+
// paths that skip that fast path — prompts over MAX_CLASSIFIER_PROMPT_LENGTH,
|
|
236
|
+
// and image requests — so it could name a rule that did not fire.
|
|
243
237
|
if (NON_OPERATIONAL_ARTIFACT_CONTEXT.test(prompt) &&
|
|
244
238
|
NON_OPERATIONAL_ARTIFACT_ACTION.test(prompt)) {
|
|
245
239
|
return null;
|
|
@@ -255,9 +249,8 @@ const NON_OPERATIONAL_ARTIFACT_ACTION = /\b(?:review\b[\s\S]{0,120}\bhook\s+orde
|
|
|
255
249
|
/**
|
|
256
250
|
* Deterministic policy floor for unambiguous intents.
|
|
257
251
|
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
* null and continue to the semantic classifier below.
|
|
252
|
+
* Clear reserved work fails closed by rule. Everything else returns null and
|
|
253
|
+
* is read by the semantic classifier below: the floor never clears.
|
|
261
254
|
*/
|
|
262
255
|
/** The non-operational artifact exemption over a text. classifyDeterministic
|
|
263
256
|
* Layer1 applies it to the text it is given; prism_infer's history screen
|
|
@@ -281,19 +274,25 @@ export function classifyDeterministicLayer1(userPrompt, opts) {
|
|
|
281
274
|
// the crisis form" escaping the restraint rule, and a wrong answer there
|
|
282
275
|
// costs more than an unnecessary escalation. Clinical intent still fails
|
|
283
276
|
// closed first, before anything can exempt it.
|
|
277
|
+
//
|
|
278
|
+
// The floor only refuses; it never clears. Every bypass found in review was
|
|
279
|
+
// a rule clearing a request no model read: a routine or artifact clause
|
|
280
|
+
// beside "a prone hold protocol for staff", "how many mL of Benadryl to
|
|
281
|
+
// give" or "find paths allowing unauthenticated access" was served in
|
|
282
|
+
// 0 ms, and each word-list patch was followed by new wording. Anything a
|
|
283
|
+
// rule does not refuse is read by the classifier.
|
|
284
284
|
const clinicalReserved = matchesAny(userPrompt, CLINICAL_RESERVED_RULES);
|
|
285
285
|
if (clinicalReserved)
|
|
286
286
|
return "OBVIOUS_RESERVED";
|
|
287
|
-
if (opts?.operational === false)
|
|
288
|
-
return
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
287
|
+
if (opts?.operational === false)
|
|
288
|
+
return null;
|
|
289
|
+
// A non-operational artifact edit is never refused by an operational rule
|
|
290
|
+
// (an auth_bypass fixture label is not reserved work); the classifier
|
|
291
|
+
// reads it.
|
|
292
|
+
if (isNonOperationalArtifact(userPrompt))
|
|
293
|
+
return null;
|
|
293
294
|
if (matchesAny(userPrompt, RESERVED_INTENT_RULES))
|
|
294
295
|
return "OBVIOUS_RESERVED";
|
|
295
|
-
if (matchesAny(userPrompt, ROUTINE_BCBA_INTENT_RULES))
|
|
296
|
-
return "OBVIOUS_NOT_RESERVED";
|
|
297
296
|
return null;
|
|
298
297
|
}
|
|
299
298
|
/**
|
|
@@ -451,13 +450,6 @@ images, opts) {
|
|
|
451
450
|
const deterministic = opts?.deterministic === false ? null : classifyDeterministicLayer1(userPrompt);
|
|
452
451
|
if (deterministic === "OBVIOUS_RESERVED")
|
|
453
452
|
return deterministic;
|
|
454
|
-
// The routine-work fast path may only skip the model when there is nothing
|
|
455
|
-
// to look at. It reads the PROMPT, so with an image attached it is a free
|
|
456
|
-
// bypass: pair "write an operational definition" — or any ROUTINE_BCBA
|
|
457
|
-
// phrasing — with a clinical screenshot and the gate returns NOT_RESERVED
|
|
458
|
-
// without a single call. Text keeps the shortcut; images earn the screen.
|
|
459
|
-
if (!oversize && !hasImages && deterministic === "OBVIOUS_NOT_RESERVED")
|
|
460
|
-
return deterministic;
|
|
461
453
|
if (oversize && keywordBackstop(userPrompt) === "OBVIOUS_RESERVED") {
|
|
462
454
|
// The regex floor has no length limit — reserved vocabulary anywhere
|
|
463
455
|
// in the full text (including the middle the excerpt can't see)
|
package/dist/utils/safetyGate.js
CHANGED
|
@@ -31,7 +31,13 @@ const CRISIS_INPUT_RE = [
|
|
|
31
31
|
// Swift copy); the generator's other patterns drift from this file —
|
|
32
32
|
// reported; apply single lines, and keep the portal copy in step.
|
|
33
33
|
/(?:(?<!\b(?:a|an|the|this|that|good|great|solid|useful|our|your|my|as)\s{1,4})\bjump(?:ing)?[\s-]+off\b|\bjump(?:ing)?[\s-]+off\b(?![\s-]+points?\b))/i,
|
|
34
|
-
|
|
34
|
+
// "die", "dies", "died" and "dieing" as whole words: "how much is a diet
|
|
35
|
+
// coke" and "how to get a hoodie" matched (2026-10-15). Only the "it is to
|
|
36
|
+
// die for" idiom (ending its clause, or before and/but/so/too) and
|
|
37
|
+
// "to-die-for" are exempt; "how to die for the insurance" still matches.
|
|
38
|
+
// "starve myself to death" is named because "diet" no longer stands in for
|
|
39
|
+
// it. Same pattern as the portal copy.
|
|
40
|
+
/how\s+(?:many|much|to).*(?:pills|overdose|starv\w*\s+(?:my)?self\s+to\s+death|(?<!\b(?:is|are|was|were|s|re)\s{1,3}to\s{1,3})(?<!\bto-)\bdie(?:[sd]|ing)?\b|\bdie(?:[sd]|ing)?\b(?!\s+for(?:\s*[.!?,;:—–-]|\s*$|\s+(?:and|but|so|too)\b)|-for\b))/i,
|
|
35
41
|
// Spanish — block hyperbole nouns only (NOT hunger/thirst/cold — may be literal for neglected child)
|
|
36
42
|
/quiero\s+morir(?!\s+de\s+(?:risa|la\s+risa|vergüenza|ganas|envidia|aburrimiento)\b)/i,
|
|
37
43
|
/(?:voy\s+a\s+)?matarme(?!\s+(?:estudiando|trabajando|riendo|de\s+(?:risa|la\s+risa))\b)/i,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prism-mcp-server",
|
|
3
|
-
"version": "20.21.
|
|
3
|
+
"version": "20.21.27",
|
|
4
4
|
"mcpName": "io.github.dcostenco/prism-coder",
|
|
5
5
|
"description": "Persistent session memory for AI coding agents that never leaves your machine — including the on-device model that reasons over it. Restores your prior decisions, open TODOs, and changed files across sessions; adds associative recall of related past work, semantic drift detection, and local inference. Local-first by default. Works with Claude Code, Cursor, and Codex.",
|
|
6
6
|
"module": "index.ts",
|
package/scripts/dev/browse.py
CHANGED
|
@@ -42,8 +42,10 @@ MODES:
|
|
|
42
42
|
"""
|
|
43
43
|
|
|
44
44
|
import argparse
|
|
45
|
+
import codecs
|
|
45
46
|
import datetime
|
|
46
47
|
import hashlib
|
|
48
|
+
import io
|
|
47
49
|
import json
|
|
48
50
|
import os
|
|
49
51
|
import random
|
|
@@ -2198,10 +2200,17 @@ def run_repl(session, sanitize=False, cleanup=False, fail_fast=False):
|
|
|
2198
2200
|
# ---------------------------------------------------------------------------
|
|
2199
2201
|
# Pipe/batch mode
|
|
2200
2202
|
# ---------------------------------------------------------------------------
|
|
2203
|
+
class _PipeIdleTimeout(Exception):
|
|
2204
|
+
"""The producer held the pipe open past the idle timeout."""
|
|
2205
|
+
|
|
2206
|
+
|
|
2201
2207
|
def _stdin_lines_with_idle_timeout(timeout_sec):
|
|
2202
2208
|
"""
|
|
2203
2209
|
Yield stdin lines, giving up if the producer holds the pipe open without
|
|
2204
|
-
sending anything.
|
|
2210
|
+
sending anything. Iterates the stream itself when it has no descriptor,
|
|
2211
|
+
and reads blocking when select() cannot watch the descriptor. Raises
|
|
2212
|
+
_PipeIdleTimeout at the timeout, so a command still being written is
|
|
2213
|
+
dropped rather than run cut short.
|
|
2205
2214
|
"""
|
|
2206
2215
|
stream = sys.stdin
|
|
2207
2216
|
try:
|
|
@@ -2209,31 +2218,73 @@ def _stdin_lines_with_idle_timeout(timeout_sec):
|
|
|
2209
2218
|
except Exception:
|
|
2210
2219
|
yield from stream
|
|
2211
2220
|
return
|
|
2221
|
+
# Read the descriptor directly and split lines here. stream.readline()
|
|
2222
|
+
# pulled a whole chunk into Python's own buffer and returned one line; the
|
|
2223
|
+
# rest sat there while select() watched the empty pipe, so commands that
|
|
2224
|
+
# had already arrived waited for the producer's next write, or for this
|
|
2225
|
+
# timeout. A producer that pauses between blocks (waiting on a result the
|
|
2226
|
+
# held-back commands would produce) deadlocked until then (2026-10-16).
|
|
2227
|
+
#
|
|
2228
|
+
# Decode as the text stream did: one incremental decoder for the whole
|
|
2229
|
+
# stream, in its encoding and error policy, with universal newlines. A
|
|
2230
|
+
# character split across two reads stays whole, and CRLF and a bare CR
|
|
2231
|
+
# both end a line, so a backslash continuation written from Windows
|
|
2232
|
+
# still joins.
|
|
2233
|
+
decoder = io.IncrementalNewlineDecoder(
|
|
2234
|
+
codecs.getincrementaldecoder(getattr(stream, "encoding", None) or "utf-8")(
|
|
2235
|
+
getattr(stream, "errors", None) or "strict"),
|
|
2236
|
+
translate=True,
|
|
2237
|
+
)
|
|
2238
|
+
pending = ""
|
|
2239
|
+
selectable = True
|
|
2212
2240
|
while True:
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
yield
|
|
2217
|
-
|
|
2218
|
-
if
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2241
|
+
newline = pending.find("\n")
|
|
2242
|
+
if newline >= 0:
|
|
2243
|
+
line, pending = pending[:newline + 1], pending[newline + 1:]
|
|
2244
|
+
yield line
|
|
2245
|
+
continue
|
|
2246
|
+
if selectable:
|
|
2247
|
+
try:
|
|
2248
|
+
ready, _, _ = select.select([fileno], [], [], timeout_sec)
|
|
2249
|
+
except Exception:
|
|
2250
|
+
# A descriptor select() cannot watch (a Windows pipe): read it
|
|
2251
|
+
# blocking from here on, keeping any partial line already read.
|
|
2252
|
+
selectable = False
|
|
2253
|
+
ready = True
|
|
2254
|
+
if not ready:
|
|
2255
|
+
audit_log("pipe_idle_timeout", "", f"timeout={timeout_sec}s")
|
|
2256
|
+
print(json.dumps({
|
|
2257
|
+
"status": "error",
|
|
2258
|
+
"action": "pipe",
|
|
2259
|
+
"message": f"No input for {timeout_sec}s. Closing browser to prevent zombie process.",
|
|
2260
|
+
}))
|
|
2261
|
+
sys.stdout.flush()
|
|
2262
|
+
raise _PipeIdleTimeout()
|
|
2263
|
+
chunk = os.read(fileno, 65536)
|
|
2264
|
+
if not chunk:
|
|
2265
|
+
pending += decoder.decode(b"", final=True)
|
|
2266
|
+
if pending:
|
|
2267
|
+
yield pending
|
|
2229
2268
|
return
|
|
2230
|
-
|
|
2269
|
+
pending += decoder.decode(chunk)
|
|
2270
|
+
|
|
2271
|
+
|
|
2272
|
+
def _pipe_commands(timeout_sec):
|
|
2273
|
+
"""
|
|
2274
|
+
The pipe's commands, continuations joined. At the idle timeout the reader
|
|
2275
|
+
raises before the joiner can flush, so a continued command whose tail never
|
|
2276
|
+
came is dropped, not run as its head alone.
|
|
2277
|
+
"""
|
|
2278
|
+
try:
|
|
2279
|
+
yield from _join_continuations(_stdin_lines_with_idle_timeout(timeout_sec))
|
|
2280
|
+
except _PipeIdleTimeout:
|
|
2281
|
+
return
|
|
2231
2282
|
|
|
2232
2283
|
|
|
2233
2284
|
def run_pipe(session, sanitize=False, cleanup=False, fail_fast=False, cleanup_files=None):
|
|
2234
2285
|
"""Read commands from stdin, one per line. Returns True when all succeeded."""
|
|
2235
2286
|
had_error = False
|
|
2236
|
-
for line in
|
|
2287
|
+
for line in _pipe_commands(PIPE_IDLE_TIMEOUT):
|
|
2237
2288
|
line = line.strip()
|
|
2238
2289
|
if not line or line.startswith('#'):
|
|
2239
2290
|
continue
|