0xray 4.0.38 → 4.0.40

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 (127) hide show
  1. package/.grok-plugin/plugin.json +1 -1
  2. package/.xray/features.json +1 -1
  3. package/AGENTS-consumer.md +1 -1
  4. package/AGENTS.md +1 -1
  5. package/README.md +2 -2
  6. package/SKILLS.md +1 -1
  7. package/dist/AGENTS.md +1 -1
  8. package/dist/CHANGELOG.md +25 -0
  9. package/dist/README.md +2 -2
  10. package/dist/SKILLS.md +1 -1
  11. package/dist/integrations/hooks/goggles-planes.json +1 -1
  12. package/dist/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  13. package/docs-site/docs/plates/activity-log.md +1 -1
  14. package/docs-site/docs/plates/cursor-compact.md +1 -1
  15. package/docs-site/docs/plates/grok-compact.md +1 -1
  16. package/docs-site/docs/plates/grokbot.md +1 -1
  17. package/docs-site/docs/plates/house.md +1 -1
  18. package/docs-site/docs/plates/index.md +1 -1
  19. package/docs-site/docs/plates/inference-files.md +1 -1
  20. package/docs-site/docs/plates/kept-line.md +1 -1
  21. package/docs-site/docs/plates/lens-gate.md +1 -1
  22. package/docs-site/docs/plates/lens-page.md +1 -1
  23. package/docs-site/docs/plates/live-mesh.md +41 -0
  24. package/docs-site/docs/plates/notes-page.md +1 -1
  25. package/docs-site/docs/plates/package-face.md +1 -1
  26. package/docs-site/docs/plates/payload-heat.md +1 -1
  27. package/docs-site/docs/plates/pickup-stamp.md +1 -1
  28. package/docs-site/docs/plates/pre-tool.md +1 -1
  29. package/docs-site/docs/plates/record-map.md +1 -1
  30. package/docs-site/docs/plates/reflection-page.md +1 -1
  31. package/docs-site/docs/plates/review.md +1 -1
  32. package/docs-site/docs/plates/session-capture.md +1 -1
  33. package/docs-site/docs/plates/site-manual.md +1 -1
  34. package/docs-site/docs/plates/stamp-plate.md +1 -1
  35. package/docs-site/docs/plates/station-card.md +1 -1
  36. package/docs-site/docs/plates/station-heat.md +1 -1
  37. package/docs-site/docs/plates/suit-organs.md +1 -1
  38. package/docs-site/docs/plates/suit-settings.md +1 -1
  39. package/docs-site/docs/plates/suit-wear.md +1 -1
  40. package/docs-site/docs/plates/trail-state.md +1 -1
  41. package/docs-site/docs/plates/work-fresh.md +1 -1
  42. package/docs-site/docs/plates/write-home.md +1 -1
  43. package/llms.txt +1 -1
  44. package/package.json +2 -1
  45. package/scripts/foundry/plant/skills/mill/SKILL.md +1 -1
  46. package/scripts/node/consumer-install-smoke.mjs +3 -3
  47. package/scripts/node/playwright-smoke-server.cjs +27 -0
  48. package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  49. package/vendor/@0xray/repertoire/README.md +1 -1
  50. package/vendor/@0xray/repertoire/dist/RepertoireService.js +1 -0
  51. package/vendor/@0xray/repertoire/dist/orchestrator-bridge/RepertoireOrchestratorBridge.js +4 -1
  52. package/vendor/@0xray/repertoire/dist/orchestrator-bridge/signal-injector.d.ts +5 -0
  53. package/vendor/@0xray/repertoire/dist/orchestrator-bridge/signal-injector.js +16 -6
  54. package/vendor/@0xray/repertoire/dist/paths.js +1 -2
  55. package/vendor/@0xray/repertoire/dist/provider/memory-routing-provider.d.ts +16 -6
  56. package/vendor/@0xray/repertoire/dist/provider/memory-routing-provider.js +19 -2
  57. package/vendor/@0xray/repertoire/dist/registry/CuratedSignalsManager.js +34 -2
  58. package/vendor/@0xray/repertoire/dist/registry/InferenceStateManager.d.ts +3 -0
  59. package/vendor/@0xray/repertoire/dist/registry/InferenceStateManager.js +25 -1
  60. package/vendor/@0xray/repertoire/dist/synthesis/meta-inference-engine.d.ts +37 -2
  61. package/vendor/@0xray/repertoire/dist/synthesis/meta-inference-engine.js +176 -19
  62. package/vendor/@0xray/repertoire/dist/types.d.ts +6 -0
  63. package/vendor/@0xray/repertoire/package.json +2 -2
  64. package/vendor/@0xray/repertoire/scripts/verify-grok-suit.mjs +9 -6
  65. package/vendor/@0xray/repertoire/scripts/verify-synthesis-dogfood.mjs +109 -5
  66. package/xray/features.json +1 -1
  67. package/.opencode/agents/api-design.yml +0 -31
  68. package/.opencode/agents/architect.yml +0 -15
  69. package/.opencode/agents/architecture-patterns.yml +0 -14
  70. package/.opencode/agents/auto-format.yml +0 -14
  71. package/.opencode/agents/backend-engineer.yml +0 -12
  72. package/.opencode/agents/boot-orchestrator.yml +0 -14
  73. package/.opencode/agents/bug-triage-specialist.yml +0 -15
  74. package/.opencode/agents/code-analyzer.yml +0 -12
  75. package/.opencode/agents/code-reviewer.yml +0 -14
  76. package/.opencode/agents/content-creator.yml +0 -12
  77. package/.opencode/agents/database-engineer.yml +0 -12
  78. package/.opencode/agents/devops-engineer.yml +0 -12
  79. package/.opencode/agents/framework-compliance-audit.yml +0 -14
  80. package/.opencode/agents/frontend-engineer.yml +0 -12
  81. package/.opencode/agents/frontend-ui-ux-engineer.yml +0 -12
  82. package/.opencode/agents/git-workflow.yml +0 -15
  83. package/.opencode/agents/growth-strategist.yml +0 -12
  84. package/.opencode/agents/hermes-agent.yml +0 -12
  85. package/.opencode/agents/inference-improve.yml +0 -12
  86. package/.opencode/agents/lint.yml +0 -14
  87. package/.opencode/agents/log-monitor.yml +0 -12
  88. package/.opencode/agents/mobile-developer.yml +0 -12
  89. package/.opencode/agents/model-health-check.yml +0 -12
  90. package/.opencode/agents/multimodal-looker.yml +0 -12
  91. package/.opencode/agents/performance-analysis.yml +0 -12
  92. package/.opencode/agents/performance-engineer.yml +0 -12
  93. package/.opencode/agents/performance-optimization.yml +0 -12
  94. package/.opencode/agents/processor-pipeline.yml +0 -14
  95. package/.opencode/agents/project-analysis.yml +0 -12
  96. package/.opencode/agents/refactorer.yml +0 -12
  97. package/.opencode/agents/researcher.yml +0 -12
  98. package/.opencode/agents/security-auditor.yml +0 -12
  99. package/.opencode/agents/security-scan.yml +0 -12
  100. package/.opencode/agents/seo-consultant.yml +0 -12
  101. package/.opencode/agents/session-management.yml +0 -12
  102. package/.opencode/agents/state-manager.yml +0 -12
  103. package/.opencode/agents/storyteller.yml +0 -12
  104. package/.opencode/agents/strategist.yml +0 -12
  105. package/.opencode/agents/tech-writer.yml +0 -12
  106. package/.opencode/agents/testing-best-practices.yml +0 -12
  107. package/.opencode/agents/testing-lead.yml +0 -12
  108. package/.opencode/agents/ui-ux-design.yml +0 -12
  109. package/.opencode/codex.codex +0 -8
  110. package/.opencode/commands/auto-format.md +0 -99
  111. package/.opencode/commands/auto-summary-capture.md +0 -90
  112. package/.opencode/commands/dependency-audit.md +0 -184
  113. package/.opencode/commands/enforcer-daily-scan.md +0 -137
  114. package/.opencode/commands/framework-compliance-audit.md +0 -205
  115. package/.opencode/commands/interactive-validator.md +0 -75
  116. package/.opencode/commands/job-summary-logger.md +0 -68
  117. package/.opencode/commands/lint.md +0 -11
  118. package/.opencode/commands/mode-switch.md +0 -95
  119. package/.opencode/commands/model-health-check.md +0 -186
  120. package/.opencode/commands/performance-analysis.md +0 -144
  121. package/.opencode/commands/pre-commit-introspection.md +0 -185
  122. package/.opencode/commands/pre-commit-introspection.sh +0 -133
  123. package/.opencode/commands/security-scan.md +0 -157
  124. package/.opencode/commands/sisyphus-validation.md +0 -128
  125. package/.opencode/commands/summary-logger.md +0 -83
  126. package/.opencode/enforcer-config.json +0 -282
  127. package/.opencode/workflows/post-deployment-audit.yml +0 -123
