opencode-swarm 7.104.0 → 7.106.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/.opencode/skills/commit-pr/SKILL.md +15 -7
- package/.opencode/skills/swarm-pr-feedback/SKILL.md +7 -0
- package/.opencode/skills/swarm-pr-review/SKILL.md +6 -0
- package/.opencode/skills/swarm-pr-subscribe/SKILL.md +176 -0
- package/dist/background/pr-event-delivery.d.ts +107 -0
- package/dist/background/pr-event-subscribers.d.ts +36 -7
- package/dist/cli/{config-doctor-5m8z8jeg.js → config-doctor-cntnpyvj.js} +2 -2
- package/dist/cli/{guardrail-explain-2zat1bxh.js → guardrail-explain-fvjc90wa.js} +5 -5
- package/dist/cli/{guardrail-log-zhshjbpk.js → guardrail-log-fsc85hwa.js} +3 -3
- package/dist/cli/{index-t1vfvtg9.js → index-2560cctg.js} +34 -1
- package/dist/cli/{index-fk8ta8zw.js → index-7dtekwvd.js} +2 -2
- package/dist/cli/{index-5tywmqeh.js → index-esg1554h.js} +1 -1
- package/dist/cli/{index-n3ys2eny.js → index-f4yjypa4.js} +21113 -20999
- package/dist/cli/{index-kcbcy6wq.js → index-fxtrk7y8.js} +1 -1
- package/dist/cli/{index-0derejrq.js → index-w8dzxnx4.js} +1 -1
- package/dist/cli/{index-e7nhwa9k.js → index-z4t3w4xf.js} +6 -6
- package/dist/cli/index.js +4 -4
- package/dist/cli/{schema-yxkdx5g5.js → schema-vw2ffhe9.js} +1 -1
- package/dist/commands/close.d.ts +2 -0
- package/dist/commands/pr-subscribe.d.ts +5 -3
- package/dist/commands/registry.d.ts +4 -4
- package/dist/config/bundled-skills.d.ts +1 -1
- package/dist/config/schema.d.ts +46 -0
- package/dist/hooks/pr-auto-subscribe.d.ts +51 -0
- package/dist/index.js +547 -506
- package/dist/memory/config.d.ts +48 -0
- package/dist/memory/finalize-reward-sweep.d.ts +132 -0
- package/dist/memory/gateway.d.ts +0 -1
- package/dist/memory/index.d.ts +1 -2
- package/dist/memory/injector.d.ts +11 -0
- package/dist/memory/local-jsonl-provider.d.ts +3 -1
- package/dist/memory/maintenance.d.ts +16 -4
- package/dist/memory/provider.d.ts +42 -47
- package/dist/memory/q-learning.d.ts +62 -0
- package/dist/memory/reward-capture.d.ts +119 -0
- package/dist/memory/schema.d.ts +9 -9
- package/dist/memory/scoring.d.ts +60 -5
- package/dist/memory/sqlite-provider.d.ts +5 -53
- package/dist/memory/types.d.ts +25 -5
- package/dist/state.d.ts +9 -2
- package/dist/tools/convene-council.d.ts +0 -2
- package/dist/tools/submit-phase-council-verdicts.d.ts +0 -1
- package/dist/tools/swarm-memory-recall.d.ts +2 -0
- package/dist/tools/write-final-council-evidence.d.ts +2 -2
- package/package.json +2 -1
- package/dist/memory/reward.d.ts +0 -39
package/dist/memory/config.d.ts
CHANGED
|
@@ -58,6 +58,8 @@ export interface MemoryConfig {
|
|
|
58
58
|
};
|
|
59
59
|
latencyBudgetMs: number;
|
|
60
60
|
};
|
|
61
|
+
/** Q-learning-style utility tracking for memory recall/promotion/suppression. */
|
|
62
|
+
qLearning: QLearningConfig;
|
|
61
63
|
}
|
|
62
64
|
export interface ImportanceConfig {
|
|
63
65
|
wRecency: number;
|
|
@@ -70,6 +72,45 @@ export interface ImportanceConfig {
|
|
|
70
72
|
/** A memory is low-utility when importance < threshold. */
|
|
71
73
|
threshold: number;
|
|
72
74
|
}
|
|
75
|
+
export interface QLearningConfig {
|
|
76
|
+
/** EMA learning rate η for q ← (1-η)·q + η·reward. */
|
|
77
|
+
learningRate: number;
|
|
78
|
+
/** Weight of the q-value term added to recall scoring. */
|
|
79
|
+
qValueBoostWeight: number;
|
|
80
|
+
/** Memories with qValue below this are suppressed from default recall. */
|
|
81
|
+
suppressionThreshold: number;
|
|
82
|
+
/** Memories with qValue above this (and enough retrievals) are promotion candidates. */
|
|
83
|
+
promotionThreshold: number;
|
|
84
|
+
/** Minimum retrieval count before a high-q memory is a promotion candidate. */
|
|
85
|
+
promotionMinRetrievals: number;
|
|
86
|
+
/** Fraction of a reward propagated to closely-related memories. */
|
|
87
|
+
propagationFraction: number;
|
|
88
|
+
/** Max related memories a single reward may propagate to. */
|
|
89
|
+
propagationFanoutCap: number;
|
|
90
|
+
/** Only propagate to memories retrieved within this window (days). */
|
|
91
|
+
propagationWindowDays: number;
|
|
92
|
+
/**
|
|
93
|
+
* Jaccard token-overlap bar for treating two same-scope+kind memories as
|
|
94
|
+
* "related" for reward propagation (B.5). Deliberately HIGH — propagation
|
|
95
|
+
* mutates OTHER memories' learned utility, so wrong bounds = blast radius;
|
|
96
|
+
* it must reach only near-duplicate memories. Strictly higher than
|
|
97
|
+
* consolidation's clustering bar (`DEFAULT_CONSOLIDATION_CONFIG.jaccardThreshold`
|
|
98
|
+
* = 0.30, a more aggressive MERGE operation): incidental template-word
|
|
99
|
+
* overlap between distinct sibling-task memories (e.g. "Recalled by task
|
|
100
|
+
* A." vs "...task B." ≈ 0.6 Jaccard) must NOT count as related, so
|
|
101
|
+
* propagation never cross-pollinates distinct tasks' memories — which
|
|
102
|
+
* coheres with the B.2 unitId attribution the reward path already
|
|
103
|
+
* enforces. Documented judgment call (task B.5 report): deviates upward
|
|
104
|
+
* from the spec's "e.g. 0.5" example toward blast-radius safety.
|
|
105
|
+
*/
|
|
106
|
+
propagationRelatednessThreshold: number;
|
|
107
|
+
/** Bounded rate at which suppressed memories are surfaced for exploration. */
|
|
108
|
+
explorationRate: number;
|
|
109
|
+
/** Max bytes of the retained council-synthesis payload (truncated with a marker beyond this). */
|
|
110
|
+
verdictPayloadCapBytes: number;
|
|
111
|
+
/** Neutral starting utility for a new memory. */
|
|
112
|
+
initialQValue: number;
|
|
113
|
+
}
|
|
73
114
|
export interface ConsolidationConfig {
|
|
74
115
|
/** Run episodic→semantic consolidation at phase_complete. Gated by `enabled` too. */
|
|
75
116
|
enabled: boolean;
|
|
@@ -105,6 +146,13 @@ export interface MemoryLearningConfig {
|
|
|
105
146
|
}
|
|
106
147
|
export declare const DEFAULT_DECAY_HALF_LIFE_DAYS: Record<MemoryKind, number>;
|
|
107
148
|
export declare const DEFAULT_IMPORTANCE_CONFIG: ImportanceConfig;
|
|
149
|
+
export declare const DEFAULT_QLEARNING_CONFIG: QLearningConfig;
|
|
150
|
+
/** Council verdict → EMA reward on the [0,1] utility scale (coherence fix; see spec FR-001). */
|
|
151
|
+
export declare const COUNCIL_VERDICT_REWARDS: {
|
|
152
|
+
readonly APPROVE: 1;
|
|
153
|
+
readonly CONCERNS: 0.5;
|
|
154
|
+
readonly REJECT: 0;
|
|
155
|
+
};
|
|
108
156
|
export declare const DEFAULT_CONSOLIDATION_CONFIG: ConsolidationConfig;
|
|
109
157
|
export declare const DEFAULT_MEMORY_LEARNING_CONFIG: MemoryLearningConfig;
|
|
110
158
|
export declare const DEFAULT_EMBEDDINGS_CONFIG: {
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* B.6 — Deterministic negative-terminal reward sweep at session finalize.
|
|
3
|
+
*
|
|
4
|
+
* This is the RELIABLE negative learning signal (resolved design decision C-6),
|
|
5
|
+
* the deterministic counterpart to A.4's positive terminal reward
|
|
6
|
+
* (APPROVE→complete→1.0). At `/swarm close`, every task left non-complete is
|
|
7
|
+
* stamped `close_reason='session_terminated'` (see `guaranteeAllPlansComplete`
|
|
8
|
+
* in `src/commands/close.ts`). The memories recalled into those non-completed
|
|
9
|
+
* tasks earn a NEGATIVE terminal reward (0.0) so their learned utility (q-value)
|
|
10
|
+
* drifts DOWN via the shared EMA mechanism and, once it crosses
|
|
11
|
+
* `qLearning.suppressionThreshold`, they are suppressed from future default
|
|
12
|
+
* recall (FR-001 negative terminal; FR-006 makes suppression functional;
|
|
13
|
+
* SC-007 end-to-end).
|
|
14
|
+
*
|
|
15
|
+
* ── Attribution / why we DISCOVER the runId rather than assume the finalize
|
|
16
|
+
* session ──────────────────────────────────────────────────────────────
|
|
17
|
+
* `applyCouncilReward` lists recall bundles by `runId` at the provider level
|
|
18
|
+
* (`listRecallUsage({ runId })`) BEFORE narrowing them by `unitId` in memory.
|
|
19
|
+
* Recall usage is recorded with `runId = context.runId ?? context.sessionID` —
|
|
20
|
+
* i.e. the WORK session that performed the recall. Finalize, however, may run
|
|
21
|
+
* in a DIFFERENT session (plans persist across sessions via the ledger, and
|
|
22
|
+
* `/swarm close` can run in a separate process — the reason close has a
|
|
23
|
+
* cross-process `readEarliestSessionStart` fallback). So passing the finalize
|
|
24
|
+
* session id as `runId` would make the provider-level filter miss the task's
|
|
25
|
+
* bundles entirely and the sweep would be a silent no-op — unacceptable for a
|
|
26
|
+
* "reliable" signal. Passing an empty `runId` (which disables the provider
|
|
27
|
+
* filter) is also wrong: it would let UNTAGGED bundles from unrelated sessions
|
|
28
|
+
* be penalized once per swept task (blast-radius / disjointness violation).
|
|
29
|
+
*
|
|
30
|
+
* Therefore, for each closed task we FIRST discover the real run id(s) that
|
|
31
|
+
* recalled memories into it via `listRecallUsage({ unitId: taskId })` (the B.1
|
|
32
|
+
* bundle-identity join key), then invoke `applyCouncilReward` once per
|
|
33
|
+
* (task, runId) with `reward = 0.0` and `unitId = taskId`. Within each runId,
|
|
34
|
+
* `applyCouncilReward`'s unitId narrowing keeps this task's tagged bundles plus
|
|
35
|
+
* that run's untagged bundles (the documented B.2 run_id fallback) — exactly
|
|
36
|
+
* symmetric to how A.4 rewards those same bundles on completion.
|
|
37
|
+
*
|
|
38
|
+
* ── Correctness invariants ────────────────────────────────────────────────
|
|
39
|
+
* - Disjointness: only `closedTaskIds` (non-complete tasks) are swept, so a
|
|
40
|
+
* memory recalled into a COMPLETED task is never penalized. When a completed
|
|
41
|
+
* task and a closed task share one work session, the per-runId
|
|
42
|
+
* `applyCouncilReward` call still lists the whole session's bundles but its
|
|
43
|
+
* `unitId` narrowing excludes the completed task's tagged bundle (its
|
|
44
|
+
* `unitId` differs), so the completed task's memory is untouched. A memory
|
|
45
|
+
* recalled into BOTH a completed and a non-completed task legitimately gets
|
|
46
|
+
* +1.0 (A.4) and 0.0 (this sweep) for the two DIFFERENT tasks — not a
|
|
47
|
+
* double-penalty.
|
|
48
|
+
* - Cross-finalize idempotency: the sweep holds NO persistent per-task
|
|
49
|
+
* "rewarded" flag (unlike A.4's `session.taskCouncilApproved.rewarded`). It
|
|
50
|
+
* does not need one — a task closed on a prior finalize already has status
|
|
51
|
+
* `'closed'`, so `guaranteeAllPlansComplete` does not re-close it and it is
|
|
52
|
+
* NOT present in `closedTaskIds` on a subsequent finalize. (Finalize also
|
|
53
|
+
* removes `plan.json`/`plan-ledger.jsonl`, so a re-run short-circuits with an
|
|
54
|
+
* empty task set.) Within a single sweep we additionally dedupe `taskId`.
|
|
55
|
+
* Two intentional multiplicity sources are accepted, both a consequence of
|
|
56
|
+
* reusing the mandated shared reward path rather than a bug:
|
|
57
|
+
* 1. A memory recalled into the SAME closed task across MULTIPLE work
|
|
58
|
+
* sessions receives one 0.0 EMA step PER runId (`applyCouncilReward`
|
|
59
|
+
* dedupes within a runId, not across our per-runId calls). The
|
|
60
|
+
* dominant one-runId-per-task case is exactly A.4-symmetric; the
|
|
61
|
+
* multi-session case is marginally more aggressive.
|
|
62
|
+
* 2. An UNTAGGED (`unit_id == null`) bundle in a runId SHARED by N closed
|
|
63
|
+
* tasks is penalized once PER closed task that shares that runId: each
|
|
64
|
+
* per-task `applyCouncilReward` call re-keeps the same untagged bundle
|
|
65
|
+
* via the documented B.2 run_id fallback (an untagged bundle is kept
|
|
66
|
+
* regardless of which task's unitId is being narrowed for), so a
|
|
67
|
+
* memory recalled only in that untagged bundle earns N separate 0.0
|
|
68
|
+
* steps for N closed tasks in the same session, not one.
|
|
69
|
+
* - Persistence ordering: the caller invokes this sweep BEFORE the destructive
|
|
70
|
+
* git alignment stage, and the memory store lives at `.swarm/memory/` (not in
|
|
71
|
+
* finalize's clean allowlists and not touched by align's `dist`-scoped
|
|
72
|
+
* clean), so the reward writes survive finalize.
|
|
73
|
+
* - Non-blocking: the ENTIRE sweep is wrapped in try/catch that logs and
|
|
74
|
+
* continues. A sweep failure NEVER throws into finalize and never alters
|
|
75
|
+
* finalize's task/archival/align behavior — it only records rewards.
|
|
76
|
+
*
|
|
77
|
+
* NOTE: `applyCouncilReward`'s `verdictLabel` option lets this sweep label its
|
|
78
|
+
* reward events with the true reason instead of the misleading default
|
|
79
|
+
* `'APPROVE'`. Sweep events are persisted as
|
|
80
|
+
* `{ verdict: 'session_terminated', reward: 0.0 }` (see the `applyCouncilReward`
|
|
81
|
+
* call below).
|
|
82
|
+
*/
|
|
83
|
+
import type { MemoryConfig } from '../config/schema';
|
|
84
|
+
import { createConfiguredMemoryProvider } from './gateway';
|
|
85
|
+
import { applyCouncilReward } from './reward-capture';
|
|
86
|
+
/**
|
|
87
|
+
* Negative terminal reward for memories recalled into non-completed
|
|
88
|
+
* (`session_terminated`) tasks. On the [0,1] utility scale, 0.0 is the minimum:
|
|
89
|
+
* the EMA step `q ← (1-η)·q + η·0` strictly decreases any positive q toward 0.
|
|
90
|
+
*/
|
|
91
|
+
export declare const FINALIZE_NEGATIVE_TERMINAL_REWARD = 0;
|
|
92
|
+
export interface FinalizeRewardSweepArgs {
|
|
93
|
+
/** Project root; the memory store lives under `<directory>/.swarm/memory/`. */
|
|
94
|
+
directory: string;
|
|
95
|
+
/**
|
|
96
|
+
* Task ids stamped `close_reason='session_terminated'` this finalize
|
|
97
|
+
* (`ctx.guaranteeResult.closedTaskIds`). Deduped defensively inside.
|
|
98
|
+
*/
|
|
99
|
+
closedTaskIds: readonly string[];
|
|
100
|
+
/**
|
|
101
|
+
* Resolved `config.memory` (already parsed via `PluginConfigSchema`). When
|
|
102
|
+
* absent or `enabled !== true` the sweep is a complete no-op.
|
|
103
|
+
*/
|
|
104
|
+
memoryConfig: MemoryConfig | undefined;
|
|
105
|
+
/** ISO 8601 timestamp for the reward events; defaults to now. */
|
|
106
|
+
timestamp?: string;
|
|
107
|
+
}
|
|
108
|
+
export interface FinalizeRewardSweepResult {
|
|
109
|
+
/** True once the sweep loop actually ran (memory enabled + tasks present). */
|
|
110
|
+
swept: boolean;
|
|
111
|
+
/** Count of closed tasks that had at least one memory rewarded. */
|
|
112
|
+
tasksSwept: number;
|
|
113
|
+
/** Total EMA steps applied (summed across tasks and their run ids). */
|
|
114
|
+
memoriesRewarded: number;
|
|
115
|
+
/** Count of (task, runId) `applyCouncilReward` invocations performed. */
|
|
116
|
+
runIdsProcessed: number;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Apply the deterministic negative terminal reward (0.0) to memories recalled
|
|
120
|
+
* into the given non-completed tasks. See the module header for the full
|
|
121
|
+
* attribution model and correctness invariants. Never throws.
|
|
122
|
+
*/
|
|
123
|
+
export declare function runFinalizeRewardSweep(args: FinalizeRewardSweepArgs): Promise<FinalizeRewardSweepResult>;
|
|
124
|
+
/**
|
|
125
|
+
* DI seam (AGENTS.md invariant 7 — DI over `mock.module`). Tests override these
|
|
126
|
+
* to inject a throwing provider (non-blocking assertion) or to count/observe
|
|
127
|
+
* calls. Restore in `afterEach`.
|
|
128
|
+
*/
|
|
129
|
+
export declare const _internals: {
|
|
130
|
+
createConfiguredMemoryProvider: typeof createConfiguredMemoryProvider;
|
|
131
|
+
applyCouncilReward: typeof applyCouncilReward;
|
|
132
|
+
};
|
package/dist/memory/gateway.d.ts
CHANGED
package/dist/memory/index.d.ts
CHANGED
|
@@ -9,10 +9,9 @@ export { backupLegacyJsonl, getLegacyJsonlFileStatus, type JsonlBackupResult, ty
|
|
|
9
9
|
export { LocalJsonlMemoryProvider } from './local-jsonl-provider';
|
|
10
10
|
export { buildMemoryMaintenanceReport, type MemoryMaintenanceReport, type MemoryMaintenanceReportOptions, type MemoryRecallUsageByMemory, type MemoryRecallUsageByRole, type MemorySupersededChain, shouldCompactMemory, } from './maintenance';
|
|
11
11
|
export { buildRecallPromptBlock } from './prompt-block';
|
|
12
|
-
export type { MemoryCompactOptions, MemoryCompactResult, MemoryProposalStore, MemoryProvider,
|
|
12
|
+
export type { MemoryCompactOptions, MemoryCompactResult, MemoryProposalStore, MemoryProvider, MemoryRecallUsageEvent, MemoryRecallUsageFilter, } from './provider';
|
|
13
13
|
export { buildMemoryRecallPlan, type MemoryRecallPlan, type MemoryRecallPlannerInput, } from './recall-planner';
|
|
14
14
|
export { findSecrets, redactSecrets } from './redaction';
|
|
15
|
-
export { applyRecallRewardForCouncil, councilVerdictToMemoryOutcome, resolveRewardRunIds, } from './reward';
|
|
16
15
|
export { MEMORY_RECALL_PROFILES, type MemoryRecallProfile, normalizeMemoryAgentRole, resolveMemoryRecallProfile, } from './role-profiles';
|
|
17
16
|
export { appendMemoryRunLog, sanitizeRunId } from './run-log';
|
|
18
17
|
export { computeMemoryContentHash, createBundleId, createMemoryId, createProposalId, isExpired, normalizeMemoryText, validateCuratorMemoryDecision, validateMemoryProposal, validateMemoryRecordRules, } from './schema';
|
|
@@ -6,12 +6,23 @@ export interface MemoryLifecycleHookOptions {
|
|
|
6
6
|
directory: string;
|
|
7
7
|
config?: Partial<MemoryConfig>;
|
|
8
8
|
getActiveAgentName?: (sessionID: string | undefined) => string | undefined;
|
|
9
|
+
/**
|
|
10
|
+
* Resolve the task/phase unit-of-work id (e.g. plan task "1.1") for the
|
|
11
|
+
* session being processed. ADDITIVE join key threaded onto recorded recall
|
|
12
|
+
* bundles (B.1). Injectable seam (mirrors getActiveAgentName) so the injector
|
|
13
|
+
* stays swarmState-free; the production default reads the SAME session's
|
|
14
|
+
* `currentTaskId` in index.ts. MUST resolve only from the recall's OWN session
|
|
15
|
+
* — never a parent/orchestrator — because a wrong id corrupts B.2 attribution
|
|
16
|
+
* (a false join is worse than NULL). Returns undefined when unresolvable.
|
|
17
|
+
*/
|
|
18
|
+
getActiveTaskId?: (sessionID: string | undefined) => string | undefined;
|
|
9
19
|
createGateway?: (context: {
|
|
10
20
|
directory: string;
|
|
11
21
|
sessionID?: string;
|
|
12
22
|
agentRole?: string;
|
|
13
23
|
agentId?: string;
|
|
14
24
|
runId?: string;
|
|
25
|
+
unitId?: string;
|
|
15
26
|
}, options: {
|
|
16
27
|
config?: Partial<MemoryConfig>;
|
|
17
28
|
}) => Pick<MemoryGateway, 'isEnabled' | 'deriveAllowedScopes' | 'recall' | 'propose'> & Partial<Pick<MemoryGateway, 'applyCuratorDecision' | 'dispose'>>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type MemoryConfig } from './config';
|
|
2
|
-
import type { MemoryCompactOptions, MemoryCompactResult, MemoryProposalStore, MemoryProvider, MemoryRecallUsageEvent, MemoryRecallUsageFilter } from './provider';
|
|
2
|
+
import type { MemoryCompactOptions, MemoryCompactResult, MemoryProposalStore, MemoryProvider, MemoryRecallUsageEvent, MemoryRecallUsageFilter, MemoryRewardEvent, MemoryRewardEventFilter } from './provider';
|
|
3
3
|
import type { RecallScoringDiagnostics } from './scoring';
|
|
4
4
|
import type { AppliedMemoryChange, MemoryListFilter, MemoryProposal, MemoryRecord, RecallRequest, RecallResultItem, ResolvedCuratorMemoryDecision } from './types';
|
|
5
5
|
export declare class LocalJsonlMemoryProvider implements MemoryProvider, MemoryProposalStore {
|
|
@@ -22,6 +22,8 @@ export declare class LocalJsonlMemoryProvider implements MemoryProvider, MemoryP
|
|
|
22
22
|
}>;
|
|
23
23
|
recordRecallUsage(event: MemoryRecallUsageEvent): Promise<void>;
|
|
24
24
|
listRecallUsage(filter?: MemoryRecallUsageFilter): Promise<MemoryRecallUsageEvent[]>;
|
|
25
|
+
appendRewardEvent(event: Omit<MemoryRewardEvent, 'id'>): Promise<void>;
|
|
26
|
+
listRewardEvents(filter?: MemoryRewardEventFilter): Promise<MemoryRewardEvent[]>;
|
|
25
27
|
list(filter?: MemoryListFilter): Promise<MemoryRecord[]>;
|
|
26
28
|
createProposal(proposal: MemoryProposal): Promise<MemoryProposal>;
|
|
27
29
|
listProposals(filter?: {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { type QLearningConfig } from './config';
|
|
2
|
+
import type { MemoryProposalStore, MemoryProvider } from './provider';
|
|
2
3
|
import { type ImportanceWeights } from './scoring';
|
|
3
4
|
import type { MemoryProposal, MemoryRecord } from './types';
|
|
4
5
|
/**
|
|
@@ -40,8 +41,18 @@ export interface MemoryMaintenanceReport {
|
|
|
40
41
|
supersededMemories: MemoryRecord[];
|
|
41
42
|
supersededChains: MemorySupersededChain[];
|
|
42
43
|
lowUtilityMemories: MemoryRecord[];
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Memories suppressed for low LEARNED utility (q-value < suppression
|
|
46
|
+
* threshold). Distinct from `lowUtilityMemories`, which is the orthogonal
|
|
47
|
+
* importance-based (recency/frequency/confidence) axis — see FR-006/A-1.
|
|
48
|
+
*/
|
|
49
|
+
lowQValueMemories: MemoryRecord[];
|
|
50
|
+
/**
|
|
51
|
+
* Memories whose learned utility is above the promotion threshold AND that
|
|
52
|
+
* have been recalled more than the configured minimum — surfaced as
|
|
53
|
+
* candidates for promotion (not auto-promoted). FR-007.
|
|
54
|
+
*/
|
|
55
|
+
promotionCandidates: MemoryRecord[];
|
|
45
56
|
neverRecalledMemories: MemoryRecord[];
|
|
46
57
|
mostRecalledMemories: MemoryRecallUsageByMemory[];
|
|
47
58
|
recallByAgentRole: MemoryRecallUsageByRole[];
|
|
@@ -58,10 +69,11 @@ export interface MemoryMaintenanceReportOptions {
|
|
|
58
69
|
importanceWeights?: ImportanceWeights;
|
|
59
70
|
/** A memory is low-utility when importance < threshold. Defaults to DEFAULT_IMPORTANCE_THRESHOLD. */
|
|
60
71
|
importanceThreshold?: number;
|
|
72
|
+
/** Learned-utility (q-value) thresholds for the low-Q / promotion surfaces. Defaults to DEFAULT_QLEARNING_CONFIG. */
|
|
73
|
+
qLearning?: QLearningConfig;
|
|
61
74
|
}
|
|
62
75
|
type ObservableProvider = MemoryProvider & Partial<MemoryProposalStore> & {
|
|
63
76
|
listRecallUsage?: MemoryProvider['listRecallUsage'];
|
|
64
|
-
listMemoryValueLog?: MemoryProvider['listMemoryValueLog'];
|
|
65
77
|
};
|
|
66
78
|
export declare function buildMemoryMaintenanceReport(provider: ObservableProvider, options?: MemoryMaintenanceReportOptions): Promise<MemoryMaintenanceReport>;
|
|
67
79
|
export declare function shouldCompactMemory(memory: MemoryRecord, now?: Date): 'deleted' | 'superseded' | 'expired_scratch' | null;
|
|
@@ -14,61 +14,43 @@ export interface MemoryRecallUsageEvent {
|
|
|
14
14
|
tokenEstimate: number;
|
|
15
15
|
agentRole?: string;
|
|
16
16
|
runId?: string;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Task/phase unit-of-work identity (ADDITIVE — recorded alongside `runId`).
|
|
19
|
+
* Undefined when unresolvable at recording time (graceful degrade to
|
|
20
|
+
* session-scoped `runId`).
|
|
21
|
+
*/
|
|
22
|
+
unitId?: string;
|
|
20
23
|
timestamp: string;
|
|
21
24
|
}
|
|
22
25
|
export interface MemoryRecallUsageFilter {
|
|
23
26
|
limit?: number;
|
|
24
|
-
|
|
25
|
-
export type MemoryTaskOutcome = 'approved' | 'rejected' | 'concerns' | 'unknown';
|
|
26
|
-
export interface MemoryRecallRewardInput {
|
|
27
|
+
runId?: string;
|
|
27
28
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* only, no unscoped time-window fallback); all matched bundles are
|
|
31
|
-
* rewarded together. Callers should include every session id known to
|
|
32
|
-
* have actually recalled memory for this task (e.g. dispatched council
|
|
33
|
-
* member sessions), not just the submitting session, so sub-agent
|
|
34
|
-
* recalls are not silently skipped.
|
|
29
|
+
* Restrict to rows whose `unit_id` matches. Combined with `runId` the two
|
|
30
|
+
* predicates AND. Attribution prefers this filter and falls back to `runId`.
|
|
35
31
|
*/
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
success: boolean;
|
|
43
|
-
/** First matched bundle id, for back-compat display. See `bundleIds` for the full set. */
|
|
44
|
-
bundleId?: string;
|
|
45
|
-
/** Every recall-usage bundle (across all matched runIds) that received this reward. */
|
|
46
|
-
bundleIds?: string[];
|
|
47
|
-
outcome: MemoryTaskOutcome;
|
|
48
|
-
memoryIds: string[];
|
|
49
|
-
updatedMemoryIds: string[];
|
|
50
|
-
propagatedMemoryIds: string[];
|
|
51
|
-
reward: number;
|
|
52
|
-
qValue?: number;
|
|
53
|
-
reason?: string;
|
|
32
|
+
unitId?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Restrict recall events to those with `timestamp >= since` (ISO 8601).
|
|
35
|
+
* Used by buildRetrievalRecency to bound iteration over recent events only.
|
|
36
|
+
*/
|
|
37
|
+
since?: string;
|
|
54
38
|
}
|
|
55
|
-
export interface
|
|
39
|
+
export interface MemoryRewardEvent {
|
|
40
|
+
id: string;
|
|
56
41
|
memoryId: string;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
promotionCandidate: boolean;
|
|
66
|
-
suppressionCandidate: boolean;
|
|
42
|
+
runId?: string;
|
|
43
|
+
unitId?: string;
|
|
44
|
+
verdict: string;
|
|
45
|
+
reward: number;
|
|
46
|
+
qBefore?: number;
|
|
47
|
+
qAfter?: number;
|
|
48
|
+
verdictSynthesisJson?: string;
|
|
49
|
+
timestamp: string;
|
|
67
50
|
}
|
|
68
|
-
export interface
|
|
51
|
+
export interface MemoryRewardEventFilter {
|
|
52
|
+
memoryId?: string;
|
|
69
53
|
limit?: number;
|
|
70
|
-
includePromotionCandidatesOnly?: boolean;
|
|
71
|
-
includeSuppressionCandidatesOnly?: boolean;
|
|
72
54
|
}
|
|
73
55
|
export interface MemoryCompactOptions {
|
|
74
56
|
dryRun?: boolean;
|
|
@@ -81,6 +63,11 @@ export interface MemoryCompactResult {
|
|
|
81
63
|
removedExpiredScratch: number;
|
|
82
64
|
remaining: number;
|
|
83
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Lightweight transaction marker. Concrete transaction semantics are
|
|
68
|
+
* backend-specific (SQLite serialised, local-jsonl no-op).
|
|
69
|
+
*/
|
|
70
|
+
export type MemoryTransaction = object;
|
|
84
71
|
export interface MemoryProvider {
|
|
85
72
|
readonly name: string;
|
|
86
73
|
initialize?(): Promise<void>;
|
|
@@ -92,10 +79,18 @@ export interface MemoryProvider {
|
|
|
92
79
|
recallWithDiagnostics?(request: RecallRequest): Promise<MemoryRecallResult>;
|
|
93
80
|
recordRecallUsage?(event: MemoryRecallUsageEvent): Promise<void>;
|
|
94
81
|
listRecallUsage?(filter?: MemoryRecallUsageFilter): Promise<MemoryRecallUsageEvent[]>;
|
|
95
|
-
|
|
96
|
-
|
|
82
|
+
appendRewardEvent?(event: Omit<MemoryRewardEvent, 'id'>): Promise<void>;
|
|
83
|
+
listRewardEvents?(filter?: MemoryRewardEventFilter): Promise<MemoryRewardEvent[]>;
|
|
97
84
|
compactMaintenance?(options?: MemoryCompactOptions): Promise<MemoryCompactResult>;
|
|
98
85
|
list(filter: MemoryListFilter): Promise<MemoryRecord[]>;
|
|
86
|
+
/**
|
|
87
|
+
* Run `fn` atomically within a transaction. When the provider does not
|
|
88
|
+
* support transactions (e.g. local-jsonl), this is a no-op that calls
|
|
89
|
+
* `fn` directly. The applyCouncilReward loop uses this to avoid a
|
|
90
|
+
* read-then-update race between concurrent council verdicts on the same
|
|
91
|
+
* memory id.
|
|
92
|
+
*/
|
|
93
|
+
withTransaction?<T>(fn: (tx: MemoryTransaction) => Promise<T> | T): Promise<T>;
|
|
99
94
|
}
|
|
100
95
|
export interface MemoryProposalStore {
|
|
101
96
|
createProposal(proposal: MemoryProposal): Promise<MemoryProposal>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure q-learning-style utility math for memory reward propagation.
|
|
3
|
+
*
|
|
4
|
+
* No I/O, no provider imports — safe to import from any layer (hooks, scoring,
|
|
5
|
+
* tools) without pulling in sqlite/jsonl or council-verdict types. Callers
|
|
6
|
+
* (A.4 reward capture, A.5 scoring) own the wiring; this module only owns
|
|
7
|
+
* the math.
|
|
8
|
+
*
|
|
9
|
+
* `getQValue`/`setQValue` operate on any `MemoryRecord`-shaped object (a
|
|
10
|
+
* structural `{ metadata?: Record<string, unknown> }`), so no runtime or
|
|
11
|
+
* type import of `MemoryRecord` is required here.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* EMA update: q ← (1-η)·q + η·reward.
|
|
15
|
+
*
|
|
16
|
+
* A convex combination of two [0,1] inputs stays in [0,1]; the clamp guards
|
|
17
|
+
* against float drift and out-of-range callers (e.g. reward slightly outside
|
|
18
|
+
* [0,1]) rather than relying on that invariant alone.
|
|
19
|
+
*
|
|
20
|
+
* Non-finite guards (a malformed reward, learning rate, or prior must not
|
|
21
|
+
* corrupt the stored q-value): a non-finite `reward` or `eta` falls back to
|
|
22
|
+
* the clamped `qOld`; a non-finite `qOld` (for which `clamp01` would itself
|
|
23
|
+
* yield NaN) falls back to `0`. This branch is unreachable under the intended
|
|
24
|
+
* `getQValue`-first contract (which always yields a finite value in [0,1]).
|
|
25
|
+
*/
|
|
26
|
+
export declare function applyEmaUpdate(qOld: number, reward: number, eta: number): number;
|
|
27
|
+
/**
|
|
28
|
+
* B.5 — Propagation EMA step (soft Q-propagation). Applies a fractionally
|
|
29
|
+
* REDUCED reward step to a RELATED memory, reusing the SAME EMA mechanism as a
|
|
30
|
+
* direct reward but scaling the effective learning rate by `fraction`:
|
|
31
|
+
*
|
|
32
|
+
* applyPropagatedEmaUpdate(q, r, η, f) = applyEmaUpdate(q, r, η·f)
|
|
33
|
+
* = q + η·f·(r − q)
|
|
34
|
+
*
|
|
35
|
+
* so a related memory shifts by EXACTLY `fraction` times the shift a direct
|
|
36
|
+
* reward would produce from the same `qOld` (whose shift is η·(r − q)). This is
|
|
37
|
+
* the precise, testable meaning of "the related memory shifts by the fraction"
|
|
38
|
+
* (SC-005). It is algebraically identical to the alternative framing — pulling
|
|
39
|
+
* the reward `fraction` of the way from `qOld` toward `r` and then applying η —
|
|
40
|
+
* because η·(qOld + f·(r − qOld) − qOld) = η·f·(r − qOld).
|
|
41
|
+
*
|
|
42
|
+
* `fraction` is clamped to (0, 1]: a non-finite or ≤0 fraction yields NO shift
|
|
43
|
+
* (returns the clamped `qOld`) — propagation degrades to a no-op rather than
|
|
44
|
+
* corrupting the stored value; a fraction >1 is capped at 1 so a propagated
|
|
45
|
+
* step can never exceed the direct step it derives from (blast-radius guard).
|
|
46
|
+
*/
|
|
47
|
+
export declare function applyPropagatedEmaUpdate(qOld: number, reward: number, eta: number, fraction: number): number;
|
|
48
|
+
/**
|
|
49
|
+
* Read a memory record's stored q-value from `metadata.qValue`, falling back
|
|
50
|
+
* to `fallback` when absent, non-numeric, non-finite, or out of [0,1].
|
|
51
|
+
*/
|
|
52
|
+
export declare function getQValue(record: {
|
|
53
|
+
metadata?: Record<string, unknown>;
|
|
54
|
+
}, fallback?: number): number;
|
|
55
|
+
/**
|
|
56
|
+
* Return a NEW record-shaped object with `metadata.qValue` set to the
|
|
57
|
+
* clamped value, preserving all other metadata (immutable — does not
|
|
58
|
+
* mutate `record`).
|
|
59
|
+
*/
|
|
60
|
+
export declare function setQValue<T extends {
|
|
61
|
+
metadata?: Record<string, unknown>;
|
|
62
|
+
}>(record: T, value: number): T;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A.4/B.2 — Council reward capture (positive terminal reward only).
|
|
3
|
+
*
|
|
4
|
+
* Closes the memory learning loop: when an APPROVE council verdict advances a
|
|
5
|
+
* task to `complete`, every DISTINCT memory recalled and attributable to that
|
|
6
|
+
* task earns a single EMA reward step toward the terminal reward (1.0 for
|
|
7
|
+
* APPROVE).
|
|
8
|
+
*
|
|
9
|
+
* Design constraints (resolved decision C-6, Phase A; unitId narrowing B.2):
|
|
10
|
+
* - POSITIVE reward only. No negative/REJECT/CONCERNS/max-rounds trigger here;
|
|
11
|
+
* that is the finalize-time sweep (B.6).
|
|
12
|
+
* - unitId-narrowed with run_id fallback: bundles are listed by `runId`
|
|
13
|
+
* (session), then narrowed to the verdict's `opts.unitId` so sibling
|
|
14
|
+
* tasks' recalls in the same session are NOT rewarded for this verdict.
|
|
15
|
+
* A bundle is kept when it is untagged (`bundle.unitId == null`, the
|
|
16
|
+
* legacy/unattributable case), when the verdict itself has no `unitId`
|
|
17
|
+
* (cannot narrow — degrades to full session-scoped reward, today's
|
|
18
|
+
* behavior), or when `bundle.unitId === opts.unitId` (precise match). A
|
|
19
|
+
* bundle is excluded only when both ids are present and differ (a
|
|
20
|
+
* different task's tagged bundle). `unitId` is also recorded on each
|
|
21
|
+
* reward event for audit/attribution.
|
|
22
|
+
* - DISTINCT dedup: a memory recalled in several KEPT bundles this session
|
|
23
|
+
* gets exactly ONE EMA step.
|
|
24
|
+
*
|
|
25
|
+
* This module owns NO error isolation beyond the optional-method (`?.`) guards
|
|
26
|
+
* on capabilities the provider may not implement. The calling HOOK owns
|
|
27
|
+
* try/catch isolation so a reward-capture failure can never affect task
|
|
28
|
+
* completion. (Exception: the B.5 soft-propagation pass below is wrapped in its
|
|
29
|
+
* OWN best-effort try/catch so that a propagation failure can never discard the
|
|
30
|
+
* direct reward that already succeeded — the direct EMA steps are persisted and
|
|
31
|
+
* counted BEFORE propagation runs at all, so they are unaffected by any
|
|
32
|
+
* propagation-time error. Propagation itself is NOT guaranteed all-or-nothing:
|
|
33
|
+
* if a mid-loop `upsert` throws (e.g. after some propagated targets already
|
|
34
|
+
* received their step), the targets updated before the throw keep their
|
|
35
|
+
* propagated step and the remaining scheduled targets are simply never
|
|
36
|
+
* reached — partial propagation is an accepted outcome, per FR-004's "better
|
|
37
|
+
* to under-propagate than over-propagate".)
|
|
38
|
+
*
|
|
39
|
+
* B.5 — soft Q-propagation (FR-004 / SC-005). After each DIRECT reward, a
|
|
40
|
+
* FRACTION of that reward is propagated ONE HOP to closely-related memories so
|
|
41
|
+
* learning generalizes to similar memories, strictly bounded to avoid runaway
|
|
42
|
+
* updates. Relatedness = same scope + same kind + high Jaccard token overlap;
|
|
43
|
+
* only memories retrieved within `propagationWindowDays` (from recall-usage
|
|
44
|
+
* timestamps) are eligible; at most `propagationFanoutCap` per source (top-by-
|
|
45
|
+
* overlap, deterministic); each related memory gets at most ONE propagated
|
|
46
|
+
* step regardless of how many sources reach it; directly-rewarded ids and the
|
|
47
|
+
* source itself are excluded; propagated updates never re-enter propagation.
|
|
48
|
+
* See the constant/helper docs below for the exact formula and thresholds.
|
|
49
|
+
*/
|
|
50
|
+
import type { QLearningConfig } from './config';
|
|
51
|
+
import type { MemoryProvider } from './provider';
|
|
52
|
+
export interface CouncilRewardOptions {
|
|
53
|
+
/** Session id — bundles are listed by this `runId`. */
|
|
54
|
+
runId: string;
|
|
55
|
+
/**
|
|
56
|
+
* taskId — narrows bundle attribution (see module header for the exact
|
|
57
|
+
* predicate) and is recorded on reward events for audit/attribution.
|
|
58
|
+
*/
|
|
59
|
+
unitId?: string;
|
|
60
|
+
/** Terminal reward on the [0,1] utility scale (1.0 for APPROVE). */
|
|
61
|
+
reward: number;
|
|
62
|
+
/** EMA learning rate η (config.memory.qLearning.learningRate). */
|
|
63
|
+
eta: number;
|
|
64
|
+
/** Neutral fallback q-value for records without a stored qValue. */
|
|
65
|
+
initialQValue: number;
|
|
66
|
+
/**
|
|
67
|
+
* Full q-learning config. B.5 reads the propagation parameters
|
|
68
|
+
* (`propagationFraction`, `propagationFanoutCap`, `propagationWindowDays`)
|
|
69
|
+
* from here. Optional for back-compat with A.4 call sites; defaults to
|
|
70
|
+
* `DEFAULT_QLEARNING_CONFIG` when omitted. `eta`/`initialQValue` above are
|
|
71
|
+
* kept as explicit fields (the direct-reward contract predates this) and are
|
|
72
|
+
* NOT overridden by this config.
|
|
73
|
+
*/
|
|
74
|
+
qLearning?: QLearningConfig;
|
|
75
|
+
/** Already-truncated council-synthesis payload (FR-010). */
|
|
76
|
+
verdictSynthesisJson?: string;
|
|
77
|
+
/** ISO 8601 timestamp, caller-supplied. */
|
|
78
|
+
timestamp: string;
|
|
79
|
+
/**
|
|
80
|
+
* True label persisted on the DIRECT reward event's `verdict` field (and,
|
|
81
|
+
* with the `_PROPAGATED` suffix, on any B.5 propagated event it produces).
|
|
82
|
+
* Defaults to `'APPROVE'` when omitted — byte-identical to this module's
|
|
83
|
+
* pre-existing behavior, so A.4 (delegation-gate.ts, which never passes this)
|
|
84
|
+
* is unaffected. Callers with a graded or negative-terminal reward (B.3's
|
|
85
|
+
* phase verdict, B.6's finalize sweep) should pass the reason so the
|
|
86
|
+
* value-log audit is not mislabeled `'APPROVE'` for a REJECT/CONCERNS/
|
|
87
|
+
* session-terminated reward.
|
|
88
|
+
*/
|
|
89
|
+
verdictLabel?: string;
|
|
90
|
+
}
|
|
91
|
+
export interface CouncilRewardResult {
|
|
92
|
+
/** Count of distinct memories that received an EMA step. */
|
|
93
|
+
memoriesRewarded: number;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Apply the positive council reward to every distinct memory recalled during
|
|
97
|
+
* the session identified by `opts.runId` that is attributable to
|
|
98
|
+
* `opts.unitId` (with the run_id fallback described in the module header for
|
|
99
|
+
* untagged bundles and unitId-less verdicts).
|
|
100
|
+
*
|
|
101
|
+
* Uses the provider's DIRECT `upsert` (a SYSTEM-level utility update that
|
|
102
|
+
* bypasses the propose/curator flow, like maintenance/compaction). `setQValue`
|
|
103
|
+
* changes only `metadata.qValue`, leaving scope/kind/text (and therefore the
|
|
104
|
+
* record id) unchanged, so `upsert` replaces the record in place.
|
|
105
|
+
*/
|
|
106
|
+
export declare function applyCouncilReward(provider: MemoryProvider, opts: CouncilRewardOptions): Promise<CouncilRewardResult>;
|
|
107
|
+
/**
|
|
108
|
+
* FB-003 [MEDIUM]: Truncate a plain object so its JSON representation fits
|
|
109
|
+
* within a byte cap while always producing valid JSON. The source object is
|
|
110
|
+
* truncated before stringification so the output can never be a JSON fragment.
|
|
111
|
+
*
|
|
112
|
+
* Strategy:
|
|
113
|
+
* 1. If the full JSON already fits → return unchanged.
|
|
114
|
+
* 2. If adding a `__truncated__: true` marker still fits → return with marker.
|
|
115
|
+
* 3. Last resort: return a placeholder with metadata about the original size.
|
|
116
|
+
*/
|
|
117
|
+
export declare function truncateObjectForJson<T extends Record<string, unknown>>(obj: T, capBytes: number): T & {
|
|
118
|
+
__truncated__?: boolean;
|
|
119
|
+
};
|