@remnic/core 9.33.0 → 9.34.0
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 +15 -15
- package/dist/access-authorization-probe.js +16 -16
- package/dist/access-boundary.js +15 -15
- package/dist/access-cli.js +35 -34
- package/dist/access-cli.js.map +1 -1
- package/dist/access-extraction-force-flush.js +15 -15
- package/dist/access-http.js +25 -25
- package/dist/access-identity-continuity-surface.js +15 -15
- package/dist/access-lcm-surface.js +15 -15
- package/dist/access-mcp.js +20 -20
- package/dist/access-namespace-preflight.js +15 -15
- package/dist/access-observe-write-surface.js +15 -15
- package/dist/access-operations-batch.js +17 -17
- package/dist/access-operations.js +19 -19
- package/dist/access-recall-concurrency.js +15 -15
- package/dist/access-recall-response.js +15 -15
- package/dist/access-recall-surface.js +15 -15
- package/dist/access-schema.js +3 -3
- package/dist/access-service.js +15 -15
- package/dist/adapters/index.js +4 -4
- package/dist/adapters/registry.js +2 -2
- package/dist/{capsule-crypto-YO5QJ6L3.js → capsule-crypto-GWVG7LGC.js} +2 -2
- package/dist/{chunk-VDX2J7OX.js → chunk-5W7CTZ2E.js} +4 -4
- package/dist/{chunk-J6BYDVPL.js → chunk-6HXQ2YO5.js} +2 -2
- package/dist/{chunk-6NPZ3FP2.js → chunk-7CCVK67W.js} +5 -5
- package/dist/{chunk-77CNV7DY.js → chunk-BLAKS3CP.js} +4 -4
- package/dist/{chunk-YFQMW2TH.js → chunk-DG2R7M7K.js} +11 -11
- package/dist/{chunk-PTN6UAUA.js → chunk-ES36HTSC.js} +4 -4
- package/dist/{chunk-ATUURSST.js → chunk-FL3AM5KE.js} +3 -3
- package/dist/{chunk-I5DQ6KZK.js → chunk-FYO2GAZJ.js} +2 -2
- package/dist/{chunk-MPH263X2.js → chunk-HULSU7PS.js} +2 -2
- package/dist/{chunk-N2B56SIS.js → chunk-KJT3PZAR.js} +2 -2
- package/dist/chunk-KWMXR7XS.js +138 -0
- package/dist/chunk-KWMXR7XS.js.map +1 -0
- package/dist/{chunk-7RZJWZ46.js → chunk-LEPOP46W.js} +5 -5
- package/dist/{chunk-TITQYVKD.js → chunk-LRYAQGDR.js} +11 -11
- package/dist/{chunk-BXLOS5AJ.js → chunk-OWHERGF2.js} +2 -2
- package/dist/{chunk-6JE3HGS4.js → chunk-OXBPHIKH.js} +5 -5
- package/dist/{chunk-SIKCTPRK.js → chunk-QEPNZBBC.js} +3 -3
- package/dist/{chunk-2QSZNTDO.js → chunk-RKNJBZ55.js} +4 -4
- package/dist/{chunk-C4PEMCP4.js → chunk-RLOVRJZN.js} +7 -7
- package/dist/{chunk-EHNENZHZ.js → chunk-RQBF54FC.js} +3 -3
- package/dist/{chunk-SPPL77NI.js → chunk-RSFWAI4D.js} +49 -29
- package/dist/chunk-RSFWAI4D.js.map +1 -0
- package/dist/{chunk-RYHWZ52A.js → chunk-S2BBISHU.js} +4 -4
- package/dist/{chunk-V2MVDDJF.js → chunk-SUZI6BHQ.js} +2 -2
- package/dist/{chunk-DQEMWVMT.js → chunk-UVYI6VIX.js} +1 -1
- package/dist/{chunk-YJ7PWEWI.js → chunk-WFYWGDM6.js} +2 -2
- package/dist/{chunk-SFW2NABJ.js → chunk-Y6XRTQMV.js} +5 -5
- package/dist/{chunk-6NNNURLK.js → chunk-Z54EM5YZ.js} +4 -4
- package/dist/cli.js +29 -29
- package/dist/contradiction/index.js +4 -4
- package/dist/conversation-index/backend.js +2 -2
- package/dist/corpus-watermark.js +10 -10
- package/dist/index.d.ts +1 -1
- package/dist/index.js +51 -50
- package/dist/lcm/engine.js +2 -2
- package/dist/lcm/index.js +2 -2
- package/dist/namespaces/migrate.js +9 -9
- package/dist/namespaces/search.js +8 -8
- package/dist/operator-doctor-corpus.js +11 -11
- package/dist/operator-toolkit.js +12 -12
- package/dist/orchestrator.js +35 -34
- 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/tool-scoped-memory.d.ts +47 -0
- package/dist/tool-scoped-memory.js +12 -0
- package/dist/tool-scoped-memory.js.map +1 -0
- package/dist/transfer/backup.js +2 -2
- package/dist/transfer/capsule-export.js +2 -2
- package/dist/transfer/capsule-import.js +2 -2
- package/package.json +2 -2
- package/src/extraction-redaction-rules.test.ts +53 -0
- package/src/index.ts +1 -1
- package/src/orchestration/extraction-persist.ts +31 -32
- package/src/tool-scoped-memory.test.ts +199 -0
- package/src/tool-scoped-memory.ts +190 -0
- package/dist/chunk-SPPL77NI.js.map +0 -1
- /package/dist/{capsule-crypto-YO5QJ6L3.js.map → capsule-crypto-GWVG7LGC.js.map} +0 -0
- /package/dist/{chunk-VDX2J7OX.js.map → chunk-5W7CTZ2E.js.map} +0 -0
- /package/dist/{chunk-J6BYDVPL.js.map → chunk-6HXQ2YO5.js.map} +0 -0
- /package/dist/{chunk-6NPZ3FP2.js.map → chunk-7CCVK67W.js.map} +0 -0
- /package/dist/{chunk-77CNV7DY.js.map → chunk-BLAKS3CP.js.map} +0 -0
- /package/dist/{chunk-YFQMW2TH.js.map → chunk-DG2R7M7K.js.map} +0 -0
- /package/dist/{chunk-PTN6UAUA.js.map → chunk-ES36HTSC.js.map} +0 -0
- /package/dist/{chunk-ATUURSST.js.map → chunk-FL3AM5KE.js.map} +0 -0
- /package/dist/{chunk-I5DQ6KZK.js.map → chunk-FYO2GAZJ.js.map} +0 -0
- /package/dist/{chunk-MPH263X2.js.map → chunk-HULSU7PS.js.map} +0 -0
- /package/dist/{chunk-N2B56SIS.js.map → chunk-KJT3PZAR.js.map} +0 -0
- /package/dist/{chunk-7RZJWZ46.js.map → chunk-LEPOP46W.js.map} +0 -0
- /package/dist/{chunk-TITQYVKD.js.map → chunk-LRYAQGDR.js.map} +0 -0
- /package/dist/{chunk-BXLOS5AJ.js.map → chunk-OWHERGF2.js.map} +0 -0
- /package/dist/{chunk-6JE3HGS4.js.map → chunk-OXBPHIKH.js.map} +0 -0
- /package/dist/{chunk-SIKCTPRK.js.map → chunk-QEPNZBBC.js.map} +0 -0
- /package/dist/{chunk-2QSZNTDO.js.map → chunk-RKNJBZ55.js.map} +0 -0
- /package/dist/{chunk-C4PEMCP4.js.map → chunk-RLOVRJZN.js.map} +0 -0
- /package/dist/{chunk-EHNENZHZ.js.map → chunk-RQBF54FC.js.map} +0 -0
- /package/dist/{chunk-RYHWZ52A.js.map → chunk-S2BBISHU.js.map} +0 -0
- /package/dist/{chunk-V2MVDDJF.js.map → chunk-SUZI6BHQ.js.map} +0 -0
- /package/dist/{chunk-DQEMWVMT.js.map → chunk-UVYI6VIX.js.map} +0 -0
- /package/dist/{chunk-YJ7PWEWI.js.map → chunk-WFYWGDM6.js.map} +0 -0
- /package/dist/{chunk-SFW2NABJ.js.map → chunk-Y6XRTQMV.js.map} +0 -0
- /package/dist/{chunk-6NNNURLK.js.map → chunk-Z54EM5YZ.js.map} +0 -0
|
@@ -82,6 +82,7 @@ import {
|
|
|
82
82
|
stripCitationForTemplate,
|
|
83
83
|
} from "../source-attribution.js";
|
|
84
84
|
import { classifyMemoryKind } from "../himem.js";
|
|
85
|
+
import { shouldPromoteGlobalFactToShared, withholdToolScopedFromSharedNamespace } from "../tool-scoped-memory.js";
|
|
85
86
|
import {
|
|
86
87
|
buildBehaviorSignalsForMemory,
|
|
87
88
|
dedupeBehaviorSignalsByMemoryAndHash,
|
|
@@ -394,6 +395,7 @@ export class ExtractionPersistCoordinator {
|
|
|
394
395
|
// (pre-judge + write-loop) so no new scattered config.*Enabled read is
|
|
395
396
|
// introduced (ratchet scatteredConfigFlagReads; see #1523).
|
|
396
397
|
const namespacesEnabled = resolveNamespaceCapabilities(this.deps.config).namespaces;
|
|
398
|
+
const sourceConnector = sourceContext?.sourceConnector; // #2183 tool-scope guard input
|
|
397
399
|
const promoteMemoryToProfileTargets = async (options: {
|
|
398
400
|
sourceStorage: StorageManager;
|
|
399
401
|
category: string;
|
|
@@ -607,11 +609,14 @@ export class ExtractionPersistCoordinator {
|
|
|
607
609
|
observedAt?: string;
|
|
608
610
|
eventTimeSource?: "extracted" | "assumed";
|
|
609
611
|
source: string;
|
|
612
|
+
sourceConnector?: string;
|
|
613
|
+
procedureSteps?: ReadonlyArray<{ toolCall?: { kind?: string } }>;
|
|
610
614
|
/** Claim-level provenance spans (issue #1575 PR 2). */
|
|
611
615
|
sources?: ProvenanceSource[];
|
|
612
616
|
provenance?: "verified" | "unverified" | "none";
|
|
613
617
|
}): Promise<void> => {
|
|
614
618
|
await promoteMemoryToProfileTargets(options);
|
|
619
|
+
if (lifecycleCaps.extractionScopeClassification && withholdToolScopedFromSharedNamespace(options)) return;
|
|
615
620
|
if (
|
|
616
621
|
!shouldPromoteToShared(
|
|
617
622
|
this.deps.config,
|
|
@@ -629,7 +634,6 @@ export class ExtractionPersistCoordinator {
|
|
|
629
634
|
this.deps.config.sharedNamespace,
|
|
630
635
|
);
|
|
631
636
|
// Dedup gate: canonicalize content before hashing.
|
|
632
|
-
//
|
|
633
637
|
// Issue #369 (PR #401): When inline attribution is enabled,
|
|
634
638
|
// `applyInlineCitation` appends a timestamp-bearing marker (e.g.
|
|
635
639
|
// `[Source: ..., ts=2026-04-11T...]`). Because the timestamp changes
|
|
@@ -639,7 +643,6 @@ export class ExtractionPersistCoordinator {
|
|
|
639
643
|
// `fact.content`, which can already carry an inline citation (e.g. a
|
|
640
644
|
// relayed or reprocessed fact). Strip any pre-existing citation so the
|
|
641
645
|
// dedup key matches the hash stored from the original un-cited write.
|
|
642
|
-
//
|
|
643
646
|
// PR #402 round-6 (Fix #2 / chatgpt-codex P1 PRRT_kwDORJXyws56U74n):
|
|
644
647
|
// Compute the enriched content before the hash-dedup check so the
|
|
645
648
|
// lookup uses the same content that writeMemory will actually store.
|
|
@@ -652,7 +655,6 @@ export class ExtractionPersistCoordinator {
|
|
|
652
655
|
// key order and casing are canonical — identical to the enrichment
|
|
653
656
|
// applied by storage.writeMemory — preventing spurious hash misses
|
|
654
657
|
// when attribute maps arrive with different insertion orders or casing.
|
|
655
|
-
//
|
|
656
658
|
// Fix #4 (Low PRRT_kwDORJXyws56VHth): sanitize the base content before
|
|
657
659
|
// building dedupContent. writeMemory runs sanitizeMemoryContent on the
|
|
658
660
|
// enriched body before hashing; if sanitization redacts the content to
|
|
@@ -660,7 +662,6 @@ export class ExtractionPersistCoordinator {
|
|
|
660
662
|
// the raw form. Computing dedupContent from sanitized.text here ensures
|
|
661
663
|
// the hash lookup and the normalizedIncoming comparison both use the
|
|
662
664
|
// same content that writeMemory will actually store.
|
|
663
|
-
//
|
|
664
665
|
// Combined fix: strip any pre-existing citation FIRST to obtain
|
|
665
666
|
// rawContent (the canonical body), then sanitize rawContent (not
|
|
666
667
|
// options.content) when building dedupContent, so that citation
|
|
@@ -1350,20 +1351,18 @@ export class ExtractionPersistCoordinator {
|
|
|
1350
1351
|
// + PBJEe/PBKAj).
|
|
1351
1352
|
let factRedactionRules = preJudgeRedactionRules;
|
|
1352
1353
|
let factNs = preRoutedNamespaceByFact[fi];
|
|
1353
|
-
// #1713
|
|
1354
|
-
//
|
|
1355
|
-
//
|
|
1356
|
-
// namespace's rules too, or a never-store pattern under shared is
|
|
1357
|
-
// missed at pre-judge and only caught at the write gate — letting the
|
|
1358
|
-
// content reach the extraction judge/training path. Mirror the write
|
|
1359
|
-
// loop's scope-routing conditions exactly.
|
|
1354
|
+
// #1713: predict the same target namespace the write loop will route
|
|
1355
|
+
// this global fact to (shared, via shouldPromoteGlobalFactToShared) so
|
|
1356
|
+
// the pre-judge redaction filter consults that namespace's rules.
|
|
1360
1357
|
if (
|
|
1361
1358
|
!factNs &&
|
|
1362
1359
|
lifecycleCaps.extractionScopeClassification &&
|
|
1363
1360
|
namespacesEnabled &&
|
|
1364
1361
|
f.scope === "global" &&
|
|
1365
1362
|
profileAllowsSharedWrites &&
|
|
1366
|
-
this.deps.storageDirNamespace(storage.dir) !== this.deps.config.sharedNamespace
|
|
1363
|
+
this.deps.storageDirNamespace(storage.dir) !== this.deps.config.sharedNamespace &&
|
|
1364
|
+
shouldPromoteGlobalFactToShared({ scope: f.scope, content: f.content,
|
|
1365
|
+
sourceConnector, procedureSteps: f.procedureSteps })
|
|
1367
1366
|
) {
|
|
1368
1367
|
factNs = this.deps.config.sharedNamespace;
|
|
1369
1368
|
}
|
|
@@ -1611,14 +1610,9 @@ export class ExtractionPersistCoordinator {
|
|
|
1611
1610
|
}
|
|
1612
1611
|
}
|
|
1613
1612
|
|
|
1614
|
-
// Scope-based namespace routing:
|
|
1615
|
-
//
|
|
1616
|
-
// namespace
|
|
1617
|
-
// applies when namespaces are enabled and the fact was not already
|
|
1618
|
-
// routed to a specific namespace by a routing rule (routing rules
|
|
1619
|
-
// that set an explicit namespace take precedence; category-only rules
|
|
1620
|
-
// do not block scope routing). Rule 30: gated by
|
|
1621
|
-
// extractionScopeClassificationEnabled.
|
|
1613
|
+
// Scope-based namespace routing: a `global` fact (scope classification
|
|
1614
|
+
// on, no explicit routing-rule namespace) is promoted to the shared
|
|
1615
|
+
// namespace unless the tool-scope guard withholds it (#2183).
|
|
1622
1616
|
if (
|
|
1623
1617
|
lifecycleCaps.extractionScopeClassification &&
|
|
1624
1618
|
namespacesEnabled &&
|
|
@@ -1627,18 +1621,21 @@ export class ExtractionPersistCoordinator {
|
|
|
1627
1621
|
) {
|
|
1628
1622
|
const currentNs = this.deps.storageDirNamespace(targetStorage.dir);
|
|
1629
1623
|
if (currentNs !== this.deps.config.sharedNamespace && profileAllowsSharedWrites) {
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1624
|
+
if (shouldPromoteGlobalFactToShared({ scope: fact.scope, content: fact.content,
|
|
1625
|
+
sourceConnector, procedureSteps: fact.procedureSteps })) {
|
|
1626
|
+
try {
|
|
1627
|
+
targetStorage = await this.deps.getStorageRouter().storageFor(
|
|
1628
|
+
this.deps.config.sharedNamespace,
|
|
1629
|
+
);
|
|
1630
|
+
targetNamespaceName = this.deps.config.sharedNamespace;
|
|
1631
|
+
log.debug(
|
|
1632
|
+
`scope-routing: fact "${fact.content.slice(0, 60)}…" routed to shared namespace (scope=global)`,
|
|
1633
|
+
);
|
|
1634
|
+
} catch (scopeRouteErr) {
|
|
1635
|
+
log.warn(
|
|
1636
|
+
`scope-routing: failed to resolve shared namespace storage; writing to session namespace (fail-open): ${scopeRouteErr}`,
|
|
1637
|
+
);
|
|
1638
|
+
}
|
|
1642
1639
|
}
|
|
1643
1640
|
} else if (currentNs !== this.deps.config.sharedNamespace) {
|
|
1644
1641
|
log.debug(
|
|
@@ -2404,6 +2401,7 @@ export class ExtractionPersistCoordinator {
|
|
|
2404
2401
|
: {}),
|
|
2405
2402
|
source: extractionWriteSource,
|
|
2406
2403
|
...(extractionSourceConnector ? { sourceConnector: extractionSourceConnector } : {}),
|
|
2404
|
+
...(fact.procedureSteps && fact.procedureSteps.length ? { procedureSteps: fact.procedureSteps } : {}),
|
|
2407
2405
|
...(fact.sources && fact.sources.length > 0 ? { sources: fact.sources } : {}),
|
|
2408
2406
|
...(fact.provenance ? { provenance: fact.provenance } : {}),
|
|
2409
2407
|
});
|
|
@@ -2704,6 +2702,7 @@ export class ExtractionPersistCoordinator {
|
|
|
2704
2702
|
: {}),
|
|
2705
2703
|
source: extractionWriteSource,
|
|
2706
2704
|
...(extractionSourceConnector ? { sourceConnector: extractionSourceConnector } : {}),
|
|
2705
|
+
...(fact.procedureSteps && fact.procedureSteps.length ? { procedureSteps: fact.procedureSteps } : {}),
|
|
2707
2706
|
...(fact.sources && fact.sources.length > 0 ? { sources: fact.sources } : {}),
|
|
2708
2707
|
...(fact.provenance ? { provenance: fact.provenance } : {}),
|
|
2709
2708
|
});
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
referencesAgentSpecificTool,
|
|
6
|
+
shouldPromoteGlobalFactToShared,
|
|
7
|
+
withholdToolScopedFromSharedNamespace,
|
|
8
|
+
} from "./tool-scoped-memory.js";
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// referencesAgentSpecificTool — table-driven, both polarities.
|
|
12
|
+
//
|
|
13
|
+
// Tuning bias (issue #2183): a FALSE POSITIVE is cheap (the fact stays in the
|
|
14
|
+
// narrower namespace — the documented safe default); a FALSE NEGATIVE is the
|
|
15
|
+
// cross-integration tool-collision bug. Detection leans permissive but must not
|
|
16
|
+
// fire on ordinary prose. Keywords and generic names are word-anchored so
|
|
17
|
+
// "tooling"/"commander" and the "search" inside "research" do not match.
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
const REF_CASES: Array<{ content: string; expected: boolean; note: string }> = [
|
|
21
|
+
// ---- MUST match --------------------------------------------------------
|
|
22
|
+
{ content: "Prefer the search tool and provide a path when locating code.", expected: true, note: "generic name adjacent to 'tool'" },
|
|
23
|
+
{ content: "Use the `read` tool before editing a file.", expected: true, note: "backticked token adjacent to 'tool'" },
|
|
24
|
+
{ content: "The exec tool requires an absolute cwd.", expected: true, note: "generic name 'exec' adjacent to 'tool'" },
|
|
25
|
+
{ content: "Use `search` when locating code.", expected: true, note: "imperative invocation + backticked identifier" },
|
|
26
|
+
{ content: "Run `rg` before editing.", expected: true, note: "imperative invocation + backticked identifier" },
|
|
27
|
+
{ content: "Use /search with a path.", expected: true, note: "slash-command syntax" },
|
|
28
|
+
// ---- MUST NOT match ----------------------------------------------------
|
|
29
|
+
{ content: "User prefers dark mode in all editors", expected: false, note: "portable preference; no tool/command context" },
|
|
30
|
+
{ content: "PostgreSQL 15 requires the uuid-ossp extension for gen_random_uuid()", expected: false, note: "portable framework knowledge" },
|
|
31
|
+
{ content: "Magento 2.4.8 has a race condition in checkout", expected: false, note: "'condition' is not a keyword; 'checkout' is not generic" },
|
|
32
|
+
{ content: "The user reads documentation before starting a task", expected: false, note: "the bare verb 'reads' is not a tool reference" },
|
|
33
|
+
{ content: "Search tooling improves developer productivity", expected: false, note: "'tool' must not match the prefix of 'tooling'" },
|
|
34
|
+
{ content: "Research tool improves accuracy", expected: false, note: "'search' must not match inside 'research'" },
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
for (const { content, expected, note } of REF_CASES) {
|
|
38
|
+
test(`referencesAgentSpecificTool: ${expected ? "detects" : "ignores"} "${content.slice(0, 44)}…" (${note})`, () => {
|
|
39
|
+
assert.equal(referencesAgentSpecificTool(content), expected, note);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const EXTRA_POSITIVE: string[] = [
|
|
44
|
+
"The `checkout` command switches branches.",
|
|
45
|
+
"The browser tool navigates to the URL first.",
|
|
46
|
+
"Always run the memory tool after a context switch.",
|
|
47
|
+
"the tool named search returns repo hits",
|
|
48
|
+
"Pi exposes a search tool; OpenClaw exposes a same-named web tool.",
|
|
49
|
+
"Invoke `memory_store` to persist the reflection.",
|
|
50
|
+
"Use search when locating implementation",
|
|
51
|
+
"Run rg before editing",
|
|
52
|
+
"Call memory_store to persist the result",
|
|
53
|
+
"Run grep before editing",
|
|
54
|
+
"Use curl to fetch the URL",
|
|
55
|
+
"The memory_store tool persists results",
|
|
56
|
+
"Use the grep command to search files",
|
|
57
|
+
"Call memory_store() to persist the result",
|
|
58
|
+
"Use search() when locating code",
|
|
59
|
+
"Run rg() to find symbols",
|
|
60
|
+
"Use --force when reinstalling",
|
|
61
|
+
"The --format cli flag selects JSON",
|
|
62
|
+
"Run rg --files before editing",
|
|
63
|
+
"Use grep -n TODO when reviewing",
|
|
64
|
+
"Run `rg --files` before editing",
|
|
65
|
+
"The Bash tool executes commands",
|
|
66
|
+
"Use the Edit tool before saving",
|
|
67
|
+
"The WebFetch tool retrieves pages",
|
|
68
|
+
"Run git status before committing",
|
|
69
|
+
"Use npm install to add dependencies",
|
|
70
|
+
"The Bash Tool executes commands",
|
|
71
|
+
"Run git checkout -b feature before editing",
|
|
72
|
+
"Run `git status` before committing",
|
|
73
|
+
"Use `npm install` to add dependencies",
|
|
74
|
+
"Run `git checkout -b feature` before editing",
|
|
75
|
+
"Use edit before saving",
|
|
76
|
+
"Use bashExecution before editing",
|
|
77
|
+
"The bashExecution tool executes commands",
|
|
78
|
+
"Call bashExecution() to persist data",
|
|
79
|
+
"Use /remnic flush before reset",
|
|
80
|
+
"The /remnic command controls memory",
|
|
81
|
+
"Use `/remnic flush` before reset",
|
|
82
|
+
"Use `search --path src` when locating code",
|
|
83
|
+
];
|
|
84
|
+
for (const content of EXTRA_POSITIVE) {
|
|
85
|
+
test(`referencesAgentSpecificTool detects: "${content.slice(0, 44)}…"`, () => {
|
|
86
|
+
assert.equal(referencesAgentSpecificTool(content), true);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const EXTRA_NEGATIVE: string[] = [
|
|
91
|
+
"Git is a useful tool for version control.",
|
|
92
|
+
"The command line is faster than the GUI for this workflow.",
|
|
93
|
+
"She has a good command of the deployment process.",
|
|
94
|
+
"Communication is the main tool of a leader.",
|
|
95
|
+
"PostgreSQL read replicas lag under heavy load.",
|
|
96
|
+
"Memory usage spikes during compaction.",
|
|
97
|
+
"The browser caches static assets aggressively.",
|
|
98
|
+
"Reading long files is slow.",
|
|
99
|
+
"He writes detailed notes after every meeting.",
|
|
100
|
+
"Leadership requires command of the subject matter.",
|
|
101
|
+
"Writers use memory and emotion to craft stories.",
|
|
102
|
+
"I use search engines daily for research.",
|
|
103
|
+
"Use memory wisely and rest often.",
|
|
104
|
+
"Visit https://example.com/search for docs.",
|
|
105
|
+
"The service reads configuration from /etc/remnic/config.json",
|
|
106
|
+
"Logs live under /var/log/remnic",
|
|
107
|
+
"Restore the backup from /tmp/dump.sql",
|
|
108
|
+
"Use /etc/remnic/config.json for configuration",
|
|
109
|
+
"The service should use /health for readiness checks",
|
|
110
|
+
"Use PostgreSQL for durable storage",
|
|
111
|
+
"Use TypeScript in new packages",
|
|
112
|
+
"Run migrations after deployment",
|
|
113
|
+
"Use Redis for caching",
|
|
114
|
+
"Use 'least privilege' when designing authorization",
|
|
115
|
+
'Run "database migrations" after deployment',
|
|
116
|
+
"Use SQL for analytics",
|
|
117
|
+
"Use API for integration",
|
|
118
|
+
"Use Go for backend services",
|
|
119
|
+
"Use `/etc/remnic/config.json` for configuration",
|
|
120
|
+
"Run `/var/log/remnic` cleanup",
|
|
121
|
+
"Call me before you leave",
|
|
122
|
+
"Use it to simplify the workflow",
|
|
123
|
+
"Run in before deploying",
|
|
124
|
+
"Use 2024-2025 data for the baseline",
|
|
125
|
+
"The tool — a recent addition — is optional",
|
|
126
|
+
"Use `TypeScript` for new packages",
|
|
127
|
+
"Use `config.json` for configuration",
|
|
128
|
+
"Use `/health` for readiness checks",
|
|
129
|
+
"Writers use edit passes",
|
|
130
|
+
"Use macOS for development",
|
|
131
|
+
"Use iOS for mobile apps",
|
|
132
|
+
"Use eBay for purchases",
|
|
133
|
+
"Automation tool reduces repetitive work",
|
|
134
|
+
"Encryption tool selection should follow the threat model",
|
|
135
|
+
"Monitoring tool availability affects reliability",
|
|
136
|
+
"Use user_id in API responses",
|
|
137
|
+
"Use database_name for the connection",
|
|
138
|
+
"Use feature_flag when rolling out changes",
|
|
139
|
+
"Use least-privilege when designing authorization",
|
|
140
|
+
"Use TypeScript-first for new packages",
|
|
141
|
+
];
|
|
142
|
+
for (const content of EXTRA_NEGATIVE) {
|
|
143
|
+
test(`referencesAgentSpecificTool ignores prose: "${content.slice(0, 44)}…"`, () => {
|
|
144
|
+
assert.equal(referencesAgentSpecificTool(content), false);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
test("referencesAgentSpecificTool: empty/non-string input is false (no throw)", () => {
|
|
149
|
+
assert.equal(referencesAgentSpecificTool(""), false);
|
|
150
|
+
assert.equal(referencesAgentSpecificTool(" "), false);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
// shouldPromoteGlobalFactToShared — composed scope-routing predicate.
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
const PROMOTION_CASES: Array<{ name: string; args: { scope: string | null | undefined; content: string; sourceConnector?: string; procedureSteps?: ReadonlyArray<{ intent?: string; toolCall?: { kind?: string; signature?: string } }> }; expected: boolean }> = [
|
|
158
|
+
{ name: "global portable fact, no connector -> promote", args: { scope: "global", content: "User prefers dark mode in all editors" }, expected: true },
|
|
159
|
+
{ name: "global tool-scoped fact WITH connector -> withhold (#2183)", args: { scope: "global", content: "Prefer the search tool when locating code.", sourceConnector: "pi" }, expected: false },
|
|
160
|
+
{ name: "global tool-scoped fact, no knob -> withheld when connector known", args: { scope: "global", content: "The exec tool requires an absolute cwd.", sourceConnector: "openclaw" }, expected: false },
|
|
161
|
+
{ name: "global tool-scoped fact WITHOUT connector -> promote (unattributed)", args: { scope: "global", content: "Prefer the search tool when locating code." }, expected: true },
|
|
162
|
+
{ name: "project-scoped fact -> never promote via this path", args: { scope: "project", content: "Prefer the search tool when locating code.", sourceConnector: "pi" }, expected: false },
|
|
163
|
+
{ name: "empty connector string is treated as unknown -> promote", args: { scope: "global", content: "Prefer the search tool when locating code.", sourceConnector: "" }, expected: true },
|
|
164
|
+
{ name: "global portable-title procedure with tool-bearing steps + connector -> withhold (#2183 P2, scope-routing)", args: { scope: "global", content: "Workflow for locating implementation", sourceConnector: "pi", procedureSteps: [{ intent: "find the symbol", toolCall: { kind: "search", signature: "search('foo')" } }, { intent: "open the file", toolCall: { kind: "read", signature: "read('bar')" } }] }, expected: false },
|
|
165
|
+
];
|
|
166
|
+
for (const { name, args, expected } of PROMOTION_CASES) {
|
|
167
|
+
test(`shouldPromoteGlobalFactToShared: ${name}`, () => {
|
|
168
|
+
assert.equal(shouldPromoteGlobalFactToShared(args), expected);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// withholdToolScopedFromSharedNamespace — the SINGLE primitive every shared-
|
|
174
|
+
// namespace promotion path consults. Includes structured procedure tool
|
|
175
|
+
// identity (issue #2183 P2): a portable title + tool-bearing steps withholds.
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
const WITHHOLD_CASES: Array<{
|
|
179
|
+
name: string;
|
|
180
|
+
args: { content: string; sourceConnector?: string; procedureSteps?: ReadonlyArray<{ intent?: string; toolCall?: { kind?: string; signature?: string } }> };
|
|
181
|
+
expected: boolean;
|
|
182
|
+
}> = [
|
|
183
|
+
{ name: "tool-scoped + connector -> withhold", args: { content: "Prefer the search tool when locating code.", sourceConnector: "pi" }, expected: true },
|
|
184
|
+
{ name: "tool-scoped + empty connector -> do not withhold", args: { content: "Prefer the search tool when locating code.", sourceConnector: "" }, expected: false },
|
|
185
|
+
{ name: "tool-scoped + whitespace-only connector -> do not withhold", args: { content: "Prefer the search tool when locating code.", sourceConnector: " " }, expected: false },
|
|
186
|
+
{ name: "tool-scoped + no connector -> do not withhold", args: { content: "Prefer the search tool when locating code." }, expected: false },
|
|
187
|
+
{ name: "portable + connector -> do not withhold", args: { content: "User prefers dark mode in all editors", sourceConnector: "pi" }, expected: false },
|
|
188
|
+
{ name: "portable title + connector + tool-bearing steps -> withhold (#2183 P2)", args: { content: "When locating implementation", sourceConnector: "pi", procedureSteps: [{ intent: "find", toolCall: { kind: "search", signature: "search('foo')" } }] }, expected: true },
|
|
189
|
+
{ name: "portable title + connector + steps without toolCall -> do not withhold", args: { content: "When locating implementation", sourceConnector: "pi", procedureSteps: [{ intent: "read the docs" }] }, expected: false },
|
|
190
|
+
{ name: "portable title + connector + steps with empty kind -> do not withhold", args: { content: "When locating implementation", sourceConnector: "pi", procedureSteps: [{ intent: "x", toolCall: { kind: " ", signature: "y" } }] }, expected: false },
|
|
191
|
+
{ name: "tool-bearing steps but NO connector -> do not withhold (unattributed)", args: { content: "When locating implementation", procedureSteps: [{ intent: "find", toolCall: { kind: "search", signature: "search('foo')" } }] }, expected: false },
|
|
192
|
+
{ name: "portable title + intent-only tool step + connector -> withhold (#2183 intent text)", args: { content: "Workflow for locating implementation", sourceConnector: "pi", procedureSteps: [{ intent: "Run grep before editing" }] }, expected: true },
|
|
193
|
+
{ name: "portable title + all-portable steps + connector -> do not withhold", args: { content: "Workflow for code review", sourceConnector: "pi", procedureSteps: [{ intent: "Read the changes carefully" }, { intent: "Approve the work" }] }, expected: false },
|
|
194
|
+
];
|
|
195
|
+
for (const { name, args, expected } of WITHHOLD_CASES) {
|
|
196
|
+
test(`withholdToolScopedFromSharedNamespace: ${name}`, () => {
|
|
197
|
+
assert.equal(withholdToolScopedFromSharedNamespace(args), expected);
|
|
198
|
+
});
|
|
199
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool-scoped memory guard (issue #2183).
|
|
3
|
+
*
|
|
4
|
+
* A tool-related fact extracted from a specific agent integration carries an
|
|
5
|
+
* UNQUALIFIED tool name — "use the search tool" — with no agent identity
|
|
6
|
+
* attached (the conversation handed to the extraction LLM is rendered as
|
|
7
|
+
* `[role] content` only). Promoting such a fact to the shared namespace lets a
|
|
8
|
+
* DIFFERENT integration that exposes a same-named but incompatible tool (Pi
|
|
9
|
+
* `search` = repo code search vs OpenClaw `search` = web search) consume it.
|
|
10
|
+
*
|
|
11
|
+
* `referencesAgentSpecificTool` flags fact text that references a specific
|
|
12
|
+
* tool/command invocation rather than portable knowledge. The promotion guard
|
|
13
|
+
* in `orchestration/extraction-persist.ts` uses it — when the producing
|
|
14
|
+
* integration is known (`sourceConnector`) — to keep such a fact in that
|
|
15
|
+
* integration's own namespace.
|
|
16
|
+
*
|
|
17
|
+
* Tuning bias: a FALSE POSITIVE is cheap — the fact stays in the narrower
|
|
18
|
+
* namespace, the documented safe default. A FALSE NEGATIVE is the actual
|
|
19
|
+
* cross-integration collision bug, so detection leans permissive — but it must
|
|
20
|
+
* not fire on ordinary prose.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const BT = "`";
|
|
24
|
+
|
|
25
|
+
// Generic tool names + host-tool names emitted by the agent adapters.
|
|
26
|
+
// Evidence: "edit" — Pi messages.test.ts:43 ({ type: "toolCall", name: "edit" }).
|
|
27
|
+
// "bash" — Pi messages.test.ts:70 (bashExecution tool rendering). "glob",
|
|
28
|
+
// "webfetch", "task" — standard Claude Code tools (Pi wraps Claude Code).
|
|
29
|
+
const GENERIC_TOOL_NAMES = [
|
|
30
|
+
"read", "write", "search", "fetch", "browser", "exec", "shell", "memory",
|
|
31
|
+
"edit", "bash", "glob", "webfetch", "task",
|
|
32
|
+
"remnic",
|
|
33
|
+
];
|
|
34
|
+
const KNOWN_CLI_NAMES = [
|
|
35
|
+
"grep", "curl", "git", "npm", "pnpm", "sed", "awk", "jq", "cat", "ssh",
|
|
36
|
+
"docker", "kubectl", "make", "rg", "ls", "cd",
|
|
37
|
+
];
|
|
38
|
+
const GENERIC_NAMES_ALT = GENERIC_TOOL_NAMES.join("|");
|
|
39
|
+
const KNOWN_NAMES_ALT = [...GENERIC_TOOL_NAMES, ...KNOWN_CLI_NAMES].join("|");
|
|
40
|
+
const CLI_NAMES_ALT = KNOWN_CLI_NAMES.join("|");
|
|
41
|
+
|
|
42
|
+
const FS_ROOTS = "(?:etc|var|usr|tmp|home|opt|srv|proc|dev)";
|
|
43
|
+
const FLAG = "(?:--|-)[A-Za-z][A-Za-z0-9]*(?:-[A-Za-z0-9]+)*\\b";
|
|
44
|
+
const FLAG_NAKED = "(?:--|-)[A-Za-z][A-Za-z0-9]*(?:-[A-Za-z0-9]+)*";
|
|
45
|
+
const OPCODE = "[A-Za-z0-9_./-]{1,20}";
|
|
46
|
+
|
|
47
|
+
const NO_SLASH_TOKEN = "[A-Za-z0-9_.-]{1,60}";
|
|
48
|
+
const SLASH_CMD_TOKEN = "/(?:" + GENERIC_NAMES_ALT + ")\\b";
|
|
49
|
+
const QUOTED_CONTENT = "(?:" + NO_SLASH_TOKEN + "|" + SLASH_CMD_TOKEN + ")";
|
|
50
|
+
const QUOTED_PERMISSIVE =
|
|
51
|
+
"(?:" + BT + QUOTED_CONTENT + BT + "|\"" + QUOTED_CONTENT + "\"|'" + QUOTED_CONTENT + "')";
|
|
52
|
+
|
|
53
|
+
const SNAKE = "(?:[a-z][a-z0-9]*(?:_[a-z0-9]+)+)\\b";
|
|
54
|
+
const SNAKE_NON_DATA = "(?:[a-z][a-z0-9]*(?:_[a-z0-9]+)*(?:_(?!id\\b|name\\b|flag\\b|count\\b|at\\b|url\\b|key\\b|type\\b)[a-z0-9]+))\\b";
|
|
55
|
+
const SNAKE_KEBAB = "(?:[a-z][a-z0-9]*(?:[_-][a-z0-9]+)+)\\b";
|
|
56
|
+
|
|
57
|
+
const KNOWN_NAMES_CI = "\\b(?:" + KNOWN_NAMES_ALT + ")\\b";
|
|
58
|
+
const CLI_NAMES_CI = "\\b(?:" + CLI_NAMES_ALT + ")\\b";
|
|
59
|
+
const KEYWORD = "\\b(?:mcp[ _]tool|slash[ _]command|cli[ _]flag|subcommand|tool|command)\\b";
|
|
60
|
+
|
|
61
|
+
const IDENT = "(?:" + QUOTED_PERMISSIVE + "|" + KNOWN_NAMES_CI + "|" + SNAKE_KEBAB + "|" + FLAG + ")";
|
|
62
|
+
|
|
63
|
+
const IMM = "[\\s'\"" + BT + "]{1,4}";
|
|
64
|
+
const CONN = "\\s+(?:named|called|aka)\\s+";
|
|
65
|
+
const VERB = "\\b(?:use|run|call|invoke)\\b";
|
|
66
|
+
const SLASH_GENERIC = "/(?:" + GENERIC_NAMES_ALT + ")\\b";
|
|
67
|
+
const CLAUSE_BOUNDARY = "(?:$|[.,;(:]|(?:when|before|after|to|with|for|on|in|if|unless|whenever)\\b)";
|
|
68
|
+
const ARGS = "(?:\\s+" + FLAG + "(?:\\s+" + OPCODE + "){0,4})?";
|
|
69
|
+
const CLI_ARG_TOKEN = "(?:[a-z][a-z0-9-]{0,15}|" + FLAG_NAKED + "|" + OPCODE + ")";
|
|
70
|
+
const CLI_ARG_RUN = "(?:\\s+" + CLI_ARG_TOKEN + "){1,4}";
|
|
71
|
+
|
|
72
|
+
// Shared: a CLI tool or slash command optionally followed by a bounded
|
|
73
|
+
// argument run. Used by BOTH the quoted and unquoted invocation arms so
|
|
74
|
+
// quoting cannot diverge from unquoted again (#2183 round 20).
|
|
75
|
+
const CMD_WITH_ARGS = "(?:(?:" + CLI_NAMES_CI + "|" + SLASH_GENERIC + ")" + CLI_ARG_RUN + ")";
|
|
76
|
+
const QSTRICT_CONTENT =
|
|
77
|
+
"(?:" + KNOWN_NAMES_ALT + "|[a-z][a-z0-9]*(?:[_-][a-z0-9]+)+" + "|" + FLAG_NAKED + "|" + SLASH_CMD_TOKEN +
|
|
78
|
+
"|" + CMD_WITH_ARGS + "|(?:" + KNOWN_NAMES_ALT + ")" + CLI_ARG_RUN + ")";
|
|
79
|
+
const QUOTED_STRICT =
|
|
80
|
+
"(?:" + BT + QSTRICT_CONTENT + BT + "|\"" + QSTRICT_CONTENT + "\"|'" + QSTRICT_CONTENT + "')";
|
|
81
|
+
|
|
82
|
+
const INVOCATION_FLAG_ARGS =
|
|
83
|
+
VERB + "\\s+(?:" + QUOTED_STRICT + "|" + KNOWN_NAMES_CI + "|" + SNAKE_NON_DATA + "|" + SLASH_GENERIC + "|" + FLAG + ")" + ARGS + "(?=\\s*" + CLAUSE_BOUNDARY + ")";
|
|
84
|
+
const INVOCATION_CLI_ARGS =
|
|
85
|
+
VERB + "\\s+" + CMD_WITH_ARGS + "(?=\\s*" + CLAUSE_BOUNDARY + ")";
|
|
86
|
+
const INVOCATION = "(?:" + INVOCATION_FLAG_ARGS + "|" + INVOCATION_CLI_ARGS + ")";
|
|
87
|
+
|
|
88
|
+
const TOOL_REFERENCE_CI = new RegExp(
|
|
89
|
+
"(?:" +
|
|
90
|
+
IDENT + IMM + KEYWORD + "|" + KEYWORD + IMM + IDENT + "|" +
|
|
91
|
+
KEYWORD + CONN + IDENT + "|" + IDENT + CONN + KEYWORD + "|" +
|
|
92
|
+
INVOCATION +
|
|
93
|
+
")",
|
|
94
|
+
"i",
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
function hasCapitalisedToolName(content: string): boolean {
|
|
98
|
+
// Structural discriminator: a capitalised identifier beside a tool/command
|
|
99
|
+
// keyword qualifies ONLY when preceded by an article or invocation verb (not
|
|
100
|
+
// sentence-initial), or connected via named/called/aka. This replaces the
|
|
101
|
+
// blocklist approach — a structural rule beats a growing STOP_WORDS set.
|
|
102
|
+
for (const re of [
|
|
103
|
+
/(?:the|a|an|this|that|these|those|use|run|call|invoke)\s+([A-Za-z][A-Za-z0-9]+)\b[\s'`<>]{0,4}\b(?:tool|command|subcommand)\b/gi,
|
|
104
|
+
/\b(?:tool|command|subcommand)\b\s+(?:named|called|aka)\s+([A-Za-z][A-Za-z0-9]+)\b/gi,
|
|
105
|
+
]) {
|
|
106
|
+
re.lastIndex = 0;
|
|
107
|
+
let match;
|
|
108
|
+
while ((match = re.exec(content)) !== null) {
|
|
109
|
+
const w = match[1];
|
|
110
|
+
if (w && w[0] >= "A" && w[0] <= "Z") return true;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function hasCamelCaseToolName(content: string): boolean {
|
|
117
|
+
// Case-sensitive: camelCase (lowercase start, interior uppercase) is a common
|
|
118
|
+
// host-tool naming convention (bashExecution, memoryStore, webFetch). Ordinary
|
|
119
|
+
// English words are never camelCase, and PascalCase (PostgreSQL, TypeScript)
|
|
120
|
+
// starts uppercase so is handled by hasCapitalisedToolName instead.
|
|
121
|
+
const camelRe = /\b([a-z]{3,}[a-z0-9]*[A-Z][a-z][A-Za-z0-9]*)\b/g;
|
|
122
|
+
let m;
|
|
123
|
+
while ((m = camelRe.exec(content)) !== null) {
|
|
124
|
+
const token = m[1];
|
|
125
|
+
const start = m.index!;
|
|
126
|
+
const end = start + token.length;
|
|
127
|
+
const before = content.slice(Math.max(0, start - 20), start);
|
|
128
|
+
const after = content.slice(end);
|
|
129
|
+
// Invocation: preceded by a verb, followed by a clause boundary or call syntax.
|
|
130
|
+
if (/(?:^|\s)(?:use|run|call|invoke)\s*$/i.test(before) &&
|
|
131
|
+
/^(?:\s*(?:$|[.,;(:]|(?:when|before|after|to|with|for|on|in|if|unless|whenever)\b))/i.test(after)) {
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
// Keyword-adjacent: camelCase beside a tool/command keyword.
|
|
135
|
+
if (/^(?:\s*(?:tool|command|subcommand)\b)/i.test(after) ||
|
|
136
|
+
/(?:tool|command|subcommand)\s*$/i.test(before)) {
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function referencesAgentSpecificTool(content: string): boolean {
|
|
144
|
+
if (typeof content !== "string" || content.length === 0) return false;
|
|
145
|
+
return TOOL_REFERENCE_CI.test(content) || hasCapitalisedToolName(content) || hasCamelCaseToolName(content);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface ToolScopeWithholdInputs {
|
|
149
|
+
content: string;
|
|
150
|
+
sourceConnector?: string;
|
|
151
|
+
procedureSteps?: ReadonlyArray<{ intent?: string; expectedOutcome?: string; toolCall?: { kind?: string } }>;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function withholdToolScopedFromSharedNamespace({
|
|
155
|
+
content,
|
|
156
|
+
sourceConnector,
|
|
157
|
+
procedureSteps,
|
|
158
|
+
}: ToolScopeWithholdInputs): boolean {
|
|
159
|
+
if (typeof sourceConnector !== "string" || sourceConnector.trim().length === 0) return false;
|
|
160
|
+
if (referencesAgentSpecificTool(content)) return true;
|
|
161
|
+
if (
|
|
162
|
+
procedureSteps?.some((s) => {
|
|
163
|
+
const kind = s.toolCall?.kind;
|
|
164
|
+
if (typeof kind === "string" && kind.trim().length > 0) return true;
|
|
165
|
+
const stepText = [s.intent, s.expectedOutcome]
|
|
166
|
+
.filter((t): t is string => typeof t === "string" && t.length > 0)
|
|
167
|
+
.join(" ");
|
|
168
|
+
return stepText.length > 0 && referencesAgentSpecificTool(stepText);
|
|
169
|
+
})
|
|
170
|
+
) {
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export interface GlobalFactPromotionInputs {
|
|
177
|
+
scope: string | null | undefined;
|
|
178
|
+
content: string;
|
|
179
|
+
sourceConnector?: string;
|
|
180
|
+
procedureSteps?: ReadonlyArray<{ toolCall?: { kind?: string } }>;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function shouldPromoteGlobalFactToShared(inputs: GlobalFactPromotionInputs): boolean {
|
|
184
|
+
if (inputs.scope !== "global") return false;
|
|
185
|
+
return !withholdToolScopedFromSharedNamespace({
|
|
186
|
+
content: inputs.content,
|
|
187
|
+
sourceConnector: inputs.sourceConnector,
|
|
188
|
+
procedureSteps: inputs.procedureSteps,
|
|
189
|
+
});
|
|
190
|
+
}
|