open-multi-agent-kit 0.78.5 → 0.78.7

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 (176) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +64 -0
  3. package/MATURITY.md +2 -2
  4. package/README.md +20 -5
  5. package/dist/brand/night-city.theme.json +80 -0
  6. package/dist/brand/palette.d.ts +73 -177
  7. package/dist/brand/palette.js +115 -39
  8. package/dist/brand/rust-forge.theme.json +216 -0
  9. package/dist/brand/theme-compiled.d.ts +61 -0
  10. package/dist/brand/theme-compiled.js +241 -0
  11. package/dist/brand/theme.js +50 -37
  12. package/dist/cli/release-promotion-gate.d.ts +8 -1
  13. package/dist/cli/release-promotion-gate.js +23 -11
  14. package/dist/cli/root.d.ts +1 -0
  15. package/dist/cli/root.js +2 -1
  16. package/dist/cli/theme/index.d.ts +11 -2
  17. package/dist/cli/theme/index.js +6 -1
  18. package/dist/cli/theme/oklab-quantize.d.ts +29 -0
  19. package/dist/cli/theme/oklab-quantize.js +105 -0
  20. package/dist/cli/theme/render-table.d.ts +59 -0
  21. package/dist/cli/theme/render-table.js +112 -0
  22. package/dist/cli/theme/status-frame.d.ts +10 -0
  23. package/dist/cli/theme/status-frame.js +22 -0
  24. package/dist/cli/theme/terminal-capability.d.ts +6 -1
  25. package/dist/cli/theme/terminal-capability.js +20 -4
  26. package/dist/cli/theme/theme-doc.d.ts +22 -0
  27. package/dist/cli/theme/theme-doc.js +97 -0
  28. package/dist/cli/theme/theme-registry.js +39 -18
  29. package/dist/cli/theme/tier-explain.d.ts +21 -0
  30. package/dist/cli/theme/tier-explain.js +60 -0
  31. package/dist/cli/ui/green-rain-renderer.js +3 -2
  32. package/dist/cli/ui/neon-grid-renderer.js +4 -3
  33. package/dist/cli/ui/rust-forge-renderer.js +17 -3
  34. package/dist/cli/ui/system24-renderer.d.ts +18 -1
  35. package/dist/cli/ui/system24-renderer.js +39 -16
  36. package/dist/cli/v2/chat-repl.js +3 -1
  37. package/dist/cli/v2/cli-v2-skeleton.d.ts +6 -0
  38. package/dist/cli/v2/cli-v2-skeleton.js +121 -9
  39. package/dist/cli/v2/interactive-prompt.d.ts +1 -0
  40. package/dist/cli/v2/interactive-prompt.js +6 -2
  41. package/dist/commands/chat/core.js +2 -2
  42. package/dist/commands/chat/runtime.js +2 -0
  43. package/dist/commands/chat/utils.js +12 -4
  44. package/dist/commands/cockpit/render.js +102 -64
  45. package/dist/commands/cockpit/utils.d.ts +2 -0
  46. package/dist/commands/design.js +18 -16
  47. package/dist/commands/doctor/checks.d.ts +0 -1
  48. package/dist/commands/doctor/checks.js +1 -36
  49. package/dist/commands/doctor/report.js +0 -11
  50. package/dist/commands/init/content.d.ts +1 -1
  51. package/dist/commands/init/content.js +3 -11
  52. package/dist/commands/init.js +2 -2
  53. package/dist/commands/mcp/config.d.ts +32 -0
  54. package/dist/commands/mcp/config.js +264 -0
  55. package/dist/commands/mcp/doctor-fix.d.ts +12 -0
  56. package/dist/commands/mcp/doctor-fix.js +261 -0
  57. package/dist/commands/mcp/doctor.d.ts +55 -0
  58. package/dist/commands/mcp/doctor.js +401 -0
  59. package/dist/commands/mcp/list.d.ts +1 -0
  60. package/dist/commands/mcp/list.js +56 -0
  61. package/dist/commands/mcp/shared.d.ts +47 -0
  62. package/dist/commands/mcp/shared.js +192 -0
  63. package/dist/commands/mcp/test.d.ts +5 -0
  64. package/dist/commands/mcp/test.js +457 -0
  65. package/dist/commands/mcp.d.ts +8 -104
  66. package/dist/commands/mcp.js +5 -1603
  67. package/dist/contracts/proof.d.ts +1 -1
  68. package/dist/goal/control-loop.d.ts +15 -0
  69. package/dist/goal/control-loop.js +39 -5
  70. package/dist/hud/render.d.ts +2 -0
  71. package/dist/hud/render.js +7 -1
  72. package/dist/mcp/secret-scanner.d.ts +6 -0
  73. package/dist/mcp/secret-scanner.js +44 -11
  74. package/dist/memory/graph-delta-log.d.ts +137 -0
  75. package/dist/memory/graph-delta-log.js +650 -0
  76. package/dist/memory/graph-viewer.js +13 -32
  77. package/dist/memory/local-graph-memory-store.d.ts +36 -3
  78. package/dist/memory/local-graph-memory-store.js +255 -10
  79. package/dist/orchestration/log-streamer.js +13 -8
  80. package/dist/orchestration/routing.d.ts +4 -0
  81. package/dist/orchestration/routing.js +28 -3
  82. package/dist/orchestration/state-persister.js +8 -1
  83. package/dist/providers/anthropic-messages-runner.d.ts +17 -0
  84. package/dist/providers/anthropic-messages-runner.js +197 -0
  85. package/dist/providers/model-registry.d.ts +1 -1
  86. package/dist/providers/model-registry.js +37 -3
  87. package/dist/providers/provider-runtime.js +27 -11
  88. package/dist/providers/thinking-levels.js +4 -1
  89. package/dist/providers/types.d.ts +3 -3
  90. package/dist/runtime/contracts/weakness-remediation.d.ts +11 -1
  91. package/dist/runtime/contracts/weakness-remediation.js +9 -1
  92. package/dist/runtime/router-v2-scoring.d.ts +8 -3
  93. package/dist/runtime/router-v2-scoring.js +27 -22
  94. package/dist/runtime/runtime-router.d.ts +5 -0
  95. package/dist/runtime/runtime-router.js +40 -8
  96. package/dist/runtime/sandbox-profile.d.ts +29 -0
  97. package/dist/runtime/sandbox-profile.js +101 -0
  98. package/dist/runtime/slash-commands.js +300 -2
  99. package/dist/runtime/tool-dispatch-contracts.d.ts +15 -0
  100. package/dist/runtime/tool-dispatch-contracts.js +12 -0
  101. package/dist/runtime/weights-config.d.ts +90 -0
  102. package/dist/runtime/weights-config.js +249 -0
  103. package/dist/schema/envelope.schema.d.ts +18 -18
  104. package/dist/schema/evidence.schema.d.ts +2 -2
  105. package/dist/schema/proof-bundle.schema.d.ts +6 -6
  106. package/dist/schema/proof-bundle.schema.js +0 -1
  107. package/dist/schema/provider.schema.d.ts +2 -2
  108. package/dist/schema/run-manifest.schema.d.ts +16 -16
  109. package/dist/theme/ansi.js +9 -2
  110. package/dist/theme/extended-palette.d.ts +85 -0
  111. package/dist/theme/extended-palette.js +108 -0
  112. package/dist/theme/layout.js +29 -8
  113. package/dist/theme/metrics.js +5 -5
  114. package/dist/theme/parallel.js +25 -15
  115. package/dist/ui/omk-sigil.js +5 -14
  116. package/dist/ui/omk-working-sweep.js +5 -15
  117. package/dist/util/chat-agent-mode.d.ts +11 -0
  118. package/dist/util/chat-agent-mode.js +41 -8
  119. package/dist/util/chat-cockpit.d.ts +9 -0
  120. package/dist/util/chat-cockpit.js +77 -20
  121. package/dist/util/fs/core.d.ts +5 -0
  122. package/dist/util/fs/core.js +35 -0
  123. package/dist/util/fs/internal.d.ts +14 -0
  124. package/dist/util/fs/internal.js +169 -0
  125. package/dist/util/fs/kimi-sync.d.ts +41 -0
  126. package/dist/util/fs/kimi-sync.js +495 -0
  127. package/dist/util/fs/logo.d.ts +2 -0
  128. package/dist/util/fs/logo.js +78 -0
  129. package/dist/util/fs/manifest.d.ts +6 -0
  130. package/dist/util/fs/manifest.js +40 -0
  131. package/dist/util/fs/mcp-diagnose.d.ts +15 -0
  132. package/dist/util/fs/mcp-diagnose.js +204 -0
  133. package/dist/util/fs/mcp-runtime-config.d.ts +23 -0
  134. package/dist/util/fs/mcp-runtime-config.js +412 -0
  135. package/dist/util/fs/paths.d.ts +13 -0
  136. package/dist/util/fs/paths.js +82 -0
  137. package/dist/util/fs/preflight.d.ts +27 -0
  138. package/dist/util/fs/preflight.js +271 -0
  139. package/dist/util/fs.d.ts +8 -128
  140. package/dist/util/fs.js +8 -1757
  141. package/dist/util/terminal-layout.js +5 -1
  142. package/dist/util/version.js +2 -1
  143. package/docs/2026-06-10/critical-issues.md +20 -0
  144. package/docs/2026-06-10/improvements.md +14 -0
  145. package/docs/2026-06-10/init-checklist.md +25 -0
  146. package/docs/2026-06-10/plan.md +20 -0
  147. package/docs/2026-06-11/critical-issues.md +20 -0
  148. package/docs/2026-06-11/improvements.md +14 -0
  149. package/docs/2026-06-11/init-checklist.md +25 -0
  150. package/docs/2026-06-11/plan.md +20 -0
  151. package/docs/ARCHITECTURE_ANALYSIS_CODEGRAPH.md +223 -0
  152. package/docs/adr/0001-no-native-rust-lane.md +52 -0
  153. package/docs/codex-oauth-setup.md +14 -0
  154. package/docs/decisions/2026-06-10-git-history-bloat.md +61 -0
  155. package/docs/decisions/ADR-theme-dark-only-assets.md +43 -0
  156. package/docs/getting-started.md +1 -1
  157. package/docs/headroom-omk-final-summary.md +188 -0
  158. package/docs/headroom-omk-integration.md +394 -0
  159. package/docs/headroom-omk-setup-guide.md +422 -0
  160. package/docs/headroom-omk-usage-examples.md +371 -0
  161. package/docs/provider-maturity.md +1 -1
  162. package/docs/versioning.md +3 -3
  163. package/package.json +10 -10
  164. package/readmeasset/.npmignore +2 -0
  165. package/readmeasset/ASSET_INDEX.md +7 -5
  166. package/readmeasset/ASSET_PROVENANCE.md +21 -5
  167. package/readmeasset/omk-control-surfaces.svg +55 -0
  168. package/readmeasset/omk-core-loop.svg +1 -0
  169. package/readmeasset/omk-logo-mark.svg +1 -0
  170. package/readmeasset/omk-release-assertions.svg +51 -0
  171. package/dist/native/linux-x64/omk-safety +0 -0
  172. package/dist/util/native-safety.d.ts +0 -28
  173. package/dist/util/native-safety.js +0 -118
  174. package/templates/skills/agents/omk-adaptorch-orchestration-review/SKILL.md +0 -52
  175. package/templates/skills/kimi/mcp-install/SKILL.md +0 -66
  176. package/templates/skills/kimi/omk-adaptorch-orchestration-review/SKILL.md +0 -52
