@remnic/core 9.3.708 → 9.3.710

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 (266) hide show
  1. package/dist/access-boundary.d.ts +7 -7
  2. package/dist/access-boundary.js +8 -8
  3. package/dist/access-cli.js +111 -24
  4. package/dist/access-cli.js.map +1 -1
  5. package/dist/access-http.d.ts +6 -6
  6. package/dist/access-http.js +12 -12
  7. package/dist/access-mcp.d.ts +14 -6
  8. package/dist/access-mcp.js +11 -11
  9. package/dist/access-operations-batch.js +9 -9
  10. package/dist/access-operations.d.ts +28 -8
  11. package/dist/access-operations.js +14 -10
  12. package/dist/access-schema.d.ts +6 -6
  13. package/dist/{access-service-j1c1gptF.d.ts → access-service-CoIA0NrG.d.ts} +186 -3
  14. package/dist/access-service.d.ts +6 -6
  15. package/dist/access-service.js +7 -7
  16. package/dist/access-surface-catalog.d.ts +6 -6
  17. package/dist/access-surface-catalog.js +7 -0
  18. package/dist/access-surface-catalog.js.map +1 -1
  19. package/dist/action-confidence.d.ts +1 -1
  20. package/dist/active-memory-bridge.d.ts +1 -1
  21. package/dist/active-recall.d.ts +1 -1
  22. package/dist/active-recall.js +1 -1
  23. package/dist/behavior-learner.d.ts +1 -1
  24. package/dist/behavior-signals.d.ts +1 -1
  25. package/dist/bootstrap.d.ts +4 -4
  26. package/dist/briefing.d.ts +1 -1
  27. package/dist/briefing.js +3 -3
  28. package/dist/buffer-surprise-report.d.ts +1 -1
  29. package/dist/buffer.d.ts +1 -1
  30. package/dist/calibration.d.ts +1 -1
  31. package/dist/capabilities.d.ts +1 -1
  32. package/dist/{catalog-CKxilpzS.d.ts → catalog-DQCZrBjw.d.ts} +1 -1
  33. package/dist/causal-behavior.d.ts +1 -1
  34. package/dist/causal-consolidation.d.ts +1 -1
  35. package/dist/causal-consolidation.js +4 -4
  36. package/dist/{chunk-PDJQVAGM.js → chunk-2T4TDXPC.js} +14 -14
  37. package/dist/{chunk-W2WQ4LE7.js → chunk-35YJ6KCV.js} +2 -2
  38. package/dist/{chunk-C63MK3WL.js → chunk-3TCRU4JA.js} +2 -2
  39. package/dist/{chunk-WRGPE6AW.js → chunk-3XHD3XGK.js} +44 -1
  40. package/dist/chunk-3XHD3XGK.js.map +1 -0
  41. package/dist/{chunk-GKI6LX5L.js → chunk-54TI5GLV.js} +2 -2
  42. package/dist/{chunk-IVCQW4C4.js → chunk-7IBEWQLG.js} +2 -2
  43. package/dist/{chunk-Y6YHAGQP.js → chunk-7NDYFAJS.js} +2 -2
  44. package/dist/{chunk-SPETAWFE.js → chunk-AZVHBFI3.js} +2 -2
  45. package/dist/{chunk-MLLTU5FX.js → chunk-BOGENF7P.js} +1 -1
  46. package/dist/chunk-BOGENF7P.js.map +1 -0
  47. package/dist/{chunk-3PTOZJOI.js → chunk-BQCFXAMN.js} +2 -2
  48. package/dist/{chunk-4E2QCH46.js → chunk-CNVIWMQI.js} +2 -2
  49. package/dist/{chunk-4DZATVK5.js → chunk-CXMXAC5R.js} +3 -3
  50. package/dist/{chunk-STYMKPFT.js → chunk-EVX52NCY.js} +1459 -21
  51. package/dist/chunk-EVX52NCY.js.map +1 -0
  52. package/dist/{chunk-W2S3Z5MT.js → chunk-GMRNKPWO.js} +2 -2
  53. package/dist/{chunk-HIV5E57C.js → chunk-HSDJCT3V.js} +2 -2
  54. package/dist/{chunk-NM6TSEGQ.js → chunk-JNOYYWCA.js} +2 -2
  55. package/dist/{chunk-IONFO7UK.js → chunk-JQ7XVM4V.js} +3 -3
  56. package/dist/{chunk-2VQYHHWB.js → chunk-JSDZMOT7.js} +11 -2
  57. package/dist/chunk-JSDZMOT7.js.map +1 -0
  58. package/dist/{chunk-WSWYIKXX.js → chunk-KOEKDZ6A.js} +62 -5
  59. package/dist/chunk-KOEKDZ6A.js.map +1 -0
  60. package/dist/{chunk-WQADZ3ZY.js → chunk-NK3SPJLM.js} +20 -19
  61. package/dist/chunk-NK3SPJLM.js.map +1 -0
  62. package/dist/{chunk-WDH3KUZU.js → chunk-NXL5CVE7.js} +2 -2
  63. package/dist/{chunk-BGAHTI4C.js → chunk-OK7FUX6R.js} +6 -6
  64. package/dist/{chunk-24FGNOQS.js → chunk-P6PRSI3W.js} +85 -4
  65. package/dist/chunk-P6PRSI3W.js.map +1 -0
  66. package/dist/{chunk-66TSLESZ.js → chunk-PKE7EJMX.js} +2 -2
  67. package/dist/chunk-PKE7EJMX.js.map +1 -0
  68. package/dist/{chunk-MOXFPLD6.js → chunk-QXNFQKWU.js} +2 -2
  69. package/dist/{chunk-MRX6ZXHZ.js → chunk-S6FQLQGH.js} +2 -2
  70. package/dist/{chunk-6HPJMR5I.js → chunk-SKQCFAYU.js} +2 -2
  71. package/dist/{chunk-EJDAXR7O.js → chunk-VWB3HDY6.js} +56 -8
  72. package/dist/chunk-VWB3HDY6.js.map +1 -0
  73. package/dist/{chunk-N75N5SNX.js → chunk-XD33EX2F.js} +3 -3
  74. package/dist/{chunk-ESMY4RJ4.js → chunk-XKU4YE6Z.js} +2 -2
  75. package/dist/{chunk-XKMDDM7P.js → chunk-Y6PIFKXX.js} +2 -2
  76. package/dist/{chunk-RH2OSRQY.js → chunk-Z56IHRVV.js} +2 -2
  77. package/dist/{cli-CbT-pyM4.d.ts → cli-qex-L3GT.d.ts} +3 -3
  78. package/dist/cli.d.ts +6 -6
  79. package/dist/cli.js +26 -26
  80. package/dist/compounding/engine.d.ts +1 -1
  81. package/dist/compounding/engine.js +3 -3
  82. package/dist/compounding/preference-consolidator.d.ts +1 -1
  83. package/dist/compression-optimizer.d.ts +1 -1
  84. package/dist/config.d.ts +1 -1
  85. package/dist/config.js +1 -1
  86. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  87. package/dist/connectors/codex-materialize-runner.js +3 -3
  88. package/dist/connectors/codex-materialize.d.ts +1 -1
  89. package/dist/connectors/index.d.ts +1 -1
  90. package/dist/connectors/index.js +3 -3
  91. package/dist/consolidation-provenance-check.d.ts +1 -1
  92. package/dist/consolidation-undo.d.ts +1 -1
  93. package/dist/contradiction/index.d.ts +1 -1
  94. package/dist/conversation-index/backend.d.ts +1 -1
  95. package/dist/conversation-index/chunker.d.ts +1 -1
  96. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  97. package/dist/conversation-index/indexer.d.ts +1 -1
  98. package/dist/conversation-index/search.d.ts +1 -1
  99. package/dist/day-summary.d.ts +1 -1
  100. package/dist/delinearize.d.ts +1 -1
  101. package/dist/direct-answer-wiring.d.ts +1 -1
  102. package/dist/direct-answer.d.ts +1 -1
  103. package/dist/embedding-fallback.d.ts +1 -1
  104. package/dist/enrichment/index.d.ts +1 -1
  105. package/dist/entity-retrieval.d.ts +1 -1
  106. package/dist/entity-retrieval.js +3 -3
  107. package/dist/entity-schema.d.ts +1 -1
  108. package/dist/explicit-capture.d.ts +4 -4
  109. package/dist/extraction-faithfulness.d.ts +1 -1
  110. package/dist/extraction-judge-telemetry.d.ts +1 -1
  111. package/dist/extraction-judge-training.d.ts +1 -1
  112. package/dist/extraction-judge.d.ts +1 -1
  113. package/dist/extraction.d.ts +1 -1
  114. package/dist/fallback-llm.d.ts +1 -1
  115. package/dist/identity-continuity.d.ts +1 -1
  116. package/dist/importance.d.ts +1 -1
  117. package/dist/index.d.ts +9 -9
  118. package/dist/index.js +32 -32
  119. package/dist/intent.d.ts +1 -1
  120. package/dist/lcm/engine.d.ts +1 -1
  121. package/dist/lcm/index.d.ts +1 -1
  122. package/dist/lcm/tools.d.ts +1 -1
  123. package/dist/lifecycle.d.ts +1 -1
  124. package/dist/live-connectors-runner.d.ts +1 -1
  125. package/dist/local-llm.d.ts +1 -1
  126. package/dist/maintenance/memory-governance.d.ts +1 -1
  127. package/dist/maintenance/memory-governance.js +3 -3
  128. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
  129. package/dist/maintenance/rebuild-memory-projection.js +4 -4
  130. package/dist/mcp-memory-inspector-app.d.ts +6 -6
  131. package/dist/memory-action-policy.d.ts +1 -1
  132. package/dist/memory-cache.d.ts +1 -1
  133. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  134. package/dist/memory-projection-store.d.ts +1 -1
  135. package/dist/memory-provenance.d.ts +1 -1
  136. package/dist/memory-worth-outcomes.d.ts +1 -1
  137. package/dist/models-json.d.ts +1 -1
  138. package/dist/namespaces/migrate.d.ts +2 -2
  139. package/dist/namespaces/migrate.js +4 -4
  140. package/dist/namespaces/principal.d.ts +1 -1
  141. package/dist/namespaces/search.d.ts +1 -1
  142. package/dist/namespaces/storage.d.ts +2 -2
  143. package/dist/namespaces/storage.js +3 -3
  144. package/dist/native-knowledge.d.ts +1 -1
  145. package/dist/operator-toolkit.d.ts +1 -1
  146. package/dist/operator-toolkit.js +9 -9
  147. package/dist/orchestration/maintenance.d.ts +2 -2
  148. package/dist/orchestration/maintenance.js +5 -5
  149. package/dist/{orchestrator-D4ovYV3x.d.ts → orchestrator-DsVKLEBk.d.ts} +3 -3
  150. package/dist/orchestrator.d.ts +4 -4
  151. package/dist/orchestrator.js +15 -15
  152. package/dist/patterns-cli.d.ts +1 -1
  153. package/dist/policy-runtime.d.ts +1 -1
  154. package/dist/provenance.d.ts +1 -1
  155. package/dist/qmd-recall-cache.d.ts +1 -1
  156. package/dist/qmd.d.ts +1 -1
  157. package/dist/recall-disclosure-escalation.d.ts +1 -1
  158. package/dist/recall-explain-renderer.d.ts +1 -1
  159. package/dist/recall-explain-renderer.js +3 -3
  160. package/dist/recall-planner-llm.d.ts +1 -1
  161. package/dist/recall-state.d.ts +1 -1
  162. package/dist/recall-tag-filter.d.ts +1 -1
  163. package/dist/recall-xray-cli.d.ts +1 -1
  164. package/dist/recall-xray-cli.js +4 -4
  165. package/dist/recall-xray-renderer.d.ts +1 -1
  166. package/dist/recall-xray-renderer.js +3 -3
  167. package/dist/recall-xray.d.ts +1 -1
  168. package/dist/recall-xray.js +2 -2
  169. package/dist/resolve-auth-token.d.ts +1 -1
  170. package/dist/resume-bundles.js +2 -2
  171. package/dist/retrieval-agents.d.ts +1 -1
  172. package/dist/retrieval-tiers.d.ts +1 -1
  173. package/dist/routing/engine.d.ts +1 -1
  174. package/dist/routing/store.d.ts +1 -1
  175. package/dist/schemas.d.ts +2 -2
  176. package/dist/search/embed-helper.d.ts +1 -1
  177. package/dist/search/factory.d.ts +1 -1
  178. package/dist/search/index.d.ts +1 -1
  179. package/dist/search/lancedb-backend.d.ts +1 -1
  180. package/dist/search/meilisearch-backend.d.ts +1 -1
  181. package/dist/search/noop-backend.d.ts +1 -1
  182. package/dist/search/orama-backend.d.ts +1 -1
  183. package/dist/search/port.d.ts +1 -1
  184. package/dist/search/remote-backend.d.ts +1 -1
  185. package/dist/{semantic-consolidation-DyMUCsfN.d.ts → semantic-consolidation-_hVxkTuF.d.ts} +1 -1
  186. package/dist/semantic-consolidation.d.ts +2 -2
  187. package/dist/semantic-consolidation.js +4 -4
  188. package/dist/semantic-rule-promotion.js +3 -3
  189. package/dist/semantic-rule-verifier.d.ts +1 -1
  190. package/dist/semantic-rule-verifier.js +3 -3
  191. package/dist/session-observer-bands.d.ts +1 -1
  192. package/dist/session-observer-state.d.ts +1 -1
  193. package/dist/shared-context/manager.d.ts +1 -1
  194. package/dist/signal.d.ts +1 -1
  195. package/dist/storage.d.ts +8 -1
  196. package/dist/storage.js +2 -2
  197. package/dist/summarizer.d.ts +1 -1
  198. package/dist/summary-snapshot.d.ts +1 -1
  199. package/dist/temporal-supersession.d.ts +1 -1
  200. package/dist/temporal-validity.d.ts +1 -1
  201. package/dist/threading.d.ts +1 -1
  202. package/dist/tier-migration.d.ts +1 -1
  203. package/dist/tier-routing.d.ts +1 -1
  204. package/dist/topics.d.ts +1 -1
  205. package/dist/transcript.d.ts +1 -1
  206. package/dist/transfer/types.d.ts +12 -12
  207. package/dist/{types-Couvz-L3.d.ts → types-DUK4vVnN.d.ts} +27 -0
  208. package/dist/types.d.ts +1 -1
  209. package/dist/types.js +1 -1
  210. package/dist/utility-runtime.d.ts +1 -1
  211. package/dist/verified-recall.js +3 -3
  212. package/package.json +2 -2
  213. package/src/access-boundary.ts +2 -0
  214. package/src/access-cli.ts +107 -2
  215. package/src/access-http.ts +70 -1
  216. package/src/access-mcp.ts +65 -0
  217. package/src/access-operations.ts +122 -0
  218. package/src/access-service.ts +92 -0
  219. package/src/access-surface-catalog.test.ts +6 -1
  220. package/src/access-surface-catalog.ts +7 -0
  221. package/src/cli.ts +1 -0
  222. package/src/config.test.ts +10 -0
  223. package/src/config.ts +43 -0
  224. package/src/correction/correction-access-wiring.ts +887 -0
  225. package/src/correction/correction-contract.ts +416 -0
  226. package/src/correction/correction-executor.test.ts +742 -0
  227. package/src/correction/correction-executor.ts +473 -0
  228. package/src/correction/correction-planner.test.ts +446 -0
  229. package/src/correction/correction-planner.ts +546 -0
  230. package/src/correction/correction-service.ts +180 -0
  231. package/src/correction/correction-surfaces.test.ts +245 -0
  232. package/src/correction/index.ts +43 -0
  233. package/src/storage.ts +10 -0
  234. package/src/types.ts +27 -0
  235. package/dist/chunk-24FGNOQS.js.map +0 -1
  236. package/dist/chunk-2VQYHHWB.js.map +0 -1
  237. package/dist/chunk-66TSLESZ.js.map +0 -1
  238. package/dist/chunk-EJDAXR7O.js.map +0 -1
  239. package/dist/chunk-MLLTU5FX.js.map +0 -1
  240. package/dist/chunk-STYMKPFT.js.map +0 -1
  241. package/dist/chunk-WQADZ3ZY.js.map +0 -1
  242. package/dist/chunk-WRGPE6AW.js.map +0 -1
  243. package/dist/chunk-WSWYIKXX.js.map +0 -1
  244. /package/dist/{chunk-PDJQVAGM.js.map → chunk-2T4TDXPC.js.map} +0 -0
  245. /package/dist/{chunk-W2WQ4LE7.js.map → chunk-35YJ6KCV.js.map} +0 -0
  246. /package/dist/{chunk-C63MK3WL.js.map → chunk-3TCRU4JA.js.map} +0 -0
  247. /package/dist/{chunk-GKI6LX5L.js.map → chunk-54TI5GLV.js.map} +0 -0
  248. /package/dist/{chunk-IVCQW4C4.js.map → chunk-7IBEWQLG.js.map} +0 -0
  249. /package/dist/{chunk-Y6YHAGQP.js.map → chunk-7NDYFAJS.js.map} +0 -0
  250. /package/dist/{chunk-SPETAWFE.js.map → chunk-AZVHBFI3.js.map} +0 -0
  251. /package/dist/{chunk-3PTOZJOI.js.map → chunk-BQCFXAMN.js.map} +0 -0
  252. /package/dist/{chunk-4E2QCH46.js.map → chunk-CNVIWMQI.js.map} +0 -0
  253. /package/dist/{chunk-4DZATVK5.js.map → chunk-CXMXAC5R.js.map} +0 -0
  254. /package/dist/{chunk-W2S3Z5MT.js.map → chunk-GMRNKPWO.js.map} +0 -0
  255. /package/dist/{chunk-HIV5E57C.js.map → chunk-HSDJCT3V.js.map} +0 -0
  256. /package/dist/{chunk-NM6TSEGQ.js.map → chunk-JNOYYWCA.js.map} +0 -0
  257. /package/dist/{chunk-IONFO7UK.js.map → chunk-JQ7XVM4V.js.map} +0 -0
  258. /package/dist/{chunk-WDH3KUZU.js.map → chunk-NXL5CVE7.js.map} +0 -0
  259. /package/dist/{chunk-BGAHTI4C.js.map → chunk-OK7FUX6R.js.map} +0 -0
  260. /package/dist/{chunk-MOXFPLD6.js.map → chunk-QXNFQKWU.js.map} +0 -0
  261. /package/dist/{chunk-MRX6ZXHZ.js.map → chunk-S6FQLQGH.js.map} +0 -0
  262. /package/dist/{chunk-6HPJMR5I.js.map → chunk-SKQCFAYU.js.map} +0 -0
  263. /package/dist/{chunk-N75N5SNX.js.map → chunk-XD33EX2F.js.map} +0 -0
  264. /package/dist/{chunk-ESMY4RJ4.js.map → chunk-XKU4YE6Z.js.map} +0 -0
  265. /package/dist/{chunk-XKMDDM7P.js.map → chunk-Y6PIFKXX.js.map} +0 -0
  266. /package/dist/{chunk-RH2OSRQY.js.map → chunk-Z56IHRVV.js.map} +0 -0