@@ -4,6 +4,7 @@
4
4
  * Loaded dynamically by 0xRay via features.json memory_routing.module_path.
5
5
  * Other providers can follow the same createMemoryRoutingProvider() export pattern.
6
6
  */
7
+ import { type MetaInferenceFailureReason, type MetaInferenceModel } from '../synthesis/meta-inference-engine.js';
7
8
  /** Mirrors 0xRay memory-routing/types.ts — kept local to avoid compile-time coupling */
8
9
  export interface MemoryAgentCapability {
9
10
  capabilities: string[];
@@ -80,6 +81,14 @@ export interface OrchestratorFeedbackEntry {
80
81
  dynamoResult?: Record<string, unknown>;
81
82
  lesson?: string;
82
83
  }
84
+ export type MetaInferenceRefreshReason = 'synthesized' | 'nothing_to_process' | MetaInferenceFailureReason;
85
+ export interface MetaInferenceRefreshResult {
86
+ refreshed: boolean;
87
+ /** Machine-readable outcome. A failed model is never `synthesized`. */
88
+ reason: MetaInferenceRefreshReason;
89
+ /** Present when the model was missing or the call failed. */
90
+ error?: string;
91
+ }
83
92
  export interface MemoryRoutingProviderConfig {
84
93
  dataDir?: string;
85
94
  signalsPath?: string;
@@ -87,6 +96,11 @@ export interface MemoryRoutingProviderConfig {
87
96
  statePath?: string;
88
97
  feedbackDir?: string;
89
98
  projectRoot?: string;
99
+ /**
100
+ * Meta-inference model. `null` means none is configured (refresh fails closed).
101
+ * Omit to keep the engine's hermes CLI default.
102
+ */
103
+ hermesCommand?: MetaInferenceModel | null;
90
104
  }
91
105
  export interface MemoryRoutingProvider {
92
106
  readonly id: string;
@@ -104,9 +118,7 @@ export interface MemoryRoutingProvider {
104
118
  projectRoot: string;
105
119
  dueReason?: string | null;
106
120
  }): Record<string, unknown> | null;
107
- refreshMetaInference?(): Promise<{
108
- refreshed: boolean;
109
- }>;
121
+ refreshMetaInference?(): Promise<MetaInferenceRefreshResult>;
110
122
  }
111
123
  export type ProviderAvailabilityReason = 'ok' | 'empty_registry' | 'signals_unreadable' | 'signals_missing';
112
124
  export interface ProviderAvailabilityStatus {
@@ -143,9 +155,7 @@ export declare class RepertoireMemoryRoutingProvider implements MemoryRoutingPro
143
155
  projectRoot: string;
144
156
  dueReason?: string | null;
145
157
  }): Record<string, unknown> | null;