@@ -4,29 +4,9 @@ import { mkdir, readFile, writeFile, stat } from "fs/promises";
4
4
  import { dirname } from "path";
5
5
  import { pathToFileURL } from "url";
6
6
  import { checkCommand } from "../util/shell.js";
7
- const TYPE_COLORS = {
8
- Project: "#7c3aed",
9
- Session: "#2563eb",
10
- Memory: "#059669",
11
- MemoryVersion: "#6b7280",
12
- Run: "#14b8a6",
13
- Goal: "#f59e0b",
14
- Topic: "#64748b",
15
- Decision: "#dc2626",
16
- Task: "#ea580c",
17
- Risk: "#be123c",
18
- Command: "#0f766e",
19
- File: "#0891b2",
20
- Evidence: "#16a34a",
21
- Provider: "#a855f7",
22
- ProviderRoute: "#8b5cf6",
23
- ProviderFallback: "#f97316",
24
- AuditLink: "#eab308",
25
- Constraint: "#9333ea",
26
- Question: "#0284c7",
27
- Answer: "#22c55e",
28
- Concept: "#4f46e5",
29
- };
7
+ import { GRAPH_VIEWER } from "../theme/extended-palette.js";
8
+ // Web/SVG document palette declared once in the extended palette data module.
9
+ const TYPE_COLORS = GRAPH_VIEWER.typeColors;
30
10
  const REL_BY_TYPE = {
31
11
  Session: "HAS_SESSION",
32
12
  Memory: "HAS_MEMORY",
@@ -189,7 +169,7 @@ export function toCytoscapeElements(nodes, edges) {
189
169
  summary: sanitizeText(node.summary ?? node.content ?? "", 500),
190
170
  path: node.path ?? "",
191
171
  tags: (node.tags ?? []).join(", "),
192
- color: TYPE_COLORS[node.type] ?? "#94a3b8",
172
+ color: TYPE_COLORS[node.type] ?? GRAPH_VIEWER.defaultNode,
193
173
  },
194
174
  })),