@@ -8,6 +8,7 @@ import { fileURLToPath, URL } from "node:url";
8
8
  import { gunzipSync } from "node:zlib";
9
9
  import { log } from "./logger.js";
10
10
  import { EngramAccessInputError, type EngramAccessService, type EngramAccessMemoryResponse, type EngramAccessWriteResponse } from "./access-service.js";
11
+ import { CorrectionContractError } from "./correction/correction-contract.js";
11
12
  import { WearablesInputError } from "./wearables/errors.js";
12
13
  import { EngramMcpServer } from "./access-mcp.js";
13
14
  import { validateRequest, type SchemaName, type SchemaTypeFor } from "./access-schema.js";
@@ -319,6 +320,7 @@ export class EngramAccessHttpServer {
319
320
  architectureCardVisible: this.service.architectureCardSurfaceVisible,
320
321
  codegraphVisible: this.service.codegraphSurfaceVisible,
321
322
  sessionDeltaVisible: this.service.sessionDeltaSurfaceVisible,
323
+ correctionVisible: this.service.correctionSurfaceVisible,
322
324
  });
323
325
  }
324
326
 
@@ -343,6 +345,10 @@ export class EngramAccessHttpServer {
343
345
  this.respondJson(res, 400, { error: err.message, code: "input_error" });
344
346
  return;
345
347
  }
