opencode-swarm 7.130.2 → 7.131.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/cli/{config-doctor-489wej17.js → config-doctor-rjvb0hhn.js} +2 -2
- package/dist/cli/{curation-policy-aaz0hap6.js → curation-policy-f5bybzmh.js} +2 -2
- package/dist/cli/{curator-s2ynewsr.js → curator-kjzqa5m7.js} +13 -13
- package/dist/cli/{curator-llm-factory-mxjcctn8.js → curator-llm-factory-ezq4fakt.js} +13 -13
- package/dist/cli/{evidence-summary-service-hf7qq777.js → evidence-summary-service-jfagmvrh.js} +1 -1
- package/dist/cli/{guardrail-explain-s85kv81r.js → guardrail-explain-n93qh8k6.js} +14 -14
- package/dist/cli/{guardrail-log-2cejc33f.js → guardrail-log-h4tmcjwx.js} +3 -3
- package/dist/cli/{hive-promoter-qdgxajht.js → hive-promoter-mammxe5p.js} +13 -13
- package/dist/cli/{index-jvyn76xe.js → index-01nyrk95.js} +3 -3
- package/dist/cli/{index-kzvwb2se.js → index-03zyn94g.js} +110 -85
- package/dist/cli/{index-xa29259t.js → index-1atz85c8.js} +1 -1
- package/dist/cli/{index-6fqj8fcp.js → index-6vg0qczz.js} +2 -2
- package/dist/cli/{index-zft4hp35.js → index-cnwh164k.js} +1 -1
- package/dist/cli/{index-hjar4crm.js → index-cq22pnd2.js} +1 -1
- package/dist/cli/{index-etdgtk3m.js → index-d2tgc580.js} +2688 -1240
- package/dist/cli/{index-jxha73tm.js → index-fepakwhp.js} +1 -1
- package/dist/cli/{index-5xxgeksy.js → index-hynbzh7s.js} +46 -2
- package/dist/cli/{index-p6qgjkbt.js → index-kbfhyxtv.js} +9 -1
- package/dist/cli/{index-3rnkyb60.js → index-m6zfm7cm.js} +5 -5
- package/dist/cli/{index-s8h2cedv.js → index-pft1qxqh.js} +1 -1
- package/dist/cli/{index-ts9wh1ba.js → index-q68sprcy.js} +60 -8
- package/dist/cli/{index-agte9w1e.js → index-szkswbqf.js} +2 -2
- package/dist/cli/{index-nr5pr54q.js → index-tw33v84d.js} +1 -1
- package/dist/cli/{index-jvwkxz8q.js → index-xhd797bz.js} +15 -15
- package/dist/cli/{index-wy3q73j2.js → index-xv2ydncg.js} +18 -14
- package/dist/cli/index.js +13 -13
- package/dist/cli/{knowledge-escalator-1ypz0266.js → knowledge-escalator-cf1kkmfe.js} +3 -3
- package/dist/cli/{knowledge-events-yvcczmja.js → knowledge-events-p8t5ev44.js} +1 -1
- package/dist/cli/{knowledge-store-h8bp9eje.js → knowledge-store-vpdmcwem.js} +3 -1
- package/dist/cli/{knowledge-validator-sz9v1fdq.js → knowledge-validator-w1t0sw5f.js} +4 -4
- package/dist/cli/{scan-cursor-k9zyt374.js → scan-cursor-z4c51902.js} +2 -2
- package/dist/cli/{schema-fkpn2r1s.js → schema-yqzzfavj.js} +5 -1
- package/dist/cli/{skill-generator-a6a9jxfd.js → skill-generator-aaabj00n.js} +5 -5
- package/dist/commands/consolidate.d.ts +14 -0
- package/dist/commands/curate.d.ts +2 -0
- package/dist/commands/promote.d.ts +25 -0
- package/dist/commands/registry.d.ts +2 -2
- package/dist/config/schema.d.ts +101 -0
- package/dist/consensus/contracts.d.ts +479 -0
- package/dist/consensus/corpus.d.ts +261 -0
- package/dist/consensus/miner.d.ts +200 -0
- package/dist/consensus/public-api.d.ts +42 -0
- package/dist/consensus/store.d.ts +119 -0
- package/dist/evidence/immutable-store.d.ts +86 -0
- package/dist/evidence/manager.d.ts +31 -1
- package/dist/hooks/actionability-predicate.d.ts +36 -0
- package/dist/hooks/curator.d.ts +3 -0
- package/dist/hooks/hive-policy.d.ts +11 -0
- package/dist/hooks/hive-promoter.d.ts +27 -1
- package/dist/hooks/knowledge-dedup-sweep.d.ts +145 -0
- package/dist/hooks/knowledge-injector.d.ts +4 -0
- package/dist/hooks/knowledge-store.d.ts +26 -0
- package/dist/hooks/knowledge-types.d.ts +6 -0
- package/dist/hooks/knowledge-validator.d.ts +10 -14
- package/dist/hooks/micro-reflector.d.ts +106 -2
- package/dist/hooks/realtime-learning-nudge.d.ts +19 -0
- package/dist/index.js +426 -418
- package/dist/knowledge/entry-merge.d.ts +112 -0
- package/dist/knowledge/family-migration.d.ts +6 -2
- package/dist/learning/admission.d.ts +154 -0
- package/dist/learning/candidate-queue.d.ts +178 -0
- package/dist/learning/fingerprint.d.ts +56 -0
- package/dist/learning/prm-pattern-support.d.ts +124 -0
- package/dist/learning/provenance.d.ts +86 -0
- package/dist/prm/index.d.ts +46 -1
- package/dist/services/recommendation-ledger.d.ts +266 -0
- package/dist/services/skill-improver.d.ts +4 -0
- package/dist/services/status-service.d.ts +13 -0
- package/dist/services/trajectory-cluster.d.ts +65 -2
- package/dist/tools/consensus-mine.d.ts +172 -0
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/knowledge-add.d.ts +34 -0
- package/dist/tools/manifest.d.ts +1 -0
- package/dist/tools/tool-metadata.d.ts +4 -0
- package/package.json +1 -1
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Field-level merge helpers for near-duplicate knowledge entries (issue #1821
|
|
3
|
+
* Lane A).
|
|
4
|
+
*
|
|
5
|
+
* Extracted verbatim-in-shape from `family-migration.ts` so the SAME merge
|
|
6
|
+
* semantics back both consumers:
|
|
7
|
+
* - the cohort family migration (`/swarm link` / `/swarm unlink`), via
|
|
8
|
+
* `mergeStoreEntries`, and
|
|
9
|
+
* - the active-store near-duplicate sweep (`hooks/knowledge-dedup-sweep.ts`).
|
|
10
|
+
*
|
|
11
|
+
* A near-duplicate merge is PROVENANCE-PRESERVING, not lossless: the losing
|
|
12
|
+
* entry's id survives in `merged_from`, and every field that represents
|
|
13
|
+
* independently-earned evidence is unioned rather than overwritten. Both sides
|
|
14
|
+
* of a merge are genuine near-duplicates, so a predicate present on only one
|
|
15
|
+
* side was still earned and must survive.
|
|
16
|
+
*
|
|
17
|
+
* ## What this module fixes relative to the pre-#1821 implementation
|
|
18
|
+
*
|
|
19
|
+
* 1. **Actionability carry.** `required_actions`, `forbidden_actions`,
|
|
20
|
+
* `verification_checks`, `applies_to_agents`, `applies_to_tools`,
|
|
21
|
+
* `triggers`, and `source_knowledge_ids` were dropped entirely — a merge
|
|
22
|
+
* could silently turn an actionable directive into an inert lesson. They are
|
|
23
|
+
* now unioned through `dedupeCapped`.
|
|
24
|
+
* 2. **CAS integrity.** Swapping in the loser's longer lesson without
|
|
25
|
+
* recomputing `content_hash` left the CAS token describing the DISCARDED
|
|
26
|
+
* text, so every later authorized curation of a merged entry failed the
|
|
27
|
+
* `transactKnowledgeWithCas` comparison. The hash is now recomputed and
|
|
28
|
+
* `revision` bumped on any lesson swap.
|
|
29
|
+
* 3. **No shared arrays.** `unionConfirmedBy` returned a source array BY
|
|
30
|
+
* REFERENCE when one side was absent, so a surviving winner and an archived
|
|
31
|
+
* loser shared one mutable array. It always copies now.
|
|
32
|
+
* 4. **Symmetric guards.** `source_refs` and `retrieval_outcomes` required BOTH
|
|
33
|
+
* sides to be populated before merging, so a sweep over mixed
|
|
34
|
+
* legacy/enriched entries silently dropped the loser's refs and counters.
|
|
35
|
+
* The one-sided cases are handled.
|
|
36
|
+
* 5. **Tags are unioned under the SAME bounded rule as their five siblings.**
|
|
37
|
+
* See `TAG_UNION_RULE` below.
|
|
38
|
+
*
|
|
39
|
+
* ## TAG_UNION_RULE — the one place tags can still be lost, stated explicitly
|
|
40
|
+
*
|
|
41
|
+
* `tags` is a bounded field: the store's write boundary
|
|
42
|
+
* (`normalizeEntryArraysForWrite` in `knowledge-store.ts`) runs `dedupeCapped`
|
|
43
|
+
* over it at `WRITE_FIELD_CAP` = 20 on every transaction, and `knowledge_add`
|
|
44
|
+
* caps its producer at 20 as well. A merge therefore CANNOT promise to preserve
|
|
45
|
+
* every tag from both sides — two entries at the cap carry up to 40 distinct
|
|
46
|
+
* tags and only 20 can survive. Pretending otherwise is what the pre-fix code
|
|
47
|
+
* did: it unioned with a bare, uncapped, case-SENSITIVE `new Set(...)`, and the
|
|
48
|
+
* write boundary then silently truncated the result to the first 20 — which are
|
|
49
|
+
* the WINNER's, because the winner's tags come first in the union. The loser was
|
|
50
|
+
* archived in the same transaction, so the discarded tags were unrecoverable.
|
|
51
|
+
*
|
|
52
|
+
* The rule this module now implements and pins is:
|
|
53
|
+
*
|
|
54
|
+
* 1. Winner tags are retained FIRST, in their existing order.
|
|
55
|
+
* 2. Loser tags fill the remaining slots, in their existing order.
|
|
56
|
+
* 3. Dedup is CASE-INSENSITIVE and first-spelling-wins (`dedupeCapped`), the
|
|
57
|
+
* same comparison the write boundary uses — so a merge no longer emits a
|
|
58
|
+
* list the very next write would rewrite.
|
|
59
|
+
* 4. Anything past `MERGE_FIELD_CAP` (20) is DROPPED at merge time, visibly,
|
|
60
|
+
* instead of being handed to the write boundary to drop invisibly.
|
|
61
|
+
*
|
|
62
|
+
* Consequence, stated so nobody has to rediscover it: a winner already holding
|
|
63
|
+
* 20 tags absorbs ZERO tags from the loser. That is not new data loss — it is
|
|
64
|
+
* the pre-existing store cap, now applied where it can be reasoned about and
|
|
65
|
+
* asserted. Order is the whole lever: if tag retention priority ever needs to
|
|
66
|
+
* change (e.g. prefer a rarer loser tag over a generic winner tag), it changes
|
|
67
|
+
* HERE, in the argument order of the union, not at the write boundary.
|
|
68
|
+
*
|
|
69
|
+
* Pinned by `tests/unit/hooks/knowledge-dedup-sweep-tag-cap.test.ts`.
|
|
70
|
+
*
|
|
71
|
+
* ## What this module deliberately does NOT change
|
|
72
|
+
*
|
|
73
|
+
* `weightedConfidence` reads POST-merge target state (it runs after
|
|
74
|
+
* `unionConfirmedBy` and `sumRetrievalOutcomes` have already mutated `target`),
|
|
75
|
+
* so the source's evidence is counted on both sides of the ratio. That is a
|
|
76
|
+
* real wart, but the formula defines `/swarm link` cohort-merge semantics and
|
|
77
|
+
* changing it is out of scope for #1821 Lane A. It is pinned by
|
|
78
|
+
* `tests/unit/knowledge/entry-merge-characterization-confidence.test.ts`.
|
|
79
|
+
*/
|
|
80
|
+
import type { KnowledgeEntryBase } from '../hooks/knowledge-types.js';
|
|
81
|
+
/** Field-level union of `src` into `target` (mutates target). */
|
|
82
|
+
export declare function mergeEntryFields(target: KnowledgeEntryBase, src: KnowledgeEntryBase): void;
|
|
83
|
+
/**
|
|
84
|
+
* Union two `confirmed_by` lists, deduping on the identifying triple
|
|
85
|
+
* `phase_number|project_name|confirmed_at`.
|
|
86
|
+
*
|
|
87
|
+
* ALWAYS returns a fresh array. The pre-#1821 short-circuits returned an input
|
|
88
|
+
* array by reference, so a surviving winner and the archived loser it absorbed
|
|
89
|
+
* shared one mutable list — a later `push` on either side corrupted the other.
|
|
90
|
+
*/
|
|
91
|
+
export declare function unionConfirmedBy(a: KnowledgeEntryBase['confirmed_by'], b: KnowledgeEntryBase['confirmed_by']): KnowledgeEntryBase['confirmed_by'];
|
|
92
|
+
/**
|
|
93
|
+
* Sum `src`'s retrieval counters into `target`'s (mutates target).
|
|
94
|
+
*
|
|
95
|
+
* One-sided cases are real on mixed legacy/enriched stores and are handled:
|
|
96
|
+
* a target with no counters ADOPTS a copy of the source's (a copy, so the
|
|
97
|
+
* winner and the archived loser never share one mutable record), and a source
|
|
98
|
+
* with no counters simply contributes nothing. The pre-#1821 guard bailed
|
|
99
|
+
* unless BOTH sides had counters, silently discarding the loser's evidence.
|
|
100
|
+
*/
|
|
101
|
+
export declare function sumRetrievalOutcomes(target: KnowledgeEntryBase, src: KnowledgeEntryBase): void;
|
|
102
|
+
/**
|
|
103
|
+
* Evidence-weighted average of the two confidences.
|
|
104
|
+
*
|
|
105
|
+
* INTENTIONALLY UNCHANGED by #1821 Lane A. It runs AFTER `unionConfirmedBy`
|
|
106
|
+
* and `sumRetrievalOutcomes` have mutated `target`, so `wT` reflects POST-merge
|
|
107
|
+
* state and the source's evidence is counted on both sides of the ratio.
|
|
108
|
+
* Correcting that would change `/swarm link` cohort-merge results, which is out
|
|
109
|
+
* of scope for this issue; the behavior is pinned by the characterization
|
|
110
|
+
* suite.
|
|
111
|
+
*/
|
|
112
|
+
export declare function weightedConfidence(target: KnowledgeEntryBase, src: KnowledgeEntryBase): number;
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
*/
|
|
28
28
|
import type { CounterRollup } from '../hooks/knowledge-events.js';
|
|
29
29
|
import type { KnowledgeEntryBase } from '../hooks/knowledge-types.js';
|
|
30
|
+
import { mergeEntryFields } from './entry-merge.js';
|
|
30
31
|
import { type KnowledgeFamilyMember } from './family-manifest.js';
|
|
31
32
|
/**
|
|
32
33
|
* Lock config for the migration critical section. Re-exported from
|
|
@@ -58,14 +59,17 @@ declare function keySelectorFor(member: KnowledgeFamilyMember): (obj: unknown) =
|
|
|
58
59
|
* confidence becomes an evidence-weighted average; the losing entry's `id` is
|
|
59
60
|
* preserved in `merged_from` for retraction traceability. A `merge` is NOT
|
|
60
61
|
* silent. (Critic C8: this is "provenance-preserving", not "lossless".)
|
|
62
|
+
*
|
|
63
|
+
* The field-level merge itself lives in `./entry-merge.ts` (issue #1821 Lane A)
|
|
64
|
+
* so the active-store near-duplicate sweep in `hooks/knowledge-dedup-sweep.ts`
|
|
65
|
+
* shares EXACTLY these semantics. `DEDUP_THRESHOLD` and this function stay here
|
|
66
|
+
* because they are migration policy, not merge mechanics.
|
|
61
67
|
*/
|
|
62
68
|
declare function mergeStoreEntries(destination: KnowledgeEntryBase[], source: KnowledgeEntryBase[]): {
|
|
63
69
|
merged: KnowledgeEntryBase[];
|
|
64
70
|
added: number;
|
|
65
71
|
skipped: number;
|
|
66
72
|
};
|
|
67
|
-
/** Field-level union of `src` into `target` (mutates target). */
|
|
68
|
-
declare function mergeEntryFields(target: KnowledgeEntryBase, src: KnowledgeEntryBase): void;
|
|
69
73
|
/**
|
|
70
74
|
* Append-union / append-concat: append source lines whose key is not already
|
|
71
75
|
* present on the destination. The key is extracted by `keyOf` — the member's id
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Real-time knowledge admission (issue #1821, Workstream B).
|
|
3
|
+
*
|
|
4
|
+
* `admitCandidate` takes ONE insight candidate all the way into the swarm
|
|
5
|
+
* knowledge store; `drainSessionQueue` runs a bounded batch of them for a live
|
|
6
|
+
* session. Together they close the loop that previously required a phase
|
|
7
|
+
* boundary: a lesson learned at step 3 becomes retrievable at step 40 of the
|
|
8
|
+
* same session.
|
|
9
|
+
*
|
|
10
|
+
* ## The bounding rule (read before touching the await graph)
|
|
11
|
+
*
|
|
12
|
+
* `withTimeout` (`src/utils/timeout.ts`) is a `Promise.race`. A race does NOT
|
|
13
|
+
* cancel the loser. Racing the knowledge-store transaction would therefore be
|
|
14
|
+
* actively harmful: the "timed out" admission keeps running, keeps holding the
|
|
15
|
+
* `.swarm/` directory lock, and the next drain blocks on that lock while the
|
|
16
|
+
* caller has already been told the work finished. So:
|
|
17
|
+
*
|
|
18
|
+
* - the `transactKnowledge` call is NEVER raced or timed out;
|
|
19
|
+
* - only genuinely cancellable work (the optional LLM screening call) is
|
|
20
|
+
* bounded, via a manual `AbortController` + `setTimeout` (see
|
|
21
|
+
* `timeoutSignal` below) + `isAbortError`. This deliberately does NOT use
|
|
22
|
+
* `AbortSignal.timeout(...)` (unlike `micro-reflector.ts`): Bun on Windows
|
|
23
|
+
* has a native bug where that signal's `abort` event never fires when
|
|
24
|
+
* awaited by a plain JS Promise (oven-sh/bun#29546) — exactly the shape
|
|
25
|
+
* `screenCandidate`'s caller uses — which hung this file's own tests for
|
|
26
|
+
* the full CI wall-clock kill on Windows;
|
|
27
|
+
* - `max_drain_wall_time_ms` is enforced BETWEEN candidates — it stops the
|
|
28
|
+
* drain from STARTING new work, and never interrupts work in flight.
|
|
29
|
+
*
|
|
30
|
+
* Concurrency uses `p-limit` (an existing dependency, see
|
|
31
|
+
* `src/evaluation/runner.ts`) rather than a hand-rolled limiter.
|
|
32
|
+
*/
|
|
33
|
+
import type { KnowledgeConfig } from '../config/schema.js';
|
|
34
|
+
import type { CuratorLLMDelegate } from '../hooks/curator.js';
|
|
35
|
+
import type { InsightCandidate } from '../hooks/micro-reflector.js';
|
|
36
|
+
import type { LearningProvenanceV1 } from './provenance.js';
|
|
37
|
+
/** Outcome of a single admission attempt. */
|
|
38
|
+
export type AdmissionOutcome = 'admitted' | 'reinforced' | 'rejected';
|
|
39
|
+
export interface AdmissionResult {
|
|
40
|
+
outcome: AdmissionOutcome;
|
|
41
|
+
/** Present only for `rejected`, and on the two non-rejected paths for tracing. */
|
|
42
|
+
reason?: string;
|
|
43
|
+
/** Id of the entry created or reinforced. */
|
|
44
|
+
entryId?: string;
|
|
45
|
+
/** The `insight:<id>` marker this candidate is identified by. */
|
|
46
|
+
marker: string;
|
|
47
|
+
/** Validated provenance record for the write. */
|
|
48
|
+
provenance?: LearningProvenanceV1;
|
|
49
|
+
}
|
|
50
|
+
/** Everything `admitCandidate` needs that is not the candidate itself. */
|
|
51
|
+
export interface AdmissionDeps {
|
|
52
|
+
knowledgeConfig: KnowledgeConfig;
|
|
53
|
+
projectName: string;
|
|
54
|
+
/**
|
|
55
|
+
* Phase recorded in the entry's `confirmed_by`. NOTE: this value is NOT used
|
|
56
|
+
* for idempotency — the fold-in resolves phase numbers differently across its
|
|
57
|
+
* five callers, which is exactly why D1 keys on candidate identity instead.
|
|
58
|
+
*/
|
|
59
|
+
phaseNumber: number;
|
|
60
|
+
sessionID?: string;
|
|
61
|
+
/** Optional screening delegate. Absent → admission runs with no LLM call. */
|
|
62
|
+
llmDelegate?: CuratorLLMDelegate;
|
|
63
|
+
/** Per-candidate LLM deadline. 0 or absent disables the bound (and the call). */
|
|
64
|
+
llmTimeoutMs?: number;
|
|
65
|
+
/** Per-session LLM ceilings. Absent → no screening call is made. */
|
|
66
|
+
llmBudget?: {
|
|
67
|
+
maxLlmCallsPerSession: number;
|
|
68
|
+
maxTokensPerSession: number;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Milliseconds left in the enclosing drain's wall-clock budget. Supplied by
|
|
72
|
+
* `drainSessionQueue` so a screening call can never outlive the budget that
|
|
73
|
+
* is supposed to bound the whole drain. Absent → unbounded by the drain.
|
|
74
|
+
*/
|
|
75
|
+
remainingBudgetMs?: () => number;
|
|
76
|
+
/** Called after a write actually changed the store — bumps injector caches. */
|
|
77
|
+
onKnowledgeChanged?: () => void;
|
|
78
|
+
/** Injectable clock/path seams for tests. */
|
|
79
|
+
now?: () => number;
|
|
80
|
+
resolveKnowledgePath?: (directory: string) => string;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Admit one candidate into the swarm knowledge store.
|
|
84
|
+
*
|
|
85
|
+
* Order: shape gate → actionability gate → near-duplicate lookup → reinforce or
|
|
86
|
+
* append, all inside ONE `transactKnowledge` so the dedup decision and the
|
|
87
|
+
* write cannot be separated by a concurrent writer.
|
|
88
|
+
*/
|
|
89
|
+
export declare function admitCandidate(directory: string, candidate: InsightCandidate, deps: AdmissionDeps): Promise<AdmissionResult>;
|
|
90
|
+
/** The subset of a `tool.execute.after` input the drain adapter reads. */
|
|
91
|
+
export interface RealtimeAdmissionInput {
|
|
92
|
+
tool: unknown;
|
|
93
|
+
sessionID?: unknown;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* `tool.execute.after` adapter for real-time admission.
|
|
97
|
+
*
|
|
98
|
+
* Called UNCONDITIONALLY from the plugin hook chain and SELF-GATES here, in
|
|
99
|
+
* this order:
|
|
100
|
+
*
|
|
101
|
+
* 1. non-`Task` tool → return immediately (mirrors `micro-reflector.ts`);
|
|
102
|
+
* 2. feature disabled → return;
|
|
103
|
+
* 3. no session id → return;
|
|
104
|
+
* 4. empty queue → return (an O(1) `Map.get(...).length` probe).
|
|
105
|
+
*
|
|
106
|
+
* Only past gate 4 does anything touch the filesystem. This ordering is the
|
|
107
|
+
* contract that keeps the hook off the hot path: it is awaited on EVERY tool
|
|
108
|
+
* call, so the non-`Task` path must cost a single string comparison and
|
|
109
|
+
* perform NO knowledge-store read, write, or lock acquisition.
|
|
110
|
+
*
|
|
111
|
+
* `resolveDeps` is a FACTORY, not a value, so resolving the plan (project name
|
|
112
|
+
* and phase number) happens only after all four gates pass.
|
|
113
|
+
*/
|
|
114
|
+
export declare function realtimeAdmissionAfter(directory: string, input: RealtimeAdmissionInput, config: RealtimeAdmissionConfig | undefined, resolveDeps: () => AdmissionDeps | Promise<AdmissionDeps>): Promise<DrainSummary | undefined>;
|
|
115
|
+
/** Per-drain tallies. */
|
|
116
|
+
export interface DrainSummary {
|
|
117
|
+
attempted: number;
|
|
118
|
+
admitted: number;
|
|
119
|
+
reinforced: number;
|
|
120
|
+
rejected: number;
|
|
121
|
+
/** Candidates put back because the wall-clock budget ran out first. */
|
|
122
|
+
deferred: number;
|
|
123
|
+
/** Candidates abandoned after exhausting their retry budget. */
|
|
124
|
+
failed: number;
|
|
125
|
+
retries: number;
|
|
126
|
+
}
|
|
127
|
+
/** `learning.realtime_admission`, as consumed by the drain. */
|
|
128
|
+
export type RealtimeAdmissionConfig = {
|
|
129
|
+
enabled: boolean;
|
|
130
|
+
max_queue_size: number;
|
|
131
|
+
min_drain: number;
|
|
132
|
+
max_drain: number;
|
|
133
|
+
drain_depth_factor: number;
|
|
134
|
+
drain_velocity_factor: number;
|
|
135
|
+
max_llm_calls_per_session: number;
|
|
136
|
+
max_tokens_per_session: number;
|
|
137
|
+
max_concurrent_admissions: number;
|
|
138
|
+
max_retries_per_candidate: number;
|
|
139
|
+
per_candidate_llm_timeout_ms: number;
|
|
140
|
+
max_drain_wall_time_ms: number;
|
|
141
|
+
supersede_nudge: boolean;
|
|
142
|
+
};
|
|
143
|
+
/**
|
|
144
|
+
* Drain a bounded, adaptively-sized batch of a session's pending candidates.
|
|
145
|
+
*
|
|
146
|
+
* Bounds, in the order they apply:
|
|
147
|
+
* 1. `computeDrainSize` caps how many candidates are claimed at all;
|
|
148
|
+
* 2. `p-limit` caps how many run concurrently;
|
|
149
|
+
* 3. `max_retries_per_candidate` caps rework per candidate;
|
|
150
|
+
* 4. `max_drain_wall_time_ms` stops NEW candidates from starting.
|
|
151
|
+
*
|
|
152
|
+
* Never throws: a drain is best-effort background work behind a live tool call.
|
|
153
|
+
*/
|
|
154
|
+
export declare function drainSessionQueue(directory: string, sessionID: string, config: RealtimeAdmissionConfig, deps: AdmissionDeps): Promise<DrainSummary>;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-keyed bounded candidate queue (issue #1821, Workstream B).
|
|
3
|
+
*
|
|
4
|
+
* The micro-reflector emits insight candidates while a session is live. Before
|
|
5
|
+
* this module they were only readable at a phase boundary, so a lesson learned
|
|
6
|
+
* in step 3 could not help step 40 of the same session. This queue is the
|
|
7
|
+
* in-memory hand-off that makes same-session admission possible: the reflector
|
|
8
|
+
* enqueues, and the `Task`-tool drain adapter admits.
|
|
9
|
+
*
|
|
10
|
+
* Everything here is bounded on purpose (AGENTS.md invariant 8):
|
|
11
|
+
*
|
|
12
|
+
* - the KEY count is capped at `MAX_TRACKED_SESSIONS` with FIFO eviction,
|
|
13
|
+
* mirroring `recentToolCallsBySession` in `src/hooks/adversarial-detector.ts`;
|
|
14
|
+
* - each session's item list is capped at `max_queue_size` with drop-oldest
|
|
15
|
+
* plus a `dropped` counter, so a flood is observable rather than silent;
|
|
16
|
+
* - the LLM-call, token, and retry budgets are per-session ceilings that only
|
|
17
|
+
* ever count up within a session and are released wholesale on reset.
|
|
18
|
+
*
|
|
19
|
+
* This module performs NO I/O and holds no clock dependency beyond `Date.now`
|
|
20
|
+
* (injectable through `_internals` for deterministic tests). The admission side
|
|
21
|
+
* effects live in `./admission.ts`; keeping them apart lets the queue be tested
|
|
22
|
+
* without touching the knowledge store.
|
|
23
|
+
*/
|
|
24
|
+
import type { InsightCandidate } from '../hooks/micro-reflector.js';
|
|
25
|
+
/**
|
|
26
|
+
* Hard ceiling on how many drain cycles a single candidate may be claimed by.
|
|
27
|
+
*
|
|
28
|
+
* A candidate deferred by the wall-clock budget is requeued for the next drain.
|
|
29
|
+
* Without this bound a session whose budget always expires would bounce the same
|
|
30
|
+
* candidate forever, so the per-candidate retry cap would be satisfied while the
|
|
31
|
+
* queue still never made progress (AGENTS.md invariant 8).
|
|
32
|
+
*/
|
|
33
|
+
export declare const MAX_CANDIDATE_DRAIN_ATTEMPTS = 5;
|
|
34
|
+
/** Per-session ceilings applied when a candidate is enqueued or budget is spent. */
|
|
35
|
+
export interface CandidateQueueLimits {
|
|
36
|
+
/** Hard cap on pending candidates; the OLDEST is dropped past this. */
|
|
37
|
+
maxQueueSize: number;
|
|
38
|
+
/** Per-session ceiling on admission LLM calls. 0 disables LLM admission. */
|
|
39
|
+
maxLlmCallsPerSession: number;
|
|
40
|
+
/** Per-session ceiling on admission tokens. */
|
|
41
|
+
maxTokensPerSession: number;
|
|
42
|
+
}
|
|
43
|
+
/** Inputs to the adaptive drain sizer. Mirrors `learning.realtime_admission`. */
|
|
44
|
+
export interface DrainSizeLimits {
|
|
45
|
+
/** Minimum candidates drained per cycle. */
|
|
46
|
+
minDrain: number;
|
|
47
|
+
/** Maximum candidates drained per cycle. */
|
|
48
|
+
maxDrain: number;
|
|
49
|
+
/** Weight of queue depth when sizing a drain (0 = ignore depth). */
|
|
50
|
+
drainDepthFactor: number;
|
|
51
|
+
/** Weight of arrival velocity when sizing a drain (0 = ignore rate). */
|
|
52
|
+
drainVelocityFactor: number;
|
|
53
|
+
}
|
|
54
|
+
/** One queued candidate plus the bookkeeping the drain needs. */
|
|
55
|
+
export interface QueuedCandidate {
|
|
56
|
+
candidate: InsightCandidate;
|
|
57
|
+
/** Epoch millis the candidate entered the queue. */
|
|
58
|
+
enqueuedAt: number;
|
|
59
|
+
/** How many drain attempts this candidate has already survived. */
|
|
60
|
+
attempts: number;
|
|
61
|
+
}
|
|
62
|
+
/** Read-only projection of a session's queue state. Test/observability seam. */
|
|
63
|
+
export interface QueueStats {
|
|
64
|
+
depth: number;
|
|
65
|
+
dropped: number;
|
|
66
|
+
llmCallsUsed: number;
|
|
67
|
+
tokensUsed: number;
|
|
68
|
+
retriesUsed: number;
|
|
69
|
+
lastDrainAt: number;
|
|
70
|
+
enqueuedSinceLastDrain: number;
|
|
71
|
+
}
|
|
72
|
+
export interface EnqueueResult {
|
|
73
|
+
/** False only when the candidate was rejected outright (invalid session id). */
|
|
74
|
+
enqueued: boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Cumulative drop count for the session.
|
|
77
|
+
*
|
|
78
|
+
* There is deliberately no `depth` here. It was returned and read by nobody —
|
|
79
|
+
* not by `micro-reflector.ts`, not by `prm/index.ts` (both discard this whole
|
|
80
|
+
* value), and not by a test. The drain path that genuinely needs a depth
|
|
81
|
+
* already asks for it at the moment it drains, via `getQueueStats(...).depth`;
|
|
82
|
+
* a depth captured at enqueue time is stale by then. Reporting a number
|
|
83
|
+
* nothing consumes is unwired code (issue #1821).
|
|
84
|
+
*/
|
|
85
|
+
dropped: number;
|
|
86
|
+
/** True when this enqueue evicted the oldest pending candidate. */
|
|
87
|
+
evictedOldest: boolean;
|
|
88
|
+
}
|
|
89
|
+
/** Injectable clock — tests pin time instead of sleeping. */
|
|
90
|
+
export declare const _internals: {
|
|
91
|
+
now: () => number;
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Enqueue one candidate for same-session admission.
|
|
95
|
+
*
|
|
96
|
+
* Drop-oldest (not drop-newest) on overflow: a flood of candidates is far more
|
|
97
|
+
* likely to be a repeating failure mode, and the NEWEST observation is the one
|
|
98
|
+
* most likely to reflect the agent's current state. The `dropped` counter makes
|
|
99
|
+
* the loss observable to `getQueueStats` rather than silent.
|
|
100
|
+
*/
|
|
101
|
+
export declare function enqueueCandidate(sessionID: string, candidate: InsightCandidate, limits: Pick<CandidateQueueLimits, 'maxQueueSize'>): EnqueueResult;
|
|
102
|
+
/**
|
|
103
|
+
* Adaptive drain size (AC9). Pure: no module state is read, so it is unit
|
|
104
|
+
* testable in isolation.
|
|
105
|
+
*
|
|
106
|
+
* size = ceil(minDrain + depthFactor * depth + velocityFactor * velocity)
|
|
107
|
+
*
|
|
108
|
+
* then clamped into `[minDrain, maxDrain]` and finally capped by the actual
|
|
109
|
+
* depth — a drain never claims more than exists. `ceil` (rather than `round`)
|
|
110
|
+
* biases marginally toward draining, because a candidate left in the queue is
|
|
111
|
+
* a lesson the current session cannot use yet.
|
|
112
|
+
*
|
|
113
|
+
* @param depth Pending candidates in the queue.
|
|
114
|
+
* @param velocity Arrivals per second since the last drain.
|
|
115
|
+
*/
|
|
116
|
+
export declare function computeDrainSize(depth: number, velocity: number, limits: DrainSizeLimits): number;
|
|
117
|
+
declare function clampUnit(value: unknown): number;
|
|
118
|
+
/**
|
|
119
|
+
* Arrivals per second since the session's last drain. Returns 0 for a session
|
|
120
|
+
* that has never been drained within the same millisecond as its first
|
|
121
|
+
* enqueue, so velocity can never be infinite.
|
|
122
|
+
*/
|
|
123
|
+
export declare function computeArrivalVelocity(stats: QueueStats, now: number): number;
|
|
124
|
+
/**
|
|
125
|
+
* Remove and return up to `size` oldest candidates, marking the drain point.
|
|
126
|
+
* Resets the velocity numerator so the next cycle measures a fresh interval.
|
|
127
|
+
*/
|
|
128
|
+
export declare function takeDrainBatch(sessionID: string, size: number): QueuedCandidate[];
|
|
129
|
+
/**
|
|
130
|
+
* Return an unfinished candidate to the FRONT of the queue so the next drain
|
|
131
|
+
* retries it before newer arrivals. Respects `maxQueueSize` by dropping the
|
|
132
|
+
* newest tail entry rather than the requeued item — the item already consumed
|
|
133
|
+
* budget, so discarding it would waste that spend.
|
|
134
|
+
*/
|
|
135
|
+
export declare function requeueCandidate(sessionID: string, item: QueuedCandidate, limits: Pick<CandidateQueueLimits, 'maxQueueSize'>): void;
|
|
136
|
+
/** O(1) pending-work probe used by the hot-path drain adapter. */
|
|
137
|
+
export declare function getQueueDepth(sessionID?: string): number;
|
|
138
|
+
/**
|
|
139
|
+
* Reserve LLM budget for one admission call. Returns false when either the
|
|
140
|
+
* call ceiling or the token ceiling would be exceeded; the caller then admits
|
|
141
|
+
* without LLM assistance rather than failing the candidate.
|
|
142
|
+
*/
|
|
143
|
+
export declare function reserveLlmBudget(sessionID: string, estimatedTokens: number, limits: Pick<CandidateQueueLimits, 'maxLlmCallsPerSession' | 'maxTokensPerSession'>): boolean;
|
|
144
|
+
/**
|
|
145
|
+
* Record that a retry was spent, returning the session's running total.
|
|
146
|
+
*
|
|
147
|
+
* The BOUND lives on the admission side, per candidate
|
|
148
|
+
* (`max_retries_per_candidate`); this counter is the session-wide observability
|
|
149
|
+
* view. Total retries per drain is therefore bounded by
|
|
150
|
+
* `max_retries_per_candidate * batchSize`, and `batchSize` is itself bounded by
|
|
151
|
+
* `max_drain`, so the product is bounded without a second cap here.
|
|
152
|
+
*/
|
|
153
|
+
export declare function recordRetry(sessionID: string): number;
|
|
154
|
+
/** Read-only stats for a session. Returns a zeroed record for unknown ids. */
|
|
155
|
+
export declare function getQueueStats(sessionID?: string): QueueStats;
|
|
156
|
+
/** Drop one session's queue, or every session when `sessionID` is omitted. */
|
|
157
|
+
export declare function resetSessionQueue(sessionID?: string): void;
|
|
158
|
+
/** Number of distinct sessions currently tracked. Bound-eviction test seam. */
|
|
159
|
+
declare function getTrackedSessionCount(): number;
|
|
160
|
+
/**
|
|
161
|
+
* Tier-0 pure-function seam (see the writing-tests skill).
|
|
162
|
+
*
|
|
163
|
+
* `clampUnit` is defensive: `computeDrainSize` caps its result by the actual
|
|
164
|
+
* depth, so an un-clamped factor is not observable through the public function
|
|
165
|
+
* and could be deleted with the whole suite green. Exporting it here lets the
|
|
166
|
+
* clamp be asserted directly instead of being untestable dead defence.
|
|
167
|
+
*
|
|
168
|
+
* `MAX_TRACKED_SESSIONS` and `getTrackedSessionCount` live here for the same
|
|
169
|
+
* reason and were moved behind the seam by the issue #1821 dead-export pass: the
|
|
170
|
+
* eviction bound has no production reader and the count probe has no production
|
|
171
|
+
* caller, so as bare exports they were indistinguishable from public API.
|
|
172
|
+
*/
|
|
173
|
+
export declare const _test_exports: {
|
|
174
|
+
clampUnit: typeof clampUnit;
|
|
175
|
+
getTrackedSessionCount: typeof getTrackedSessionCount;
|
|
176
|
+
MAX_TRACKED_SESSIONS: number;
|
|
177
|
+
};
|
|
178
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable recommendation fingerprints (issue #1821, Lane 0a).
|
|
3
|
+
*
|
|
4
|
+
* The curator sweep, the skill improver, and the consensus miner all emit
|
|
5
|
+
* learning recommendations. Without a shared identity function the three
|
|
6
|
+
* producers happily re-propose the same lesson under three different ids and
|
|
7
|
+
* the knowledge store accumulates triplicates. `computeRecommendationFingerprint`
|
|
8
|
+
* gives every producer one canonical id so downstream dedup/supersede logic can
|
|
9
|
+
* compare recommendations across mechanisms.
|
|
10
|
+
*
|
|
11
|
+
* This module is intentionally pure: no filesystem, no network, no clock, no
|
|
12
|
+
* `process.cwd()`, no module-level mutable state (AGENTS.md invariants 1 and 8).
|
|
13
|
+
* The only dependency is the canonical JSON hasher shared with the evaluation
|
|
14
|
+
* subsystem, so a fingerprint computed in a hook matches one computed in a tool.
|
|
15
|
+
*/
|
|
16
|
+
/** Which learning mechanism produced a recommendation. */
|
|
17
|
+
export type RecommendationKind = 'curator' | 'improver' | 'miner';
|
|
18
|
+
export interface RecommendationFingerprintInput {
|
|
19
|
+
/** Producing mechanism. Two mechanisms proposing the same statement about the
|
|
20
|
+
* same target intentionally fingerprint differently — dedup across mechanisms
|
|
21
|
+
* is a policy decision made by the caller, not baked into the identity. */
|
|
22
|
+
kind: RecommendationKind;
|
|
23
|
+
/** What the recommendation is about (skill slug, knowledge id, file path, …). */
|
|
24
|
+
target: string;
|
|
25
|
+
/** The human-readable recommendation body. Normalized before hashing. */
|
|
26
|
+
statement: string;
|
|
27
|
+
/** Optional scope keys (e.g. `stableScopeKey` outputs). Order-insensitive. */
|
|
28
|
+
scopeKeys?: string[];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Normalize a recommendation statement for identity purposes.
|
|
32
|
+
*
|
|
33
|
+
* Mirrors `normalizeMemoryText` in `src/memory/schema.ts` (whitespace-run
|
|
34
|
+
* collapse + trim) and the `.toLowerCase()` that `computeMemoryContentHash`
|
|
35
|
+
* applies on top of it, then additionally strips trailing sentence punctuation
|
|
36
|
+
* so "Prefer DI over mock.module." and "prefer DI over mock.module" collapse to
|
|
37
|
+
* one identity.
|
|
38
|
+
*/
|
|
39
|
+
export declare function normalizeRecommendationStatement(statement: string): string;
|
|
40
|
+
/**
|
|
41
|
+
* Deduplicate and sort scope keys so caller-side ordering can never change the
|
|
42
|
+
* fingerprint. Empty/whitespace-only keys are dropped: they carry no scope
|
|
43
|
+
* information and would otherwise split identity for two equivalent inputs.
|
|
44
|
+
*/
|
|
45
|
+
export declare function normalizeScopeKeys(scopeKeys?: string[]): string[];
|
|
46
|
+
/**
|
|
47
|
+
* Compute the stable fingerprint for a learning recommendation.
|
|
48
|
+
*
|
|
49
|
+
* Returns `lrec_` followed by the first 16 hex characters of the canonical
|
|
50
|
+
* sha256 of `{ kind, target, normalizedStatement, sortedScopeKeys }`. Because
|
|
51
|
+
* `canonicalHash` sorts object keys, the shape of the input literal cannot
|
|
52
|
+
* change the result.
|
|
53
|
+
*/
|
|
54
|
+
export declare function computeRecommendationFingerprint(input: RecommendationFingerprintInput): string;
|
|
55
|
+
/** Shape check for a value that claims to be a recommendation fingerprint. */
|
|
56
|
+
export declare function isRecommendationFingerprint(value: string): boolean;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PRM pattern persistence support tracking (issue #1821, AC10).
|
|
3
|
+
*
|
|
4
|
+
* PRM already detects recurring failure patterns (repetition loops, ping-pong,
|
|
5
|
+
* context thrash, …) and injects a course correction. Those detections were
|
|
6
|
+
* purely ephemeral: the same crew rediscovered the same pattern next session.
|
|
7
|
+
* This module decides WHEN a detected pattern has earned a durable knowledge
|
|
8
|
+
* entry, and turns it into an insight candidate for the normal bounded
|
|
9
|
+
* admission path.
|
|
10
|
+
*
|
|
11
|
+
* Three properties matter, and each one is a defence against a specific way
|
|
12
|
+
* this could go wrong:
|
|
13
|
+
*
|
|
14
|
+
* 1. **Identity** is `pattern|sortedAgents|sortedTargets`. Two different
|
|
15
|
+
* repetition loops in different files are different lessons.
|
|
16
|
+
* 2. **Support counts DISTINCT OCCURRENCE STARTS.** PRM re-reports a live
|
|
17
|
+
* pattern on every subsequent tool call, and `pattern-detector.ts` emits
|
|
18
|
+
* `stepRange: [startStep, endStep]` with an ADVANCING `endStep` — so
|
|
19
|
+
* [1,2], [1,3], [1,4] are all the SAME incident. Counting whole ranges would
|
|
20
|
+
* let one continuous loop self-confirm to the threshold in seconds, so
|
|
21
|
+
* support is keyed on `stepRange[0]` instead.
|
|
22
|
+
* 3. **Evidence is POINTERS ONLY** — `prm:<sid>:<pattern>:<start>-<end>`.
|
|
23
|
+
* Transcript text and model reasoning are NEVER copied into a durable
|
|
24
|
+
* record; a knowledge entry is shared across sessions and must not carry
|
|
25
|
+
* another session's content.
|
|
26
|
+
*
|
|
27
|
+
* Module state follows the same bounded shape as `./candidate-queue.ts`: FIFO
|
|
28
|
+
* key eviction at `MAX_TRACKED_SESSIONS` plus a per-session identity cap
|
|
29
|
+
* (AGENTS.md invariant 8). No I/O happens here — this runs on the PRM
|
|
30
|
+
* `toolAfter` hot path.
|
|
31
|
+
*/
|
|
32
|
+
import type { InsightCandidate } from '../hooks/micro-reflector.js';
|
|
33
|
+
import type { PatternMatch } from '../prm/types.js';
|
|
34
|
+
/** Tuning knobs, mirroring `learning.prm_persistence`. */
|
|
35
|
+
export interface PrmSupportLimits {
|
|
36
|
+
/** Distinct observations required before a pattern is persistable. */
|
|
37
|
+
minSupport: number;
|
|
38
|
+
/** Cooldown between persists for one identity, in milliseconds. */
|
|
39
|
+
cooldownMs: number;
|
|
40
|
+
}
|
|
41
|
+
export interface PrmObservationResult {
|
|
42
|
+
identity: string;
|
|
43
|
+
/** Number of DISTINCT occurrence starts observed for this identity. */
|
|
44
|
+
support: number;
|
|
45
|
+
/** True when support met the threshold AND the cooldown has elapsed. */
|
|
46
|
+
persistable: boolean;
|
|
47
|
+
/** Why `persistable` is false. Absent when it is true. */
|
|
48
|
+
reason?: 'below_support' | 'cooling_down' | 'unactionable';
|
|
49
|
+
/** Bounded evidence pointers. Never contains transcript or reasoning text. */
|
|
50
|
+
evidenceRefs: string[];
|
|
51
|
+
/** Present only when `persistable` — ready for `enqueueCandidate`. */
|
|
52
|
+
candidate?: InsightCandidate;
|
|
53
|
+
}
|
|
54
|
+
/** Injectable clock — tests pin time instead of sleeping. */
|
|
55
|
+
export declare const _internals: {
|
|
56
|
+
now: () => number;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Stable identity for a detected pattern.
|
|
60
|
+
*
|
|
61
|
+
* Agents and targets are sorted so detection order cannot mint a second
|
|
62
|
+
* identity for the same underlying situation.
|
|
63
|
+
*/
|
|
64
|
+
declare function computePatternIdentity(match: Pick<PatternMatch, 'pattern' | 'affectedAgents' | 'affectedTargets'>): string;
|
|
65
|
+
/**
|
|
66
|
+
* Build ONE evidence pointer. Deliberately carries only the session id, the
|
|
67
|
+
* pattern name, and the step window — enough to re-derive the evidence from
|
|
68
|
+
* that session's own trajectory, and nothing that leaks its content.
|
|
69
|
+
*/
|
|
70
|
+
declare function buildPrmEvidenceRef(sessionID: string, pattern: string, stepRange: [number, number]): string;
|
|
71
|
+
/**
|
|
72
|
+
* Build the durable lesson for a supported pattern.
|
|
73
|
+
*
|
|
74
|
+
* Returns undefined when the pattern yields no usable scope — an entry with no
|
|
75
|
+
* `applies_to_agents` / `applies_to_tools` fails the Layer-5 actionability gate
|
|
76
|
+
* and would only be quarantined, so it is better not to emit it at all.
|
|
77
|
+
*/
|
|
78
|
+
declare function buildPrmPatternCandidate(match: PatternMatch, evidenceRefs: string[], createdAt: string): InsightCandidate | undefined;
|
|
79
|
+
/**
|
|
80
|
+
* Record one PRM pattern detection and report whether it now warrants a durable
|
|
81
|
+
* knowledge entry.
|
|
82
|
+
*
|
|
83
|
+
* MUST be called only after PRM's `matches.length === 0` early return, so the
|
|
84
|
+
* overwhelmingly common no-match path costs nothing.
|
|
85
|
+
*/
|
|
86
|
+
export declare function recordPatternObservation(sessionID: string, match: PatternMatch, limits: PrmSupportLimits): PrmObservationResult;
|
|
87
|
+
/** Read-only support view for one identity. Test/observability seam. */
|
|
88
|
+
declare function getPatternSupport(sessionID: string, identity: string): {
|
|
89
|
+
support: number;
|
|
90
|
+
lastPersistedAt: number;
|
|
91
|
+
occurrenceStarts: number[];
|
|
92
|
+
};
|
|
93
|
+
/** Drop one session's support state, or every session when omitted. */
|
|
94
|
+
export declare function resetPrmPatternSupport(sessionID?: string): void;
|
|
95
|
+
/** Number of distinct sessions tracked. Bound-eviction test seam. */
|
|
96
|
+
declare function getTrackedPrmSessionCount(): number;
|
|
97
|
+
/** Number of distinct identities tracked for a session. */
|
|
98
|
+
declare function getTrackedPrmIdentityCount(sessionID: string): number;
|
|
99
|
+
/**
|
|
100
|
+
* Tier-0 pure-function and observability seam (see the writing-tests skill, and
|
|
101
|
+
* the sibling seam in `candidate-queue.ts`).
|
|
102
|
+
*
|
|
103
|
+
* Everything here was a bare `export` with no importer outside the tests — the
|
|
104
|
+
* three caps, the three pure builders, and the three read-only probes. The issue
|
|
105
|
+
* #1821 dead-export pass moved them behind this seam rather than deleting them:
|
|
106
|
+
* each is genuinely needed to assert a bound or a pure mapping that the public
|
|
107
|
+
* `recordPatternObservation` path only exercises indirectly, but as bare exports
|
|
108
|
+
* they were indistinguishable from public API and read as unwired code.
|
|
109
|
+
*
|
|
110
|
+
* `_internals` stays separate and stays small: it is the DI seam tests
|
|
111
|
+
* SUBSTITUTE (the clock). Nothing below is substitutable.
|
|
112
|
+
*/
|
|
113
|
+
export declare const _test_exports: {
|
|
114
|
+
buildPrmEvidenceRef: typeof buildPrmEvidenceRef;
|
|
115
|
+
buildPrmPatternCandidate: typeof buildPrmPatternCandidate;
|
|
116
|
+
computePatternIdentity: typeof computePatternIdentity;
|
|
117
|
+
getPatternSupport: typeof getPatternSupport;
|
|
118
|
+
getTrackedPrmIdentityCount: typeof getTrackedPrmIdentityCount;
|
|
119
|
+
getTrackedPrmSessionCount: typeof getTrackedPrmSessionCount;
|
|
120
|
+
MAX_IDENTITIES_PER_SESSION: number;
|
|
121
|
+
MAX_OCCURRENCES_PER_IDENTITY: number;
|
|
122
|
+
MAX_TRACKED_SESSIONS: number;
|
|
123
|
+
};
|
|
124
|
+
export {};
|