146
- refreshMetaInference(): Promise<{
147
- refreshed: boolean;
148
- }>;
158
+ refreshMetaInference(): Promise<MetaInferenceRefreshResult>;
149
159
  }
150
160
  /** Factory export — 0xRay provider-loader calls this by name */
151
161
  export declare function createMemoryRoutingProvider(config?: MemoryRoutingProviderConfig): MemoryRoutingProvider;
@@ -7,6 +7,7 @@
7
7
  import { existsSync } from 'node:fs';
8
8
  import { DEFAULT_SIGNALS_PATH, defaultWritablePaths, hydrateWritableSignals, resolveReadableConfigPath, resolveWritableConfigPath, } from '../paths.js';
9
9
  import { RepertoireService } from '../RepertoireService.js';
10
+ import { MetaInferenceModelError, } from '../synthesis/meta-inference-engine.js';
10
11
  function toRepertoireCaps(caps) {
11
12
  return {
12
13
  capabilities: caps.capabilities,
@@ -71,6 +72,9 @@ export class RepertoireMemoryRoutingProvider {
71
72
  logDir: resolveWritableConfigPath(config.logDir, cwd, writable.logDir),
72
73
  feedbackDir: resolveWritableConfigPath(config.feedbackDir, cwd, writable.feedbackDir),
73
74
  });
75
+ if (config.hermesCommand !== undefined) {
76
+ this.service.metaInference.configureModel(config.hermesCommand);
77
+ }
74
78
  }
