0xray 4.0.22 → 4.0.23

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 (65) hide show
  1. package/.grok-plugin/plugin.json +1 -1
  2. package/.opencode/skills/orchestrator/SKILL.md +7 -7
  3. package/.xray/features.json +1 -1
  4. package/dist/CHANGELOG.md +24 -1
  5. package/dist/inference/inference-accumulator.js +36 -4
  6. package/dist/inference/inference-cycle.d.ts +12 -0
  7. package/dist/inference/inference-cycle.js +158 -16
  8. package/dist/inference/inference-proposal-generator.js +148 -5
  9. package/dist/inference/session-capture.d.ts +4 -0
  10. package/dist/integrations/governance/index.d.ts +5 -0
  11. package/dist/integrations/governance/index.js +22 -2
  12. package/dist/integrations/governance/types.d.ts +11 -0
  13. package/dist/integrations/governance/types.js +16 -0
  14. package/dist/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  15. package/dist/mcps/governance.server.d.ts +2 -0
  16. package/dist/mcps/governance.server.js +12 -1
  17. package/dist/memory-routing/index.d.ts +1 -1
  18. package/dist/memory-routing/index.js +1 -1
  19. package/dist/memory-routing/lesson-pickup.d.ts +2 -0
  20. package/dist/memory-routing/lesson-pickup.js +57 -0
  21. package/dist/memory-routing/provider-loader.d.ts +5 -0
  22. package/dist/memory-routing/provider-loader.js +46 -0
  23. package/dist/memory-routing/provider-registry.d.ts +7 -2
  24. package/dist/memory-routing/provider-registry.js +26 -6
  25. package/dist/memory-routing/record-lesson.d.ts +16 -0
  26. package/dist/memory-routing/record-lesson.js +26 -0
  27. package/dist/memory-routing/types.d.ts +15 -0
  28. package/dist/nucleus/thin-dispatch.d.ts +2 -0
  29. package/dist/nucleus/thin-dispatch.js +3 -2
  30. package/dist/skills/orchestrator/SKILL.md +7 -7
  31. package/package.json +1 -1
  32. package/scripts/foundry/reconcile-version.mjs +23 -1
  33. package/scripts/foundry/release.mjs +1 -1
  34. package/scripts/foundry/version-manager.mjs +53 -0
  35. package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  36. package/src/mcps/governance.server.ts +12 -1
  37. package/src/skills/orchestrator/SKILL.md +7 -7
  38. package/vendor/@0xray/repertoire/data/stack-overlay.json +4 -4
  39. package/vendor/@0xray/repertoire/data/subject-overlay.json +3 -3
  40. package/vendor/@0xray/repertoire/dist/RepertoireService.d.ts +4 -0
  41. package/vendor/@0xray/repertoire/dist/RepertoireService.d.ts.map +1 -1
  42. package/vendor/@0xray/repertoire/dist/RepertoireService.js +4 -4
  43. package/vendor/@0xray/repertoire/dist/RepertoireService.js.map +1 -1
  44. package/vendor/@0xray/repertoire/dist/ingestion/xray-session-ingester.d.ts.map +1 -1
  45. package/vendor/@0xray/repertoire/dist/ingestion/xray-session-ingester.js.map +1 -1
  46. package/vendor/@0xray/repertoire/dist/orchestrator-bridge/confidence-gate.d.ts.map +1 -1
  47. package/vendor/@0xray/repertoire/dist/orchestrator-bridge/confidence-gate.js +5 -0
  48. package/vendor/@0xray/repertoire/dist/orchestrator-bridge/confidence-gate.js.map +1 -1
  49. package/vendor/@0xray/repertoire/dist/orchestrator-bridge/signal-injector.d.ts.map +1 -1
  50. package/vendor/@0xray/repertoire/dist/orchestrator-bridge/signal-injector.js +16 -0
  51. package/vendor/@0xray/repertoire/dist/orchestrator-bridge/signal-injector.js.map +1 -1
  52. package/vendor/@0xray/repertoire/dist/paths.d.ts +3 -3
  53. package/vendor/@0xray/repertoire/dist/paths.d.ts.map +1 -1
  54. package/vendor/@0xray/repertoire/dist/paths.js.map +1 -1
  55. package/vendor/@0xray/repertoire/dist/provider/memory-routing-provider.d.ts +11 -0
  56. package/vendor/@0xray/repertoire/dist/provider/memory-routing-provider.d.ts.map +1 -1
  57. package/vendor/@0xray/repertoire/dist/provider/memory-routing-provider.js +2 -0
  58. package/vendor/@0xray/repertoire/dist/provider/memory-routing-provider.js.map +1 -1
  59. package/vendor/@0xray/repertoire/dist/registry/CuratedSignalsManager.d.ts +54 -4
  60. package/vendor/@0xray/repertoire/dist/registry/CuratedSignalsManager.d.ts.map +1 -1
  61. package/vendor/@0xray/repertoire/dist/registry/CuratedSignalsManager.js +326 -11
  62. package/vendor/@0xray/repertoire/dist/registry/CuratedSignalsManager.js.map +1 -1
  63. package/vendor/@0xray/repertoire/dist/types.d.ts +21 -0
  64. package/vendor/@0xray/repertoire/dist/types.d.ts.map +1 -1
  65. package/xray/features.json +1 -1