348
+ if (err instanceof CorrectionContractError) {
349
+ this.respondJson(res, 400, { error: err.message, code: "correction_contract_error" });
350
+ return;
351
+ }
346
352
  if (res.headersSent) {
347
353
  res.destroy(err as Error);
348
354
  return;
@@ -1432,6 +1438,58 @@ export class EngramAccessHttpServer {
1432
1438
  return;
1433
1439
  }
1434
1440
 
1441
+ // ── Correction Contract (issue #1580) — plan / apply / pending ─────────
1442
+ // All three routes dispatch through the boundary operations so schema
1443
+ // validation + namespace policy reach every correction path (rule 22/39).
1444
+ if (req.method === "POST" && pathname === "/engram/v1/correction/plan") {
1445
+ // Planning persists a pending plan JSON file on every successful call
1446
+ // (review thread UhA), so it is a state write for rate-limit purposes —
1447
+ // mirror the apply route's precheck + accounting to bound files under
1448
+ // state/corrections/pending instead of letting an HTTP client create
1449
+ // unbounded plan files without consuming write quota.
1450
+ this.ensureWriteRateLimitAvailable();
1451
+ const body = await this.readJsonBody(req);
1452
+ const op = getOperation("memory_correct_plan");
1453
+ if (!op) {
1454
+ throw new EngramAccessInputError("access-boundary: operation not registered: memory_correct_plan");
1455
+ }
1456
+ const output = (await op.run(body, {
1457
+ service: this.service,
1458
+ authenticatedPrincipal: this.resolveRequestPrincipal(req),
1459
+ })) as { result: unknown };
1460
+ this.recordWriteRateLimitHit();
1461
+ this.respondJson(res, 200, output.result);
1462
+ return;
1463
+ }
1464
+
1465
+ if (req.method === "POST" && pathname === "/engram/v1/correction/apply") {
1466
+ this.ensureWriteRateLimitAvailable();
1467
+ const body = await this.readJsonBody(req);
1468
+ const op = getOperation("memory_correct_apply");
1469
+ if (!op) {
1470
+ throw new EngramAccessInputError("access-boundary: operation not registered: memory_correct_apply");
1471
+ }
1472
+ const output = (await op.run(body, {
1473
+ service: this.service,
1474
+ authenticatedPrincipal: this.resolveRequestPrincipal(req),
1475
+ })) as { result: unknown };
1476
+ this.recordWriteRateLimitHit();
1477
+ this.respondJson(res, 200, output.result);
1478
+ return;
1479
+ }
1480
+
1481
+ if (req.method === "GET" && pathname === "/engram/v1/correction/pending") {
1482
+ const namespace = parsed.searchParams.get("namespace") ?? undefined;
1483
+ const sessionKey = parsed.searchParams.get("sessionKey") ?? undefined;
1484
+ const plans = await this.service.correctionListPending({
1485
+ ...(namespace ? { namespace } : {}),
1486
+ ...(sessionKey ? { sessionKey } : {}),
1487
+ principal: this.resolveRequestPrincipal(req),
1488
+ });
1489
+ this.respondJson(res, 200, plans);
1490
+ return;
1491
+ }
1492
+
1435
1493
  if (req.method === "POST" && pathname === "/engram/v1/suggestions") {
1436
1494
  void getOperation("suggestion_submit"); // boundary dispatch (issue #1525)
1437
1495
  const body = await this.readValidatedBody(req, "suggestionSubmit");
@@ -2527,6 +2585,12 @@ export class EngramAccessHttpServer {
2527
2585
  toolName === "remnic.memory_action_apply"
2528
2586
  )
2529
2587
  ) ||
2588
+ (
2589
+ toolName === "engram.memory_correct_apply" ||
2590
+ toolName === "remnic.memory_correct_apply" ||
2591
+ toolName === "engram.memory_correct_plan" ||
2592
+ toolName === "remnic.memory_correct_plan"
2593
+ ) ||
2530
2594
  codingDecisionWrite ||
2531
2595
  codingArchitectureWrite ||
2532
2596
  codegraphWrite
@@ -2561,7 +2625,12 @@ export class EngramAccessHttpServer {
2561
2625
  // would consume the write quota despite no mutation occurring
2562
2626
  // (issue #1554 review thread: don't bill rejected calls as writes).
2563
2627
  const isRejectedCodegraph = structured?.ok === false;
2564
- if (!isError && !isRejectedCodegraph && structured && this.shouldCountWriteRateLimit(structured)) {
2628
+ // A write tool that succeeded without structuredContent (e.g.
2629
+ // memory_correct_apply, which returns a CorrectionOutcome) still
2630
+ // consumed a write — count it. Tools WITH structuredContent use the
2631
+ // dryRun/idempotencyReplay guards.
2632
+ const counts = structured ? this.shouldCountWriteRateLimit(structured) : true;
2633
+ if (!isError && !isRejectedCodegraph && counts) {
2565
2634
  this.recordWriteRateLimitHit();
2566
2635
  }
2567
2636
  }
package/src/access-mcp.ts CHANGED
@@ -127,6 +127,9 @@ const MCP_MIGRATED_OPERATIONS: Readonly<Record<string, OperationName>> = {
127
127
  "engram.codegraph_manage_adr": "codegraph_manage_adr",
128
128
  "engram.codegraph_ingest_traces": "codegraph_ingest_traces",
129
129
  "engram.coding_delta": "coding_delta",
130
+ // Correction Contract (issue #1580) — one plan/apply pipeline.
131
+ "engram.memory_correct_plan": "memory_correct_plan",
132
+ "engram.memory_correct_apply": "memory_correct_apply",
130
133
  };
131
134
 
132
135
  function resolveChatGptInspectorRecallSessionKey(
@@ -354,6 +357,13 @@ export class EngramMcpServer {
354
357
  */
355
358
  private readonly codegraphVisible: boolean;
356
359
  private readonly sessionDeltaVisible: boolean;
360
+ /**
361
+ * Whether the two correction tools (memory_correct_plan / memory_correct_apply)
362
+ * should appear in `tools/list` (issue #1580). Gated on `correction.enabled`
363
+ * (default true — plan is read-only; safe on). When false the tools array is
364
+ * byte-identical to pre-feature.
365
+ */
366
+ private readonly correctionVisible: boolean;
357
367
 
358
368
  constructor(
359
369
  private readonly service: EngramAccessService,
@@ -366,6 +376,7 @@ export class EngramMcpServer {
366
376
  architectureCardVisible?: boolean;
367
377
  codegraphVisible?: boolean;
368
378
  sessionDeltaVisible?: boolean;
379
+ correctionVisible?: boolean;
369
380
  } = {},
370
381
  ) {
371
382
  this.citationsEnabled = options.citationsEnabled === true;
@@ -375,6 +386,8 @@ export class EngramMcpServer {
375
386
  this.architectureCardVisible = options.architectureCardVisible === true;
376
387
  this.codegraphVisible = options.codegraphVisible === true;
377
388
  this.sessionDeltaVisible = options.sessionDeltaVisible === true;
389
+ // correction defaults to visible (enabled by default — plan is read-only).
390
+ this.correctionVisible = options.correctionVisible !== false;
378
391
  this.authenticatedPrincipal =
379
392
  options.principal?.trim() ||
380
393
  readEnvVar("OPENCLAW_ENGRAM_ACCESS_PRINCIPAL")?.trim() ||
@@ -2149,6 +2162,58 @@ export class EngramMcpServer {
2149
2162
  );
2150
2163
  this.tools = [...this.tools, ...deltaTools];
2151
2164
  }
2165
+ if (this.correctionVisible) {
2166
+ // Correction Contract (issue #1580) — one plan/apply pipeline for every
2167
+ // memory correction. Both tools dispatch through the boundary operations
2168
+ // (memory_correct_plan / memory_correct_apply) which delegate to the
2169
+ // CorrectionService.
2170
+ const planTool = withToolAliases(
2171
+ {
2172
+ name: "engram.memory_correct_plan",
2173
+ description:
2174
+ "Plan a memory correction from a plain-language statement (issue #1580). Returns a CorrectionPlan with a diff preview; apply via memory_correct_apply.",
2175
+ inputSchema: {
2176
+ type: "object",
2177
+ properties: {
2178
+ text: {
2179
+ type: "string",
2180
+ description: "The natural-language correction (e.g. \"we migrated to MySQL in March\").",
2181
+ },
2182
+ targetIds: {
2183
+ type: "array",
2184
+ items: { type: "string" },
2185
+ description: "Optional explicit target memory ids. When omitted, the planner searches.",
2186
+ },
2187
+ sessionKey: { type: "string", description: "Session identifier for namespace resolution." },
2188
+ namespace: { type: "string", description: "Optional explicit namespace (validated by policy)." },
2189
+ },
2190
+ required: ["text"],
2191
+ additionalProperties: false,
2192
+ },
2193
+ },
2194
+ this.emitLegacyTools,
2195
+ );
2196
+ const applyTool = withToolAliases(
2197
+ {
2198
+ name: "engram.memory_correct_apply",
2199
+ description:
2200
+ "Apply a planned memory correction by planId (issue #1580). Requires confirm: true when correction.applyRequiresConfirm is on (default).",
2201
+ inputSchema: {
2202
+ type: "object",
2203
+ properties: {
2204
+ planId: { type: "string", description: "The plan id returned by memory_correct_plan." },
2205
+ confirm: { type: "boolean", description: "Must be true to apply (safety guard, rule 48)." },
2206
+ sessionKey: { type: "string", description: "Session identifier for namespace resolution." },
2207
+ namespace: { type: "string", description: "Optional explicit namespace (validated by policy)." },
2208
+ },
2209
+ required: ["planId"],
2210
+ additionalProperties: false,
2211
+ },
2212
+ },
2213
+ this.emitLegacyTools,
2214
+ );
2215
+ this.tools = [...this.tools, ...planTool, ...applyTool];
2216
+ }
2152
2217
  }
2153
2218
 
2154
2219
  /** Get clientInfo for a specific MCP session. Returns undefined for non-MCP requests. */
@@ -16,6 +16,8 @@ import { z } from "zod";
16
16
 
17
17
  import { defineOperation } from "./access-boundary.js";
18
18
  import { memoryStoreRequestSchema, type MemoryStoreRequest } from "./access-schema.js";
19
+ import { EngramAccessInputError } from "./access-service.js";
20
+ import { CorrectionContractError } from "./correction/correction-contract.js";
19
21
  import type {
20
22
  EngramAccessMemoryResponse,
21
23
  EngramAccessWriteResponse,
@@ -344,6 +346,124 @@ export const codingDeltaOperation = defineOperation<
344
346
  },
345
347
  });
346
348
 
349
+ // ---------------------------------------------------------------------------
350
+ // memory_correct_plan / memory_correct_apply — Correction Contract (#1580)
351
+ // ---------------------------------------------------------------------------
352
+ //
353
+ // One plan/apply pipeline for every memory correction (supersession,
354
+ // invalidation, tombstone, edit, rescope, redaction). The MCP/HTTP/CLI
355
+ // surfaces all dispatch through these two ops so the boundary's validation +
356
+ // error mapping reach every correction path (rule 22 / 39).
357
+
358
+ const memoryCorrectPlanSchema = z.preprocess(
359
+ (data) => {
360
+ if (data && typeof data === "object" && !Array.isArray(data)) {
361
+ const out: Record<string, unknown> = {};
362
+ for (const [k, v] of Object.entries(data as Record<string, unknown>)) {
363
+ if (v !== null) out[k] = v;
364
+ }
365
+ return out;
366
+ }
367
+ return data;
368
+ },
369
+ z.object({
370
+ text: z.string().trim().min(1).max(10_000),
371
+ targetIds: z.array(z.string().trim().min(1).max(512)).optional(),
372
+ sessionKey: z.string().trim().max(512).optional(),
373
+ namespace: z.string().trim().max(256).optional(),
374
+ }),
375
+ );
376
+
377
+ export interface MemoryCorrectPlanInput {
378
+ text: string;
379
+ targetIds?: string[];
380
+ sessionKey?: string;
381
+ namespace?: string;
382
+ }
383
+
384
+ export interface MemoryCorrectPlanOutput {
385
+ readonly result: unknown; // CorrectionPlan — opaque to the boundary
386
+ }
387
+
388
+ export const memoryCorrectPlanOperation = defineOperation<
389
+ MemoryCorrectPlanInput,
390
+ MemoryCorrectPlanOutput
391
+ >({
392
+ name: "memory_correct_plan",
393
+ description:
394
+ "Plan a memory correction from a plain-language statement (issue #1580). Returns a CorrectionPlan with a diff preview; apply via memory_correct_apply.",
395
+ schema: memoryCorrectPlanSchema as z.ZodType<MemoryCorrectPlanInput>,
396
+ handler: async (input, ctx) => {
397
+ try {
398
+ const result = await ctx.service.correctionPlan({
399
+ text: input.text,
400
+ ...(input.targetIds ? { targetIds: input.targetIds } : {}),
401
+ ...(input.sessionKey ? { sessionKey: input.sessionKey } : {}),
402
+ ...(input.namespace ? { namespace: input.namespace } : {}),
403
+ ...(ctx.authenticatedPrincipal ? { principal: ctx.authenticatedPrincipal } : {}),
404
+ });
405
+ return { result };
406
+ } catch (err) {
407
+ if (err instanceof CorrectionContractError) throw new EngramAccessInputError(err.message);
408
+ throw err;
409
+ }
410
+ },
411
+ });
412
+
413
+ const memoryCorrectApplySchema = z.preprocess(
414
+ (data) => {
415
+ if (data && typeof data === "object" && !Array.isArray(data)) {
416
+ const out: Record<string, unknown> = {};
417
+ for (const [k, v] of Object.entries(data as Record<string, unknown>)) {
418
+ if (v !== null) out[k] = v;
419
+ }
420
+ return out;
421
+ }
422
+ return data;
423
+ },
424
+ z.object({
425
+ planId: z.string().trim().min(1).max(512),
426
+ confirm: z.boolean().optional(),
427
+ sessionKey: z.string().trim().max(512).optional(),
428
+ namespace: z.string().trim().max(256).optional(),
429
+ }),
430
+ );
431
+
432
+ export interface MemoryCorrectApplyInput {
433
+ planId: string;
434
+ confirm?: boolean;
435
+ sessionKey?: string;
436
+ namespace?: string;
437
+ }
438
+
439
+ export interface MemoryCorrectApplyOutput {
440
+ readonly result: unknown; // CorrectionOutcome — opaque to the boundary
441
+ }
442
+
443
+ export const memoryCorrectApplyOperation = defineOperation<
444
+ MemoryCorrectApplyInput,
445
+ MemoryCorrectApplyOutput
446
+ >({
447
+ name: "memory_correct_apply",
448
+ description:
449
+ "Apply a planned memory correction by planId (issue #1580). Requires confirm: true when correction.applyRequiresConfirm is on (default).",
450
+ schema: memoryCorrectApplySchema as z.ZodType<MemoryCorrectApplyInput>,
451
+ handler: async (input, ctx) => {
452
+ try {
453
+ const result = await ctx.service.correctionApply(input.planId, {
454
+ confirm: input.confirm === true,
455
+ ...(input.sessionKey ? { sessionKey: input.sessionKey } : {}),
456
+ ...(input.namespace ? { namespace: input.namespace } : {}),
457
+ ...(ctx.authenticatedPrincipal ? { principal: ctx.authenticatedPrincipal } : {}),
458
+ });
459
+ return { result };
460
+ } catch (err) {
461
+ if (err instanceof CorrectionContractError) throw new EngramAccessInputError(err.message);
462
+ throw err;
463
+ }
464
+ },
465
+ });
466
+
347
467
  // ---------------------------------------------------------------------------
348
468
  // Surface registration map — what each transport calls the pilot ops
349
469
  // ---------------------------------------------------------------------------
@@ -574,4 +694,6 @@ export const REGISTERED_OPERATIONS = [
574
694
  codegraphManageAdrOperation.spec.name,
575
695
  codegraphIngestTracesOperation.spec.name,
576
696
  codingDeltaOperation.spec.name,
697
+ memoryCorrectPlanOperation.spec.name,
698
+ memoryCorrectApplyOperation.spec.name,
577
699
  ] as const;
@@ -49,6 +49,14 @@ import {
49
49
  type DeltaSurfaceStorage,
50
50
  } from "./coding/session-delta-surfaces.js";
51
51
  import { defaultGitInvoker } from "./coding/git-context.js";
52
+ import {
53
+ createCorrectionService,
54
+ isCorrectionFeatureEnabled,
55
+ CorrectionService,
56
+ type CorrectionOutcome,
57
+ type CorrectionPlan,
58
+ type CorrectionRequest,
59
+ } from "./correction/index.js";
52
60
  import { createVersion } from "./page-versioning.js";
53
61
  import { WorkStorage } from "./work/storage.js";
54
62
  import {
@@ -4715,6 +4723,90 @@ export class EngramAccessService {
4715
4723
  });
4716
4724
  }
4717
4725
 
4726
+ // -------------------------------------------------------------------------
4727
+ // Correction Contract (issue #1580) — one plan/apply pipeline for every
4728
+ // memory correction. The CorrectionService owns the planner + executor; the
4729
+ // access-service constructs it lazily via the wiring helper and delegates.
4730
+ // Each method below is thin wiring (≤4 lines) per the god-file ratchet.
4731
+ // -------------------------------------------------------------------------
4732
+ /** Whether the memory_correct_plan / memory_correct_apply tools should appear in tools/list (rule 39). Same reader as the runtime gate so visibility + enforcement stay in sync. */
4733
+ get correctionSurfaceVisible(): boolean {
4734
+ return isCorrectionFeatureEnabled(this.orchestrator.config);
4735
+ }
4736
+
4737
+
4738
+ private _correctionService: CorrectionService | null = null;
4739
+
4740
+ /** Lazily construct + cache the CorrectionService with deps wired from the orchestrator. */
4741
+ private correctionService(): CorrectionService {
4742
+ if (this._correctionService) return this._correctionService;
4743
+ const service = createCorrectionService({
4744
+ orchestrator: this.orchestrator,
4745
+ resolveAuthorizedNamespace: async (req) =>
4746
+ this.resolveWritableNamespace(req.namespace, req.sessionKey, req.principal),
4747
+ resolveReadableNamespaces: (req) => {
4748
+ const principal = this.resolveRequestPrincipal(req.sessionKey, req.principal);
4749
+ return recallNamespacesForPrincipal(principal, this.orchestrator.config);
4750
+ },
4751
+ canWriteNamespace: (req) => {
4752
+ // resolveWritableNamespace resolves + checks writability in one step;
4753
+ // reusing it avoids a second ad-hoc namespace-resolution call site.
4754
+ try {
4755
+ this.resolveWritableNamespace(req.namespace, req.sessionKey, req.principal);
4756
+ return Promise.resolve(true);
4757
+ } catch {
4758
+ return Promise.resolve(false);
4759
+ }
4760
+ },
4761
+ // Wire the orchestrator's extraction LLM so classify+draft actually
4762
+ // drafts actions instead of always hitting the deterministic fallback
4763
+ // (review thread PG5). The planner's classifyAndDraft already falls back
4764
+ // on any LLM outage, so returning null (LLM disabled/cooldown) or a
4765
+ // thrown error both degrade safely to the deterministic path (rule 13).
4766
+ llmComplete: async ({ system, user }) => {
4767
+ const result = await this.orchestrator.localLlm.chatCompletion(
4768
+ [
4769
+ { role: "system", content: system },
4770
+ { role: "user", content: user },
4771
+ ],
4772
+ { operation: "correction-classify", priority: "background" },
4773
+ );
4774
+ if (!result) {
4775
+ throw new Error("correction classify+draft: local LLM unavailable (disabled or in cooldown)");
4776
+ }
4777
+ return result.content;
4778
+ },
4779
+ });
4780
+ this._correctionService = service;
4781
+ return service;
4782
+ }
4783
+
4784
+ async correctionPlan(request: CorrectionRequest): Promise<CorrectionPlan> {
4785
+ return this.correctionService().plan(request);
4786
+ }
4787
+
4788
+ async correctionApply(
4789
+ planId: string,
4790
+ opts: { confirm?: boolean; namespace?: string; sessionKey?: string; principal?: string },
4791
+ ): Promise<CorrectionOutcome> {
4792
+ return this.correctionService().apply(planId, opts);
4793
+ }
4794
+
4795
+ async correctionListPending(opts: {
4796
+ namespace?: string;
4797
+ sessionKey?: string;
4798
+ principal?: string;
4799
+ }): Promise<CorrectionPlan[]> {
4800
+ return this.correctionService().listPending(opts);
4801
+ }
4802
+
4803
+ async correctionDiscard(
4804
+ planId: string,
4805
+ opts: { namespace?: string; sessionKey?: string; principal?: string },
4806
+ ): Promise<void> {
4807
+ return this.correctionService().discard(planId, opts);
4808
+ }
4809
+
4718
4810
  async memoryBrowse(
4719
4811
  request: EngramAccessMemoryBrowseRequest = {},
4720
4812
  ): Promise<EngramAccessMemoryBrowseResponse> {
@@ -44,7 +44,12 @@ import {
44
44
  // catalog-completeness correction: adding routes that were always live but
45
45
  // omitted from the catalog (review-caught). Such a bump MUST be accompanied
46
46
  // by the newly-cataloged entries; the higher count is the honest baseline.
47
- const UNMIGRATED_HANDLER_BASELINE = 80;
47
+ // #1580 Correction Contract adds GET /engram/v1/correction/pending — a read-only
48
+ // list route (namespace/sessionKey query params, no body), so it stays unmigrated
49
+ // alongside the other GET list routes (baseline 80 from #1525 + 1 = 81). Also a
50
+ // catalog-completeness correction: POST /engram/v1/memories (operation memory_store)
51
+ // was always live but omitted from HTTP_ROUTES — it is migrated, so no count change.
52
+ const UNMIGRATED_HANDLER_BASELINE = 81;
48
53
 
49
54
  // Keep the import live — `getOperation` is the call surfaces use at dispatch
50
55
  // time; referencing it here pins the registry's lookup contract.
@@ -72,6 +72,9 @@ export const MCP_TOOLS: readonly McpToolEntry[] = [
72
72
  { tool: "memory_get", operation: "memory_get" },
73
73
  { tool: "memory_timeline", operation: null },
74
74
  { tool: "memory_store", operation: "memory_store" },
75
+ // Correction Contract (issue #1580) — one plan/apply pipeline.
76
+ { tool: "memory_correct_plan", operation: "memory_correct_plan" },
77
+ { tool: "memory_correct_apply", operation: "memory_correct_apply" },
75
78
  { tool: "coding_decision", operation: "coding_decision" },
76
79
  { tool: "coding_architecture", operation: "coding_architecture" },
77
80
  // codegraph parity tools (issue #1554)
@@ -184,6 +187,10 @@ export const HTTP_ROUTES: readonly HttpRouteEntry[] = [
184
187
  { method: "POST", pathname: "/engram/v1/lcm/compaction/flush", operation: "lcm_compaction_flush" },
185
188
  { method: "POST", pathname: "/engram/v1/lcm/compaction/record", operation: "lcm_compaction_record" },
186
189
  { method: "GET", pathname: "/engram/v1/lcm/status", operation: "lcm_status" },
190
+ // Correction Contract (issue #1580) — plan/apply/pending.
191
+ { method: "POST", pathname: "/engram/v1/correction/plan", operation: "memory_correct_plan" },
192
+ { method: "POST", pathname: "/engram/v1/correction/apply", operation: "memory_correct_apply" },
193
+ { method: "GET", pathname: "/engram/v1/correction/pending", operation: null },
187
194
  { method: "POST", pathname: "/engram/v1/memories", operation: "memory_store" },
188
195
  { method: "POST", pathname: "/engram/v1/coding/decisions", operation: "coding_decision" },
189
196
  { method: "POST", pathname: "/engram/v1/coding/architecture", operation: "coding_architecture" },
package/src/cli.ts CHANGED
@@ -2807,6 +2807,7 @@ export async function runAccessMcpServeCliCommand(
2807
2807
  architectureCardVisible: service.architectureCardSurfaceVisible,
2808
2808
  codegraphVisible: service.codegraphSurfaceVisible,
2809
2809
  sessionDeltaVisible: service.sessionDeltaSurfaceVisible,
2810
+ correctionVisible: service.correctionSurfaceVisible,
2810
2811
  });
2811
2812
  await server.runStdio(options.stdin ?? process.stdin, options.stdout ?? process.stdout);
2812
2813
  return { ok: true };
@@ -2082,3 +2082,13 @@ test("parseConfig extractionFaithfulnessTimeoutMs rejects non-numeric and non-in
2082
2082
  );
2083
2083
  }
2084
2084
  });
2085
+
2086
+ test("parseConfig rejects invalid correction.enabled instead of silently enabling (#1580)", () => {
2087
+ assert.throws(() => parseConfig({ correction: { enabled: "flase" } }), /Invalid correction\.enabled/);
2088
+ assert.throws(() => parseConfig({ correctionEnabled: "nope" }), /Invalid correction\.enabled/);
2089
+ assert.throws(() => parseConfig({ correction: { applyRequiresConfirm: "sure" } }), /Invalid correction\.applyRequiresConfirm/);
2090
+ // Valid + absent still resolve correctly (no regression).
2091
+ assert.equal(parseConfig({ correction: { enabled: false } }).correctionEnabled, false);
2092
+ assert.equal(parseConfig({ correctionEnabled: "0" }).correctionEnabled, false);
2093
+ assert.equal(parseConfig({}).correctionEnabled, true);
2094
+ });
package/src/config.ts CHANGED
@@ -2097,6 +2097,49 @@ export function parseConfig(
2097
2097
  // (docs kept honest — review: wire resolver before exposing the gate).
2098
2098
  temporalBiTemporal: coerceBool(cfg.temporalBiTemporal) ?? false,
2099
2099
  temporalExpiredInInjection: coerceBool(cfg.temporalExpiredInInjection) ?? false,
2100
+ // Correction Contract (issue #1580). Parsed here so operators can actually
2101
+ // set the documented toggles/limits (review thread Txp): parseConfig builds
2102
+ // an explicit output, so without these lines the raw `correction` block /
2103
+ // flat legacy keys are dropped and the runtime always sees defaults. Nested
2104
+ // `correction.<key>` wins; flat `correction<Key>` is the legacy fallback.
2105
+ correctionEnabled: (() => {
2106
+ const nested = (cfg.correction as Record<string, unknown> | undefined)?.enabled;
2107
+ const raw = nested !== undefined ? nested : cfg.correctionEnabled;
2108
+ if (raw === undefined || raw === null) return true;
2109
+ const v = coerceBool(raw);
2110
+ if (v === undefined) {
2111
+ throw new Error(`Invalid correction.enabled: expected a boolean, got ${JSON.stringify(raw)}`);
2112
+ }
2113
+ return v;
2114
+ })(),
2115
+ correctionApplyRequiresConfirm: (() => {
2116
+ const nested = (cfg.correction as Record<string, unknown> | undefined)?.applyRequiresConfirm;
2117
+ const raw = nested !== undefined ? nested : cfg.correctionApplyRequiresConfirm;
2118
+ if (raw === undefined || raw === null) return true;
2119
+ const v = coerceBool(raw);
2120
+ if (v === undefined) {
2121
+ throw new Error(`Invalid correction.applyRequiresConfirm: expected a boolean, got ${JSON.stringify(raw)}`);
2122
+ }
2123
+ return v;
2124
+ })(),
2125
+ correctionMaxAffected: (() => {
2126
+ const rawNested = (cfg.correction as Record<string, unknown> | undefined)?.maxAffected;
2127
+ const raw = rawNested !== undefined ? rawNested : cfg.correctionMaxAffected;
2128
+ if (raw === undefined || raw === null) return 10;
2129
+ const n = coerceNumber(raw);
2130
+ if (n === undefined || !Number.isFinite(n) || n < 1) {
2131
+ throw new Error(`Invalid correction.maxAffected: expected an integer >= 1, got ${JSON.stringify(raw)}`);
2132
+ }
2133
+ return Math.floor(n);
2134
+ })(),
2135
+ correctionPlanTtlHours: (() => {
2136
+ const nested = (cfg.correction as Record<string, unknown> | undefined)?.planTtlHours;
2137
+ const fromNested = nested !== undefined ? coerceNumber(nested) : undefined;
2138
+ if (fromNested !== undefined && fromNested > 0) return fromNested;
2139
+ const fromFlat = coerceNumber(cfg.correctionPlanTtlHours);
2140
+ if (fromFlat !== undefined && fromFlat > 0) return fromFlat;
2141
+ return 24;
2142
+ })(),
2100
2143
  // Tombstones — non-resurrection invariant (issue #1579). The invariant is
2101
2144
  // the point, so it ships ON by default; `false` restores pre-feature
2102
2145
  // behavior for rollback safety (rule 30). Uses coerceBool so CLI string