75
79
  getAvailabilityStatus() {
76
80
  try {
@@ -199,8 +203,21 @@ export class RepertoireMemoryRoutingProvider {
199
203
  return this.service.buildSynthesisContext(opts.projectRoot, opts.dueReason ?? null);
200
204
  }
201
205
  async refreshMetaInference() {
202
- const report = await this.service.runMetaInference();
203
- return { refreshed: report !== null };
206
+ try {
207
+ const report = await this.service.runMetaInference();
208
+ if (report === null) {
209
+ return { refreshed: false, reason: 'nothing_to_process' };
210
+ }
211
+ return { refreshed: true, reason: 'synthesized' };
212
+ }
213
+ catch (error) {
214
+ if (error instanceof MetaInferenceModelError) {
215
+ return { refreshed: false, reason: error.reason, error: error.message };
216
+ }
217
+ const message = error instanceof Error ? error.message : String(error);
218
+ process.stderr.write(`[meta-inference] state_error: ${message}\n`);
219
+ return { refreshed: false, reason: 'state_error', error: message };
220
+ }
204
221
  }
205
222
  }
206
223
  /** Factory export — 0xRay provider-loader calls this by name */
@@ -150,6 +150,38 @@ function isSpeechMint(signal) {
150
150
  return signal.status === 'proposed' && signal.tags.includes('learned');
151
151
  }
152
152
  const FIELD_PRIMITIVE_NAME = /^[A-Za-z][A-Za-z0-9_-]{2,119}$/;
153
+ /** A content word after a spaced law extends the id. A function word is the sentence. */
154
+ const PROSE_TAIL = new Set([
155
+ 'a', 'an', 'the', 'as', 'and', 'or', 'but', 'if', 'of', 'to', 'for', 'in', 'on', 'at', 'by',
156
+ 'with', 'from', 'into', 'over', 'after', 'before', 'about', 'than', 'then', 'that', 'this',
157
+ 'these', 'those', 'it', 'its', 'is', 'are', 'was', 'were', 'be', 'been', 'not', 'no', 'so',
158
+ 'when', 'while', 'where', 'which', 'who', 'what', 'how', 'also', 'only', 'just', 'still',
159
+ 'never', 'today',
160
+ ]);
161
+ /** A longer id does not count. Spaced prose may continue after a hyphenated id. */
162
+ function boundedId(text, id, spaced) {
163
+ if (!id)
164
+ return false;
165
+ const escaped = id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
166
+ const re = new RegExp(`(?:^|[^a-z0-9-])${escaped}`, 'g');
167
+ let match;
168
+ while ((match = re.exec(text)) !== null) {
169
+ const after = text.slice(match.index + match[0].length);
170
+ if (spaced ? spacedTailIsProse(after) : !/^[a-z0-9-]/.test(after))
171
+ return true;
172
+ if (match.index === re.lastIndex)
173
+ re.lastIndex += 1;
174
+ }
175
+ return false;
176
+ }
177
+ function spacedTailIsProse(after) {
178
+ if (/^-[a-z0-9]/.test(after) || /^[a-z0-9]/.test(after))
179
+ return false;
180
+ const word = /^\s*([a-z0-9]+)/.exec(after);
181
+ if (!word || !/^\s/.test(after))
182
+ return true;
183
+ return PROSE_TAIL.has(word[1]);
184
+ }
153
185
  /**
154
186
  * The diary names a law only when it contains the signal id, or the id with
155
187
  * hyphens read as spaces. A repo tail, leftover tokens, and two definition
@@ -160,10 +192,10 @@ export function signalNameInText(text, name) {
160
192
  const id = name.toLowerCase();
161
193
  if (!id)
162
194
  return false;
163
- if (normalized.includes(id))
195
+ if (boundedId(normalized, id, false))
164
196
  return true;
165
197
  const spaced = id.replace(/-/g, ' ');
166
- return spaced !== id && normalized.includes(spaced);
198
+ return spaced !== id && boundedId(normalized, spaced, true);
167
199
  }
168
200
  const GROOVER_EXPERIMENT_NAMES = new Set([
169
201
  'criteria_selection_gap',
@@ -3,6 +3,9 @@ export declare class InferenceStateManager {
3
3
  private readonly filePath;
4
4
  constructor(filePath?: string);
5
5
  load(): InferenceState;
6
+ /** Remember a failed model attempt without marking entries processed or touching lastRun. */
7
+ recordModelFailure(failedAt: string): void;
8
+ clearModelBackoff(): void;
6
9
  save(state: InferenceState): void;
7
10
  isProcessed(id: string): boolean;
8
11
  markProcessed(ids: string[], kind?: 'comment' | 'session' | 'post'): void;
@@ -10,12 +10,30 @@ export class InferenceStateManager {
10
10
  return this.createEmpty();
11
11
  }
12
12
  const raw = JSON.parse(readFileSync(this.filePath, 'utf8'));
13
- return {
13
+ const state = {
14
14
  processedCommentIds: raw.processedCommentIds ?? [],
15
15
  processedSessionIds: raw.processedSessionIds ?? [],
16
16
  processedPostIds: raw.processedPostIds ?? [],
17
17
  lastRun: raw.lastRun ?? null,
18
18
  };
19
+ const backoff = readBackoff(raw.modelBackoff);
20
+ if (backoff)
21
+ state.modelBackoff = backoff;
22
+ return state;
23
+ }
24
+ /** Remember a failed model attempt without marking entries processed or touching lastRun. */
25
+ recordModelFailure(failedAt) {
26
+ const state = this.load();
27
+ const failures = (state.modelBackoff?.failures ?? 0) + 1;
28
+ state.modelBackoff = { failedAt, failures };
29
+ this.save(state);
30
+ }
31
+ clearModelBackoff() {
32
+ const state = this.load();
33
+ if (!state.modelBackoff)
34
+ return;
35
+ delete state.modelBackoff;
36
+ this.save(state);
19
37
  }
20
38
  save(state) {
21
39
  const dir = dirname(this.filePath);
@@ -58,4 +76,10 @@ export class InferenceStateManager {
58
76
  };
59
77
  }
60
78
  }