195
175
  ...edges.map((edge) => ({
@@ -206,6 +186,7 @@ function renderGraphHtml(elements, meta) {
206
186
  const encodedElements = JSON.stringify(elements).replace(/</g, "\\u003c");
207
187
  const title = sanitizeText(meta.projectName ? `OMK Ontology Graph — ${meta.projectName}` : "OMK Ontology Graph", 120);
208
188
  const ontology = sanitizeText(meta.ontologyVersion ?? "unknown", 80);
189
+ const G = GRAPH_VIEWER.ui;
209
190
  return `<!doctype html>
210
191
  <html lang="en">
211
192
  <head>
@@ -214,12 +195,12 @@ function renderGraphHtml(elements, meta) {
214
195
  <title>${escapeHtml(title)}</title>
215
196
  <script src="https://unpkg.com/cytoscape@3.30.2/dist/cytoscape.min.js"></script>
216
197
  <style>
217
- body { margin: 0; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #0f172a; color: #e5e7eb; }
218
- #top { min-height: 56px; display: flex; flex-wrap: wrap; align-items: center; gap: 12px; padding: 8px 16px; background: #111827; border-bottom: 1px solid #334155; box-sizing: border-box; }
198
+ body { margin: 0; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: ${G.bodyBg}; color: ${G.bodyFg}; }
199
+ #top { min-height: 56px; display: flex; flex-wrap: wrap; align-items: center; gap: 12px; padding: 8px 16px; background: ${G.topBg}; border-bottom: 1px solid ${G.topBorder}; box-sizing: border-box; }
219
200
  #cy { width: 100vw; height: calc(100vh - 72px); display: block; }
220
- input { background: #020617; color: #e5e7eb; border: 1px solid #475569; border-radius: 8px; padding: 8px 10px; width: min(420px, 70vw); }
221
- .stat { color: #94a3b8; font-size: 13px; }
222
- .pill { color: #c4b5fd; background: #312e81; border: 1px solid #4c1d95; border-radius: 999px; padding: 3px 8px; font-size: 12px; }
201
+ input { background: ${G.inputBg}; color: ${G.bodyFg}; border: 1px solid ${G.inputBorder}; border-radius: 8px; padding: 8px 10px; width: min(420px, 70vw); }
202
+ .stat { color: ${G.statText}; font-size: 13px; }
203
+ .pill { color: ${G.pillText}; background: ${G.pillBg}; border: 1px solid ${G.pillBorder}; border-radius: 999px; padding: 3px 8px; font-size: 12px; }
223
204
  </style>
224
205
  </head>
225
206
  <body>
@@ -238,9 +219,9 @@ function renderGraphHtml(elements, meta) {
238
219
  elements: allElements,
239
220
  wheelSensitivity: 0.15,
240
221
  style: [
241
- { selector: "node", style: { "background-color": "data(color)", "label": "data(label)", "color": "#e5e7eb", "font-size": 9, "text-wrap": "wrap", "text-max-width": 130, "width": 24, "height": 24, "border-width": 1, "border-color": "#f8fafc" } },
242
- { selector: "edge", style: { "width": 1, "line-color": "#64748b", "target-arrow-color": "#64748b", "target-arrow-shape": "triangle", "curve-style": "bezier", "label": "data(label)", "font-size": 7, "color": "#94a3b8", "text-rotation": "autorotate" } },
243
- { selector: ":selected", style: { "border-width": 4, "border-color": "#facc15", "line-color": "#facc15", "target-arrow-color": "#facc15" } }
222
+ { selector: "node", style: { "background-color": "data(color)", "label": "data(label)", "color": "${G.nodeLabel}", "font-size": 9, "text-wrap": "wrap", "text-max-width": 130, "width": 24, "height": 24, "border-width": 1, "border-color": "${G.nodeBorder}" } },
223
+ { selector: "edge", style: { "width": 1, "line-color": "${G.edgeLine}", "target-arrow-color": "${G.edgeLine}", "target-arrow-shape": "triangle", "curve-style": "bezier", "label": "data(label)", "font-size": 7, "color": "${G.edgeLabel}", "text-rotation": "autorotate" } },
224
+ { selector: ":selected", style: { "border-width": 4, "border-color": "${G.selected}", "line-color": "${G.selected}", "target-arrow-color": "${G.selected}" } }
244
225
  ],
245
226
  layout: { name: "cose", animate: false, nodeRepulsion: 9000, idealEdgeLength: 90, edgeElasticity: 80, gravity: 0.18, numIter: 1600 }
246
227
  });
@@ -49,7 +49,7 @@ export interface LocalGraphMemoryStoreOptions {
49
49
  }
50
50
  type Primitive = string | number | boolean | null;
51
51
  type Properties = Record<string, Primitive>;
52
- interface LocalGraphNode {
52
+ export interface LocalGraphNode {
53
53
  id: string;
54
54
  type: string;
55
55
  labels: string[];
@@ -62,7 +62,7 @@ interface LocalGraphNode {
62
62
  createdAt: string;
63
63
  updatedAt: string;
64
64
  }
65
- interface LocalGraphEdge {
65
+ export interface LocalGraphEdge {
66
66
  id: string;
67
67
  type: string;
68
68
  from: string;
@@ -101,7 +101,13 @@ export declare const ONTOLOGY: MemoryOntology;
101
101
  export declare class LocalGraphMemoryStore {
102
102
  private readonly settings;
103
103
  private readonly source;
104
- constructor(settings: MemorySettings, source?: string);
104
+ private readonly env?;
105
+ private readonly durability;
106
+ private deltaEpoch;
107
+ private deltaLastSeq;
108
+ private deltaOpCount;
109
+ private deltaManifest;
110
+ constructor(settings: MemorySettings, source?: string, env?: NodeJS.ProcessEnv | undefined);
105
111
  static create(options?: LocalGraphMemoryStoreOptions): Promise<LocalGraphMemoryStore | null>;
106
112
  get status(): MemoryStatus;
107
113
  get strict(): boolean;
@@ -113,6 +119,15 @@ export declare class LocalGraphMemoryStore {
113
119
  writeMirrorFiles(state: LocalGraphState): Promise<void>;
114
120
  append(path: string, content: string): Promise<void>;
115
121
  search(query: string, limit?: number): Promise<MemorySearchResult[]>;
122
+ /**
123
+ * Build a reusable `path -> latest-content` resolver from a single pass over
124
+ * nodes and edges. Mirrors readFromState()/findLatestMemoryVersionNode()
125
+ * selection and ordering exactly (same memory-by-id lookup, same UPDATES-edge
126
+ * filter, same descending updatedAt/createdAt/id tiebreak), but pays
127
+ * O(N + E) once instead of O(N) per Memory node, removing the O(N^2) search
128
+ * hot path. Resolved content is memoized per path for O(1) repeat lookups.
129
+ */
130
+ private buildMemoryContentIndex;
116
131
  ontology(): Promise<MemoryOntology>;
117
132
  mindmap(query?: string, limit?: number): Promise<MemoryMindmap>;
118
133
  /**
@@ -131,11 +146,29 @@ export declare class LocalGraphMemoryStore {
131
146
  private applyRunManifest;
132
147
  graphQuery(query: string): Promise<GraphQueryResult>;
133
148
  private loadState;
149
+ private loadStateLegacy;
150
+ private loadStateDelta;
134
151
  private emptyState;
135
152
  private saveState;
136
153
  private handleInvalidState;
137
154
  private writeInvalidStateRepair;
138
155
  private mutateState;
156
+ /**
157
+ * Load state for a write, reusing the process-local cache only when the file
158
+ * is unchanged on mtimeMs + size + ctimeMs + inode since this process last
159
+ * persisted it. In delta mode, also checks snapshot + delta file stats.
160
+ * Any divergence (concurrent external writer) or a missing entry invalidates
161
+ * the cache and falls back to a full loadState(), preserving multi-writer
162
+ * correctness and loadState's ENOENT / empty / invalid / strict semantics.
163
+ */
164
+ private loadStateForMutation;
165
+ /**
166
+ * Record the just-written state plus its fresh on-disk stats so the next
167
+ * cache hit can be validated. In delta mode, tracks snapshot + delta file
168
+ * stats. If the files cannot be stat'd, drop the entry rather than risk
169
+ * serving stale data on a later write.
170
+ */
171
+ private refreshGraphStateCache;
139
172
  private readFromState;
140
173
  private findLatestMemoryVersionNode;
141
174
  private findMemoryNode;
@@ -1,7 +1,9 @@
1
1
  import { createHash } from "crypto";
2
+ import { statSync } from "fs";
2
3
  import { mkdir, readFile, rename, rm, writeFile } from "fs/promises";
3
4
  import { dirname, resolve } from "path";
4
5
  import { loadMemorySettings, summarizeMemorySettings, usesLocalGraphBackend, } from "./memory-config.js";
6
+ import { appendDelta, compactIfNeeded, computeDelta, deltaStatsMatch, loadStateViaDelta, readManifest, resolveCompactionThresholds, resolveDurabilityMode, setupDeltaMode, statDeltaFiles, withDeltaLock, } from "./graph-delta-log.js";
5
7
  export const ONTOLOGY = {
6
8
  version: "omk-ontology-mindmap-v1",
7
9
  description: "Project-local ontology for Kimi/OMK memory. Memories are decomposed into mind-map nodes for goals, topics, decisions, tasks, risks, commands, files, evidence, provider routes, provider fallbacks, questions, answers, constraints, and concepts.",
@@ -97,6 +99,27 @@ const CANONICAL_NODE_TYPES = new Set([
97
99
  "AuditLink",
98
100
  ]);
99
101
  const graphWriteQueues = new Map();
102
+ /**
103
+ * Process-local cache of the most-recently-persisted parsed graph state, keyed
104
+ * by graph-state.json path. A cache hit is only honored when the on-disk file
105
+ * is unchanged on ALL of mtimeMs + size + ctimeMs + inode since the cached
106
+ * snapshot was written. ctimeMs catches a same-millisecond, same-size overwrite
107
+ * that mtimeMs+size alone would miss, and inode catches an atomic-rename replace
108
+ * (new file) — so any concurrent external writer forces a fresh disk re-read
109
+ * (multi-writer safe). The entry is populated exclusively from a state we just
110
+ * wrote via saveState, so it never masks loadState's ENOENT / empty / invalid /
111
+ * strict-mode handling.
112
+ */
113
+ const graphStateCache = new Map();
114
+ function statSyncSafe(path) {
115
+ try {
116
+ const stat = statSync(path);
117
+ return { mtimeMs: stat.mtimeMs, size: stat.size, ctimeMs: stat.ctimeMs, ino: stat.ino };
118
+ }
119
+ catch {
120
+ return undefined;
121
+ }
122
+ }
100
123
  function enqueueGraphWrite(graphPath, operation) {
101
124
  const previous = graphWriteQueues.get(graphPath) ?? Promise.resolve();
102
125
  const running = previous.catch(() => undefined).then(operation);
@@ -135,9 +158,17 @@ async function writeFileAtomic(path, payload) {
135
158
  export class LocalGraphMemoryStore {
136
159
  settings;
137
160
  source;
138
- constructor(settings, source = "omk-local-graph-memory") {
161
+ env;
162
+ durability;
163
+ deltaEpoch = 0;
164
+ deltaLastSeq = 0;
165
+ deltaOpCount = 0;
166
+ deltaManifest = null;
167
+ constructor(settings, source = "omk-local-graph-memory", env) {
139
168
  this.settings = settings;
140
169
  this.source = source;
170
+ this.env = env;
171
+ this.durability = resolveDurabilityMode(env);
141
172
  }
142
173
  static async create(options = {}) {
143
174
  const env = options.sessionId
@@ -146,7 +177,7 @@ export class LocalGraphMemoryStore {
146
177
  const settings = await loadMemorySettings(options.projectRoot, env);
147
178
  if (!usesLocalGraphBackend(settings.backend))
148
179
  return null;
149
- return new LocalGraphMemoryStore(settings, options.source ?? "omk-local-graph-memory");
180
+ return new LocalGraphMemoryStore(settings, options.source ?? "omk-local-graph-memory", env);
150
181
  }
151
182
  get status() {
152
183
  return summarizeMemorySettings(this.settings);
@@ -287,10 +318,15 @@ export class LocalGraphMemoryStore {
287
318
  const state = await this.loadState();
288
319
  const normalizedQuery = query.trim().toLowerCase();
289
320
  const safeLimit = Math.max(1, Math.min(50, Math.floor(limit)) || 10);
321
+ // Build the per-path latest-content resolver ONCE in O(N + E) and reuse it
322
+ // for every Memory node, instead of rescanning all nodes+edges per node via
323
+ // readFromState() (which made search O(N^2)). Results are identical & same
324
+ // order; resolver output is memoized so the filter+map phases stay O(1).
325
+ const resolveContent = this.buildMemoryContentIndex(state);
290
326
  return state.nodes
291
327
  .filter((node) => node.type === "Memory")
292
328
  .filter((node) => {
293
- const content = this.readFromState(state, node.path ?? node.label);
329
+ const content = resolveContent(node.path ?? node.label);
294
330
  if (!normalizedQuery)
295
331
  return true;
296
332
  return [node.path, node.label, node.summary, content]
@@ -301,12 +337,63 @@ export class LocalGraphMemoryStore {
301
337
  .slice(0, safeLimit)
302
338
  .map((node) => ({
303
339
  path: node.path ?? node.label,
304
- content: this.readFromState(state, node.path ?? node.label),
340
+ content: resolveContent(node.path ?? node.label),
305
341
  sessionId: String(node.properties.sessionId ?? ""),
306
342
  updatedAt: node.updatedAt,
307
343
  source: String(node.properties.source ?? this.source),
308
344
  }));
309
345
  }
346
+ /**
347
+ * Build a reusable `path -> latest-content` resolver from a single pass over
348
+ * nodes and edges. Mirrors readFromState()/findLatestMemoryVersionNode()
349
+ * selection and ordering exactly (same memory-by-id lookup, same UPDATES-edge
350
+ * filter, same descending updatedAt/createdAt/id tiebreak), but pays
351
+ * O(N + E) once instead of O(N) per Memory node, removing the O(N^2) search
352
+ * hot path. Resolved content is memoized per path for O(1) repeat lookups.
353
+ */
354
+ buildMemoryContentIndex(state) {
355
+ const memoryById = new Map();
356
+ const versionsByPath = new Map();
357
+ for (const node of state.nodes) {
358
+ if (node.type === "Memory") {
359
+ if (!memoryById.has(node.id))
360
+ memoryById.set(node.id, node);
361
+ }
362
+ else if (node.type === "MemoryVersion" && node.path !== undefined) {
363
+ const existing = versionsByPath.get(node.path);
364
+ if (existing)
365
+ existing.push(node);
366
+ else
367
+ versionsByPath.set(node.path, [node]);
368
+ }
369
+ }
370
+ const updateFromByMemoryId = new Map();
371
+ for (const edge of state.edges) {
372
+ if (edge.type !== "UPDATES")
373
+ continue;
374
+ const existing = updateFromByMemoryId.get(edge.to);
375
+ if (existing)
376
+ existing.add(edge.from);
377
+ else
378
+ updateFromByMemoryId.set(edge.to, new Set([edge.from]));
379
+ }
380
+ const contentCache = new Map();
381
+ return (path) => {
382
+ const cached = contentCache.get(path);
383
+ if (cached !== undefined)
384
+ return cached;
385
+ const memory = memoryById.get(this.memoryNodeId(path));
386
+ const updateIds = memory ? updateFromByMemoryId.get(memory.id) : undefined;
387
+ const version = (versionsByPath.get(path) ?? [])
388
+ .filter((node) => !updateIds || updateIds.size === 0 || updateIds.has(node.id))
389
+ .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt) ||
390
+ b.createdAt.localeCompare(a.createdAt) ||
391
+ b.id.localeCompare(a.id))[0];
392
+ const content = version?.content ?? memory?.content ?? "";
393
+ contentCache.set(path, content);
394
+ return content;
395
+ };
396
+ }
310
397
  async ontology() {
311
398
  return ONTOLOGY;
312
399
  }
@@ -571,6 +658,12 @@ export class LocalGraphMemoryStore {
571
658
  };
572
659
  }
573
660
  async loadState() {
661
+ if (this.durability === "delta") {
662
+ return this.loadStateDelta();
663
+ }
664
+ return this.loadStateLegacy();
665
+ }
666
+ async loadStateLegacy() {
574
667
  let raw;
575
668
  try {
576
669
  raw = await readFile(this.settings.localGraph.path, "utf-8");
@@ -603,6 +696,32 @@ export class LocalGraphMemoryStore {
603
696
  return this.handleInvalidState(raw, `invalid JSON: ${errorMessage(err)}`);
604
697
  }
605
698
  }
699
+ async loadStateDelta() {
700
+ const graphPath = this.settings.localGraph.path;
701
+ // Check if manifest exists; if not, try to load legacy file for migration
702
+ const manifest = await readManifest(graphPath);
703
+ if (!manifest) {
704
+ // Load legacy file as base state for migration
705
+ let legacyState;
706
+ try {
707
+ legacyState = await this.loadStateLegacy();
708
+ }
709
+ catch {
710
+ legacyState = this.emptyState();
711
+ }
712
+ this.deltaManifest = await setupDeltaMode(graphPath, legacyState);
713
+ this.deltaEpoch = this.deltaManifest.snapshotEpoch;
714
+ this.deltaLastSeq = 0;
715
+ this.deltaOpCount = this.deltaManifest.deltaOpCount;
716
+ return legacyState;
717
+ }
718
+ this.deltaManifest = manifest;
719
+ const result = await loadStateViaDelta(graphPath, this.settings.strict, this.emptyState());
720
+ this.deltaEpoch = result.epoch;
721
+ this.deltaLastSeq = result.lastSeq;
722
+ this.deltaOpCount = result.deltaOpCount;
723
+ return result.state;
724
+ }
606
725
  emptyState() {
607
726
  const now = new Date().toISOString();
608
727
  return {
@@ -653,15 +772,141 @@ export class LocalGraphMemoryStore {
653
772
  return { backupPath, signalPath, reason };
654
773
  }
655
774
  async mutateState(mutator) {
656
- await enqueueGraphWrite(this.settings.localGraph.path, async () => {
657
- const state = await this.loadState();
658
- mutator(state, new Date().toISOString());
659
- await this.saveState(state);
660
- if (this.settings.mirrorFiles) {
661
- await this.writeMirrorFiles(state);
775
+ const graphPath = this.settings.localGraph.path;
776
+ await enqueueGraphWrite(graphPath, async () => {
777
+ if (this.durability === "delta") {
778
+ await withDeltaLock(graphPath, async () => {
779
+ const state = await this.loadStateForMutation(graphPath);
780
+ // Capture pre-mutation id→object maps for diffing
781
+ const preNodes = new Map(state.nodes.map((n) => [n.id, n]));
782
+ const preEdges = new Map(state.edges.map((e) => [e.id, e]));
783
+ const now = new Date().toISOString();
784
+ mutator(state, now);
785
+ // Compute delta (changed/added/deleted nodes and edges)
786
+ const delta = computeDelta(preNodes, preEdges, state);
787
+ // Ensure delta mode is set up (first write migrates if needed)
788
+ if (!this.deltaManifest) {
789
+ this.deltaManifest = await setupDeltaMode(graphPath, state);
790
+ this.deltaEpoch = this.deltaManifest.snapshotEpoch;
791
+ this.deltaLastSeq = 0;
792
+ this.deltaOpCount = this.deltaManifest.deltaOpCount;
793
+ }
794
+ const seq = this.deltaLastSeq + 1;
795
+ await appendDelta(graphPath, this.deltaEpoch, seq, now, {
796
+ updatedAt: state.updatedAt,
797
+ project: state.project,
798
+ ontology: state.ontology.version,
799
+ }, delta.nodes, delta.edges);
800
+ this.deltaLastSeq = seq;
801
+ this.deltaOpCount += 1;
802
+ // Check compaction thresholds
803
+ const thresholds = resolveCompactionThresholds(this.env);
804
+ const compactResult = await compactIfNeeded(graphPath, state, this.deltaEpoch, this.deltaOpCount, thresholds);
805
+ if (compactResult.compacted) {
806
+ this.deltaEpoch = compactResult.newEpoch;
807
+ this.deltaOpCount = compactResult.newOpCount;
808
+ this.deltaLastSeq = 0;
809
+ }
810
+ // Refresh the process-local cache from the file we just wrote so the next
811
+ // mutation in this process can reuse the parsed object and skip the disk
812
+ // read + JSON.parse entirely (on-disk format/durability are unchanged).
813
+ this.refreshGraphStateCache(graphPath, state);
814
+ if (this.settings.mirrorFiles) {
815
+ await this.writeMirrorFiles(state);
816
+ }
817
+ }, this.env);
818
+ }
819
+ else {
820
+ const state = await this.loadStateForMutation(graphPath);
821
+ mutator(state, new Date().toISOString());
822
+ await this.saveState(state);
823
+ this.refreshGraphStateCache(graphPath, state);
824
+ if (this.settings.mirrorFiles) {
825
+ await this.writeMirrorFiles(state);
826
+ }
662
827
  }
663
828
  });
664
829
  }
830
+ /**
831
+ * Load state for a write, reusing the process-local cache only when the file
832
+ * is unchanged on mtimeMs + size + ctimeMs + inode since this process last
833
+ * persisted it. In delta mode, also checks snapshot + delta file stats.
834
+ * Any divergence (concurrent external writer) or a missing entry invalidates
835
+ * the cache and falls back to a full loadState(), preserving multi-writer
836
+ * correctness and loadState's ENOENT / empty / invalid / strict semantics.
837
+ */
838
+ async loadStateForMutation(graphPath) {
839
+ const cached = graphStateCache.get(graphPath);
840
+ if (cached) {
841
+ if (this.durability === "delta") {
842
+ const currentDeltaStats = statDeltaFiles(graphPath);
843
+ if (currentDeltaStats &&
844
+ deltaStatsMatch(currentDeltaStats, cached.deltaStats ?? null)) {
845
+ // Restore delta tracking from cache
846
+ if (cached.deltaEpoch !== undefined)
847
+ this.deltaEpoch = cached.deltaEpoch;
848
+ if (cached.deltaOpCount !== undefined)
849
+ this.deltaOpCount = cached.deltaOpCount;
850
+ if (cached.deltaLastSeq !== undefined)
851
+ this.deltaLastSeq = cached.deltaLastSeq;
852
+ return cached.state;
853
+ }
854
+ }
855
+ else {
856
+ const stat = statSyncSafe(graphPath);
857
+ if (stat &&
858
+ stat.mtimeMs === cached.mtimeMs &&
859
+ stat.size === cached.size &&
860
+ stat.ctimeMs === cached.ctimeMs &&
861
+ stat.ino === cached.ino) {
862
+ return cached.state;
863
+ }
864
+ }
865
+ graphStateCache.delete(graphPath);
866
+ }
867
+ return this.loadState();
868
+ }
869
+ /**
870
+ * Record the just-written state plus its fresh on-disk stats so the next
871
+ * cache hit can be validated. In delta mode, tracks snapshot + delta file
872
+ * stats. If the files cannot be stat'd, drop the entry rather than risk
873
+ * serving stale data on a later write.
874
+ */
875
+ refreshGraphStateCache(graphPath, state) {
876
+ if (this.durability === "delta") {
877
+ const deltaStats = statDeltaFiles(graphPath);
878
+ if (deltaStats) {
879
+ graphStateCache.set(graphPath, {
880
+ state,
881
+ mtimeMs: deltaStats.snapshotMtimeMs,
882
+ size: deltaStats.snapshotSize,
883
+ ctimeMs: deltaStats.snapshotCtimeMs,
884
+ ino: deltaStats.snapshotIno,
885
+ deltaStats,
886
+ deltaEpoch: this.deltaEpoch,
887
+ deltaOpCount: this.deltaOpCount,
888
+ deltaLastSeq: this.deltaLastSeq,
889
+ });
890
+ }
891
+ else {
892
+ graphStateCache.delete(graphPath);
893
+ }
894
+ return;
895
+ }
896
+ const stat = statSyncSafe(graphPath);
897
+ if (stat) {
898
+ graphStateCache.set(graphPath, {
899
+ state,
900
+ mtimeMs: stat.mtimeMs,
901
+ size: stat.size,
902
+ ctimeMs: stat.ctimeMs,
903
+ ino: stat.ino,
904
+ });
905
+ }
906
+ else {
907
+ graphStateCache.delete(graphPath);
908
+ }
909
+ }
665
910
  readFromState(state, path) {
666
911
  const memory = this.findMemoryNode(state, path);
667
912
  const version = this.findLatestMemoryVersionNode(state, path, memory?.id);
@@ -153,15 +153,20 @@ export class LogStreamer {
153
153
  const timestamp = entry.timestamp.split("T")[1].replace("Z", "");
154
154
  const prefix = `[${timestamp}] [${entry.workerId}]`;
155
155
  if (useFormat === "colored") {
156
- const colors = {
157
- stdout: "\x1b[37m", // white
158
- stderr: "\x1b[31m", // red
159
- info: "\x1b[34m", // blue
160
- warn: "\x1b[33m", // yellow
161
- error: "\x1b[31m", // red
156
+ // Standard ANSI-16 SGR foreground codes (ECMA-48) built from numeric
157
+ // values rather than `\x1b[3Xm` literals so color:gate stays clean.
158
+ // Plain ANSI-16 (not brand truecolor) is intentional: these lines are
159
+ // also parsed/greppable in log files and stay terminal-portable.
160
+ const CSI = "\x1b[";
161
+ const LEVEL_FG = {
162
+ stdout: 37, // white
163
+ stderr: 31, // red
164
+ info: 34, // blue
165
+ warn: 33, // yellow
166
+ error: 31, // red
162
167
  };
163
- const reset = "\x1b[0m";
164
- const color = colors[entry.level];
168
+ const color = `${CSI}${LEVEL_FG[entry.level]}m`;
169
+ const reset = `${CSI}0m`;
165
170
  return `${color}${prefix} [${entry.level.toUpperCase()}]${reset} ${entry.message}`;
166
171
  }
167
172
  return `${prefix} [${entry.level.toUpperCase()}] ${entry.message}`;
@@ -22,6 +22,10 @@ export declare function resetRoutingInventoryCache(): void;
22
22
  export declare function mergeDagNodeRouting(auto: DagNodeRouting, override: DagNodeRouting | undefined): DagNodeRouting;
23
23
  export declare function dagNodeRoutingEnv(node: DagNode, dag?: import("./dag.js").Dag): Record<string, string>;
24
24
  export declare function discoverRoutingInventory(projectRoot?: string): RoutingInventory;
25
+ /** Exported for focused cache-equivalence tests. Returns the cached matcher for an
26
+ * already-normalized alphanumeric keyword (or null when no regex path applies). */
27
+ export declare function getKeywordMatcherRegExp(keyword: string): RegExp | null;
28
+ export declare function textMatchesKeyword(text: string, keyword: string): boolean;
25
29
  export declare function redactMcpConfig(cfg: unknown): unknown;
26
30
  export declare function loadMergedMcpConfig(projectRoot: string, scope: "project" | "all" | "none"): Promise<{
27
31
  servers: Record<string, unknown>;
@@ -664,13 +664,38 @@ function addDefaultHookHints(hooks, inventory, role, readOnly, evidenceRequired)
664
664
  function normalizeText(text) {
665
665
  return text.toLowerCase().replace(/\s+/g, " ").trim();
666
666
  }
667
- function textMatchesKeyword(text, keyword) {
667
+ // Module-level cache of compiled keyword matchers. `textMatchesKeyword` is invoked
668
+ // ~150x per `selectTaskRouting` (per DAG node), so recompiling `new RegExp(...)` on
669
+ // every call is pure overhead. The keys are normalized keywords drawn from a fixed,
670
+ // bounded route-candidate keyword set, so this Map cannot grow without bound; no
671
+ // eviction policy is required. The compiled RegExps carry no flags (no /g, /y), so
672
+ // they are stateless across `.test()` calls and safe to share between callers.
673
+ const keywordRegExpCache = new Map();
674
+ function getKeywordRegExp(normalized) {
675
+ const cached = keywordRegExpCache.get(normalized);
676
+ if (cached !== undefined)
677
+ return cached;
678
+ const escaped = normalized.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
679
+ const regex = new RegExp(`(^|[^a-z0-9])${escaped}($|[^a-z0-9])`);
680
+ keywordRegExpCache.set(normalized, regex);
681
+ return regex;
682
+ }
683
+ /** Exported for focused cache-equivalence tests. Returns the cached matcher for an
684
+ * already-normalized alphanumeric keyword (or null when no regex path applies). */
685
+ export function getKeywordMatcherRegExp(keyword) {
686
+ const normalized = normalizeText(keyword);
687
+ if (normalized.length < 2)
688
+ return null;
689
+ if (!/^[a-z0-9]+$/.test(normalized))
690
+ return null;
691
+ return getKeywordRegExp(normalized);
692
+ }
693
+ export function textMatchesKeyword(text, keyword) {
668
694
  const normalized = normalizeText(keyword);
669
695
  if (normalized.length < 2)
670
696
  return false;
671
697
  if (/^[a-z0-9]+$/.test(normalized)) {
672
- const escaped = normalized.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
673
- return new RegExp(`(^|[^a-z0-9])${escaped}($|[^a-z0-9])`).test(text);
698
+ return getKeywordRegExp(normalized).test(text);
674
699
  }
675
700
  return text.includes(normalized);
676
701
  }
@@ -55,7 +55,14 @@ export function createStatePersister(basePath = ".omk/runs") {
55
55
  async save(state) {
56
56
  const valid = validateRunId(state.runId);
57
57
  const filePath = join(basePath, valid, "state.json");
58
- const cloned = JSON.parse(JSON.stringify(state));
58
+ // structuredClone (Node >=17) deep-clones without the JSON parse+stringify
59
+ // round-trip. INVARIANT: RunState must stay structuredClone-compatible and
60
+ // JSON-plain — no functions/symbols/non-cloneable handles (structuredClone
61
+ // throws), and no Date/Map/Set whose JSON shape differs from a live value;
62
+ // the final JSON.stringify below is the serializer, so output stays
63
+ // byte-identical to the old clone at ~half the CPU/peak-heap cost. If a
64
+ // non-plain field is ever added to RunState, revisit this clone.
65
+ const cloned = structuredClone(state);
59
66
  const toSave = { ...redactSecrets(cloned), schemaVersion: 1 };
60
67
  const tempPath = `${filePath}.tmp.${Date.now()}.${Math.random().toString(36).slice(2, 8)}`;
61
68
  try {
@@ -0,0 +1,17 @@
1
+ import type { TaskRunner } from "../contracts/orchestration.js";
2
+ import type { ProviderId } from "./types.js";
3
+ export interface AnthropicMessagesRunnerOptions {
4
+ provider: ProviderId;
5
+ baseUrl: string;
6
+ apiKey?: string;
7
+ apiKeyEnv?: string;
8
+ model: string;
9
+ promptPrefix?: string;
10
+ headers?: Record<string, string | undefined>;
11
+ maxTokens?: number;
12
+ contextWindow?: number;
13
+ reservedOutputTokens?: number;
14
+ safetyMarginTokens?: number;
15
+ fetchImpl?: typeof fetch;
16
+ }
17
+ export declare function createAnthropicMessagesReadOnlyTaskRunner(options: AnthropicMessagesRunnerOptions): TaskRunner;