@@ -1,3 +1,87 @@
1
+ function primitivesForSessions(corpus, sessionIds) {
2
+ const wanted = new Set(sessionIds);
3
+ const names = [];
4
+ for (const session of corpus.sessions) {
5
+ if (!wanted.has(session.sessionId))
6
+ continue;
7
+ const listed = session.matched_primitives ?? session.matchedPrimitives ?? [];
8
+ for (const name of listed)
9
+ names.push(name);
10
+ }
11
+ return [...new Set(names.filter((name) => name.length > 0))];
12
+ }
13
+ function signalsForPattern(corpus, pattern) {
14
+ const names = primitivesForSessions(corpus, pattern.sessions);
15
+ if (pattern.name.length > 0)
16
+ names.push(pattern.name);
17
+ return [...new Set(names)];
18
+ }
19
+ /** Solid band starts at 0.82. Landed work with no wrong turn clears it. */
20
+ const LANDED_RESONANCE = 0.85;
21
+ const WRONG_TURN_RESONANCE = 0.4;
22
+ function resonanceForNamedSignal(corpus, sessionIds) {
23
+ const sessions = corpus.sessions.filter((session) => sessionIds.includes(session.sessionId));
24
+ if (sessions.some((session) => (session.wrongTurns?.length ?? 0) > 0))
25
+ return WRONG_TURN_RESONANCE;
26
+ if (sessions.some((session) => (session.approaches?.length ?? 0) > 0 || (session.solutions?.length ?? 0) > 0)) {
27
+ return LANDED_RESONANCE;
28
+ }
29
+ let confidence = 0.7;
30
+ for (const session of sessions) {
31
+ for (const pattern of session.patterns) {
32
+ if (pattern.confidence > confidence)
33
+ confidence = pattern.confidence;
34
+ }
35
+ }
36
+ return confidence;
37
+ }
38
+ function sessionsForWrongTurn(corpus, turn) {
39
+ return corpus.sessions
40
+ .filter((session) => session.wrongTurns.includes(turn))
41
+ .map((session) => session.sessionId);
42
+ }
43
+ const LESSON_TEXT_CAP = 400;
44
+ /** The trace a graded line stores. Wrong turns, then approaches, then solutions. */
45
+ function lessonTrace(corpus, sessionIds) {
46
+ const wanted = new Set(sessionIds);
47
+ const lines = [];
48
+ for (const session of corpus.sessions) {
49
+ if (!wanted.has(session.sessionId))
50
+ continue;
51
+ for (const turn of session.wrongTurns ?? []) {
52
+ if (turn.trim())
53
+ lines.push(turn.trim());
54
+ }
55
+ for (const line of [...(session.approaches ?? []), ...(session.solutions ?? [])]) {
56
+ if (line.trim())
57
+ lines.push(line.trim());
58
+ }
59
+ }
60
+ return [...new Set(lines)].join("; ").slice(0, LESSON_TEXT_CAP);
61
+ }
62
+ function sortedSessionIds(sessionIds) {
63
+ return [...new Set(sessionIds.filter((sessionId) => sessionId.length > 0))].sort();
64
+ }
65
+ /** Sessions already included in a prior proposal of this exact prefix. */
66
+ function coveredSessions(history, prefix) {
67
+ const seen = new Set();
68
+ for (const cycle of history ?? []) {
69
+ for (const proposal of cycle.proposals) {
70
+ if (!proposal.id.startsWith(prefix))
71
+ continue;
72
+ for (const sessionId of proposal.id.slice(prefix.length).split(",")) {
73
+ if (sessionId.length > 0)
74
+ seen.add(sessionId);
75
+ }
76
+ }
77
+ }
78
+ return seen;
79
+ }
80
+ /** Sessions that have not already been graded under this prefix. */
81
+ function freshSessions(history, prefix, sessionIds) {
82
+ const covered = coveredSessions(history, prefix);
83
+ return sortedSessionIds(sessionIds).filter((sessionId) => !covered.has(sessionId));
84
+ }
1
85
  function classifyProposalType(problemPattern) {
2
86
  const lower = problemPattern.toLowerCase();
3
87
  if (lower.includes("bug") || lower.includes("fix") || lower.includes("stability"))
@@ -63,6 +147,8 @@ function adjustConfidenceFromHistory(proposals, history) {
63
147
  }
64
148
  }
