@remnic/core 9.69.61 → 9.69.64
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.
- package/dist/access-admin-ops-surface.js +27 -27
- package/dist/access-authorization-probe.js +28 -28
- package/dist/access-boundary.js +27 -27
- package/dist/access-cli.js +68 -68
- package/dist/access-extraction-force-flush.js +27 -27
- package/dist/access-http-briefing.js +28 -28
- package/dist/access-http-query.js +29 -29
- package/dist/access-http.js +44 -44
- package/dist/access-identity-continuity-surface.js +27 -27
- package/dist/access-lcm-surface.js +27 -27
- package/dist/access-mcp-output-schemas.d.ts.map +1 -1
- package/dist/access-mcp-output-schemas.js +1 -1
- package/dist/access-mcp.js +37 -37
- package/dist/access-namespace-preflight.js +27 -27
- package/dist/access-observe-write-surface.js +27 -27
- package/dist/access-operations-batch.js +28 -28
- package/dist/access-operations.js +35 -35
- package/dist/access-recall-concurrency.js +27 -27
- package/dist/access-recall-response.d.ts.map +1 -1
- package/dist/access-recall-response.js +27 -27
- package/dist/access-recall-surface.js +27 -27
- package/dist/access-service.d.ts +4 -0
- package/dist/access-service.d.ts.map +1 -1
- package/dist/access-service.js +27 -27
- package/dist/access-surface-catalog.js +29 -29
- package/dist/active-recall.js +9 -9
- package/dist/briefing.js +10 -10
- package/dist/buffer.js +5 -5
- package/dist/causal-consolidation.js +11 -11
- package/dist/{chunk-52VL6LCY.js → chunk-2VH2JWYN.js} +2 -2
- package/dist/{chunk-WD32KTVI.js → chunk-2ZVSQPAA.js} +32 -16
- package/dist/chunk-2ZVSQPAA.js.map +1 -0
- package/dist/{chunk-G4WZDQ32.js → chunk-3NG5OOCX.js} +2 -2
- package/dist/{chunk-KG2OOAFF.js → chunk-3NTVCMDD.js} +2 -2
- package/dist/{chunk-7OC3Q5X4.js → chunk-3S4R2HA5.js} +2 -2
- package/dist/{chunk-NORFVW2H.js → chunk-4RWPQZYD.js} +2 -2
- package/dist/{chunk-B5GMOOY2.js → chunk-5AABWM72.js} +3 -3
- package/dist/{chunk-KRPMISWO.js → chunk-5C765XYU.js} +2 -2
- package/dist/{chunk-WLZYGLJ4.js → chunk-5MA7ZLPD.js} +5 -7
- package/dist/chunk-5MA7ZLPD.js.map +1 -0
- package/dist/{chunk-XOLVM3AT.js → chunk-5WXJE5TV.js} +2 -2
- package/dist/{chunk-2C5Z7CWB.js → chunk-6YTL3JYX.js} +2 -2
- package/dist/{chunk-F4PEETM4.js → chunk-755VRFBS.js} +2 -2
- package/dist/{chunk-HNOKNHFO.js → chunk-7L747CFR.js} +2 -2
- package/dist/{chunk-WPGR3RW4.js → chunk-7VO6E5UD.js} +2 -2
- package/dist/{chunk-LMLPLIT4.js → chunk-AQ6IBVFQ.js} +2 -2
- package/dist/{chunk-XN4CPSCN.js → chunk-B2JE56NB.js} +2 -2
- package/dist/{chunk-L4WBADKG.js → chunk-B5EZWPSM.js} +2 -2
- package/dist/{chunk-EIU642TN.js → chunk-BE4GMH45.js} +2 -2
- package/dist/{chunk-NLE6TSLG.js → chunk-BLVHDY2U.js} +5 -7
- package/dist/chunk-BLVHDY2U.js.map +1 -0
- package/dist/{chunk-JTTLH646.js → chunk-C2W6JKQQ.js} +2 -2
- package/dist/{chunk-FG5QU7A2.js → chunk-CFCAFAJC.js} +2 -2
- package/dist/{chunk-ZJ6MEVTX.js → chunk-CZM4MHZF.js} +37 -32
- package/dist/chunk-CZM4MHZF.js.map +1 -0
- package/dist/{chunk-M4PHCEPU.js → chunk-DDY7LJMP.js} +2 -2
- package/dist/{chunk-2L5DIVDU.js → chunk-DT33WNG2.js} +4 -4
- package/dist/{chunk-SZAWZPY6.js → chunk-E7SKAUEW.js} +7 -7
- package/dist/{chunk-NRVGODSE.js → chunk-EK4MOTHM.js} +6 -6
- package/dist/{chunk-QEGKMZNQ.js → chunk-FO5NDQZH.js} +3 -3
- package/dist/chunk-G26YNEOC.js +37 -0
- package/dist/chunk-G26YNEOC.js.map +1 -0
- package/dist/{chunk-GMHWCPLP.js → chunk-GZLSIPYU.js} +2 -2
- package/dist/{chunk-ZFUPRODV.js → chunk-HPWJ5B7D.js} +39 -3
- package/dist/chunk-HPWJ5B7D.js.map +1 -0
- package/dist/{chunk-SEHKTE4F.js → chunk-I4IKTNZP.js} +3 -3
- package/dist/{chunk-W65KK3X6.js → chunk-II4TIQCU.js} +2 -2
- package/dist/{chunk-FWVDTLRK.js → chunk-IJQEVNEF.js} +1 -1
- package/dist/{chunk-KXJ7XYUT.js → chunk-IOUQUXHO.js} +2 -2
- package/dist/{chunk-V3JM4XEI.js → chunk-ITLFUILH.js} +2 -2
- package/dist/{chunk-ZNFHWNDM.js → chunk-IULYVB5X.js} +5 -5
- package/dist/{chunk-Z3YQOV45.js → chunk-JLU7WK22.js} +2 -2
- package/dist/{chunk-NRTVZSPO.js → chunk-L4HT5QZ5.js} +2 -2
- package/dist/{chunk-5QZHDS4X.js → chunk-LI4IWBLN.js} +2 -2
- package/dist/{chunk-LY3SDHMI.js → chunk-LQ4OSM5V.js} +3 -3
- package/dist/{chunk-2OUSWDY6.js → chunk-M3TMVHQ2.js} +2 -2
- package/dist/{chunk-IDRSTHVR.js → chunk-MB3HMRXM.js} +2 -2
- package/dist/{chunk-4P3ZBL5E.js → chunk-MUGLJAZ3.js} +2 -2
- package/dist/{chunk-ZOASYXBS.js → chunk-MY2PLXSM.js} +6 -2
- package/dist/chunk-MY2PLXSM.js.map +1 -0
- package/dist/{chunk-4IFSMCRS.js → chunk-NYN4ZMPL.js} +2 -2
- package/dist/{chunk-K7S2IMGJ.js → chunk-O7QF3TCV.js} +322 -25
- package/dist/{chunk-K7S2IMGJ.js.map → chunk-O7QF3TCV.js.map} +1 -1
- package/dist/{chunk-KV44HTMJ.js → chunk-OBD77TYC.js} +2 -2
- package/dist/{chunk-GYNCJSQ7.js → chunk-OCEIHKH5.js} +3 -3
- package/dist/{chunk-DC5CTLCZ.js → chunk-PSGRZFBH.js} +10 -10
- package/dist/{chunk-FQGY7XB2.js → chunk-S52VKUUW.js} +2 -2
- package/dist/{chunk-D7MAYFQP.js → chunk-S7LWX2PQ.js} +2 -2
- package/dist/{chunk-TYP7GYRI.js → chunk-SB4ZWVKI.js} +2 -2
- package/dist/{chunk-KVO2BTA4.js → chunk-T2YX56QI.js} +2 -2
- package/dist/{chunk-G6NQ3L4T.js → chunk-T3FMRLWQ.js} +2 -2
- package/dist/{chunk-CDQO5DCD.js → chunk-TO3G56GM.js} +2 -2
- package/dist/{chunk-WEQZ5OYK.js → chunk-U6GKPSZH.js} +2 -2
- package/dist/{chunk-FM5HN7LL.js → chunk-WQC54W25.js} +9 -17
- package/dist/chunk-WQC54W25.js.map +1 -0
- package/dist/{chunk-XDH5MO6L.js → chunk-WZ33AIOP.js} +2 -2
- package/dist/{chunk-TM7S5UHD.js → chunk-XDIFADVN.js} +2 -2
- package/dist/{chunk-SVWYZXIL.js → chunk-XDOXR3U6.js} +2 -2
- package/dist/{chunk-VKDWYDFQ.js → chunk-XGLBVYEA.js} +5 -5
- package/dist/{chunk-5Y7VCE3A.js → chunk-XJNSOI23.js} +2 -2
- package/dist/{chunk-LDP6LBDS.js → chunk-Y3AFTYK4.js} +2 -2
- package/dist/{chunk-YL2W6NUG.js → chunk-YGJMMMPN.js} +4 -4
- package/dist/{chunk-KIOXEHWF.js → chunk-YIDBKTZJ.js} +2 -2
- package/dist/{chunk-F5ZCNJ3V.js → chunk-ZPPWW576.js} +2 -2
- package/dist/{chunk-TQAXUYUE.js → chunk-ZSXSMVW6.js} +2 -2
- package/dist/{chunk-6RD7MSIO.js → chunk-ZUT4FK5W.js} +2 -2
- package/dist/cli.js +54 -54
- package/dist/coding/export-okf-codegraph.js +9 -9
- package/dist/compounding/engine.js +10 -10
- package/dist/config.js +9 -9
- package/dist/connectors/codex-materialize-runner.js +10 -10
- package/dist/connectors/index.js +10 -10
- package/dist/convergence-refresh.js +12 -12
- package/dist/corpus-watermark.js +12 -12
- package/dist/entity-retrieval.js +10 -10
- package/dist/external-wiki-access.js +28 -28
- package/dist/extraction.js +16 -16
- package/dist/harmonic-retrieval.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +70 -68
- package/dist/location/index.js +12 -12
- package/dist/maintenance/memory-governance.js +10 -10
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +10 -10
- package/dist/maintenance/rebuild-memory-projection.js +11 -11
- package/dist/memory-browse-operations.js +28 -28
- package/dist/memory-browse.js +5 -5
- package/dist/model-registry.d.ts.map +1 -1
- package/dist/model-registry.js +1 -1
- package/dist/namespaces/migrate.js +11 -11
- package/dist/namespaces/search.js +8 -8
- package/dist/namespaces/storage.js +10 -10
- package/dist/operator-doctor-corpus.js +13 -13
- package/dist/operator-toolkit.js +18 -18
- package/dist/orchestration/maintenance.js +18 -18
- package/dist/orchestration/recall-internal.d.ts.map +1 -1
- package/dist/orchestrator.js +68 -68
- package/dist/profiling.d.ts.map +1 -1
- package/dist/profiling.js +1 -1
- package/dist/qmd.js +2 -2
- package/dist/recall-composition-decision.d.ts +9 -0
- package/dist/recall-composition-decision.d.ts.map +1 -1
- package/dist/recall-composition-decision.js +2 -1
- package/dist/recall-navigation-operations.js +28 -28
- package/dist/recall-navigation.js +27 -27
- package/dist/recall-state-view-admission.js +5 -5
- package/dist/recall-why-service.d.ts.map +1 -1
- package/dist/recall-why-service.js +5 -5
- package/dist/resume-bundles.js +9 -9
- package/dist/search/factory.js +7 -7
- package/dist/search/index.js +7 -7
- package/dist/search/lancedb-backend.js +2 -2
- package/dist/search/meilisearch-backend.js +2 -2
- package/dist/search/orama-backend.js +2 -2
- package/dist/search/port.d.ts +9 -0
- package/dist/search/port.d.ts.map +1 -1
- package/dist/search/port.js +7 -1
- package/dist/search/remote-backend.js +2 -2
- package/dist/security/audit-memory-cli.d.ts.map +1 -1
- package/dist/security/audit-memory.d.ts +10 -0
- package/dist/security/audit-memory.d.ts.map +1 -1
- package/dist/security/injection-screen.d.ts +1 -2
- package/dist/security/injection-screen.d.ts.map +1 -1
- package/dist/semantic-consolidation.js +11 -11
- package/dist/semantic-rule-promotion.js +10 -10
- package/dist/semantic-rule-verifier.js +10 -10
- package/dist/source-agent-qualifier.js +13 -13
- package/dist/standup/build.js +10 -10
- package/dist/storage.js +9 -9
- package/dist/summarizer.js +2 -2
- package/dist/support-passport/index.js +30 -30
- package/dist/transfer/export-okf.js +10 -10
- package/dist/verified-recall.js +10 -10
- package/dist/who-knows-cli.js +11 -11
- package/dist/who-knows.js +10 -10
- package/package.json +12 -2
- package/src/access-mcp-output-schemas.ts +4 -0
- package/src/access-recall-response.ts +26 -1
- package/src/access-service.ts +1 -0
- package/src/index.ts +1 -1
- package/src/model-registry.test.ts +22 -1
- package/src/model-registry.ts +7 -6
- package/src/orchestration/recall-internal.ts +8 -5
- package/src/profiling.test.ts +20 -1
- package/src/profiling.ts +7 -7
- package/src/recall-composition-decision.test.ts +75 -0
- package/src/recall-composition-decision.ts +52 -3
- package/src/recall-retrieval-failure.test.ts +245 -0
- package/src/recall-why-service.ts +8 -24
- package/src/search/port.ts +32 -0
- package/src/security/audit-memory-cli.test.ts +90 -0
- package/src/security/audit-memory-cli.ts +7 -0
- package/src/security/audit-memory.ts +14 -0
- package/src/security/injection-screen.test.ts +245 -0
- package/src/security/injection-screen.ts +341 -11
- package/dist/chunk-FM5HN7LL.js.map +0 -1
- package/dist/chunk-NLE6TSLG.js.map +0 -1
- package/dist/chunk-U2VDCFXW.js +0 -19
- package/dist/chunk-U2VDCFXW.js.map +0 -1
- package/dist/chunk-WD32KTVI.js.map +0 -1
- package/dist/chunk-WLZYGLJ4.js.map +0 -1
- package/dist/chunk-ZFUPRODV.js.map +0 -1
- package/dist/chunk-ZJ6MEVTX.js.map +0 -1
- package/dist/chunk-ZOASYXBS.js.map +0 -1
- /package/dist/{chunk-52VL6LCY.js.map → chunk-2VH2JWYN.js.map} +0 -0
- /package/dist/{chunk-G4WZDQ32.js.map → chunk-3NG5OOCX.js.map} +0 -0
- /package/dist/{chunk-KG2OOAFF.js.map → chunk-3NTVCMDD.js.map} +0 -0
- /package/dist/{chunk-7OC3Q5X4.js.map → chunk-3S4R2HA5.js.map} +0 -0
- /package/dist/{chunk-NORFVW2H.js.map → chunk-4RWPQZYD.js.map} +0 -0
- /package/dist/{chunk-B5GMOOY2.js.map → chunk-5AABWM72.js.map} +0 -0
- /package/dist/{chunk-KRPMISWO.js.map → chunk-5C765XYU.js.map} +0 -0
- /package/dist/{chunk-XOLVM3AT.js.map → chunk-5WXJE5TV.js.map} +0 -0
- /package/dist/{chunk-2C5Z7CWB.js.map → chunk-6YTL3JYX.js.map} +0 -0
- /package/dist/{chunk-F4PEETM4.js.map → chunk-755VRFBS.js.map} +0 -0
- /package/dist/{chunk-HNOKNHFO.js.map → chunk-7L747CFR.js.map} +0 -0
- /package/dist/{chunk-WPGR3RW4.js.map → chunk-7VO6E5UD.js.map} +0 -0
- /package/dist/{chunk-LMLPLIT4.js.map → chunk-AQ6IBVFQ.js.map} +0 -0
- /package/dist/{chunk-XN4CPSCN.js.map → chunk-B2JE56NB.js.map} +0 -0
- /package/dist/{chunk-L4WBADKG.js.map → chunk-B5EZWPSM.js.map} +0 -0
- /package/dist/{chunk-EIU642TN.js.map → chunk-BE4GMH45.js.map} +0 -0
- /package/dist/{chunk-JTTLH646.js.map → chunk-C2W6JKQQ.js.map} +0 -0
- /package/dist/{chunk-FG5QU7A2.js.map → chunk-CFCAFAJC.js.map} +0 -0
- /package/dist/{chunk-M4PHCEPU.js.map → chunk-DDY7LJMP.js.map} +0 -0
- /package/dist/{chunk-2L5DIVDU.js.map → chunk-DT33WNG2.js.map} +0 -0
- /package/dist/{chunk-SZAWZPY6.js.map → chunk-E7SKAUEW.js.map} +0 -0
- /package/dist/{chunk-NRVGODSE.js.map → chunk-EK4MOTHM.js.map} +0 -0
- /package/dist/{chunk-QEGKMZNQ.js.map → chunk-FO5NDQZH.js.map} +0 -0
- /package/dist/{chunk-GMHWCPLP.js.map → chunk-GZLSIPYU.js.map} +0 -0
- /package/dist/{chunk-SEHKTE4F.js.map → chunk-I4IKTNZP.js.map} +0 -0
- /package/dist/{chunk-W65KK3X6.js.map → chunk-II4TIQCU.js.map} +0 -0
- /package/dist/{chunk-FWVDTLRK.js.map → chunk-IJQEVNEF.js.map} +0 -0
- /package/dist/{chunk-KXJ7XYUT.js.map → chunk-IOUQUXHO.js.map} +0 -0
- /package/dist/{chunk-V3JM4XEI.js.map → chunk-ITLFUILH.js.map} +0 -0
- /package/dist/{chunk-ZNFHWNDM.js.map → chunk-IULYVB5X.js.map} +0 -0
- /package/dist/{chunk-Z3YQOV45.js.map → chunk-JLU7WK22.js.map} +0 -0
- /package/dist/{chunk-NRTVZSPO.js.map → chunk-L4HT5QZ5.js.map} +0 -0
- /package/dist/{chunk-5QZHDS4X.js.map → chunk-LI4IWBLN.js.map} +0 -0
- /package/dist/{chunk-LY3SDHMI.js.map → chunk-LQ4OSM5V.js.map} +0 -0
- /package/dist/{chunk-2OUSWDY6.js.map → chunk-M3TMVHQ2.js.map} +0 -0
- /package/dist/{chunk-IDRSTHVR.js.map → chunk-MB3HMRXM.js.map} +0 -0
- /package/dist/{chunk-4P3ZBL5E.js.map → chunk-MUGLJAZ3.js.map} +0 -0
- /package/dist/{chunk-4IFSMCRS.js.map → chunk-NYN4ZMPL.js.map} +0 -0
- /package/dist/{chunk-KV44HTMJ.js.map → chunk-OBD77TYC.js.map} +0 -0
- /package/dist/{chunk-GYNCJSQ7.js.map → chunk-OCEIHKH5.js.map} +0 -0
- /package/dist/{chunk-DC5CTLCZ.js.map → chunk-PSGRZFBH.js.map} +0 -0
- /package/dist/{chunk-FQGY7XB2.js.map → chunk-S52VKUUW.js.map} +0 -0
- /package/dist/{chunk-D7MAYFQP.js.map → chunk-S7LWX2PQ.js.map} +0 -0
- /package/dist/{chunk-TYP7GYRI.js.map → chunk-SB4ZWVKI.js.map} +0 -0
- /package/dist/{chunk-KVO2BTA4.js.map → chunk-T2YX56QI.js.map} +0 -0
- /package/dist/{chunk-G6NQ3L4T.js.map → chunk-T3FMRLWQ.js.map} +0 -0
- /package/dist/{chunk-CDQO5DCD.js.map → chunk-TO3G56GM.js.map} +0 -0
- /package/dist/{chunk-WEQZ5OYK.js.map → chunk-U6GKPSZH.js.map} +0 -0
- /package/dist/{chunk-XDH5MO6L.js.map → chunk-WZ33AIOP.js.map} +0 -0
- /package/dist/{chunk-TM7S5UHD.js.map → chunk-XDIFADVN.js.map} +0 -0
- /package/dist/{chunk-SVWYZXIL.js.map → chunk-XDOXR3U6.js.map} +0 -0
- /package/dist/{chunk-VKDWYDFQ.js.map → chunk-XGLBVYEA.js.map} +0 -0
- /package/dist/{chunk-5Y7VCE3A.js.map → chunk-XJNSOI23.js.map} +0 -0
- /package/dist/{chunk-LDP6LBDS.js.map → chunk-Y3AFTYK4.js.map} +0 -0
- /package/dist/{chunk-YL2W6NUG.js.map → chunk-YGJMMMPN.js.map} +0 -0
- /package/dist/{chunk-KIOXEHWF.js.map → chunk-YIDBKTZJ.js.map} +0 -0
- /package/dist/{chunk-F5ZCNJ3V.js.map → chunk-ZPPWW576.js.map} +0 -0
- /package/dist/{chunk-TQAXUYUE.js.map → chunk-ZSXSMVW6.js.map} +0 -0
- /package/dist/{chunk-6RD7MSIO.js.map → chunk-ZUT4FK5W.js.map} +0 -0
|
@@ -177,14 +177,285 @@ const RESPONSE_CONTROL_PATTERNS: readonly RegExp[] = [
|
|
|
177
177
|
];
|
|
178
178
|
|
|
179
179
|
/**
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
180
|
+
* Corroborating signals that separate an injected response control from
|
|
181
|
+
* ordinary prose about response content (issue #3078). "The API response
|
|
182
|
+
* must include a Content-Type header" is directive-SHAPED but names an
|
|
183
|
+
* ordinary field; an injection additionally carries one of:
|
|
184
|
+
*
|
|
185
|
+
* - an opaque payload to emit: a marker token, a long hex/UUID run, a URL;
|
|
186
|
+
* - verbatim control: literal / exact / word-for-word / "with no explanation";
|
|
187
|
+
* - cross-turn persistence: remember / persist / store this / any later
|
|
188
|
+
* retrieval / across conversations / from now on;
|
|
189
|
+
* - an agent-directed subject: your answer, the assistant's reply.
|
|
190
|
+
*
|
|
191
|
+
* Requiring one of these keeps every frozen attack payload flagged while
|
|
192
|
+
* ordinary technical and policy sentences fall to zero.
|
|
193
|
+
*/
|
|
194
|
+
/**
|
|
195
|
+
* Corroborating cues. Measured trade (PR #3079 r4): these are evaluated over
|
|
196
|
+
* the whole memory, not the directive's sentence. Thirty frozen attack
|
|
197
|
+
* payloads state the directive and name the payload in DIFFERENT sentences,
|
|
198
|
+
* so sentence-local cues lose real detections; the price is that a marker in
|
|
199
|
+
* an unrelated sentence can corroborate an ordinary directive. The
|
|
200
|
+
* emission-slot check below stays sentence-local, where locality is free.
|
|
201
|
+
*/
|
|
202
|
+
const RESPONSE_CONTROL_CORROBORATORS: readonly RegExp[] = [
|
|
203
|
+
/\bCANARY\b/i,
|
|
204
|
+
/https?:\/\//i,
|
|
205
|
+
/\b[0-9a-f]{8,}\b/i,
|
|
206
|
+
/\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\b/,
|
|
207
|
+
// Naming the emission as a phrase/marker/token is a payload cue: ordinary
|
|
208
|
+
// prose names a field ("the request id"), not "the phrase".
|
|
209
|
+
/\b(?:the|this|that|following)[ \t]{1,8}(?:phrase|marker|token|string|sequence|literal|codeword|passphrase)\b/i,
|
|
210
|
+
// Verbatim control over the wording, not the content.
|
|
211
|
+
/\b(?:literal(?:ly)?|verbatim|word[- ]for[- ]word|exactly[ \t]{1,8}(?:this|that|as)|character[- ]for[- ]character)\b/i,
|
|
212
|
+
/\bno[ \t]{1,8}(?:explanation|commentary|preamble|deviation|other[ \t]{1,8}text)\b/i,
|
|
213
|
+
/\b(?:only|nothing[ \t]{1,8}else|and[ \t]{1,8}nothing)\b[^\n]{0,30}\b(?:answers?|responses?|repl(?:y|ies)|output|say|said)\b/i,
|
|
214
|
+
// Persistence across turns: an ordinary fact does not legislate later turns.
|
|
215
|
+
/\b(?:remember|persist|store[ \t]{1,8}this|retain[ \t]{1,8}this)\b/i,
|
|
216
|
+
/\bany[ \t]{1,8}(?:later|future|subsequent)\b/i,
|
|
217
|
+
/\b(?:future|later|subsequent|following|upcoming)[ \t]{1,8}(?:answers?|responses?|repl(?:y|ies)|output|turns?|messages?|conversations?|sessions?)\b/i,
|
|
218
|
+
/\b(?:across|between)[ \t]{1,8}(?:conversations|sessions|chats)\b/i,
|
|
219
|
+
/\b(?:from[ \t]{1,8}now[ \t]{1,8}on|going[ \t]{1,8}forward|in[ \t]{1,8}(?:all|every)[ \t]{1,8}(?:future|later)[ \t]{1,8}(?:turns?|answers?|responses?))\b/i,
|
|
220
|
+
/\b(?:whenever|every[ \t]{1,8}time|each[ \t]{1,8}time|any[ \t]{1,8}time)\b[^\n]{0,60}\b(?:asked|discussed|mentioned|queried|retrieved|recalled)\b/i,
|
|
221
|
+
// The subject is the agent, not a system under discussion.
|
|
222
|
+
/\byour[ \t]{1,8}(?:answers?|responses?|repl(?:y|ies)|output|behaviou?r|memory)\b/i,
|
|
223
|
+
/\b(?:assistant|agent|model)(?:'s)?[ \t]{1,8}(?:answers?|responses?|repl(?:y|ies)|output|behaviou?r)\b/i,
|
|
224
|
+
// A hidden activation phrase is itself the second signal.
|
|
225
|
+
/\b(?:activation|trigger)[ \t]{1,8}(?:phrase|key|word|code)\b/i,
|
|
226
|
+
/\b(?:approved|required|correct|canonical)[ \t]{1,8}(?:response|answer|reply)\b/i,
|
|
227
|
+
];
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Acronyms that appear as ordinary content in technical memories, so an
|
|
232
|
+
* all-caps token alone is not evidence of an injected marker.
|
|
233
|
+
*/
|
|
234
|
+
const ORDINARY_CAPS_TOKENS: ReadonlySet<string> = new Set([
|
|
235
|
+
"API", "HTTP", "HTTPS", "JSON", "XML", "HTML", "CSS", "CSV", "TSV", "YAML", "TOML",
|
|
236
|
+
"SQL", "URL", "URI", "UUID", "ID", "IDS", "UTC", "ISO", "PDF", "PNG", "JPEG", "SVG",
|
|
237
|
+
"TLS", "SSL", "SSH", "DNS", "TCP", "UDP", "IP", "CDN", "CORS", "CSRF", "JWT", "OAUTH",
|
|
238
|
+
"REST", "GRPC", "RPC", "SDK", "CLI", "GUI", "OK", "YES", "NO", "TODO", "FAQ", "PR",
|
|
239
|
+
"CI", "CD", "QA", "UX", "UI", "MIT", "GPL", "EU", "US", "AM", "PM", "FYI", "ETA",
|
|
240
|
+
// Crypto, encoding, and spec vocabulary (trailing digits are stripped
|
|
241
|
+
// before this lookup, so SHA256/SHA-256/UTF8/ISO8601/RFC3339 all reduce).
|
|
242
|
+
"SHA", "MD", "HMAC", "AES", "RSA", "ECDSA", "PBKDF", "BCRYPT", "SCRYPT", "ARGON",
|
|
243
|
+
"UTF", "ASCII", "BASE", "HEX", "CRC", "GZIP", "ZSTD", "RFC", "ANSI", "IEEE",
|
|
244
|
+
"SMTP", "IMAP", "LDAP", "SAML", "OIDC", "SSO", "MFA", "TOTP", "ACL", "RBAC",
|
|
245
|
+
"AWS", "GCP", "GPU", "CPU", "RAM", "SSD", "OS", "VM", "K8S", "NPM", "SEMVER",
|
|
246
|
+
"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "TRACE",
|
|
247
|
+
]);
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Capitalized words that open ordinary templates ("replies must begin with
|
|
251
|
+
* Dear"), so a leading capital alone is not a marker (PR #3079 r3).
|
|
252
|
+
*/
|
|
253
|
+
const ORDINARY_CAPITALIZED_WORDS: ReadonlySet<string> = new Set([
|
|
254
|
+
"Dear", "Hello", "Hi", "Greetings", "Please", "Thanks", "Thank", "Regards",
|
|
255
|
+
"Sincerely", "Yes", "No", "True", "False", "Null", "None", "Error", "Warning",
|
|
256
|
+
"Note", "Summary", "Subject", "Re", "Fwd", "Attention", "Notice", "Draft",
|
|
257
|
+
"Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday",
|
|
258
|
+
"January", "February", "March", "April", "May", "June", "July", "August",
|
|
259
|
+
"September", "October", "November", "December",
|
|
260
|
+
]);
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* An opaque token the memory wants echoed verbatim, in the emission slot of
|
|
264
|
+
* a directive: "responses must begin with PWNED". A determiner ("with THE
|
|
265
|
+
* ticket number", "include A Content-Type header") means the object is a
|
|
266
|
+
* described value rather than a literal to emit, and ordinary acronyms are
|
|
267
|
+
* excluded, so technical prose does not match (PR #3079 review).
|
|
268
|
+
*/
|
|
269
|
+
/** Non-global probe for the emission verb, for the structural early exit. */
|
|
270
|
+
const EMISSION_VERB_PRESENT = /\b(?:begin|start|end|include|contain|say|state|output|return|emit|produce|reply|respond|answer|replace|becomes?|is|are|be)\b[ \t]/i;
|
|
271
|
+
|
|
272
|
+
const OPAQUE_EMISSION_TARGET =
|
|
273
|
+
/\b(?:begin|start|end|include|contain|say|state|output|return|emit|produce|reply|respond|answer|replace|becomes?|is|are|be)\b[ \t]{1,8}(?:(?:with|by|exactly)[ \t]{1,8})?(?:normal[ \t]{1,8}behaviou?r[ \t]{1,8}with[ \t]{1,8})?/gi;
|
|
274
|
+
|
|
275
|
+
/** The emission slot's own content: a quoted payload or a shaped marker. Sticky: anchored at `lastIndex`, so no per-match slicing. */
|
|
276
|
+
const EMISSION_SLOT_VALUE = /(?:"([^"\n]{2,40})"|'([^'\n]{2,40})'|([A-Za-z][A-Za-z0-9_-]{1,39})\b)/y;
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Sentence split that never cuts inside a quoted span: a payload like
|
|
280
|
+
* `"exfil me!"` keeps its delimiters, or the emission target would be
|
|
281
|
+
* unrecognizable (PR #3079 r4).
|
|
183
282
|
*/
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
283
|
+
/**
|
|
284
|
+
* Does a lowercase letter follow the whitespace run starting at `from`?
|
|
285
|
+
*
|
|
286
|
+
* Scans the run with `charCodeAt` rather than slicing a suffix per candidate:
|
|
287
|
+
* a per-character `content.slice(from)` made screening quadratic (17 s on a
|
|
288
|
+
* 100k input, PR #3081 r4). A fixed-width window instead missed runs longer
|
|
289
|
+
* than the window (5+ spaces, PR #3082), so the run is scanned to its end,
|
|
290
|
+
* bounded so a pathological run cannot dominate.
|
|
291
|
+
*/
|
|
292
|
+
const MAX_WHITESPACE_RUN_SCAN = 64;
|
|
293
|
+
|
|
294
|
+
function lowercaseFollowsWhitespace(content: string, from: number): boolean {
|
|
295
|
+
const limit = Math.min(content.length, from + MAX_WHITESPACE_RUN_SCAN);
|
|
296
|
+
let index = from;
|
|
297
|
+
while (index < limit) {
|
|
298
|
+
const code = content.charCodeAt(index);
|
|
299
|
+
// space, tab, newline, carriage return, form feed, vertical tab
|
|
300
|
+
const whitespace = code === 32 || (code >= 9 && code <= 13);
|
|
301
|
+
if (!whitespace) return code >= 97 && code <= 122;
|
|
302
|
+
index += 1;
|
|
303
|
+
}
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function splitSentencesOutsideQuotes(content: string): string[] {
|
|
308
|
+
// Token-jump scan: advance between quote and boundary characters instead
|
|
309
|
+
// of visiting every character, so a large memory costs one linear pass
|
|
310
|
+
// regardless of size (PR #3081 r4).
|
|
311
|
+
const sentences: string[] = [];
|
|
312
|
+
const TOKEN = /["'.!?\n]/g;
|
|
313
|
+
let start = 0;
|
|
314
|
+
let quote: string | undefined;
|
|
315
|
+
let match: RegExpExecArray | null;
|
|
316
|
+
while ((match = TOKEN.exec(content)) !== null) {
|
|
317
|
+
const char = match[0];
|
|
318
|
+
const index = match.index;
|
|
319
|
+
if (quote !== undefined) {
|
|
320
|
+
if (char === quote) quote = undefined;
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
if (char === '"' || (char === "'" && !/[A-Za-z0-9]/.test(content[index - 1] ?? " "))) {
|
|
324
|
+
quote = char;
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
const next = content[index + 1];
|
|
328
|
+
// Token-internal only applies to sentence punctuation: a `.!?` glued to
|
|
329
|
+
// a non-whitespace character is part of a token (URL, decimal). A
|
|
330
|
+
// NEWLINE is never token-internal -- an ordinary next line starts with
|
|
331
|
+
// text, and only the explicit colon/list continuation joins lines (PR
|
|
332
|
+
// #3081 post-cap).
|
|
333
|
+
const insideToken = char !== "\n" && next !== undefined && !/\s/.test(next);
|
|
334
|
+
// Terminal URL punctuation ("payload? to", "payload! to") binds the
|
|
335
|
+
// unit ONLY when a URL actually precedes it: lowercase prose after a
|
|
336
|
+
// question mark is ordinary ("...header? canary deployments...") and
|
|
337
|
+
// must stay a separate unit (post-cap r8).
|
|
338
|
+
const terminalUrlPunctuation = (char === "?" || char === "!")
|
|
339
|
+
&& lowercaseFollowsWhitespace(content, index + 1)
|
|
340
|
+
&& /:\/\/[^\s]*$/.test(content.slice(Math.max(0, index - 300), index));
|
|
341
|
+
const newlineContinues = char === "\n" && /[:\-][ \t]*$/.test(content.slice(Math.max(0, index - 4), index));
|
|
342
|
+
const endsUnit = !insideToken && !terminalUrlPunctuation && !newlineContinues
|
|
343
|
+
&& (char === "." || char === "!" || char === "?" || char === "\n");
|
|
344
|
+
if (endsUnit) {
|
|
345
|
+
if (index > start) sentences.push(content.slice(start, index));
|
|
346
|
+
start = index + 1;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (start < content.length) sentences.push(content.slice(start));
|
|
350
|
+
return sentences;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** Strip trailing digits so SHA256, UTF8, and ISO8601 reduce to their acronym. */
|
|
354
|
+
function reduceToken(token: string): string {
|
|
355
|
+
return token.replace(/[0-9]+$/, "");
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** Is one emission-slot token an ordinary described value rather than a payload? */
|
|
359
|
+
function isOrdinaryToken(token: string): boolean {
|
|
360
|
+
const segments = token.split(/[-_]+/).filter((segment) => segment.length > 0);
|
|
361
|
+
if (segments.length === 0) return true;
|
|
362
|
+
// A hyphenated Title-Case name is a field or header ("Content-Type",
|
|
363
|
+
// "Cache-Control"), not a marker: markers are all-caps or carry digits.
|
|
364
|
+
if (segments.length > 1 && segments.every((segment) => /^[A-Z][a-z]+$/.test(segment))) return true;
|
|
365
|
+
return segments.every((segment) => {
|
|
366
|
+
if (/^[0-9]+$/.test(segment)) return true;
|
|
367
|
+
const reduced = reduceToken(segment);
|
|
368
|
+
if (ORDINARY_CAPS_TOKENS.has(segment) || ORDINARY_CAPS_TOKENS.has(reduced)) return true;
|
|
369
|
+
return ORDINARY_CAPITALIZED_WORDS.has(segment);
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* An opaque token the memory wants echoed verbatim, in the emission slot of
|
|
375
|
+
* a directive IN THE SAME SENTENCE: "responses must begin with PWNED". A
|
|
376
|
+
* determiner ("with THE ticket number", "include A Content-Type header")
|
|
377
|
+
* means the object is a described value; ordinary acronyms, spec vocabulary,
|
|
378
|
+
* and template openers are excluded, quoted or not, so technical prose does
|
|
379
|
+
* not match (PR #3079 r2/r3).
|
|
380
|
+
*/
|
|
381
|
+
function hasOpaqueEmissionTarget(sentence: string): boolean {
|
|
382
|
+
// The verb is matched case-insensitively ("must BEGIN with"), the marker
|
|
383
|
+
// case-sensitively: an all-lowercase word in the slot is ordinary prose
|
|
384
|
+
// and cannot be told from a marker (PR #3079 r4).
|
|
385
|
+
// Every verb occurrence is tried: the emission verb is not always the
|
|
386
|
+
// first one in the sentence ("The ANSWER must INCLUDE ...", r4).
|
|
387
|
+
OPAQUE_EMISSION_TARGET.lastIndex = 0;
|
|
388
|
+
for (
|
|
389
|
+
let verb = OPAQUE_EMISSION_TARGET.exec(sentence);
|
|
390
|
+
verb !== null;
|
|
391
|
+
verb = OPAQUE_EMISSION_TARGET.exec(sentence)
|
|
392
|
+
) {
|
|
393
|
+
EMISSION_SLOT_VALUE.lastIndex = verb.index + verb[0].length;
|
|
394
|
+
const match = EMISSION_SLOT_VALUE.exec(sentence);
|
|
395
|
+
if (!match) continue;
|
|
396
|
+
const quoted = match[1] ?? match[2];
|
|
397
|
+
if (quoted !== undefined) {
|
|
398
|
+
const inner = quoted.trim();
|
|
399
|
+
// A quoted multi-word payload is opaque; a single quoted technical
|
|
400
|
+
// value ("OK", "Content-Type") is the same described value as unquoted.
|
|
401
|
+
if (!/\s/.test(inner) && isOrdinaryToken(inner)) continue;
|
|
402
|
+
return true;
|
|
403
|
+
}
|
|
404
|
+
const token = match[3];
|
|
405
|
+
if (token === undefined) continue;
|
|
406
|
+
// Uppercase-initial tokens are judged by shape (marker vs acronym or
|
|
407
|
+
// field name). Lowercase words are NOT admitted as markers: ordinary
|
|
408
|
+
// English is open-ended ("must include citations"), so absence from a
|
|
409
|
+
// finite wordlist is not positive marker evidence (#3080, PR #3081 r1).
|
|
410
|
+
if (/^[a-z]/.test(token)) continue;
|
|
411
|
+
if (!isOrdinaryToken(token)) return true;
|
|
412
|
+
}
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* A directive over what the agent's future answer must contain or say,
|
|
418
|
+
* corroborated by a second signal. Ordinary facts that merely pair
|
|
419
|
+
* "policy"/"note"/"response" with a common verb ("the return policy requires
|
|
420
|
+
* a receipt", "the API response must include a Content-Type header") are not
|
|
421
|
+
* directives and are not flagged.
|
|
422
|
+
*/
|
|
423
|
+
function findResponseControlDirective(
|
|
424
|
+
content: string,
|
|
425
|
+
profile: InjectionScreenProfile = "default",
|
|
426
|
+
): InjectionScreenFinding | undefined {
|
|
427
|
+
const directive = RESPONSE_CONTROL_PATTERNS.map((pattern) =>
|
|
428
|
+
findingFor("response-control-directive", content, pattern),
|
|
429
|
+
).find((finding) => finding !== undefined);
|
|
430
|
+
if (!directive) return undefined;
|
|
431
|
+
// `hardened` is the profile an operator opts into for strict security: it
|
|
432
|
+
// keeps the pre-corroboration behavior, so no directive shape can be
|
|
433
|
+
// talked past the gate there. `default` (custom mode) requires a second
|
|
434
|
+
// signal, which is what keeps ordinary response prose out of quarantine.
|
|
435
|
+
if (profile === "hardened") return directive;
|
|
436
|
+
// Cue scope is a measured trade; see RESPONSE_CONTROL_CORROBORATORS.
|
|
437
|
+
// Cheap structural exit: if the whole memory carries no corroborator and
|
|
438
|
+
// no emission verb, locality cannot produce a finding, and the splitter's
|
|
439
|
+
// per-character scan is skipped entirely (large ordinary memories never
|
|
440
|
+
// pay for it) — PR #3081 r4.
|
|
441
|
+
const anyCorroborator = RESPONSE_CONTROL_CORROBORATORS.some((pattern) => pattern.test(content));
|
|
442
|
+
const anyEmissionVerb = !anyCorroborator
|
|
443
|
+
? EMISSION_VERB_PRESENT.test(content)
|
|
444
|
+
: true;
|
|
445
|
+
if (!anyCorroborator && !anyEmissionVerb) return undefined;
|
|
446
|
+
// Corroborators are sentence-local now that sentence splitting no longer
|
|
447
|
+
// breaks URLs: a cue in an unrelated sentence ("The fixture value is
|
|
448
|
+
// CANARY.") cannot corroborate an ordinary directive, while a payload the
|
|
449
|
+
// directive sentence itself names ("...add https://... to answers...")
|
|
450
|
+
// still does (#3080).
|
|
451
|
+
// Cue locality: a corroborator must sit in the directive's own sentence
|
|
452
|
+
// (continuation lines included). The emission slot needs no splitting --
|
|
453
|
+
// the verb and its slot are adjacent by construction, so that check is
|
|
454
|
+
// intrinsically local and runs over the whole content (PR #3081 r4).
|
|
455
|
+
for (const sentence of splitSentencesOutsideQuotes(content)) {
|
|
456
|
+
if (!RESPONSE_CONTROL_PATTERNS.some((pattern) => pattern.test(sentence))) continue;
|
|
457
|
+
if (anyCorroborator && RESPONSE_CONTROL_CORROBORATORS.some((pattern) => pattern.test(sentence))) return directive;
|
|
458
|
+
if (hasOpaqueEmissionTarget(sentence)) return directive;
|
|
188
459
|
}
|
|
189
460
|
return undefined;
|
|
190
461
|
}
|
|
@@ -210,9 +481,61 @@ function findAuthorityEscalation(content: string): InjectionScreenFinding | unde
|
|
|
210
481
|
}
|
|
211
482
|
|
|
212
483
|
/** Screen a candidate fact without model calls, I/O, or mutable state. */
|
|
213
|
-
|
|
484
|
+
/**
|
|
485
|
+
* Normalize whitespace and invisible formatting before screening.
|
|
486
|
+
*
|
|
487
|
+
* Two evasion families, both attacker-only (ordinary prose carries neither):
|
|
488
|
+
*
|
|
489
|
+
* 1. The rule patterns bound their inter-word whitespace (a ReDoS-safety
|
|
490
|
+
* requirement, PR #3079 r4), so a long run of spaces split a directive
|
|
491
|
+
* from its corroborator: `must include<40 spaces>CANARY` screened clean
|
|
492
|
+
* while the 1-space form quarantined. ASCII runs collapse to one space.
|
|
493
|
+
* 2. The patterns match ASCII space and tab, so ANY other space-like
|
|
494
|
+
* separator defeated them as a SINGLETON -- `The response must\u00a0include
|
|
495
|
+
* CANARY.` screened clean, and the same held for U+2000-U+200A, U+202F,
|
|
496
|
+
* U+205F, U+3000, VT, FF, U+0085, and U+2028 (PR #3094 r1). Each
|
|
497
|
+
* occurrence becomes a plain space, singletons included.
|
|
498
|
+
*
|
|
499
|
+
* The vertical members (VT, FF, U+0085, U+2028) are folded to a space
|
|
500
|
+
* deliberately, NOT treated as locality boundaries like `\n`: only `\n`
|
|
501
|
+
* appears in ordinary prose, so honoring the others as boundaries would
|
|
502
|
+
* hand an attacker a one-character directive split that the screen then
|
|
503
|
+
* refuses to read as one unit (PR #3094 r2, declined behavior change).
|
|
504
|
+
*
|
|
505
|
+
* Zero-width and directionality characters are ambiguous: inside a word
|
|
506
|
+
* `inc\u200blude` reads as `include`, so they must be DELETED, but between
|
|
507
|
+
* words `must\u200binclude` reads as two words, so they must become a SPACE.
|
|
508
|
+
* Telling those apart needs a lexicon, so both readings are screened and the
|
|
509
|
+
* stronger result wins -- an attacker gains nothing from either placement.
|
|
510
|
+
*
|
|
511
|
+
* Newlines survive every pass -- the continuation rule reads them as
|
|
512
|
+
* locality boundaries.
|
|
513
|
+
*/
|
|
514
|
+
const INVISIBLE_FORMATTING = /[\u00ad\u200b-\u200f\u2060\u2028\u2029\ufeff]/g;
|
|
515
|
+
const NON_ASCII_SPACE_LIKE = /[\f\v\u0085\u1680\u00a0\u2000-\u200a\u202f\u205f\u3000]/g;
|
|
516
|
+
const ASCII_SPACE_RUN = /[ \t]{2,}/g;
|
|
517
|
+
|
|
518
|
+
function normalizeForScreening(content: string): readonly string[] {
|
|
519
|
+
const spaced = content
|
|
520
|
+
.replace(NON_ASCII_SPACE_LIKE, " ")
|
|
521
|
+
.replace(INVISIBLE_FORMATTING, " ")
|
|
522
|
+
.replace(ASCII_SPACE_RUN, " ");
|
|
523
|
+
const deleted = content
|
|
524
|
+
.replace(INVISIBLE_FORMATTING, "")
|
|
525
|
+
.replace(NON_ASCII_SPACE_LIKE, " ")
|
|
526
|
+
.replace(ASCII_SPACE_RUN, " ");
|
|
527
|
+
return spaced === deleted ? [spaced] : [spaced, deleted];
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
export function screenCandidateFact(
|
|
531
|
+
rawContent: string,
|
|
532
|
+
profile: InjectionScreenProfile = "default",
|
|
533
|
+
): InjectionScreenResult {
|
|
534
|
+
const variants = normalizeForScreening(rawContent);
|
|
214
535
|
const findings: InjectionScreenFinding[] = [];
|
|
215
|
-
const rules: Array<
|
|
536
|
+
const rules: Array<
|
|
537
|
+
[string, (value: string, screenProfile: InjectionScreenProfile) => InjectionScreenFinding | undefined]
|
|
538
|
+
> = [
|
|
216
539
|
["imperative-to-agent", findImperativeToAgent],
|
|
217
540
|
["tool-invocation-syntax", findToolInvocationSyntax],
|
|
218
541
|
["encoded-blob", findEncodedBlob],
|
|
@@ -222,9 +545,16 @@ export function screenCandidateFact(content: string, profile: InjectionScreenPro
|
|
|
222
545
|
["response-control-directive", findResponseControlDirective],
|
|
223
546
|
["tool-routing-directive", findToolRoutingDirective],
|
|
224
547
|
];
|
|
548
|
+
// A rule fires if it fires under EITHER reading of the invisible
|
|
549
|
+
// characters (see normalizeForScreening); the first hit wins its excerpt.
|
|
225
550
|
for (const [rule, find] of rules) {
|
|
226
|
-
const
|
|
227
|
-
|
|
551
|
+
for (const content of variants) {
|
|
552
|
+
const finding = find(content, profile);
|
|
553
|
+
if (finding) {
|
|
554
|
+
findings.push({ rule, excerpt: finding.excerpt });
|
|
555
|
+
break;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
228
558
|
}
|
|
229
559
|
const score = findings.reduce((sum, finding) => sum + ruleWeight(finding.rule, profile), 0);
|
|
230
560
|
return { score, findings, quarantine: score >= INJECTION_SCREEN_THRESHOLD };
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/recall-why-service.ts"],"sourcesContent":["/**\n * Access-surface binding for the recall-miss diagnosis (issue #3033).\n *\n * `recall-why.ts` holds the stage logic and a port; this module binds that\n * port to the live orchestrator. It lives beside `access-recall-surface.ts`\n * rather than inside it because that file is at its file-size ratchet\n * ceiling (AGENTS.md: extract the addition to a sibling module).\n *\n * Namespace scope is enforced BEFORE the recall fires and the expected-memory\n * lookup is confined to namespaces the caller may read, so the diagnosis can\n * never confirm the existence of a memory in a namespace the caller has no\n * read access to. Read path and write path resolve through the same namespace\n * layer (Review Prevention Checklist #30): the lookup consumes exactly the\n * `recallNamespacesForPrincipal` set the recall itself is scoped by.\n */\n\nimport { resolveNamespaceCapabilities, resolveQmdCapabilities } from \"./capabilities.js\";\nimport { planRecallMode } from \"./intent.js\";\nimport { canReadNamespace, recallNamespacesForPrincipal, resolvePrincipal } from \"./namespaces/principal.js\";\nimport { isGenericRecallExcludedPath } from \"./orchestration/generic-recall-paths.js\";\nimport type { Orchestrator } from \"./orchestrator.js\";\nimport {\n explainRecallMiss,\n RecallWhyInputError,\n type RecallWhyDeps,\n type RecallWhyMemoryRef,\n type RecallWhyReport,\n} from \"./recall-why.js\";\nimport { summarizeRecallWhy } from \"./recall-why-renderer.js\";\nimport type { SearchDegradation } from \"./search/port.js\";\nimport type { MemoryFile, MemoryStatus } from \"./types.js\";\n\n/**\n * Degradation codes that mean the search backend could NOT answer, so an\n * empty pipeline carries no information. `vector_tier_unavailable` is\n * excluded on purpose: the lexical tier still answered, so the result set\n * is degraded but real.\n */\nconst FATAL_DEGRADATION_CODES: Partial<Record<SearchDegradation[\"code\"], true>> = {\n backend_unavailable: true,\n backend_error: true,\n daemon_timeout: true,\n daemon_loading: true,\n subprocess_error: true,\n deadline_exceeded: true,\n remote_error: true,\n};\n\nfunction describeDegradations(degradations: readonly SearchDegradation[]): string {\n return degradations.map((d) => `${d.backend}:${d.code}${d.detail !== undefined ? ` (${d.detail})` : \"\"}`).join(\"; \");\n}\n\nexport interface RecallWhyRequest {\n query: string;\n /** Memory id or substring to trace through every stage. */\n expect?: string;\n sessionKey?: string;\n namespace?: string;\n authenticatedPrincipal?: string;\n sourceConnector?: string;\n abortSignal?: AbortSignal;\n}\n\nexport interface RecallWhyResponse {\n /** False when the namespace ACL denied the diagnosis. */\n reportFound: boolean;\n report?: RecallWhyReport;\n /** One-line verdict, for agent callers that do not parse the full report. */\n summary?: string;\n}\n\nexport interface RecallWhyServiceDeps {\n readonly orchestrator: Orchestrator;\n /** The surface's shared namespace resolver. */\n resolveNamespace(namespace?: string): string;\n}\n\n/**\n * Run the recall-miss diagnosis for one request.\n *\n * Throws {@link RecallWhyInputError} for caller-input faults so surfaces can\n * map them to a 400; backend faults are reported inside the report as\n * `backend_unavailable` rather than thrown, because an outage is a diagnosis\n * result, not a diagnosis failure.\n */\nexport async function runRecallWhy(deps: RecallWhyServiceDeps, request: RecallWhyRequest): Promise<RecallWhyResponse> {\n const query = typeof request.query === \"string\" ? request.query : \"\";\n if (query.trim().length === 0) {\n throw new RecallWhyInputError(\"recallWhy: query is required and must be non-empty\");\n }\n const orchestrator = deps.orchestrator;\n const config = orchestrator.config;\n const namespacesEnabled = resolveNamespaceCapabilities(config).namespaces;\n const requestedNamespace = request.namespace?.trim() ? deps.resolveNamespace(request.namespace) : undefined;\n const authenticatedPrincipal = request.authenticatedPrincipal?.trim();\n const principal = authenticatedPrincipal || resolvePrincipal(request.sessionKey, config);\n\n if (requestedNamespace !== undefined) {\n if (!canReadNamespace(principal, requestedNamespace, config)) {\n return { reportFound: false };\n }\n } else if (namespacesEnabled && !authenticatedPrincipal && !request.sessionKey?.trim()) {\n // Namespaces on but no identity supplied: refuse rather than diagnose\n // against the global namespace (least-privileged default).\n return { reportFound: false };\n }\n\n const readableNamespaces =\n requestedNamespace !== undefined ? [requestedNamespace] : [...recallNamespacesForPrincipal(principal, config)];\n const recallSessionKey = request.sessionKey?.trim() || undefined;\n\n const whyDeps: RecallWhyDeps = {\n plannerMode: (prompt) => planRecallMode(prompt),\n scopedNamespaces: readableNamespaces,\n namespacesEnabled,\n isExcludedPath: (memoryPath) => isGenericRecallExcludedPath(memoryPath, config),\n runRecall: async (recallQuery, options) => {\n // A configured-but-unreachable search backend is an OUTAGE, and must\n // never render as \"0 candidates\" (Review Prevention Checklist #22).\n // This is the same pair of expressions the pipeline emits as\n // `qmdEnabled` / `qmdAvailable` on its recall_summary trace.\n if (resolveQmdCapabilities(config).qmd && !orchestrator.qmd.isAvailable()) {\n return {\n ok: false,\n reason: \"backend_unavailable\",\n detail: \"the search backend is enabled but reported itself unavailable\",\n };\n }\n // A backend that answers the availability probe can still fail\n // mid-recall (daemon timeout, deadline exceeded, subprocess error).\n // The pipeline already collects those; `degradationSink` hands us the\n // same array it fills, so the outage is observed rather than inferred.\n const degradations: SearchDegradation[] = [];\n try {\n const captured = await orchestrator.recallWithXrayCapture(recallQuery, options.sessionKey, {\n ...(options.namespace !== undefined ? { namespace: options.namespace } : {}),\n ...(authenticatedPrincipal ? { principalOverride: authenticatedPrincipal } : {}),\n ...(request.sourceConnector ? { sourceConnector: request.sourceConnector } : {}),\n ...(options.abortSignal !== undefined ? { abortSignal: options.abortSignal } : {}),\n degradationSink: degradations,\n });\n const fatal = degradations.filter((d) => FATAL_DEGRADATION_CODES[d.code] === true);\n if (fatal.length > 0) {\n return { ok: false, reason: \"backend_unavailable\", detail: describeDegradations(fatal) };\n }\n return { ok: true, snapshot: captured.snapshot };\n } catch (err) {\n // Cancellation is the caller's decision, not a backend outage.\n if (err instanceof Error && err.name === \"AbortError\") throw err;\n const observed = degradations.length > 0 ? `${describeDegradations(degradations)}; ` : \"\";\n return {\n ok: false,\n reason: \"backend_unavailable\",\n detail: `${observed}${err instanceof Error ? err.message : \"recall failed\"}`,\n };\n }\n },\n findExpected: (expect) => findExpectedMemory(orchestrator, readableNamespaces, expect),\n };\n\n const report = await explainRecallMiss(query, {\n deps: whyDeps,\n ...(request.expect !== undefined ? { expect: request.expect } : {}),\n ...(recallSessionKey !== undefined ? { sessionKey: recallSessionKey } : {}),\n ...(requestedNamespace !== undefined ? { namespace: requestedNamespace } : {}),\n ...(request.abortSignal !== undefined ? { abortSignal: request.abortSignal } : {}),\n });\n\n return { reportFound: true, report, summary: summarizeRecallWhy(report) };\n}\n\n/**\n * Resolve `expect` to one stored memory, searching ONLY namespaces the\n * caller may read. An exact id match wins over a substring match; among\n * substring matches the lowest (memoryId, path) pair wins, so repeated runs\n * report the same memory.\n */\nasync function findExpectedMemory(\n orchestrator: Orchestrator,\n namespaces: readonly string[],\n expect: string\n): Promise<RecallWhyMemoryRef | null> {\n const substringMatches: RecallWhyMemoryRef[] = [];\n const seenNamespaces = new Set<string>();\n for (const namespace of namespaces) {\n if (seenNamespaces.has(namespace)) continue;\n seenNamespaces.add(namespace);\n let memories: MemoryFile[];\n try {\n const storage = await orchestrator.getStorageForNamespace(namespace);\n memories = await storage.readAllMemories();\n } catch {\n // A namespace whose storage cannot be read contributes no candidates;\n // the diagnosis still reports on the ones that could be read.\n continue;\n }\n for (const memory of memories) {\n const memoryId = typeof memory.frontmatter.id === \"string\" ? memory.frontmatter.id : \"\";\n if (memoryId.length === 0) continue;\n const ref: RecallWhyMemoryRef = {\n memoryId,\n path: memory.path,\n status: (memory.frontmatter.status as MemoryStatus | undefined) ?? \"active\",\n namespace,\n };\n if (memoryId === expect) return ref;\n if (memoryId.includes(expect) || memory.path.includes(expect)) substringMatches.push(ref);\n }\n }\n if (substringMatches.length === 0) return null;\n // Total comparator: -1 / 0 / 1, and 0 for genuinely equal rows (#12).\n substringMatches.sort((a, b) => {\n if (a.memoryId !== b.memoryId) return a.memoryId < b.memoryId ? -1 : 1;\n if (a.path !== b.path) return a.path < b.path ? -1 : 1;\n return 0;\n });\n return substringMatches[0] ?? null;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/profiling.ts"],"sourcesContent":["// Performance profiling collector for recall and extraction traces.\n// Zero external dependencies — uses only node:fs and node:path.\n\nimport {\n existsSync,\n mkdirSync,\n writeFileSync,\n promises as fsp,\n} from \"node:fs\";\nimport { join } from \"node:path\";\n\nimport { log } from \"./logger.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface ProfileSpan {\n name: string;\n startOffsetMs: number;\n durationMs: number;\n}\n\nexport interface ProfileParallelGroupMember {\n name: string;\n durationMs: number;\n resolvedIndex: number;\n}\n\nexport interface ProfileParallelGroup {\n name: string;\n startOffsetMs: number;\n wallMs: number;\n members: ProfileParallelGroupMember[];\n}\n\nexport interface ProfileTrace {\n ts: string;\n kind: \"recall\" | \"extraction\";\n traceId: string;\n sessionKey?: string;\n totalMs: number;\n spans: ProfileSpan[];\n parallelGroups?: ProfileParallelGroup[];\n configSnapshot?: Record<string, unknown>;\n}\n\nexport interface ProfilingConfig {\n enabled: boolean;\n storageDir: string;\n maxTraces: number;\n}\n\nexport interface ParallelGroupHandle {\n name: string;\n startOffsetMs: number;\n traceId?: string;\n}\n\nexport interface ProfilingStats {\n byKind: Record<string, { count: number; avgMs: number; p50Ms: number; p95Ms: number; maxMs: number }>;\n bySpan: Record<string, { count: number; avgMs: number; p50Ms: number; p95Ms: number; maxMs: number }>;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction percentile(sorted: number[], p: number): number {\n if (sorted.length === 0) return 0;\n const idx = Math.ceil((p / 100) * sorted.length) - 1;\n return sorted[Math.max(0, Math.min(idx, sorted.length - 1))];\n}\n\nfunction aggregateStats(values: number[]): { count: number; avgMs: number; p50Ms: number; p95Ms: number; maxMs: number } {\n const count = values.length;\n if (count === 0) return { count: 0, avgMs: 0, p50Ms: 0, p95Ms: 0, maxMs: 0 };\n const sorted = [...values].sort((a, b) => a - b);\n const sum = sorted.reduce((s, v) => s + v, 0);\n return {\n count,\n avgMs: Math.round(sum / count),\n p50Ms: Math.round(percentile(sorted, 50)),\n p95Ms: Math.round(percentile(sorted, 95)),\n maxMs: sorted[sorted.length - 1],\n };\n}\n\nlet traceCounter = 0;\n\n// ---------------------------------------------------------------------------\n// ProfilingCollector\n// ---------------------------------------------------------------------------\n\nexport class ProfilingCollector {\n private enabled: boolean;\n private storageDir: string;\n private maxTraces: number;\n private traces: ProfileTrace[] = [];\n private prunePromise: Promise<void> | null = null;\n\n // Active trace state — keyed by traceId so concurrent pipelines are isolated.\n private activeTraces = new Map<\n string,\n {\n kind: \"recall\" | \"extraction\";\n start: number;\n sessionKey?: string;\n configSnapshot?: Record<string, unknown>;\n spans: ProfileSpan[];\n spanStarts: Map<string, number>;\n parallelGroups: ProfileParallelGroup[];\n }\n >();\n private latestTraceId = \"\";\n\n constructor(config: ProfilingConfig) {\n this.enabled = config.enabled;\n this.storageDir = config.storageDir;\n this.maxTraces = Math.max(0, config.maxTraces);\n\n if (this.enabled) {\n if (!existsSync(this.storageDir)) {\n mkdirSync(this.storageDir, { recursive: true });\n log.debug(`profiling: created storage dir ${this.storageDir}`);\n }\n }\n }\n\n get isEnabled(): boolean {\n return this.enabled;\n }\n\n // ---- Trace lifecycle ---------------------------------------------------\n\n startTrace(kind: \"recall\" | \"extraction\", sessionKey?: string, configSnapshot?: Record<string, unknown>): string {\n if (!this.enabled) return \"\";\n traceCounter++;\n const traceId = `t${traceCounter}-${Date.now().toString(36)}`;\n this.activeTraces.set(traceId, {\n kind,\n start: Date.now(),\n sessionKey,\n configSnapshot,\n spans: [],\n spanStarts: new Map(),\n parallelGroups: [],\n });\n this.latestTraceId = traceId;\n log.debug(`profiling: started trace ${traceId} kind=${kind}`);\n return traceId;\n }\n\n startSpan(name: string, traceId?: string): void {\n const tid = traceId ?? this.latestTraceId;\n const t = tid ? this.activeTraces.get(tid) : undefined;\n if (!t) return;\n const offset = Date.now() - t.start;\n t.spanStarts.set(name, Date.now());\n log.debug(`profiling: span ${name} started at +${offset}ms (trace=${tid})`);\n }\n\n endSpan(name: string, traceId?: string): void {\n const tid = traceId ?? this.latestTraceId;\n const t = tid ? this.activeTraces.get(tid) : undefined;\n if (!t) return;\n const start = t.spanStarts.get(name);\n if (start === undefined) return;\n const duration = Date.now() - start;\n const startOffset = start - t.start;\n t.spans.push({ name, startOffsetMs: startOffset, durationMs: duration });\n t.spanStarts.delete(name);\n log.debug(`profiling: span ${name} ended ${duration}ms (trace=${tid})`);\n }\n\n endTrace(traceId?: string): ProfileTrace | null {\n const tid = traceId ?? this.latestTraceId;\n const t = tid ? this.activeTraces.get(tid) : undefined;\n if (!t) return null;\n\n // Auto-close any spans still open when the trace finalizes.\n const now = Date.now();\n for (const [name, start] of t.spanStarts) {\n const duration = now - start;\n const startOffset = start - t.start;\n t.spans.push({ name, startOffsetMs: startOffset, durationMs: duration });\n log.debug(`profiling: auto-closed span ${name} at trace end (${duration}ms, trace=${tid})`);\n }\n t.spanStarts.clear();\n\n const trace: ProfileTrace = {\n ts: new Date().toISOString(),\n kind: t.kind,\n traceId: tid,\n totalMs: Date.now() - t.start,\n spans: t.spans,\n configSnapshot: t.configSnapshot,\n };\n\n if (t.sessionKey) {\n trace.sessionKey = t.sessionKey;\n }\n if (t.parallelGroups.length > 0) {\n trace.parallelGroups = t.parallelGroups;\n }\n\n // Remove from active map.\n this.activeTraces.delete(tid);\n\n if (!this.enabled) {\n log.debug(\"profiling: trace discarded (disabled)\");\n return null;\n }\n\n // Persist.\n this.persistTrace(trace);\n\n // Buffer in memory (FIFO).\n this.traces.push(trace);\n if (this.traces.length > this.maxTraces) {\n this.traces.shift();\n }\n\n // Single-flight: if a prune is already running, piggyback on it;\n // otherwise start one. This prevents concurrent prunes from racing\n // on stale directory snapshots.\n if (!this.prunePromise) {\n this.prunePromise = this.pruneFiles().finally(() => {\n this.prunePromise = null;\n });\n }\n log.debug(`profiling: trace ${trace.traceId} finalized totalMs=${trace.totalMs}`);\n return trace;\n }\n\n // ---- Parallel group tracking -------------------------------------------\n\n startParallelGroup(name: string, traceId?: string): ParallelGroupHandle {\n const tid = traceId ?? this.latestTraceId;\n const t = tid ? this.activeTraces.get(tid) : undefined;\n const startOffsetMs = t ? Date.now() - t.start : 0;\n return { name, startOffsetMs, traceId: tid };\n }\n\n async endParallelGroup(\n handle: ParallelGroupHandle,\n members: Array<{ name: string; promise: Promise<unknown> }>,\n ): Promise<void> {\n const wallStart = Date.now();\n\n let nextResolvedIndex = 0;\n const resolutionOrder = new Map<string, number>();\n\n const timed = members.map(async (m) => {\n const t0 = Date.now();\n try {\n await m.promise;\n } catch {\n // settled — still record order\n }\n resolutionOrder.set(m.name, nextResolvedIndex++);\n return { name: m.name, durationMs: Date.now() - t0 };\n });\n const timedResults = await Promise.allSettled(timed);\n\n const tid = handle.traceId ?? this.latestTraceId;\n const t = tid ? this.activeTraces.get(tid) : undefined;\n if (!t) return;\n\n const wallMs = Date.now() - wallStart;\n\n const groupMembers: ProfileParallelGroupMember[] = members.map((m, i) => {\n const timedResult = timedResults[i].status === \"fulfilled\"\n ? timedResults[i].value\n : { name: m.name, durationMs: wallMs };\n return {\n name: timedResult.name,\n durationMs: timedResult.durationMs,\n resolvedIndex: resolutionOrder.get(m.name) ?? i,\n };\n });\n\n t.parallelGroups.push({\n name: handle.name,\n startOffsetMs: handle.startOffsetMs,\n wallMs,\n members: groupMembers,\n });\n\n log.debug(`profiling: parallel group ${handle.name} wallMs=${wallMs} (trace=${tid})`);\n }\n\n // ---- Query methods -----------------------------------------------------\n\n getRecentTraces(limit?: number): ProfileTrace[] {\n if (limit === undefined || limit === Infinity) {\n return [...this.traces];\n }\n if (!Number.isFinite(limit)) {\n return [];\n }\n const n = Math.floor(limit);\n if (n <= 0) {\n return [];\n }\n return this.traces.slice(-n);\n }\n\n getStats(): ProfilingStats {\n const byKind: Record<string, number[]> = {};\n const bySpan: Record<string, number[]> = {};\n\n for (const trace of this.traces) {\n if (!byKind[trace.kind]) byKind[trace.kind] = [];\n byKind[trace.kind].push(trace.totalMs);\n\n for (const span of trace.spans) {\n if (!bySpan[span.name]) bySpan[span.name] = [];\n bySpan[span.name].push(span.durationMs);\n }\n }\n\n const result: ProfilingStats = { byKind: {}, bySpan: {} };\n for (const [k, v] of Object.entries(byKind)) {\n result.byKind[k] = aggregateStats(v);\n }\n for (const [k, v] of Object.entries(bySpan)) {\n result.bySpan[k] = aggregateStats(v);\n }\n return result;\n }\n\n identifyBottleneck(): string | null {\n if (this.traces.length === 0) return null;\n const latest = this.traces[this.traces.length - 1];\n if (latest.spans.length === 0) return null;\n let slowest = latest.spans[0];\n for (const span of latest.spans) {\n if (span.durationMs > slowest.durationMs) slowest = span;\n }\n return slowest.name;\n }\n\n // ---- Persistence -------------------------------------------------------\n\n private persistTrace(trace: ProfileTrace): void {\n const filename = `${trace.kind}-${trace.traceId}.jsonl`;\n const filepath = join(this.storageDir, filename);\n try {\n writeFileSync(filepath, JSON.stringify(trace) + \"\\n\", \"utf-8\");\n log.debug(`profiling: persisted ${filename}`);\n } catch (err) {\n log.warn(`profiling: failed to persist ${filename}`, err);\n }\n }\n\n async pruneFiles(): Promise<void> {\n try {\n const dir = this.storageDir;\n const entries = await fsp.readdir(dir);\n const jsonlFiles = entries.filter((f) => f.endsWith(\".jsonl\"));\n const withMtime = await Promise.all(\n jsonlFiles.map(async (f) => ({\n name: f,\n mtime: (await fsp.stat(join(dir, f))).mtimeMs,\n })),\n );\n const files = withMtime.sort((a, b) => a.mtime - b.mtime).map((f) => f.name);\n\n while (files.length > this.maxTraces) {\n const oldest = files.shift()!;\n await fsp.unlink(join(dir, oldest));\n log.debug(`profiling: pruned ${oldest}`);\n }\n } catch (err) {\n log.warn(\"profiling: prune failed\", err);\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// ASCII formatter\n// ---------------------------------------------------------------------------\n\nexport function formatProfileTraceAscii(trace: ProfileTrace): string {\n const lines: string[] = [];\n const BAR_WIDTH = 40;\n\n lines.push(`=== Profile: ${trace.kind} ===`);\n lines.push(`Trace ID : ${trace.traceId}`);\n lines.push(`Total : ${trace.totalMs}ms`);\n if (trace.sessionKey) lines.push(`Session : ${trace.sessionKey}`);\n lines.push(\"\");\n\n // Identify bottleneck.\n let bottleneckName: string | null = null;\n if (trace.spans.length > 0) {\n let slowest = trace.spans[0];\n for (const s of trace.spans) {\n if (s.durationMs > slowest.durationMs) slowest = s;\n }\n bottleneckName = slowest.name;\n }\n\n // Spans.\n if (trace.spans.length > 0) {\n const maxDuration = Math.max(...trace.spans.map((s) => s.durationMs), 1);\n lines.push(\"Spans:\");\n for (const span of trace.spans) {\n const barLen = Math.max(1, Math.round((span.durationMs / maxDuration) * BAR_WIDTH));\n const bar = \"\\u2588\".repeat(barLen);\n const suffix = span.name === bottleneckName ? \" \\u2190 bottleneck\" : \"\";\n lines.push(` ${span.name.padEnd(30)} ${String(span.durationMs).padStart(6)}ms ${bar}${suffix}`);\n }\n lines.push(\"\");\n }\n\n // Parallel groups.\n if (trace.parallelGroups && trace.parallelGroups.length > 0) {\n lines.push(\"Parallel Groups:\");\n for (const group of trace.parallelGroups) {\n lines.push(` ${group.name}:`);\n lines.push(` Wall time : ${group.wallMs}ms`);\n const efficiency = parallelEfficiency(group);\n if (efficiency !== null) {\n lines.push(` Efficiency : ${efficiency}%`);\n }\n for (const member of group.members) {\n lines.push(` [${String(member.resolvedIndex).padStart(2)}] ${member.name.padEnd(28)} ${String(member.durationMs).padStart(6)}ms`);\n }\n }\n lines.push(\"\");\n }\n\n lines.push(\"---\");\n return lines.join(\"\\n\");\n}\n\nfunction parallelEfficiency(group: ProfileParallelGroup): number | null {\n if (group.members.length <= 1) return null;\n const idealMs = Math.max(...group.members.map((m) => m.durationMs));\n if (group.wallMs === 0) return null;\n return Math.round((idealMs / group.wallMs) * 100);\n}\n"],"mappings":"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","names":[]}
|
package/dist/chunk-U2VDCFXW.js
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
// src/search/port.ts
|
|
2
|
-
function reportSearchDegradation(execution, degradation) {
|
|
3
|
-
try {
|
|
4
|
-
execution?.onDegradation?.(degradation);
|
|
5
|
-
} catch {
|
|
6
|
-
}
|
|
7
|
-
}
|
|
8
|
-
function resolveEnsureCollectionArgs(collectionOrExecution, execution) {
|
|
9
|
-
if (typeof collectionOrExecution === "string") {
|
|
10
|
-
return { collection: collectionOrExecution, execution };
|
|
11
|
-
}
|
|
12
|
-
return { collection: void 0, execution: collectionOrExecution ?? execution };
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export {
|
|
16
|
-
reportSearchDegradation,
|
|
17
|
-
resolveEnsureCollectionArgs
|
|
18
|
-
};
|
|
19
|
-
//# sourceMappingURL=chunk-U2VDCFXW.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/search/port.ts"],"sourcesContent":["import type { QmdSearchResult } from \"../types.js\";\n\n/** Alias so consumers don't need to reference \"Qmd\" in a backend-agnostic context. */\nexport type SearchResult = QmdSearchResult;\n\nexport interface SearchQueryOptions {\n intent?: string;\n explain?: boolean;\n candidateLimit?: number;\n rerank?: boolean;\n chunkStrategy?: \"auto\" | \"regex\";\n structuredSearches?: Array<{ type: \"lex\" | \"vec\" | \"hyde\"; query: string }>;\n}\n\n/**\n * A search that returned empty (or partial) results because the backend was\n * unavailable, still loading, or timed out — cases that are otherwise\n * indistinguishable from a genuine \"no matches\" (issue #1536, CLAUDE.md\n * rule 34).\n */\nexport interface SearchDegradation {\n backend: \"qmd\" | \"remote\" | \"meilisearch\" | \"orama\" | \"lancedb\";\n code:\n | \"backend_unavailable\"\n | \"backend_error\"\n | \"daemon_timeout\"\n | \"daemon_loading\"\n | \"subprocess_error\"\n | \"deadline_exceeded\"\n | \"remote_error\"\n | \"vector_tier_unavailable\";\n detail?: string;\n}\n\nexport interface SearchExecutionOptions {\n signal?: AbortSignal;\n /**\n * Observer invoked when the backend degrades during this call (#1536).\n * Callers that need to distinguish empty-because-degraded from\n * empty-because-no-matches (recall x-ray, fallback decisions) pass a\n * collector here. Observer failures are swallowed by the notifier —\n * observability must never break search.\n */\n onDegradation?: (degradation: SearchDegradation) => void;\n}\n\nexport function reportSearchDegradation(\n execution: SearchExecutionOptions | undefined,\n degradation: SearchDegradation\n): void {\n try {\n execution?.onDegradation?.(degradation);\n } catch {\n // Observability must never break search.\n }\n}\n\nexport function resolveEnsureCollectionArgs(\n collectionOrExecution?: string | SearchExecutionOptions,\n execution?: SearchExecutionOptions\n): { collection?: string; execution?: SearchExecutionOptions } {\n if (typeof collectionOrExecution === \"string\") {\n return { collection: collectionOrExecution, execution };\n }\n return { collection: undefined, execution: collectionOrExecution ?? execution };\n}\n\n/**\n * Optional status report for search backends that track embedding backlog.\n * Backends without embedding pipelines return null from status().\n */\nexport interface SearchBackendStatus {\n pendingEmbeddings: number | null;\n oldestPendingAgeMs: number | null;\n totalFiles: number | null;\n embeddedFiles: number | null;\n}\n\n/**\n * Abstract search backend interface.\n *\n * Implementations:\n * - QmdClient (default, local hybrid BM25+vector+reranking)\n * - OramaBackend (embedded, pure JS, hybrid FTS+vector)\n * - LanceDbBackend (embedded, native Arrow bindings, RRF reranking)\n * - MeilisearchBackend (server-based SDK, hybrid search)\n * - RemoteSearchBackend (HTTP REST adapter)\n * - NoopSearchBackend (graceful degradation)\n *\n * See docs/writing-a-search-backend.md for the implementation guide.\n */\nexport interface SearchBackend {\n // ── Lifecycle ──\n probe(execution?: SearchExecutionOptions): Promise<boolean>;\n /**\n * Optional non-mutating availability probe for health/readiness checks.\n * Implementations must avoid auto-upgrades, collection creation, daemon\n * startup, or any other runtime-modifying side effects.\n */\n checkAvailability?(execution?: SearchExecutionOptions): Promise<boolean>;\n isAvailable(): boolean;\n debugStatus(): string;\n /** Optional embedding backlog status for health surfaces. */\n status?(): Promise<SearchBackendStatus>;\n\n // ── Search ──\n search(\n query: string,\n collection?: string,\n maxResults?: number,\n options?: SearchQueryOptions,\n execution?: SearchExecutionOptions\n ): Promise<SearchResult[]>;\n searchGlobal(query: string, maxResults?: number, execution?: SearchExecutionOptions): Promise<SearchResult[]>;\n bm25Search(\n query: string,\n collection?: string,\n maxResults?: number,\n execution?: SearchExecutionOptions\n ): Promise<SearchResult[]>;\n vectorSearch(\n query: string,\n collection?: string,\n maxResults?: number,\n execution?: SearchExecutionOptions\n ): Promise<SearchResult[]>;\n hybridSearch(\n query: string,\n collection?: string,\n maxResults?: number,\n execution?: SearchExecutionOptions\n ): Promise<SearchResult[]>;\n\n // ── Maintenance ──\n update(execution?: SearchExecutionOptions): Promise<void>;\n /**\n * Wall-clock ms of the most recent SUCCESSFUL index refresh, or null when the\n * backend has never run one. Ordinary update() is fail-open: it returns\n * silently (without throwing or advancing this timestamp) when the backend is\n * unavailable or the call is suppressed by a min-interval / failure backoff.\n * Callers that must know whether update() actually indexed — so they can\n * invalidate dependent caches only on a real refresh — snapshot this before\n * the call and treat the backend as refreshed iff the value advanced. Optional\n * because not every backend tracks it; absent means \"assume refreshed\".\n */\n readonly lastUpdateRanAtMs?: number | null;\n /**\n * Optional strict refresh used by callers that must know whether the backend\n * was actually refreshed before writing success markers. Ordinary update\n * calls remain fail-open for migration/maintenance resilience.\n */\n updateStrict?(execution?: SearchExecutionOptions): Promise<void>;\n updateCollection(collection: string, execution?: SearchExecutionOptions): Promise<void>;\n updateCollectionFromDir?(collection: string, memoryDir: string, execution?: SearchExecutionOptions): Promise<void>;\n /**\n * True when update() refreshes every indexed collection, not just this\n * backend's configured collection. Namespace routers use this to avoid\n * repeating the same expensive global update once per namespace.\n */\n updatesAllCollections?(): boolean;\n /**\n * Optional strict refresh used by callers that must know whether a collection\n * was actually refreshed before writing success markers. Ordinary update\n * calls remain fail-open for migration/maintenance resilience.\n */\n updateCollectionStrict?(collection: string, execution?: SearchExecutionOptions): Promise<void>;\n embed(): Promise<void>;\n /**\n * Optional strict embed used by callers that must know vectors were actually\n * refreshed before writing success markers.\n */\n embedStrict?(): Promise<void>;\n embedCollection(collection: string): Promise<void>;\n /**\n * Optional strict collection embed used by callers that must know vectors were\n * actually refreshed before writing success markers.\n */\n embedCollectionStrict?(collection: string): Promise<void>;\n\n // ── Collection management ──\n /**\n * True only when the backend can bind, isolate, search, and delete collections\n * outside Remnic's primary memory roots.\n */\n supportsAdditionalCollections?(): boolean;\n /**\n * Prevent a collection from participating in unscoped/global searches.\n * Dedicated corpora must call this immediately after collection creation.\n */\n excludeCollectionFromGlobalSearch?(collection: string, execution?: SearchExecutionOptions): Promise<void>;\n /** Remove a non-primary collection and its backend-owned index state. */\n deleteCollection?(collection: string, execution?: SearchExecutionOptions): Promise<boolean>;\n /** Canonical source root currently bound to a named collection. */\n collectionRoot?(collection: string, execution?: SearchExecutionOptions): Promise<string | null>;\n /** Collection-scoped status for observability surfaces. */\n collectionStatus?(collection: string): Promise<Pick<SearchBackendStatus, \"totalFiles\">>;\n /**\n * Optional non-mutating collection probe. Backends that can distinguish a\n * missing collection from a transient probe failure should implement this so\n * callers can avoid auto-creating collections in unsafe layouts.\n */\n checkCollection?(\n collectionOrExecution?: string | SearchExecutionOptions,\n execution?: SearchExecutionOptions\n ): Promise<\"present\" | \"missing\" | \"unknown\" | \"skipped\">;\n ensureCollection(\n memoryDir: string,\n execution?: SearchExecutionOptions\n ): Promise<\"present\" | \"missing\" | \"unknown\" | \"skipped\">;\n ensureCollection(\n memoryDir: string,\n collection?: string,\n execution?: SearchExecutionOptions\n ): Promise<\"present\" | \"missing\" | \"unknown\" | \"skipped\">;\n}\n"],"mappings":";AA8CO,SAAS,wBACd,WACA,aACM;AACN,MAAI;AACF,eAAW,gBAAgB,WAAW;AAAA,EACxC,QAAQ;AAAA,EAER;AACF;AAEO,SAAS,4BACd,uBACA,WAC6D;AAC7D,MAAI,OAAO,0BAA0B,UAAU;AAC7C,WAAO,EAAE,YAAY,uBAAuB,UAAU;AAAA,EACxD;AACA,SAAO,EAAE,YAAY,QAAW,WAAW,yBAAyB,UAAU;AAChF;","names":[]}
|