0xray 4.0.23 → 4.0.24

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 (56) hide show
  1. package/.grok-plugin/plugin.json +1 -1
  2. package/.xray/features.json +11 -1
  3. package/.xray/features.schema.json +17 -0
  4. package/AGENTS-consumer.md +1 -1
  5. package/AGENTS.md +1 -1
  6. package/README.md +2 -2
  7. package/SKILLS.md +1 -1
  8. package/dist/AGENTS.md +1 -1
  9. package/dist/CHANGELOG.md +20 -0
  10. package/dist/README.md +2 -2
  11. package/dist/SKILLS.md +1 -1
  12. package/dist/cli/index.js +131 -100
  13. package/dist/core/features-config.d.ts +4 -0
  14. package/dist/core/features-config.js +10 -0
  15. package/dist/inference/inference-accumulator.d.ts +2 -0
  16. package/dist/inference/inference-accumulator.js +74 -2
  17. package/dist/inference/inference-cycle.js +1 -0
  18. package/dist/inference/inference-proposal-generator.js +65 -7
  19. package/dist/inference/inference-run-gate.d.ts +8 -0
  20. package/dist/inference/inference-run-gate.js +10 -0
  21. package/dist/inference/session-capture.d.ts +27 -0
  22. package/dist/inference/session-capture.js +277 -1
  23. package/dist/integrations/cursor/hooks/pre-compact.js +89 -1
  24. package/dist/integrations/hooks/plates.cjs +165 -0
  25. package/dist/integrations/hooks/station-hook-runtime.cjs +29 -11
  26. package/dist/integrations/hooks/station-memory-ingest.mjs +109 -42
  27. package/dist/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  28. package/dist/mcps/researcher-confidence.d.ts +5 -1
  29. package/dist/mcps/researcher-confidence.js +37 -0
  30. package/dist/mcps/researcher.server.js +2 -2
  31. package/dist/memory-routing/plates.d.ts +16 -0
  32. package/dist/memory-routing/plates.js +71 -0
  33. package/dist/memory-routing/types.d.ts +2 -0
  34. package/dist/memory-routing/types.js +2 -1
  35. package/dist/processors/implementations/inference-improvement-processor.d.ts +5 -0
  36. package/dist/processors/implementations/inference-improvement-processor.js +101 -2
  37. package/dist/processors/implementations/storytelling-trigger-processor.js +1 -11
  38. package/dist/processors/processor-manager.d.ts +1 -1
  39. package/dist/processors/processor-manager.js +52 -20
  40. package/llms.txt +1 -1
  41. package/package.json +1 -1
  42. package/scripts/foundry/plant/skills/mill/SKILL.md +1 -1
  43. package/scripts/node/consumer-install-smoke.mjs +3 -3
  44. package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  45. package/src/mcps/researcher-confidence.ts +56 -3
  46. package/src/mcps/researcher.server.ts +4 -5
  47. package/vendor/@0xray/repertoire/README.md +3 -1
  48. package/vendor/@0xray/repertoire/dist/registry/CuratedSignalsManager.d.ts +23 -3
  49. package/vendor/@0xray/repertoire/dist/registry/CuratedSignalsManager.d.ts.map +1 -1
  50. package/vendor/@0xray/repertoire/dist/registry/CuratedSignalsManager.js +119 -16
  51. package/vendor/@0xray/repertoire/dist/registry/CuratedSignalsManager.js.map +1 -1
  52. package/vendor/@0xray/repertoire/dist/types.d.ts +1 -1
  53. package/vendor/@0xray/repertoire/dist/types.d.ts.map +1 -1
  54. package/vendor/@0xray/repertoire/package.json +1 -1
  55. package/xray/features.json +11 -1
  56. package/xray/features.schema.json +17 -0