79
+ function readBackoff(value) {
80
+ if (!value || typeof value.failedAt !== 'string')
81
+ return null;
82
+ const failures = typeof value.failures === 'number' && value.failures > 0 ? value.failures : 1;
83
+ return { failedAt: value.failedAt, failures };
84
+ }
61
85
  //# sourceMappingURL=InferenceStateManager.js.map
@@ -1,11 +1,32 @@
1
1
  import type { SynthesisReport } from '../types.js';
2
+ export type MetaInferenceModel = (prompt: string) => unknown;
3
+ export type MetaInferenceFailureReason = 'model_unavailable' | 'model_call_failed' | 'model_output_invalid' | 'model_backoff' | 'state_error';
4
+ /** First wait after a failed model attempt. Doubles until MODEL_BACKOFF_CAP_MS. */
5
+ export declare const MODEL_BACKOFF_BASE_MS: number;
6
+ /** Upper bound so a dead hermes CLI cannot push the wait without limit. */
7
+ export declare const MODEL_BACKOFF_CAP_MS: number;
8
+ export declare function modelBackoffDelayMs(failures: number): number;
9
+ export declare function formatMetaInferenceFailure(error: unknown): string;
10
+ /** Raised when synthesis cannot be produced by a model. Callers must not treat it as a report. */
11
+ export declare class MetaInferenceModelError extends Error {
12
+ readonly reason: MetaInferenceFailureReason;
13
+ constructor(reason: MetaInferenceFailureReason, message: string, options?: {
14
+ cause?: unknown;
15
+ });
16
+ }
2
17
  export interface MetaInferenceEngineOptions {
3
18
  logDir?: string;
4
19
  statePath?: string;
5
20
  reportPath?: string;
6
21
  batchSize?: number;
7
22
  maxEntries?: number;
8
- hermesCommand?: (prompt: string) => string;
23
+ /**
24
+ * Model call. Omit to use the hermes CLI.
25
+ * `null` means no model is configured — the run fails closed and does not call out.
26
+ */
27
+ hermesCommand?: MetaInferenceModel | null;
28
+ /** Pinned in tests. Defaults to the system clock. */
29
+ now?: () => Date;
9
30
  }