65
149
  for (const proposal of proposals) {
150
+ if (proposal.id.startsWith("named:"))
151
+ continue;
66
152
  const approved = approvedTypes.get(proposal.type) ?? 0;
67
153
  const rejected = rejectedTypes.get(proposal.type) ?? 0;
68
154
  const total = approved + rejected;
@@ -80,15 +166,21 @@ function adjustConfidenceFromHistory(proposals, history) {
80
166
  export function generateProposals(corpus, history) {
81
167
  const proposals = [];
82
168
  for (const problem of corpus.recurringProblems) {
169
+ const prefix = `problem:${problem.pattern}:`;
170
+ const sessions = freshSessions(history, prefix, problem.sessions);
171
+ if (sessions.length === 0)
172
+ continue;
83
173
  proposals.push({
84
- id: `prop-${Date.now()}-${proposals.length}`,
174
+ id: `${prefix}${sessions.join(",")}`,
85
175
  type: classifyProposalType(problem.pattern),
86
176
  title: generateTitle(problem),
87
177
  description: `Recurring across ${problem.occurrences} sessions: ${problem.pattern}`,
88
- evidence: problem.sessions.map((s) => `Seen in session ${s}`),
178
+ evidence: sessions.map((s) => `Seen in session ${s}`),
89
179
  confidence: Math.min(0.95, 0.5 + problem.occurrences * 0.15),
90
180
  source: "recurring_problem",
91
181
  status: "pending",
182
+ namedSignals: primitivesForSessions(corpus, sessions),
183
+ lesson: lessonTrace(corpus, sessions),
92
184
  });
93
185
  }
94
186
  const seenPatterns = new Set(corpus.recurringProblems.map((p) => p.pattern));
@@ -99,8 +191,14 @@ export function generateProposals(corpus, history) {
99
191
  continue;
100
192
  if (proposals.length >= 5)
101
193
  break;
194
+ const prefix = `problem:${normalized}:`;
195
+ const sessions = freshSessions(history, prefix, [session]);
196
+ if (sessions.length === 0) {
197
+ seenPatterns.add(normalized);
198
+ continue;
199
+ }
102
200
  proposals.push({
103
- id: `prop-${Date.now()}-${proposals.length}`,
201
+ id: `${prefix}${sessions.join(",")}`,
104
202
  type: classifyProposalType(problem),
105
203
  title: `Investigate: ${problem.substring(0, 80)}`,
106
204
  description: `Observed in session ${session}: ${problem}`,
@@ -108,15 +206,21 @@ export function generateProposals(corpus, history) {
108
206
  confidence: 0.4,
109
207
  source: "recurring_problem",
110
208
  status: "pending",
209
+ namedSignals: primitivesForSessions(corpus, sessions),
210
+ lesson: lessonTrace(corpus, sessions),
111
211
  });
112
212
  seenPatterns.add(normalized);
113
213
  }
114
214
  for (const pattern of corpus.recurringPatterns) {
115
215
  if (pattern.occurrences < 2)
116
216
  continue;
217
+ const prefix = `pattern:${pattern.name}:`;
218
+ const sessions = freshSessions(history, prefix, pattern.sessions);
219
+ if (sessions.length === 0)
220
+ continue;
117
221
  const type = patternToProposalType(pattern);
118
222
  proposals.push({
119
- id: `prop-${Date.now()}-${proposals.length}`,
223
+ id: `${prefix}${sessions.join(",")}`,
120
224
  type,
121
225
  title: `Codify ${pattern.name} pattern`,
122
226
  description: `${pattern.name} detected across ${pattern.occurrences} sessions (avg confidence: ${Math.round(pattern.avgConfidence * 100)}%). ${pattern.description}`,
@@ -124,13 +228,19 @@ export function generateProposals(corpus, history) {
124
228
  confidence: pattern.avgConfidence,
125
229
  source: "recurring_pattern",
126
230
  status: "pending",
231
+ namedSignals: signalsForPattern(corpus, { ...pattern, sessions }),
232
+ lesson: lessonTrace(corpus, sessions),
127
233
  });
128
234
  }
129
235
  const wrongTurns = corpus.allWrongTurns.slice(0, 2);
130
236
  for (const wt of wrongTurns) {
131
237
  const summary = wt.length > 50 ? `${wt.substring(0, 47)}...` : wt;
238
+ const prefix = `wrong:${wt}:`;
239
+ const sessions = freshSessions(history, prefix, sessionsForWrongTurn(corpus, wt));
240
+ if (sessions.length === 0)
241
+ continue;
132
242
  proposals.push({
133
- id: `prop-${Date.now()}-${proposals.length}`,
243
+ id: `${prefix}${sessions.join(",")}`,
134
244
  type: "guard",
135
245
  title: `Guard against: ${summary}`,
136
246
  description: `Recurring wrong turn detected: ${wt}. Add a guard or validation to prevent this pattern.`,
@@ -138,9 +248,42 @@ export function generateProposals(corpus, history) {
138
248
  confidence: 0.7,
139
249
  source: "wrong_turn",
140
250
  status: "pending",
251
+ namedSignals: primitivesForSessions(corpus, sessions),
252
+ lesson: lessonTrace(corpus, sessions),
141
253
  });
142
254
  }
143
255
  const sorted = proposals.sort((a, b) => b.confidence - a.confidence).slice(0, 3);
256
+ const alreadyNamed = new Set(sorted.flatMap((proposal) => proposal.namedSignals ?? []));
257
+ const bySignal = new Map();
258
+ for (const session of corpus.sessions) {
259
+ const listed = session.matched_primitives ?? session.matchedPrimitives ?? [];
260
+ for (const name of listed) {
261
+ if (name.length === 0 || alreadyNamed.has(name))
262
+ continue;
263
+ const sessions = bySignal.get(name) ?? [];
264
+ if (!sessions.includes(session.sessionId))
265
+ sessions.push(session.sessionId);
266
+ bySignal.set(name, sessions);
267
+ }
268
+ }
269
+ for (const [name, sessionIds] of bySignal) {
270
+ const sessions = freshSessions(history, `named:${name}:`, sessionIds);
271
+ if (sessions.length === 0)
272
+ continue;
273
+ const id = `named:${name}:${sessions.join(",")}`;
274
+ sorted.push({
275
+ id,
276
+ type: "codify",
277
+ title: `Grade named signal ${name}`,
278
+ description: `Sessions ${sessions.join(", ")} named ${name}.`,
279
+ evidence: sessions.map((sessionId) => `Seen in session ${sessionId}`),
280
+ confidence: resonanceForNamedSignal(corpus, sessions),
281
+ source: "recurring_pattern",
282
+ status: "pending",
283
+ namedSignals: [name],
284
+ lesson: lessonTrace(corpus, sessions),
285
+ });
286
+ }
144
287
  if (history && history.length > 0) {
145
288
  adjustConfidenceFromHistory(sorted, history);
146
289
  sorted.sort((a, b) => b.confidence - a.confidence);
@@ -13,6 +13,10 @@ export interface SessionInference {
13
13
  reasoningChain: ReasoningLink[];
14
14
  patterns: StructuralPattern[];
15
15
  metrics: SessionMetrics;
16
+ /** Signals this session already named. Session JSON stores this list. */
17
+ matched_primitives?: string[];
18
+ /** Same list after load, so callers can read either field. */
19
+ matchedPrimitives?: string[];
16
20
  }
17
21
  export interface ReasoningLink {
18
22
  from: string;
@@ -39,6 +39,11 @@ export declare class InferenceGovernanceIntegration extends BaseIntegration {
39
39
  * Check if governance is enabled and available
40
40
  */
41
41
  isAvailable(): boolean;
42
+ /**
43
+ * Start the Dynamo client when a caller required external governance
44
+ * and boot skipped it because inference_governance.enabled is false.
45
+ */
46
+ ensureDynamoClient(): Promise<void>;
42
47
  /**
43
48
  * Check a single inference proposal via the appropriate governance tool
44
49
  *
@@ -7,7 +7,7 @@
7
7
  * @since 2026-05-11
8
8
  */
9
9
  import { BaseIntegration } from '../base/index.js';
10
- import { DEFAULT_GOVERNANCE_CONFIG } from './types.js';
10
+ import { DEFAULT_GOVERNANCE_CONFIG, resolveDynamoBaseUrl } from './types.js';
11
11
  import { GovernanceClient } from './governance-client.js';
12
12
  import { frameworkLogger } from '../../core/framework-logger.js';
13
13
  import { featuresConfigLoader } from '../../core/features-config.js';
@@ -84,7 +84,27 @@ export class InferenceGovernanceIntegration extends BaseIntegration {
84
84
  * Check if governance is enabled and available
85
85
  */
86
86
  isAvailable() {
87
- return this.configData.enabled && this.client !== null && this.status === 'initialized';
87
+ return this.client !== null && this.status === 'initialized';
88
+ }
89
+ /**
90
+ * Start the Dynamo client when a caller required external governance
91
+ * and boot skipped it because inference_governance.enabled is false.
92
+ */
93
+ async ensureDynamoClient() {
94
+ if (this.status !== 'initialized') {
95
+ await this.initialize();
96
+ }
97
+ if (this.client !== null) {
98
+ return;
99
+ }
100
+ const endpoint = resolveDynamoBaseUrl(this.configData.endpointUrl);
101
+ this.client = new GovernanceClient({
102
+ baseUrl: endpoint,
103
+ timeoutMs: this.configData.requestTimeoutMs,
104
+ });
105
+ await this.log('info', 'Dynamo client armed for required external governance', {
106
+ endpoint,
107
+ });
88
108
  }
89
109
  /**
90
110
  * Check a single inference proposal via the appropriate governance tool
@@ -56,6 +56,17 @@ export interface GovernanceIntegrationConfig {
56
56
  voteWeightMultiplier: number;
57
57
  };
58
58
  }
59
+ /**
60
+ * Public Dynamo Solar SSOT. Used only when a caller requires external
61
+ * governance and neither features nor GOVERNANCE_ENDPOINT names a host.
62
+ * The client posts to /call_connected_tool on this origin.
63
+ */
64
+ export declare const PUBLIC_DYNAMO_SSOT = "https://mcp-production-80e2.up.railway.app";
65
+ /**
66
+ * Origin for POST /call_connected_tool.
67
+ * Preference: configured endpoint, then GOVERNANCE_ENDPOINT, then the public SSOT.
68
+ */
69
+ export declare function resolveDynamoBaseUrl(configured: string): string;
59
70
  /**
60
71
  * Default governance configuration
61
72
  */
@@ -6,6 +6,22 @@
6
6
  *
7
7
  * @since 2026-05-11
8
8
  */
9
+ /**
10
+ * Public Dynamo Solar SSOT. Used only when a caller requires external
11
+ * governance and neither features nor GOVERNANCE_ENDPOINT names a host.
12
+ * The client posts to /call_connected_tool on this origin.
13
+ */
14
+ export const PUBLIC_DYNAMO_SSOT = 'https://mcp-production-80e2.up.railway.app';
15
+ /**
16
+ * Origin for POST /call_connected_tool.
17
+ * Preference: configured endpoint, then GOVERNANCE_ENDPOINT, then the public SSOT.
18
+ */
19
+ export function resolveDynamoBaseUrl(configured) {
20
+ const fromConfig = configured.trim();
21
+ const fromEnv = (process.env.GOVERNANCE_ENDPOINT || '').trim();
22
+ const raw = fromConfig || fromEnv || PUBLIC_DYNAMO_SSOT;
23
+ return raw.replace(/\/governance\/?$/, '').replace(/\/$/, '');
24
+ }
9
25
  /**
10
26
  * Default governance configuration
11
27
  */
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xray-pre-tool",
3
- "version": "4.0.22",
3
+ "version": "4.0.23",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "0xRay constitution + temperament PreToolUse for OpenClaw",
@@ -20,6 +20,8 @@ declare class GovernanceServer extends XrayKnowledgeSkillBase {
20
20
  protected setupToolHandlers(): void;
21
21
  /** Align tool default with features.json inference_governance.enabled (P0.2). */
22
22
  private resolveRequireExternal;
23
+ /** Boot stays flag-gated. A required external call starts the client on demand. */
24
+ private armRequiredDynamo;
23
25
  private handleGovernProposals;
24
26
  private handleGovernReflection;
25
27
  private handleGetActiveCodex;
@@ -196,8 +196,19 @@ class GovernanceServer extends XrayKnowledgeSkillBase {
196
196
  return false;
197
197
  }
198
198
  }
199
+ /** Boot stays flag-gated. A required external call starts the client on demand. */
200
+ async armRequiredDynamo(requireExternal) {
201
+ if (!requireExternal)
202
+ return;
203
+ const integration = await initializeGovernanceIntegration();
204
+ if (!integration.isAvailable()) {
205
+ await integration.ensureDynamoClient();
206
+ }
207
+ }
199
208
  async handleGovernProposals(args) {
200
209
  const service = getGovernanceService();
210
+ const requireExternal = this.resolveRequireExternal(args.options?.require_external);
211
+ await this.armRequiredDynamo(requireExternal);
201
212
  const request = {
202
213
  proposals: args.proposals.map((p, i) => ({
203
214
  id: p.id || `prop-${Date.now()}-${i}`,
@@ -210,7 +221,7 @@ class GovernanceServer extends XrayKnowledgeSkillBase {
210
221
  })),
211
222
  context: args.context || {},
212
223
  options: {
213
- requireExternalDynamo: this.resolveRequireExternal(args.options?.require_external),
224
+ requireExternalDynamo: requireExternal,
214
225
  },
215
226
  };
216
227
  frameworkLogger.log("governance-mcp", "delegating-to-governance-service", "info", {
@@ -1,5 +1,5 @@
1
1
  export type { MemoryRoutingProvider, MemoryRoutingContext, MemoryInheritedContext, MemoryAgentCapability, MemoryOrchestrationTask, MemoryThinDispatchResult, MemoryRoutingConfig, OrchestratorFeedbackEntry, } from './types.js';
2
- export { getMemoryRoutingProvider, getMemoryRoutingProviderSync, getMemoryRoutingConfig, resetMemoryRoutingProvider, initializeMemoryRouting, } from './provider-registry.js';
2
+ export { getMemoryRoutingProvider, getMemoryRoutingProviderSync, ensureMemoryRoutingProviderSync, getMemoryRoutingConfig, resetMemoryRoutingProvider, initializeMemoryRouting, } from './provider-registry.js';
3
3
  export { NullMemoryRoutingProvider, createMemoryRoutingProvider } from './null-provider.js';
4
4
  export { loadMemoryRoutingProvider, isLeftoverMemoryRoutingOff, isExplicitMemoryRoutingOptOut, resolveLeftoverEnabledConfig, } from './provider-loader.js';
5
5
  export { validateMemoryRoutingConfig } from './validate-config.js';
@@ -1,4 +1,4 @@
1
- export { getMemoryRoutingProvider, getMemoryRoutingProviderSync, getMemoryRoutingConfig, resetMemoryRoutingProvider, initializeMemoryRouting, } from './provider-registry.js';
1
+ export { getMemoryRoutingProvider, getMemoryRoutingProviderSync, ensureMemoryRoutingProviderSync, getMemoryRoutingConfig, resetMemoryRoutingProvider, initializeMemoryRouting, } from './provider-registry.js';
2
2
  export { NullMemoryRoutingProvider, createMemoryRoutingProvider } from './null-provider.js';
3
3
  export { loadMemoryRoutingProvider, isLeftoverMemoryRoutingOff, isExplicitMemoryRoutingOptOut, resolveLeftoverEnabledConfig, } from './provider-loader.js';
4
4
  export { validateMemoryRoutingConfig } from './validate-config.js';
@@ -0,0 +1,2 @@
1
+ /** Refresh the Station lessons section from dest. Leaves the rest of the card alone. */
2
+ export declare function writeLessonPickup(projectRoot: string): void;
@@ -0,0 +1,57 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ const START = "<!-- lessons -->";
4
+ const END = "<!-- /lessons -->";
5
+ const DEST = path.join(".xray", "state", "repertoire", "curated_signals.json");
6
+ const FLOOR = 0.55;
7
+ function aboveFloor(value) {
8
+ return typeof value === "number" && value > FLOOR && Math.abs(value - FLOOR) > 1e-4;
9
+ }
10
+ function lessonBlock(signals) {
11
+ const lines = ["## Lessons"];
12
+ const graded = signals.filter((signal) => typeof signal.name === "string"
13
+ && aboveFloor(signal.observation_stats?.avg_confidence)
14
+ && (signal.lessons?.length ?? 0) > 0);
15
+ if (graded.length === 0) {
16
+ lines.push("No graded lines above the floor.");
17
+ return lines.join("\n");
18
+ }
19
+ for (const signal of graded) {
20
+ lines.push(`### ${signal.name}`);
21
+ if (typeof signal.definition === "string" && signal.definition.length > 0) {
22
+ lines.push(signal.definition);
23
+ }
24
+ for (const lesson of signal.lessons ?? []) {
25
+ const text = lesson.text.length > 0 ? lesson.text : "graded";
26
+ lines.push(`- ${lesson.decision} \`${lesson.taskId}\` — ${text}`);
27
+ }
28
+ }
29
+ return lines.join("\n");
30
+ }
31
+ /** Refresh the Station lessons section from dest. Leaves the rest of the card alone. */
32
+ export function writeLessonPickup(projectRoot) {
33
+ const stationPath = path.join(projectRoot, ".xray", "state", "STATION.md");
34
+ if (!fs.existsSync(stationPath))
35
+ return;
36
+ const destPath = path.join(projectRoot, DEST);
37
+ let signals = [];
38
+ if (fs.existsSync(destPath)) {
39
+ try {
40
+ const parsed = JSON.parse(fs.readFileSync(destPath, "utf-8"));
41
+ if (parsed && typeof parsed === "object" && "signals" in parsed && Array.isArray(parsed.signals)) {
42
+ signals = parsed.signals;
43
+ }
44
+ }
45
+ catch {
46
+ signals = [];
47
+ }
48
+ }
49
+ const section = `${START}\n${lessonBlock(signals)}\n${END}`;
50
+ const current = fs.readFileSync(stationPath, "utf-8");
51
+ const pattern = /<!-- lessons -->[\s\S]*?<!-- \/lessons -->/;
52
+ const next = pattern.test(current)
53
+ ? current.replace(pattern, section)
54
+ : `${current.trimEnd()}\n\n${section}\n`;
55
+ if (next !== current)
56
+ fs.writeFileSync(stationPath, next);
57
+ }
@@ -5,4 +5,9 @@ export declare function isExplicitMemoryRoutingOptOut(config: MemoryRoutingConfi
5
5
  * Explicit opt-out is enabled: false + provider: repertoire. */
6
6
  export declare function isLeftoverMemoryRoutingOff(config: MemoryRoutingConfig): boolean;
7
7
  export declare function resolveLeftoverEnabledConfig(config: MemoryRoutingConfig, cwd: string): MemoryRoutingConfig | null;
8
+ /**
9
+ * First route cannot wait on the async import. Node loads this ESM with require.
10
+ * Returns null when routing is off or the module cannot be constructed.
11
+ */
12
+ export declare function loadMemoryRoutingProviderSync(config: MemoryRoutingConfig, cwd?: string): MemoryRoutingProvider | null;
8
13
  export declare function loadMemoryRoutingProvider(config: MemoryRoutingConfig, cwd?: string): Promise<MemoryRoutingProvider>;
@@ -1,4 +1,5 @@
1
1
  import { existsSync } from 'node:fs';
2
+ import { createRequire } from 'node:module';
2
3
  import { resolve, dirname, join } from 'node:path';
3
4
  import { pathToFileURL } from 'node:url';
4
5
  import { frameworkLogger } from '../core/framework-logger.js';
@@ -19,6 +20,7 @@ function resolveUnavailableReason(provider) {
19
20
  return null;
20
21
  }
21
22
  }
23
+ const require = createRequire(import.meta.url);
22
24
  const REPERTOIRE_CANDIDATE_PATHS = [
23
25
  'node_modules/@0xray/repertoire/dist/provider/memory-routing-provider.js',
24
26
  'vendor/@0xray/repertoire/dist/provider/memory-routing-provider.js',
@@ -99,6 +101,50 @@ export function resolveLeftoverEnabledConfig(config, cwd) {
99
101
  enabled.config = config.config;
100
102
  return enabled;
101
103
  }
104
+ function providerFromModule(mod, modulePath, effective) {
105
+ const factory = mod.createMemoryRoutingProvider ?? mod.default?.createMemoryRoutingProvider;
106
+ if (typeof factory !== 'function') {
107
+ throw new Error(`Module ${modulePath} must export createMemoryRoutingProvider()`);
108
+ }
109
+ const provider = factory((effective.config ?? {}));
110
+ if (!provider?.id || typeof provider.isAvailable !== 'function') {
111
+ throw new Error(`Invalid memory routing provider from ${modulePath}`);
112
+ }
113
+ return provider;
114
+ }
115
+ function resolveEffectiveProvider(config, cwd) {
116
+ const validation = validateMemoryRoutingConfig(config);
117
+ if (!validation.valid)
118
+ return null;
119
+ const leftover = resolveLeftoverEnabledConfig(validation.normalized, cwd);
120
+ const effective = leftover ?? validation.normalized;
121
+ if (!effective.enabled || effective.provider === 'null')
122
+ return null;
123
+ const modulePath = resolveModulePath(effective.module_path, cwd) ??
124
+ defaultPathForProvider(effective.provider, cwd);
125
+ if (!modulePath)
126
+ return null;
127
+ return { effective, modulePath };
128
+ }
129
+ /**
130
+ * First route cannot wait on the async import. Node loads this ESM with require.
131
+ * Returns null when routing is off or the module cannot be constructed.
132
+ */
133
+ export function loadMemoryRoutingProviderSync(config, cwd = process.cwd()) {
134
+ const resolved = resolveEffectiveProvider(config, cwd);
135
+ if (!resolved)
136
+ return null;
137
+ try {
138
+ const mod = require(resolved.modulePath);
139
+ const provider = providerFromModule(mod, resolved.modulePath, resolved.effective);
140
+ if (!provider.isAvailable())
141
+ return null;
142
+ return provider;
143
+ }
144
+ catch {
145
+ return null;
146
+ }
147
+ }
102
148
  export async function loadMemoryRoutingProvider(config, cwd = process.cwd()) {
103
149
  const validation = validateMemoryRoutingConfig(config);
104
150
  if (!validation.valid) {
@@ -1,8 +1,13 @@
1
1
  import type { MemoryRoutingConfig, MemoryRoutingProvider } from './types.js';
2
2
  export declare function getMemoryRoutingConfig(): MemoryRoutingConfig;
3
3
  export declare function getMemoryRoutingProvider(forceReload?: boolean): Promise<MemoryRoutingProvider>;
4
- /** Synchronous accessor — returns shared null singleton until async load completes. */
4
+ /** Synchronous accessor — returns shared null singleton until a load completes. */
5
5
  export declare function getMemoryRoutingProviderSync(): MemoryRoutingProvider;
6
+ /**
7
+ * The first scoreAndRoute call loads the organ before it routes.
8
+ * A later sync read sees the same provider. Off or a missing module stays null.
9
+ */
10
+ export declare function ensureMemoryRoutingProviderSync(): MemoryRoutingProvider;
6
11
  export declare function resetMemoryRoutingProvider(): void;
7
- /** Kick off background load at module init. */
12
+ /** Kick off load at module init. Sync require first so a later route does not require() an ESM module while its dynamic import is still in flight. */
8
13
  export declare function initializeMemoryRouting(): void;
@@ -1,6 +1,6 @@
1
1
  import { featuresConfigLoader } from '../core/features-config.js';
2
2
  import { NullMemoryRoutingProvider } from './null-provider.js';
3
- import { loadMemoryRoutingProvider, resolveLeftoverEnabledConfig } from './provider-loader.js';
3
+ import { loadMemoryRoutingProvider, loadMemoryRoutingProviderSync, resolveLeftoverEnabledConfig, } from './provider-loader.js';
4
4
  import { validateMemoryRoutingConfig } from './validate-config.js';
5
5
  let cachedProvider = null;
6
6
  let loadPromise = null;
@@ -12,30 +12,50 @@ export function getMemoryRoutingConfig() {
12
12
  validation.normalized);
13
13
  }
14
14
  export async function getMemoryRoutingProvider(forceReload = false) {
15
- if (!forceReload && cachedProvider)
15
+ if (!forceReload && cachedProvider && cachedProvider.id !== 'null')
16
16
  return cachedProvider;
17
17
  if (!forceReload && loadPromise)
18
18
  return loadPromise;
19
19
  const config = getMemoryRoutingConfig();
20
20
  loadPromise = loadMemoryRoutingProvider(config).then((provider) => {
21
+ if (provider.id === 'null' && cachedProvider && cachedProvider.id !== 'null') {
22
+ loadPromise = null;
23
+ return cachedProvider;
24
+ }
21
25
  cachedProvider = provider;
22
26
  loadPromise = null;
23
27
  return provider;
24
28
  });
25
29
  return loadPromise;
26
30
  }
27
- /** Synchronous accessor — returns shared null singleton until async load completes. */
31
+ /** Synchronous accessor — returns shared null singleton until a load completes. */
28
32
  export function getMemoryRoutingProviderSync() {
29
33
  return cachedProvider ?? nullProviderSingleton;
30
34
  }
35
+ /**
36
+ * The first scoreAndRoute call loads the organ before it routes.
37
+ * A later sync read sees the same provider. Off or a missing module stays null.
38
+ */
39
+ export function ensureMemoryRoutingProviderSync() {
40
+ if (cachedProvider && cachedProvider.id !== 'null')
41
+ return cachedProvider;
42
+ const config = getMemoryRoutingConfig();
43
+ if (!config.enabled)
44
+ return cachedProvider ?? nullProviderSingleton;
45
+ const loaded = loadMemoryRoutingProviderSync(config);
46
+ if (!loaded || loaded.id === 'null')
47
+ return cachedProvider ?? nullProviderSingleton;
48
+ cachedProvider = loaded;
49
+ return loaded;
50
+ }
31
51
  export function resetMemoryRoutingProvider() {
32
52
  cachedProvider = null;
33
53
  loadPromise = null;
34
54
  }
35
- /** Kick off background load at module init. */
55
+ /** Kick off load at module init. Sync require first so a later route does not require() an ESM module while its dynamic import is still in flight. */
36
56
  export function initializeMemoryRouting() {
37
- const config = getMemoryRoutingConfig();
38
- if (config.enabled) {
57
+ const loaded = ensureMemoryRoutingProviderSync();
58
+ if (loaded.id === 'null' && getMemoryRoutingConfig().enabled) {
39
59
  void getMemoryRoutingProvider();
40
60
  }
41
61
  }
@@ -0,0 +1,16 @@
1
+ export interface LessonInput {
2
+ operation: string;
3
+ success: boolean;
4
+ taskId: string;
5
+ assignedAgent: string;
6
+ sessionId: string;
7
+ /** Signals the finished task already named. When set, text matching is not used. */
8
+ signals?: string[];
9
+ /** Approaches, solutions, or the wrong turn from the sessions that named the law. */
10
+ lesson?: string;
11
+ }
12
+ /**
13
+ * A finished task names the signals its text actually matched and says
14
+ * whether the outcome was good. That is the one-tenth step. No match, no write.
15
+ */
16
+ export declare function recordLesson(input: LessonInput): string[];
@@ -0,0 +1,26 @@
1
+ import { ensureMemoryRoutingProviderSync } from "./provider-registry.js";
2
+ /**
3
+ * A finished task names the signals its text actually matched and says
4
+ * whether the outcome was good. That is the one-tenth step. No match, no write.
5
+ */
6
+ export function recordLesson(input) {
7
+ const provider = ensureMemoryRoutingProviderSync();
8
+ if (provider.id === "null" || !provider.ingestFeedback)
9
+ return [];
10
+ const named = input.signals ?? provider.buildRoutingContext(input.operation).matchedSignals;
11
+ const names = [...new Set(named.filter((name) => name.length > 0))];
12
+ if (names.length === 0)
13
+ return [];
14
+ provider.ingestFeedback({
15
+ timestamp: new Date().toISOString(),
16
+ sessionId: input.sessionId,
17
+ taskId: input.taskId,
18
+ assignedAgent: input.assignedAgent,
19
+ memorySignals: names,
20
+ complexity: 0,
21
+ success: input.success,
22
+ durationMs: 0,
23
+ lesson: input.lesson ?? "",
24
+ });
25
+ return names;
26
+ }