@@ -1,3 +1,4 @@
1
+ import { LESSON_TEXT_CAP } from "../memory-routing/types.js";
1
2
  function primitivesForSessions(corpus, sessionIds) {
2
3
  const wanted = new Set(sessionIds);
3
4
  const names = [];
@@ -35,12 +36,19 @@ function resonanceForNamedSignal(corpus, sessionIds) {
35
36
  }
36
37
  return confidence;
37
38
  }
39
+ function speechSlug(text) {
40
+ const words = text
41
+ .toLowerCase()
42
+ .split(/[^a-z0-9]+/)
43
+ .filter((word) => word.length >= 3)
44
+ .slice(0, 6);
45
+ return words.join("-").slice(0, 48).replace(/-+$/g, "");
46
+ }
38
47
  function sessionsForWrongTurn(corpus, turn) {
39
48
  return corpus.sessions
40
49
  .filter((session) => session.wrongTurns.includes(turn))
41
50
  .map((session) => session.sessionId);
42
51
  }
43
- const LESSON_TEXT_CAP = 400;
44
52
  /** The trace a graded line stores. Wrong turns, then approaches, then solutions. */
45
53
  function lessonTrace(corpus, sessionIds) {
46
54
  const wanted = new Set(sessionIds);
@@ -165,13 +173,16 @@ function adjustConfidenceFromHistory(proposals, history) {
165
173
  }
166
174
  export function generateProposals(corpus, history) {
167
175
  const proposals = [];
176
+ const proposalSessions = new Map();
168
177
  for (const problem of corpus.recurringProblems) {
169
178
  const prefix = `problem:${problem.pattern}:`;
170
179
  const sessions = freshSessions(history, prefix, problem.sessions);
171
180
  if (sessions.length === 0)
172
181
  continue;
182
+ const id = `${prefix}${sessions.join(",")}`;
183
+ proposalSessions.set(id, sessions);
173
184
  proposals.push({
174
- id: `${prefix}${sessions.join(",")}`,
185
+ id,
175
186
  type: classifyProposalType(problem.pattern),
176
187
  title: generateTitle(problem),
177
188
  description: `Recurring across ${problem.occurrences} sessions: ${problem.pattern}`,
@@ -197,8 +208,10 @@ export function generateProposals(corpus, history) {
197
208
  seenPatterns.add(normalized);
198
209
  continue;
199
210
  }
211
+ const id = `${prefix}${sessions.join(",")}`;
212
+ proposalSessions.set(id, sessions);
200
213
  proposals.push({
201
- id: `${prefix}${sessions.join(",")}`,
214
+ id,
202
215
  type: classifyProposalType(problem),
203
216
  title: `Investigate: ${problem.substring(0, 80)}`,
204
217
  description: `Observed in session ${session}: ${problem}`,
@@ -219,8 +232,10 @@ export function generateProposals(corpus, history) {
219
232
  if (sessions.length === 0)
220
233
  continue;
221
234
  const type = patternToProposalType(pattern);
235
+ const id = `${prefix}${sessions.join(",")}`;
236
+ proposalSessions.set(id, sessions);
222
237
  proposals.push({
223
- id: `${prefix}${sessions.join(",")}`,
238
+ id,
224
239
  type,
225
240
  title: `Codify ${pattern.name} pattern`,
226
241
  description: `${pattern.name} detected across ${pattern.occurrences} sessions (avg confidence: ${Math.round(pattern.avgConfidence * 100)}%). ${pattern.description}`,
@@ -239,8 +254,10 @@ export function generateProposals(corpus, history) {
239
254
  const sessions = freshSessions(history, prefix, sessionsForWrongTurn(corpus, wt));
240
255
  if (sessions.length === 0)
241
256
  continue;
257
+ const id = `${prefix}${sessions.join(",")}`;
258
+ proposalSessions.set(id, sessions);
242
259
  proposals.push({
243
- id: `${prefix}${sessions.join(",")}`,
260
+ id,
244
261
  type: "guard",
245
262
  title: `Guard against: ${summary}`,
246
263
  description: `Recurring wrong turn detected: ${wt}. Add a guard or validation to prevent this pattern.`,
@@ -253,12 +270,25 @@ export function generateProposals(corpus, history) {
253
270
  });
254
271
  }
255
272
  const sorted = proposals.sort((a, b) => b.confidence - a.confidence).slice(0, 3);
256
- const alreadyNamed = new Set(sorted.flatMap((proposal) => proposal.namedSignals ?? []));
273
+ const sessionsAlreadyNamed = new Map();
274
+ for (const proposal of sorted) {
275
+ const covered = proposalSessions.get(proposal.id) ?? [];
276
+ for (const name of proposal.namedSignals ?? []) {
277
+ if (name.length === 0)
278
+ continue;
279
+ const set = sessionsAlreadyNamed.get(name) ?? new Set();
280
+ for (const sessionId of covered)
281
+ set.add(sessionId);
282
+ sessionsAlreadyNamed.set(name, set);
283
+ }
284
+ }
257
285
  const bySignal = new Map();
258
286
  for (const session of corpus.sessions) {
259
287
  const listed = session.matched_primitives ?? session.matchedPrimitives ?? [];
260
288
  for (const name of listed) {
261
- if (name.length === 0 || alreadyNamed.has(name))
289
+ if (name.length === 0)
290
+ continue;
291
+ if (sessionsAlreadyNamed.get(name)?.has(session.sessionId))
262
292
  continue;
263
293
  const sessions = bySignal.get(name) ?? [];
264
294
  if (!sessions.includes(session.sessionId))
@@ -266,6 +296,34 @@ export function generateProposals(corpus, history) {
266
296
  bySignal.set(name, sessions);
267
297
  }
268
298
  }
299
+ const minted = new Set();
300
+ for (const session of corpus.sessions) {
301
+ if (minted.size >= 3)
302
+ break;
303
+ const listed = session.matched_primitives ?? session.matchedPrimitives ?? [];
304
+ if (listed.some((name) => name.length > 0))
305
+ continue;
306
+ const speech = lessonTrace(corpus, [session.sessionId]).trim();
307
+ const slug = speechSlug(speech);
308
+ if (slug.length === 0 || minted.has(slug))
309
+ continue;
310
+ const sessions = freshSessions(history, `mint:${slug}:`, [session.sessionId]);
311
+ if (sessions.length === 0)
312
+ continue;
313
+ minted.add(slug);
314
+ sorted.push({
315
+ id: `mint:${slug}:${sessions.join(",")}`,
316
+ type: "codify",
317
+ title: `Learn ${slug}`,
318
+ description: speech,
319
+ evidence: [speech],
320
+ confidence: resonanceForNamedSignal(corpus, sessions),
321
+ source: "recurring_pattern",
322
+ status: "pending",
323
+ namedSignals: [slug],
324
+ lesson: speech,
325
+ });
326
+ }
269
327
  for (const [name, sessionIds] of bySignal) {
270
328
  const sessions = freshSessions(history, `named:${name}:`, sessionIds);
271
329
  if (sessions.length === 0)
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Whether `inference:run` may enter InferenceCycle.
3
+ * A missing key is on: the cycle is how a suit grows memory.
4
+ * Explicit `enabled: false` stays off unless the caller passes force.
5
+ */
6
+ export declare function inferenceRunMayEnter(inference: {
7
+ enabled?: boolean;
8
+ } | undefined, force: boolean): boolean;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Whether `inference:run` may enter InferenceCycle.
3
+ * A missing key is on: the cycle is how a suit grows memory.
4
+ * Explicit `enabled: false` stays off unless the caller passes force.
5
+ */
6
+ export function inferenceRunMayEnter(inference, force) {
7
+ if (force)
8
+ return true;
9
+ return inference?.enabled !== false;
10
+ }
@@ -17,6 +17,8 @@ export interface SessionInference {
17
17
  matched_primitives?: string[];
18
18
  /** Same list after load, so callers can read either field. */
19
19
  matchedPrimitives?: string[];
20
+ /** Inference operation this session was captured from. A second completion of the same id does not write another file. */
21
+ operationId?: string;
20
22
  }
21
23
  export interface ReasoningLink {
22
24
  from: string;
@@ -32,5 +34,30 @@ export interface SessionMetrics {
32
34
  filesDeleted: number;
33
35
  uniqueDirs: number;
34
36
  }
37
+ /** Date stamp plus the operation id. Callers do not append a HEAD suffix. */
38
+ export declare function mintSessionId(now?: Date, distinct?: string): string;
35
39
  export declare function captureSessionInference(fromRef: string, toRef: string): SessionInference | null;
36
40
  export declare function saveSessionInference(inference: SessionInference, outputDir?: string): string;
41
+ export interface OperationCaptureInput {
42
+ operationId: string;
43
+ promptsDir: string;
44
+ inferenceDir: string;
45
+ signalNames: readonly string[];
46
+ }
47
+ /**
48
+ * One corpus session for a finished inference operation.
49
+ * Speech comes from that run's prompt result text. No speech, no file.
50
+ * Does not grade. The caller runs the cycle before it returns.
51
+ */
52
+ export declare function captureCompletedInferenceOperation(input: OperationCaptureInput): string | null;
53
+ /** Id, or that id with hyphens read as spaces. Same hit `patternsFromGit` stores for the proposal generator. */
54
+ export declare function reflectionSessionId(relativePath: string): string;
55
+ /** Reflection markdown is the speech. No session file is written. */
56
+ export declare function captureReflectionInference(input: {
57
+ relativePath: string;
58
+ text: string;
59
+ modifiedAt: Date;
60
+ signalNames: readonly string[];
61
+ }): SessionInference | null;
62
+ /** The gauge file. One session log per reflection, skipped when that id is already on disk. */
63
+ export declare function saveReflectionSession(session: SessionInference, inferenceDir: string): string | null;
@@ -2,6 +2,16 @@ import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  import { execSync } from "child_process";
4
4
  import { analyzeStructuralPatterns } from "./semantic-patterns.js";
5
+ /** Date stamp plus the operation id. Callers do not append a HEAD suffix. */
6
+ export function mintSessionId(now = new Date(), distinct) {
7
+ const day = now.toISOString().slice(0, 10);
8
+ const suffix = (distinct ?? "")
9
+ .trim()
10
+ .toLowerCase()
11
+ .replace(/[^a-z0-9]+/g, "-")
12
+ .replace(/^-+|-+$/g, "");
13
+ return suffix.length > 0 ? `session-${day}-${suffix}` : `session-${day}`;
14
+ }
5
15
  export function captureSessionInference(fromRef, toRef) {
6
16
  const commits = getCommitData(fromRef, toRef);
7
17
  if (commits.length === 0)
@@ -14,7 +24,7 @@ export function captureSessionInference(fromRef, toRef) {
14
24
  const solutions = extractSolutions(commits, patterns);
15
25
  const reasoningChain = buildReasoningChain(problems, approaches, wrongTurns, solutions);
16
26
  return {
17
- sessionId: `session-${new Date().toISOString().slice(0, 10)}`,
27
+ sessionId: mintSessionId(),
18
28
  timestamp: new Date().toISOString(),
19
29
  span: { from: fromRef, to: toRef },
20
30
  problems,
@@ -282,6 +292,272 @@ function buildReasoningChain(problems, approaches, wrongTurns, solutions) {
282
292
  }
283
293
  return chain;
284
294
  }
295
+ /** Same cap `lessonTrace` applies before a vote can store the line. */
296
+ const OPERATION_LESSON_CAP = 400;
297
+ /**
298
+ * One corpus session for a finished inference operation.
299
+ * Speech comes from that run's prompt result text. No speech, no file.
300
+ * Does not grade. The caller runs the cycle before it returns.
301
+ */
302
+ export function captureCompletedInferenceOperation(input) {
303
+ const operationId = input.operationId.trim();
304
+ if (!operationId)
305
+ return null;
306
+ if (operationAlreadyCaptured(input.inferenceDir, operationId))
307
+ return null;
308
+ const raw = readOperationSpeech(input.promptsDir, operationId);
309
+ const speech = parseOperationSpeech(raw);
310
+ const heard = speech.approaches.length + speech.solutions.length + speech.wrongTurns.length;
311
+ if (!raw.trim() || heard === 0)
312
+ return null;
313
+ const named = storedSignalsNamedBySpeech(raw, input.signalNames);
314
+ const now = new Date().toISOString();
315
+ const session = {
316
+ sessionId: mintSessionId(new Date(), operationId),
317
+ timestamp: now,
318
+ span: { from: operationId, to: operationId },
319
+ problems: [],
320
+ approaches: speech.approaches,
321
+ wrongTurns: speech.wrongTurns,
322
+ solutions: speech.solutions,
323
+ reasoningChain: [],
324
+ patterns: [],
325
+ metrics: {
326
+ commits: 0,
327
+ filesChanged: 0,
328
+ insertions: 0,
329
+ deletions: 0,
330
+ filesAdded: 0,
331
+ filesDeleted: 0,
332
+ uniqueDirs: 0,
333
+ },
334
+ matched_primitives: named,
335
+ matchedPrimitives: named,
336
+ operationId,
337
+ };
338
+ return saveSessionInference(session, input.inferenceDir);
339
+ }
340
+ function operationAlreadyCaptured(inferenceDir, operationId) {
341
+ if (!fs.existsSync(inferenceDir))
342
+ return false;
343
+ for (const name of fs.readdirSync(inferenceDir)) {
344
+ if (!name.startsWith("session-") || !name.endsWith(".json"))
345
+ continue;
346
+ try {
347
+ const parsed = JSON.parse(fs.readFileSync(path.join(inferenceDir, name), "utf8"));
348
+ if (parsed.operationId === operationId)
349
+ return true;
350
+ }
351
+ catch {
352
+ continue;
353
+ }
354
+ }
355
+ return false;
356
+ }
357
+ function readOperationSpeech(promptsDir, operationId) {
358
+ if (!fs.existsSync(promptsDir))
359
+ return "";
360
+ const chunks = [];
361
+ for (const name of fs.readdirSync(promptsDir).sort()) {
362
+ const full = path.join(promptsDir, name);
363
+ let stat;
364
+ try {
365
+ stat = fs.statSync(full);
366
+ }
367
+ catch {
368
+ continue;
369
+ }
370
+ if (!stat.isFile())
371
+ continue;
372
+ let text = "";
373
+ try {
374
+ text = fs.readFileSync(full, "utf8");
375
+ }
376
+ catch {
377
+ continue;
378
+ }
379
+ if (name === `${operationId}.result.md` || name === `${operationId}.result.txt`) {
380
+ chunks.push(text);
381
+ continue;
382
+ }
383
+ if (/\.result\.(md|txt)$/i.test(name) && operationDeclared(text) === operationId) {
384
+ chunks.push(text);
385
+ continue;
386
+ }
387
+ if (name.endsWith(".md") && !name.includes(".result.")) {
388
+ const section = resultSection(text);
389
+ if (section && (operationDeclared(section) === operationId || operationDeclared(text) === operationId)) {
390
+ chunks.push(section);
391
+ }
392
+ }
393
+ }
394
+ return chunks.join("\n").trim();
395
+ }
396
+ function operationDeclared(text) {
397
+ const match = /^operation:\s*(\S+)\s*$/im.exec(text);
398
+ return match?.[1] ?? null;
399
+ }
400
+ function resultSection(text) {
401
+ const parts = text.split(/\n##\s+(?:Result|Speech)\s*\n/i);
402
+ if (parts.length < 2)
403
+ return "";
404
+ return parts.slice(1).join("\n").trim();
405
+ }
406
+ function parseOperationSpeech(raw) {
407
+ const body = raw
408
+ .split("\n")
409
+ .filter((line) => !/^operation:\s*\S+\s*$/i.test(line.trim()))
410
+ .join("\n")
411
+ .trim();
412
+ const buckets = { approaches: [], solutions: [], wrongTurns: [] };
413
+ if (!body)
414
+ return buckets;
415
+ let current = null;
416
+ let labeled = false;
417
+ const sectionLines = [];
418
+ const flush = () => {
419
+ if (!current) {
420
+ sectionLines.length = 0;
421
+ return;
422
+ }
423
+ const text = fewSentences(sectionLines.join(" "));
424
+ sectionLines.length = 0;
425
+ if (text)
426
+ buckets[current].push(text);
427
+ };
428
+ for (const line of body.split("\n")) {
429
+ const trimmed = line.trim();
430
+ if (!trimmed)
431
+ continue;
432
+ const inline = /^([^:]{2,40}):\s+(.+)$/.exec(trimmed);
433
+ if (inline) {
434
+ const bucket = bucketForLabel(inline[1] ?? "");
435
+ if (bucket) {
436
+ flush();
437
+ current = bucket;
438
+ labeled = true;
439
+ sectionLines.push(inline[2] ?? "");
440
+ continue;
441
+ }
442
+ }
443
+ const heading = /^(?:#{1,3}\s*)(.+)$/.exec(trimmed);
444
+ if (heading) {
445
+ const bucket = bucketForLabel(heading[1] ?? "");
446
+ if (bucket) {
447
+ flush();
448
+ current = bucket;
449
+ labeled = true;
450
+ continue;
451
+ }
452
+ }
453
+ if (current)
454
+ sectionLines.push(trimmed);
455
+ }
456
+ flush();
457
+ if (!labeled) {
458
+ const text = fewSentences(body);
459
+ if (text)
460
+ buckets.approaches.push(text);
461
+ }
462
+ return buckets;
463
+ }
464
+ function bucketForLabel(label) {
465
+ const lower = label.toLowerCase().replace(/^#+\s*/, "").replace(/:$/, "").trim();
466
+ if (/^(tried|approaches?|what was tried)$/.test(lower))
467
+ return "approaches";
468
+ if (/^(worked|solutions?|what worked)$/.test(lower))
469
+ return "solutions";
470
+ if (/^(failed|wrong turns?|what failed)$/.test(lower))
471
+ return "wrongTurns";
472
+ return null;
473
+ }
474
+ function fewSentences(text) {
475
+ const flat = text.replace(/\s+/g, " ").trim();
476
+ if (!flat)
477
+ return "";
478
+ const sentences = flat.split(/(?<=[.!?])\s+/).filter((part) => part.length > 0).slice(0, 4);
479
+ const joined = sentences.join(" ");
480
+ if (joined.length <= OPERATION_LESSON_CAP)
481
+ return joined;
482
+ return joined.slice(0, OPERATION_LESSON_CAP);
483
+ }
484
+ /** Id, or that id with hyphens read as spaces. Same hit `patternsFromGit` stores for the proposal generator. */
485
+ export function reflectionSessionId(relativePath) {
486
+ const slug = relativePath
487
+ .replace(/\.md$/i, "")
488
+ .trim()
489
+ .toLowerCase()
490
+ .replace(/[^a-z0-9]+/g, "-")
491
+ .replace(/^-+|-+$/g, "");
492
+ return `session-reflection-${slug}`;
493
+ }
494
+ /** Reflection markdown is the speech. No session file is written. */
495
+ export function captureReflectionInference(input) {
496
+ const speech = parseOperationSpeech(input.text);
497
+ const heard = speech.approaches.length + speech.solutions.length + speech.wrongTurns.length;
498
+ if (heard === 0)
499
+ return null;
500
+ const named = storedSignalsNamedBySpeech([...speech.approaches, ...speech.solutions, ...speech.wrongTurns].join(" "), input.signalNames);
501
+ const emptyMetrics = {
502
+ commits: 0,
503
+ filesChanged: 0,
504
+ insertions: 0,
505
+ deletions: 0,
506
+ filesAdded: 0,
507
+ filesDeleted: 0,
508
+ uniqueDirs: 0,
509
+ };
510
+ return {
511
+ sessionId: reflectionSessionId(input.relativePath),
512
+ timestamp: input.modifiedAt.toISOString(),
513
+ span: { from: "reflection", to: input.relativePath },
514
+ problems: [],
515
+ approaches: speech.approaches,
516
+ wrongTurns: speech.wrongTurns,
517
+ solutions: speech.solutions,
518
+ reasoningChain: [],
519
+ patterns: [],
520
+ metrics: emptyMetrics,
521
+ matched_primitives: named,
522
+ matchedPrimitives: named,
523
+ };
524
+ }
525
+ /** The gauge file. One session log per reflection, skipped when that id is already on disk. */
526
+ export function saveReflectionSession(session, inferenceDir) {
527
+ if (reflectionAlreadySaved(inferenceDir, session.sessionId))
528
+ return null;
529
+ return saveSessionInference(session, inferenceDir);
530
+ }
531
+ function reflectionAlreadySaved(inferenceDir, sessionId) {
532
+ if (!fs.existsSync(inferenceDir))
533
+ return false;
534
+ for (const name of fs.readdirSync(inferenceDir)) {
535
+ if (!name.startsWith("session-") || !name.endsWith(".json") || name === "latest-session.json")
536
+ continue;
537
+ try {
538
+ const parsed = JSON.parse(fs.readFileSync(path.join(inferenceDir, name), "utf8"));
539
+ if (parsed.sessionId === sessionId)
540
+ return true;
541
+ }
542
+ catch {
543
+ continue;
544
+ }
545
+ }
546
+ return false;
547
+ }
548
+ function storedSignalsNamedBySpeech(speech, signalNames) {
549
+ const hay = speech.toLowerCase();
550
+ const named = [];
551
+ for (const name of signalNames) {
552
+ const id = name.trim().toLowerCase();
553
+ if (!id)
554
+ continue;
555
+ const spaced = id.replace(/-/g, " ");
556
+ if (hay.includes(id) || (spaced !== id && hay.includes(spaced)))
557
+ named.push(name.trim());
558
+ }
559
+ return [...new Set(named)];
560
+ }
285
561
  function approachKeyword(approach) {
286
562
  const lower = approach.toLowerCase();
287
563
  if (lower.includes("extract"))
@@ -22,6 +22,74 @@ import {
22
22
  writeCursorUsageReceipt,
23
23
  } from './cursor-usage-receipt.js';
24
24
 
25
+ async function loadPlates() {
26
+ const { existsSync } = await import('node:fs');
27
+ const { dirname, join } = await import('node:path');
28
+ const { fileURLToPath, pathToFileURL } = await import('node:url');
29
+ const here = dirname(fileURLToPath(import.meta.url));
30
+ const candidates = [
31
+ join(here, '../../hooks/plates.cjs'),
32
+ join(here, '../../../integrations/hooks/plates.cjs'),
33
+ ];
34
+ const found = candidates.find((candidate) => existsSync(candidate));
35
+ if (!found) return null;
36
+ const loaded = await import(pathToFileURL(found).href);
37
+ if (typeof loaded.recallPlate === 'function') return loaded;
38
+ if (loaded.default && typeof loaded.default.recallPlate === 'function') return loaded.default;
39
+ return null;
40
+ }
41
+
42
+ async function memoryRoutingModule() {
43
+ const { existsSync } = await import('node:fs');
44
+ const { dirname, join } = await import('node:path');
45
+ const { fileURLToPath, pathToFileURL } = await import('node:url');
46
+ const here = dirname(fileURLToPath(import.meta.url));
47
+ const candidates = [
48
+ join(here, '../../../memory-routing/index.js'),
49
+ join(here, '../../../../dist/memory-routing/index.js'),
50
+ ];
51
+ const found = candidates.find((candidate) => existsSync(candidate));
52
+ if (!found) throw new Error('memory-routing module missing');
53
+ return pathToFileURL(found).href;
54
+ }
55
+
56
+ async function lessonSpeechForIntent(root, intent) {
57
+ if (!intent) return [];
58
+ try {
59
+ const { readFileSync, existsSync } = await import('node:fs');
60
+ const { join } = await import('node:path');
61
+ const { loadMemoryRoutingProvider } = await import(await memoryRoutingModule());
62
+ let routing = { enabled: false, provider: 'null' };
63
+ const featuresPath = join(root, '.xray', 'features.json');
64
+ if (existsSync(featuresPath)) {
65
+ try {
66
+ routing = JSON.parse(readFileSync(featuresPath, 'utf8')).memory_routing || routing;
67
+ } catch {
68
+ /* leftover default */
69
+ }
70
+ }
71
+ const provider = await loadMemoryRoutingProvider(routing, root);
72
+ if (!provider || provider.id === 'null' || typeof provider.buildRoutingContext !== 'function') {
73
+ return [];
74
+ }
75
+ const context = provider.buildRoutingContext(String(intent));
76
+ const matched = new Set(Array.isArray(context.matchedSignals) ? context.matchedSignals : []);
77
+ const lines = [];
78
+ for (const lesson of context.lessons || []) {
79
+ if (!matched.has(lesson.name)) continue;
80
+ for (const line of lesson.lines || []) {
81
+ const text = String(line.text || '').trim().slice(0, 400);
82
+ if (text.length === 0) continue;
83
+ lines.push(`${lesson.name}: ${text}`);
84
+ if (lines.length >= 4) return lines;
85
+ }
86
+ }
87
+ return lines;
88
+ } catch {
89
+ return [];
90
+ }
91
+ }
92
+
25
93
  function extractIntent(event) {
26
94
  return (
27
95
  event.prompt ||
@@ -47,6 +115,13 @@ async function main() {
47
115
  let receiptPath = null;
48
116
  let usagePath = null;
49
117
  let stationLine = null;
118
+ let plates = null;
119
+ let plateBody = '';
120
+ try {
121
+ plates = await loadPlates();
122
+ } catch {
123
+ plates = null;
124
+ }
50
125
  for (const root of cursorHeatRoots(event)) {
51
126
  const payload = buildSessionBootPayload(root, '0xray/cursor-compact', {
52
127
  host: 'cursor',
@@ -77,6 +152,17 @@ async function main() {
77
152
  sessionId,
78
153
  usage: hostUsageFromPreCompactEvent(event),
79
154
  });
155
+ if (plates) {
156
+ try {
157
+ const named = plates.recallPlate(payload.intent || intent || '');
158
+ if (named) {
159
+ plates.stampPlateIfMissing(root, named.id);
160
+ if (!plateBody) plateBody = `\n\n${String(named.body || '').trim()}`;
161
+ }
162
+ } catch {
163
+ /* plate stamp is observational */
164
+ }
165
+ }
80
166
  }
81
167
  appendHookActivity(eventRoot, 'cursor-pre-compact', 'station-written', 'success', {
82
168
  bootPath,
@@ -85,10 +171,12 @@ async function main() {
85
171
  event_class: eventClass,
86
172
  stationLine,
87
173
  });
174
+ const lessonLines = await lessonSpeechForIntent(eventRoot, intent);
175
+ const lessonNote = lessonLines.length > 0 ? ` Lessons: ${lessonLines.join(' | ')}` : '';
88
176
  console.log(
89
177
  JSON.stringify({
90
178
  user_message:
91
- `0xRay Station written. Read .xray/state/STATION.md — do not cold-start. event_class=${eventClass}`,
179
+ `0xRay Station written. Read .xray/state/STATION.md — do not cold-start. event_class=${eventClass}${lessonNote}${plateBody}`,
92
180
  }),
93
181
  );
94
182
  process.exit(0);