10
31
  export declare class MetaInferenceEngine {
11
32
  private readonly logDir;
@@ -14,10 +35,24 @@ export declare class MetaInferenceEngine {
14
35
  private readonly batchSize;
15
36
  private readonly maxEntries;
16
37
  private readonly promptBuilder;
17
- private readonly runHermes;
38
+ private readonly now;
39
+ private model;
18
40
  constructor(options?: MetaInferenceEngineOptions);
41
+ /** `null` configures no model. The next run fails closed instead of calling the hermes CLI. */
42
+ configureModel(command: MetaInferenceModel | null): void;
19
43
  run(): Promise<SynthesisReport | null>;
20
44
  private loadUnprocessedEntries;
45
+ private loadState;
46
+ private throwIfBackingOff;
47
+ /**
48
+ * A missing, failed, or junk model result must not produce a synthesis report.
49
+ * Entries stay unprocessed so a later run can retry them.
50
+ */
51
+ private callModelOrRecord;
52
+ private callModel;
53
+ private noteModelFailure;
54
+ private fail;
55
+ private logFailure;
21
56
  private appendReport;
22
57
  private defaultHermesCommand;
23
58
  }
@@ -3,6 +3,31 @@ import { join, dirname } from 'node:path';
3
3
  import { execSync } from 'node:child_process';
4
4
  import { SynthesisPromptBuilder } from './synthesis-prompt-builder.js';
5
5
  import { InferenceStateManager } from '../registry/InferenceStateManager.js';
6
+ /** First wait after a failed model attempt. Doubles until MODEL_BACKOFF_CAP_MS. */
7
+ export const MODEL_BACKOFF_BASE_MS = 15 * 60 * 1000;
8
+ /** Upper bound so a dead hermes CLI cannot push the wait without limit. */
9
+ export const MODEL_BACKOFF_CAP_MS = 4 * 60 * 60 * 1000;
10
+ export function modelBackoffDelayMs(failures) {
11
+ const exponent = Math.max(0, Math.min(failures, 16) - 1);
12
+ const doubled = MODEL_BACKOFF_BASE_MS * 2 ** exponent;
13
+ return Math.min(doubled, MODEL_BACKOFF_CAP_MS);
14
+ }
15
+ export function formatMetaInferenceFailure(error) {
16
+ if (error instanceof MetaInferenceModelError) {
17
+ return oneLine(`meta-inference failed: ${error.reason}: ${error.message}`);
18
+ }
19
+ const message = error instanceof Error ? error.message : String(error);
20
+ return oneLine(`meta-inference failed: state_error: ${message}`);
21
+ }
22
+ /** Raised when synthesis cannot be produced by a model. Callers must not treat it as a report. */
23
+ export class MetaInferenceModelError extends Error {
24
+ reason;
25
+ constructor(reason, message, options) {
26
+ super(message, options);
27
+ this.name = 'MetaInferenceModelError';
28
+ this.reason = reason;
29
+ }
30
+ }
6
31
  const DEFAULT_BATCH_SIZE = 1;
7
32
  const DEFAULT_MAX_ENTRIES = 8;
8
33
  export class MetaInferenceEngine {
@@ -12,17 +37,26 @@ export class MetaInferenceEngine {
12
37
  batchSize;
13
38
  maxEntries;
14
39
  promptBuilder = new SynthesisPromptBuilder();
15
- runHermes;
40
+ now;
41
+ model;
16
42
  constructor(options = {}) {
17
43
  this.logDir = options.logDir ?? 'logs/groover-inference';
18
44
  this.stateManager = new InferenceStateManager(options.statePath ?? 'data/inference-state.json');
19
45
  this.reportPath = options.reportPath ?? 'logs/meta-inference/synthesis.md';
20
46
  this.batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;
21
47
  this.maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
22
- this.runHermes = options.hermesCommand ?? this.defaultHermesCommand;
48
+ this.now = options.now ?? (() => new Date());
49
+ this.model =
50
+ options.hermesCommand === null
51
+ ? null
52
+ : (options.hermesCommand ?? ((prompt) => this.defaultHermesCommand(prompt)));
53
+ }
54
+ /** `null` configures no model. The next run fails closed instead of calling the hermes CLI. */
55
+ configureModel(command) {
56
+ this.model = command;
23
57
  }
24
58
  async run() {
25
- const state = this.stateManager.load();
59
+ const state = this.loadState();
26
60
  const processed = new Set([
27
61
  ...state.processedCommentIds,
28
62
  ...state.processedSessionIds,
@@ -36,6 +70,7 @@ export class MetaInferenceEngine {
36
70
  return null;
37
71
  }
38
72
  const entries = newEntries.slice(0, this.maxEntries);
73
+ this.throwIfBackingOff(state);
39
74
  const batchResults = [];
40
75
  let totalPass = 0;
41
76
  let totalReject = 0;
@@ -68,23 +103,13 @@ export class MetaInferenceEngine {
68
103
  analyzedSoFar: i + batch.length,
69
104
  },
70
105
  });
71
- try {
72
- batchResults.push(this.runHermes(prompt));
73
- }
74
- catch {
75
- // Continue with remaining batches
76
- }
106
+ batchResults.push(this.callModelOrRecord(prompt));
77
107
  }
78
108
  const avgResonance = resonanceCount > 0 ? (resonanceSum / resonanceCount).toFixed(3) : 'N/A';
79
109
  const finalPrompt = this.promptBuilder.buildFinalSynthesisPrompt(entries.length, { pass: totalPass, reject: totalReject, avgResonance }, batchResults, entries);
80
- let finalReport = '';
81
- try {
82
- finalReport = this.runHermes(finalPrompt);
83
- this.appendReport(entries.length, totalPass, resonanceCount > 0 ? resonanceSum / resonanceCount : null, finalReport);
84
- }
85
- catch {
86
- finalReport = batchResults.join('\n\n---\n\n');
87
- }
110
+ const finalReport = this.callModelOrRecord(finalPrompt);
111
+ this.stateManager.clearModelBackoff();
112
+ this.appendReport(entries.length, totalPass, resonanceCount > 0 ? resonanceSum / resonanceCount : null, finalReport);
88
113
  const ids = entries.map((e) => e.comment_id ?? e.post_id ?? e.session_id).filter(Boolean);
89
114
  const hasSession = entries.some((e) => e.session_id);
90
115
  this.stateManager.markProcessed(ids, hasSession ? 'session' : 'comment');
@@ -118,13 +143,96 @@ export class MetaInferenceEngine {
118
143
  processed.add(id);
119
144
  }
120
145
  }
121
- catch {
122
- // skip malformed
146
+ catch (error) {
147
+ const message = error instanceof Error ? error.message : String(error);
148
+ this.logFailure('malformed_log_line', `skipped malformed log line in ${file}: ${message}`);
123
149
  }
124
150
  }
125
151
  }
126
152
  return entries;
127
153
  }
154
+ loadState() {
155
+ try {
156
+ return this.stateManager.load();
157
+ }
158
+ catch (error) {
159
+ throw this.fail('state_error', `meta-inference state could not be read: ${errorText(error)}`, error);
160
+ }
161
+ }
162
+ throwIfBackingOff(state) {
163
+ const backoff = state.modelBackoff;
164
+ if (!backoff)
165
+ return;
166
+ const failedAt = Date.parse(backoff.failedAt);
167
+ if (Number.isNaN(failedAt))
168
+ return;
169
+ const wait = modelBackoffDelayMs(backoff.failures);
170
+ if (this.now().getTime() - failedAt < wait) {
171
+ throw this.fail('model_backoff', `meta-inference model call skipped until backoff ends (${wait}ms)`);
172
+ }
173
+ }
174
+ /**
175
+ * A missing, failed, or junk model result must not produce a synthesis report.
176
+ * Entries stay unprocessed so a later run can retry them.
177
+ */
178
+ callModelOrRecord(prompt) {
179
+ try {
180
+ return this.callModel(prompt);
181
+ }
182
+ catch (error) {
183
+ this.noteModelFailure(error);
184
+ throw error;
185
+ }
186
+ }
187
+ callModel(prompt) {
188
+ if (this.model === null) {
189
+ throw this.fail('model_unavailable', 'meta-inference model is not configured');
190
+ }
191
+ let output;
192
+ try {
193
+ output = this.model(prompt);
194
+ }
195
+ catch (error) {
196
+ if (error instanceof MetaInferenceModelError) {
197
+ this.logFailure(error.reason, error.message);
198
+ throw error;
199
+ }
200
+ if (isMissingHermes(error)) {
201
+ throw this.fail('model_unavailable', 'meta-inference model is not available: hermes CLI not found', error);
202
+ }
203
+ throw this.fail('model_call_failed', `meta-inference model call failed: ${errorText(error)}`, error);
204
+ }
205
+ try {
206
+ return acceptModelOutput(output);
207
+ }
208
+ catch (error) {
209
+ if (error instanceof MetaInferenceModelError) {
210
+ this.logFailure(error.reason, error.message);
211
+ throw error;
212
+ }
213
+ throw error;
214
+ }
215
+ }
216
+ noteModelFailure(error) {
217
+ if (!(error instanceof MetaInferenceModelError))
218
+ return;
219
+ if (error.reason === 'model_backoff' || error.reason === 'state_error')
220
+ return;
221
+ try {
222
+ this.stateManager.recordModelFailure(this.now().toISOString());
223
+ }
224
+ catch (recordError) {
225
+ this.logFailure('state_error', `could not record model backoff: ${errorText(recordError)}`);
226
+ }
227
+ }
228
+ fail(reason, message, cause) {
229
+ const error = new MetaInferenceModelError(reason, message, cause === undefined ? undefined : { cause });
230
+ this.logFailure(reason, message);
231
+ return error;
232
+ }
233
+ logFailure(reason, message) {
234
+ process.stderr.write(`[meta-inference] ${reason}: ${message}\n`);
235
+ }
128
236
  appendReport(entryCount, passCount, avgResonance, report) {
129
237
  const dir = dirname(this.reportPath);
130
238
  if (!existsSync(dir))
@@ -146,4 +254,53 @@ export class MetaInferenceEngine {
146
254
  }).trim();
147
255
  }
148
256
  }
257
+ function oneLine(value) {
258
+ return value.replace(/\s+/g, ' ').trim();
259
+ }
260
+ function errorText(error) {
261
+ if (error instanceof Error)
262
+ return error.message;
263
+ return String(error);
264
+ }
265
+ function isMissingHermes(error) {
266
+ if (!error || typeof error !== 'object')
267
+ return false;
268
+ const record = error;
269
+ if (record.code === 'ENOENT')
270
+ return true;
271
+ return typeof record.message === 'string' && record.message.includes('ENOENT');
272
+ }
273
+ function isThenable(value) {
274
+ if (value === null || (typeof value !== 'object' && typeof value !== 'function'))
275
+ return false;
276
+ return typeof value.then === 'function';
277
+ }
278
+ /**
279
+ * Synthesis text is a non-empty string. A value shaped like JSON must parse.
280
+ * Anything else is not a model result.
281
+ */
282
+ function acceptModelOutput(value) {
283
+ if (isThenable(value)) {
284
+ throw new MetaInferenceModelError('model_output_invalid', 'meta-inference model returned a Promise');
285
+ }
286
+ if (value === undefined || value === null) {
287
+ throw new MetaInferenceModelError('model_output_invalid', 'meta-inference model returned empty output');
288
+ }
289
+ if (typeof value !== 'string') {
290
+ throw new MetaInferenceModelError('model_output_invalid', 'meta-inference model returned a non-string');
291
+ }
292
+ const trimmed = value.trim();
293
+ if (trimmed.length === 0) {
294
+ throw new MetaInferenceModelError('model_output_invalid', 'meta-inference model returned empty output');
295
+ }
296
+ if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
297
+ try {
298
+ JSON.parse(trimmed);
299
+ }
300
+ catch (error) {
301
+ throw new MetaInferenceModelError('model_output_invalid', `meta-inference model returned invalid JSON: ${errorText(error)}`, { cause: error });
302
+ }
303
+ }
304
+ return trimmed;
305
+ }
149
306
  //# sourceMappingURL=meta-inference-engine.js.map
@@ -100,11 +100,17 @@ export interface CuratedSignalsFile {
100
100
  };
101
101
  signals: CuratedSignal[];
102
102
  }
103
+ /** Clock for skipping another hermes call after a failed meta-inference run. */
104
+ export interface ModelBackoffState {
105
+ failedAt: string;
106
+ failures: number;
107
+ }
103
108
  export interface InferenceState {
104
109
  processedCommentIds: string[];
105
110
  processedSessionIds: string[];
106
111
  processedPostIds: string[];
107
112
  lastRun: string | null;
113
+ modelBackoff?: ModelBackoffState;
108
114
  }
109
115
  export interface SynthesisReport {
110
116
  entriesProcessed: number;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@0xray/repertoire",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "description": "Factory memory organ for 0xRay — seed primitives, memory routing, extra MCP",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -117,7 +117,7 @@
117
117
  }
118
118
  },
119
119
  "devDependencies": {
120
- "0xray": "^4.0.7",
120
+ "0xray": "^4.0.28",
121
121
  "@types/node": "^22.15.0",
122
122
  "tsx": "^4.19.0",
123
123
  "typescript": "^5.6.0",
@@ -114,10 +114,13 @@ try {
114
114
  fail('.mcp.json', e.message);
115
115
  }
116
116
 
117
- // 5. Grok plugin + UserPromptSubmit boot hook
118
- const grokPlugin = join(homedir(), '.grok/plugins/0xray');
119
- const grokHooks = join(grokPlugin, 'hooks/hooks.json');
120
- existsSync(grokPlugin) ? pass('Grok plugin', grokPlugin) : fail('Grok plugin', 'run: npx 0xray grok install --force');
117
+ // 5. Grok plugin + UserPromptSubmit boot hook.
118
+ // 0xray 4.0.28 installs the plugin into the project, not ~/.grok.
119
+ const grokPlugin = join(root, '.grok', 'plugins', '0xray');
120
+ const grokHooks = join(grokPlugin, 'hooks', 'hooks.json');
121
+ existsSync(grokPlugin)
122
+ ? pass('Grok plugin', grokPlugin)
123
+ : fail('Grok plugin', `missing ${grokPlugin} — run: npx 0xray grok install --force`);
121
124
  if (existsSync(grokHooks)) {
122
125
  const hooksRaw = readFileSync(grokHooks, 'utf8');
123
126
  hooksRaw.includes('session-start.js')
@@ -130,7 +133,7 @@ if (existsSync(grokHooks)) {
130
133
  ? pass('UserPromptSubmit boot hook')
131
134
  : fail('UserPromptSubmit boot hook', 'add session-start.js — npx 0xray grok install --force');
132
135
  } else {
133
- fail('Grok hooks.json', 'missing');
136
+ fail('Grok hooks.json', `missing ${grokHooks}`);
134
137
  }
135
138
 
136
139
  try {
@@ -297,7 +300,7 @@ if (existsSync(installedGrokHooks)) {
297
300
  fail('Grok plugin PostToolUse hook', 'run: npx 0xray grok install --force');
298
301
  }
299
302
  } else {
300
- fail('Grok plugin PostToolUse hook', 'hooks.json missing — npx 0xray grok install --force');
303
+ fail('Grok plugin PostToolUse hook', `missing ${installedGrokHooks} — run: npx 0xray grok install --force`);
301
304
  }
302
305
 
303
306
  console.log('\n' + (failed === 0 ? '🎉 Suit wearable — operate within 0xRay.' : `⚠️ ${failed} check(s) failed — fix before tuning.`));