@aitne/daemon 0.1.9 → 0.1.11
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/adapters/adapter-watchdog.d.ts +70 -0
- package/dist/adapters/adapter-watchdog.js +115 -0
- package/dist/adapters/discord.d.ts +17 -1
- package/dist/adapters/discord.js +33 -0
- package/dist/adapters/notification-manager.d.ts +27 -1
- package/dist/adapters/notification-manager.js +54 -39
- package/dist/adapters/slack-adapter.d.ts +26 -1
- package/dist/adapters/slack-adapter.js +41 -0
- package/dist/adapters/telegram-adapter.d.ts +18 -1
- package/dist/adapters/telegram-adapter.js +41 -2
- package/dist/adapters/types.d.ts +20 -0
- package/dist/adapters/whatsapp-adapter.d.ts +26 -7
- package/dist/adapters/whatsapp-adapter.js +74 -21
- package/dist/api/env-writer.d.ts +1 -0
- package/dist/api/env-writer.js +17 -7
- package/dist/api/helpers/agent-errors-registry.d.ts +5 -5
- package/dist/api/helpers/agent-errors-registry.js +5 -5
- package/dist/api/routes/agent-schedule.js +5 -1
- package/dist/api/routes/agent.js +33 -12
- package/dist/api/routes/agents/index.js +75 -16
- package/dist/api/routes/agents/views.d.ts +37 -2
- package/dist/api/routes/agents/views.js +64 -2
- package/dist/api/routes/apple-calendar.js +4 -1
- package/dist/api/routes/background-task.d.ts +22 -0
- package/dist/api/routes/background-task.js +338 -0
- package/dist/api/routes/browser-history.js +9 -1
- package/dist/api/routes/calendar.js +12 -2
- package/dist/api/routes/context/path-resolve.js +6 -1
- package/dist/api/routes/context/permissions.js +12 -2
- package/dist/api/routes/context/snapshots.js +0 -3
- package/dist/api/routes/context/write.js +3 -17
- package/dist/api/routes/dashboard/config.js +58 -12
- package/dist/api/routes/dashboard/cost-approvals.js +66 -0
- package/dist/api/routes/dashboard/notifications.js +9 -9
- package/dist/api/routes/dashboard/oauth-google.js +5 -3
- package/dist/api/routes/feedback.d.ts +3 -0
- package/dist/api/routes/feedback.js +349 -0
- package/dist/api/routes/git.js +10 -3
- package/dist/api/routes/github.js +5 -1
- package/dist/api/routes/integrations/crud-patch.js +5 -1
- package/dist/api/routes/integrations-reconcile.js +2 -2
- package/dist/api/routes/mcp.js +65 -13
- package/dist/api/routes/notion.d.ts +1 -1
- package/dist/api/routes/observations.js +7 -7
- package/dist/api/routes/obsidian.d.ts +1 -1
- package/dist/api/routes/receipts.js +5 -1
- package/dist/api/routes/setup-migrate.js +1 -1
- package/dist/api/routes/setup.js +1 -1
- package/dist/api/routes/task-flows.d.ts +1 -1
- package/dist/api/routes/task-flows.js +1 -1
- package/dist/api/routes/tuning.d.ts +29 -0
- package/dist/api/routes/tuning.js +304 -0
- package/dist/api/server.d.ts +44 -16
- package/dist/api/server.js +12 -0
- package/dist/bootstrap/adapters.d.ts +19 -0
- package/dist/bootstrap/adapters.js +61 -0
- package/dist/bootstrap/api.d.ts +5 -3
- package/dist/bootstrap/api.js +45 -13
- package/dist/bootstrap/catchup.d.ts +1 -1
- package/dist/bootstrap/catchup.js +11 -11
- package/dist/bootstrap/event-pipeline.d.ts +11 -0
- package/dist/bootstrap/event-pipeline.js +246 -8
- package/dist/bootstrap/observers.js +9 -6
- package/dist/bootstrap/schedule-helpers.d.ts +104 -6
- package/dist/bootstrap/schedule-helpers.js +172 -19
- package/dist/config.js +32 -12
- package/dist/core/agent-core.d.ts +33 -1
- package/dist/core/agent-core.js +36 -1
- package/dist/core/agents/activity-scan-cadence.d.ts +103 -0
- package/dist/core/agents/activity-scan-cadence.js +127 -0
- package/dist/core/agents/agent-route-override.d.ts +53 -0
- package/dist/core/agents/agent-route-override.js +69 -0
- package/dist/core/agents/builtin-registry.d.ts +51 -14
- package/dist/core/agents/builtin-registry.js +92 -15
- package/dist/core/agents/config-gate-reconcile.d.ts +38 -0
- package/dist/core/agents/config-gate-reconcile.js +51 -0
- package/dist/core/agents/cron-substitute.d.ts +1 -1
- package/dist/core/agents/cron-substitute.js +1 -1
- package/dist/core/agents/custom-routine-migration.d.ts +60 -0
- package/dist/core/agents/custom-routine-migration.js +149 -0
- package/dist/core/agents/firing-blocked.d.ts +1 -1
- package/dist/core/agents/hourly-cadence.d.ts +102 -0
- package/dist/core/agents/hourly-cadence.js +126 -0
- package/dist/core/agents/loader-boot.js +23 -0
- package/dist/core/agents/loader.d.ts +19 -0
- package/dist/core/agents/loader.js +34 -2
- package/dist/core/agents/override-merge.d.ts +1 -1
- package/dist/core/agents/override-merge.js +9 -1
- package/dist/core/agents/recurrence-convert.d.ts +1 -1
- package/dist/core/agents/recurrence-convert.js +1 -1
- package/dist/core/agents/recurring-schedule-adapter.js +8 -0
- package/dist/core/alerts.js +6 -6
- package/dist/core/backends/auth-health-monitor.d.ts +2 -2
- package/dist/core/backends/auth-health-monitor.js +1 -1
- package/dist/core/backends/backend-router.d.ts +27 -1
- package/dist/core/backends/backend-router.js +165 -1
- package/dist/core/backends/claude-code-core.d.ts +71 -31
- package/dist/core/backends/claude-code-core.js +282 -54
- package/dist/core/backends/cli-quota-guards.d.ts +29 -1
- package/dist/core/backends/cli-quota-guards.js +40 -5
- package/dist/core/backends/codex-core.d.ts +6 -0
- package/dist/core/backends/codex-core.js +22 -6
- package/dist/core/backends/failure-spend.d.ts +58 -0
- package/dist/core/backends/failure-spend.js +137 -0
- package/dist/core/backends/gemini-cli-core.d.ts +6 -0
- package/dist/core/backends/gemini-cli-core.js +38 -6
- package/dist/core/backends/model-registry.d.ts +1 -1
- package/dist/core/backends/model-registry.js +4 -4
- package/dist/core/backends/opencode-core.d.ts +1 -1
- package/dist/core/backends/opencode-core.js +5 -5
- package/dist/core/backends/plan-presets.js +47 -18
- package/dist/core/bang-commands/commands-cost.js +3 -1
- package/dist/core/bang-commands/commands-report.js +4 -3
- package/dist/core/bang-commands/commands-research.js +4 -1
- package/dist/core/bang-commands/commands-revert-tuning.d.ts +18 -0
- package/dist/core/bang-commands/commands-revert-tuning.js +63 -0
- package/dist/core/bang-commands/commands-stop-start.js +3 -3
- package/dist/core/bang-commands/commands-task-control.d.ts +19 -0
- package/dist/core/bang-commands/commands-task-control.js +147 -0
- package/dist/core/bang-commands/commands-wiki.js +5 -5
- package/dist/core/bang-commands/index.d.ts +2 -0
- package/dist/core/bang-commands/index.js +12 -0
- package/dist/core/bang-commands/registry.d.ts +12 -0
- package/dist/core/browser-history/research-cluster-fanout.d.ts +28 -14
- package/dist/core/browser-history/research-cluster-fanout.js +39 -16
- package/dist/core/channel-timeline.d.ts +5 -1
- package/dist/core/channel-timeline.js +13 -0
- package/dist/core/context/index-reconciler.js +5 -2
- package/dist/core/context/policy-index-reconciler.d.ts +6 -4
- package/dist/core/context/policy-index-runner.js +25 -6
- package/dist/core/context-builder-calendar.js +10 -2
- package/dist/core/context-builder-conversation.d.ts +8 -1
- package/dist/core/context-builder-conversation.js +41 -7
- package/dist/core/context-builder-yesterday.js +4 -3
- package/dist/core/context-builder.d.ts +7 -2
- package/dist/core/context-builder.js +193 -5
- package/dist/core/context-file-serializer.d.ts +1 -1
- package/dist/core/context-file-serializer.js +1 -1
- package/dist/core/context-health.js +2 -2
- package/dist/core/context-paths.d.ts +11 -1
- package/dist/core/context-paths.js +17 -1
- package/dist/core/context-validation/prepare-write.js +1 -1
- package/dist/core/context-validation/routine-rulebook.d.ts +1 -1
- package/dist/core/context-vault-aliases.d.ts +0 -13
- package/dist/core/context-vault-aliases.js +37 -0
- package/dist/core/custom-routines.d.ts +99 -0
- package/dist/core/custom-routines.js +187 -0
- package/dist/core/daemon-api-cli.js +50 -1
- package/dist/core/day-boundary.d.ts +46 -0
- package/dist/core/day-boundary.js +40 -0
- package/dist/core/dispatcher-activity-scan.d.ts +221 -0
- package/dist/core/dispatcher-activity-scan.js +775 -0
- package/dist/core/dispatcher-error-handling.d.ts +6 -11
- package/dist/core/dispatcher-error-handling.js +38 -62
- package/dist/core/dispatcher-hourly-check.js +6 -1
- package/dist/core/dispatcher-message-handler.d.ts +10 -0
- package/dist/core/dispatcher-message-handler.js +24 -0
- package/dist/core/dispatcher-morning-routine.d.ts +6 -6
- package/dist/core/dispatcher-morning-routine.js +13 -13
- package/dist/core/dispatcher-result-processor.d.ts +33 -0
- package/dist/core/dispatcher-result-processor.js +167 -11
- package/dist/core/dispatcher-scheduled-background-task.d.ts +42 -0
- package/dist/core/dispatcher-scheduled-background-task.js +89 -0
- package/dist/core/dispatcher-scheduled-tasks.d.ts +104 -1
- package/dist/core/dispatcher-scheduled-tasks.js +480 -8
- package/dist/core/dispatcher-task-delivery.d.ts +105 -0
- package/dist/core/dispatcher-task-delivery.js +555 -0
- package/dist/core/dispatcher-types.d.ts +48 -9
- package/dist/core/dispatcher-types.js +3 -3
- package/dist/core/dispatcher.d.ts +112 -31
- package/dist/core/dispatcher.js +297 -60
- package/dist/core/dm-freshness-metrics.d.ts +1 -1
- package/dist/core/drift-effects.js +2 -2
- package/dist/core/feedback/consolidation-prep.d.ts +94 -0
- package/dist/core/feedback/consolidation-prep.js +254 -0
- package/dist/core/feedback/eviction-scorer.d.ts +81 -0
- package/dist/core/feedback/eviction-scorer.js +136 -0
- package/dist/core/feedback/lesson-format.d.ts +79 -0
- package/dist/core/feedback/lesson-format.js +199 -0
- package/dist/core/feedback/lesson-injection.d.ts +98 -0
- package/dist/core/feedback/lesson-injection.js +174 -0
- package/dist/core/feedback/lesson-merge.d.ts +51 -0
- package/dist/core/feedback/lesson-merge.js +88 -0
- package/dist/core/feedback/lesson-store-overview.d.ts +46 -0
- package/dist/core/feedback/lesson-store-overview.js +42 -0
- package/dist/core/feedback/promotion-gate.d.ts +69 -0
- package/dist/core/feedback/promotion-gate.js +117 -0
- package/dist/core/feedback/regeneralization-prep.d.ts +87 -0
- package/dist/core/feedback/regeneralization-prep.js +152 -0
- package/dist/core/feedback/scope-parser.d.ts +86 -0
- package/dist/core/feedback/scope-parser.js +141 -0
- package/dist/core/feedback/self-performance-prep.d.ts +186 -0
- package/dist/core/feedback/self-performance-prep.js +541 -0
- package/dist/core/feedback/tuning-actuator.d.ts +198 -0
- package/dist/core/feedback/tuning-actuator.js +432 -0
- package/dist/core/feedback/tuning-recommender.d.ts +247 -0
- package/dist/core/feedback/tuning-recommender.js +580 -0
- package/dist/core/feedback/tuning-revert-monitor.d.ts +90 -0
- package/dist/core/feedback/tuning-revert-monitor.js +213 -0
- package/dist/core/health-monitor.d.ts +6 -0
- package/dist/core/health-monitor.js +1 -1
- package/dist/core/injection-policy.d.ts +83 -1
- package/dist/core/injection-policy.js +61 -3
- package/dist/core/integration-main-backend.js +4 -0
- package/dist/core/management-md.d.ts +2 -2
- package/dist/core/management-md.js +51 -13
- package/dist/core/morning/orchestrator.d.ts +2 -2
- package/dist/core/morning/orchestrator.js +2 -2
- package/dist/core/notification-gate.d.ts +64 -0
- package/dist/core/notification-gate.js +51 -0
- package/dist/core/notification-rate-limit.d.ts +40 -0
- package/dist/core/notification-rate-limit.js +50 -0
- package/dist/core/policy-files.d.ts +1 -1
- package/dist/core/policy-files.js +2 -2
- package/dist/core/pre-pass-freshness.d.ts +4 -4
- package/dist/core/retention.d.ts +5 -0
- package/dist/core/retention.js +20 -4
- package/dist/core/review-context.d.ts +1 -1
- package/dist/core/review-context.js +10 -5
- package/dist/core/roadmap-write-lock.d.ts +2 -1
- package/dist/core/roadmap-write-lock.js +15 -10
- package/dist/core/routine-acquisition-plan.d.ts +47 -1
- package/dist/core/routine-acquisition-plan.js +78 -20
- package/dist/core/routine-fetch-window-retry.js +7 -4
- package/dist/core/routine-fetch-window-runner.d.ts +39 -3
- package/dist/core/routine-fetch-window-runner.js +264 -13
- package/dist/core/routine-windows.d.ts +2 -2
- package/dist/core/routine-windows.js +8 -5
- package/dist/core/scheduler.d.ts +175 -16
- package/dist/core/scheduler.js +559 -102
- package/dist/core/signal-detector.d.ts +51 -1
- package/dist/core/signal-detector.js +321 -24
- package/dist/core/skills-compiler-denied-tools.js +2 -2
- package/dist/core/skills-compiler-skill-index.d.ts +2 -2
- package/dist/core/skills-compiler-skill-index.js +2 -2
- package/dist/core/skills-compiler-variants.d.ts +1 -1
- package/dist/core/skills-compiler-variants.js +8 -0
- package/dist/core/skills-compiler.d.ts +29 -26
- package/dist/core/skills-compiler.js +117 -81
- package/dist/core/skills-manifest.d.ts +37 -0
- package/dist/core/skills-manifest.js +73 -2
- package/dist/core/sleep-inhibitor.d.ts +79 -0
- package/dist/core/sleep-inhibitor.js +132 -0
- package/dist/core/slim-system-prompt-loader.d.ts +77 -0
- package/dist/core/slim-system-prompt-loader.js +141 -0
- package/dist/core/spawn-gates.d.ts +126 -0
- package/dist/core/spawn-gates.js +180 -0
- package/dist/core/today-direct-writer.d.ts +60 -14
- package/dist/core/today-direct-writer.js +90 -13
- package/dist/core/today-write-lock.d.ts +4 -2
- package/dist/core/today-write-lock.js +30 -20
- package/dist/core/wake-detector.d.ts +55 -0
- package/dist/core/wake-detector.js +80 -0
- package/dist/core/wiki/compile-lock.d.ts +1 -1
- package/dist/core/wiki/compile-lock.js +1 -1
- package/dist/core/wiki/wiki-fts.js +13 -6
- package/dist/core/workdir.js +15 -6
- package/dist/db/activity-scan-signals.d.ts +77 -0
- package/dist/db/activity-scan-signals.js +378 -0
- package/dist/db/agents-store.d.ts +28 -0
- package/dist/db/agents-store.js +62 -0
- package/dist/db/background-task-clarifications-store.d.ts +81 -0
- package/dist/db/background-task-clarifications-store.js +152 -0
- package/dist/db/background-task-store.d.ts +207 -0
- package/dist/db/background-task-store.js +380 -0
- package/dist/db/browser-history-store.d.ts +39 -6
- package/dist/db/browser-history-store.js +51 -7
- package/dist/db/browser-task-clarifications-store.d.ts +12 -0
- package/dist/db/browser-task-clarifications-store.js +35 -5
- package/dist/db/browser-task-store.d.ts +3 -0
- package/dist/db/browser-task-store.js +29 -4
- package/dist/db/deferred-dm.d.ts +86 -0
- package/dist/db/deferred-dm.js +199 -0
- package/dist/db/feedback-signals-store.d.ts +77 -0
- package/dist/db/feedback-signals-store.js +144 -0
- package/dist/db/migrations.js +380 -0
- package/dist/db/observations.d.ts +2 -2
- package/dist/db/observations.js +3 -3
- package/dist/db/schema.js +260 -22
- package/dist/db/voice-transcripts-store.d.ts +1 -1
- package/dist/index.js +86 -29
- package/dist/messaging/browser-task-mcp-notifier.d.ts +12 -70
- package/dist/messaging/browser-task-mcp-notifier.js +30 -151
- package/dist/messaging/browser-task-screenshot-attachment.d.ts +15 -0
- package/dist/messaging/browser-task-screenshot-attachment.js +63 -0
- package/dist/observers/delegated-sync-worker.d.ts +6 -6
- package/dist/observers/delegated-sync-worker.js +10 -10
- package/dist/observers/git-delegated-cron.d.ts +1 -1
- package/dist/observers/git-delegated-cron.js +2 -2
- package/dist/observers/github-poller-classifier.d.ts +3 -3
- package/dist/observers/github-poller-classifier.js +3 -3
- package/dist/observers/imminent-event-scheduler.d.ts +1 -1
- package/dist/observers/imminent-event-scheduler.js +1 -1
- package/dist/observers/mail-poller.d.ts +1 -0
- package/dist/observers/mail-poller.js +42 -3
- package/dist/observers/observation-summarizer/summarizer-client.d.ts +2 -2
- package/dist/observers/observation-summarizer/summarizer-client.js +2 -2
- package/dist/observers/observation-summarizer/worker.d.ts +2 -2
- package/dist/observers/observation-summarizer/worker.js +4 -4
- package/dist/observers/obsidian-watcher.d.ts +1 -1
- package/dist/observers/obsidian-watcher.js +1 -1
- package/dist/safety/agent-write-tracker.d.ts +4 -4
- package/dist/safety/agent-write-tracker.js +4 -4
- package/dist/safety/always-disallowed.d.ts +1 -1
- package/dist/safety/always-disallowed.js +39 -0
- package/dist/safety/audit.d.ts +43 -5
- package/dist/safety/audit.js +86 -18
- package/dist/safety/risk-classifier.d.ts +6 -0
- package/dist/safety/risk-classifier.js +97 -18
- package/dist/scheduler/activity-scan-gate.d.ts +86 -0
- package/dist/scheduler/activity-scan-gate.js +132 -0
- package/dist/services/background-task/background-task-budget.d.ts +80 -0
- package/dist/services/background-task/background-task-budget.js +91 -0
- package/dist/services/background-task/background-task-driver.d.ts +105 -0
- package/dist/services/background-task/background-task-driver.js +416 -0
- package/dist/services/background-task/background-task-runner.d.ts +96 -0
- package/dist/services/background-task/background-task-runner.js +673 -0
- package/dist/services/background-task/background-task-tools.d.ts +84 -0
- package/dist/services/background-task/background-task-tools.js +247 -0
- package/dist/services/background-task/background-task-transition-events.d.ts +43 -0
- package/dist/services/background-task/background-task-transition-events.js +54 -0
- package/dist/services/browser-history/automation/egress-denylist.d.ts +1 -1
- package/dist/services/browser-history/automation/egress-denylist.js +34 -8
- package/dist/services/browser-history/lifecycle/platform.js +44 -2
- package/dist/services/browser-history/managed-chromium/sandbox-launcher.js +0 -1
- package/dist/services/browser-task/browser-task-runner.js +53 -8
- package/dist/services/mcp/probe.js +30 -8
- package/dist/services/observations-batch.d.ts +1 -1
- package/dist/services/observations-batch.js +2 -2
- package/dist/settings/runtime-settings.d.ts +45 -12
- package/dist/settings/runtime-settings.js +215 -40
- package/dist/settings/settings-store.js +11 -3
- package/package.json +4 -4
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feedback Learning Loop — mechanical merge / dedup (FEEDBACK_LEARNING_LOOP_DESIGN.md §4 step 3, §6).
|
|
3
|
+
*
|
|
4
|
+
* The *mechanical* half of "merge, don't append": group incoming signals by a
|
|
5
|
+
* normalised summary so identical reports collapse into one candidate, and
|
|
6
|
+
* collapse near-duplicate existing lessons (summing their `ev`) before the
|
|
7
|
+
* eviction scorer runs (§6: "Near-duplicates are merged … before eviction is
|
|
8
|
+
* even considered").
|
|
9
|
+
*
|
|
10
|
+
* The *semantic* half — judging whether a candidate matches an existing
|
|
11
|
+
* lesson's *intent* and phrasing the generalization — is the LLM's job (§4
|
|
12
|
+
* division of labour). This module never paraphrases; it only collapses exact
|
|
13
|
+
* normalised-text matches, which is safe to do deterministically.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Normalise a summary for mechanical equality: lowercase, strip punctuation,
|
|
17
|
+
* collapse whitespace. Two signals/lessons with the same normalised form are
|
|
18
|
+
* treated as the same candidate. Conservative — only *identical* phrasings
|
|
19
|
+
* collapse; anything semantic is left to the LLM.
|
|
20
|
+
*/
|
|
21
|
+
export function normalizeSummary(summary) {
|
|
22
|
+
return summary
|
|
23
|
+
.toLowerCase()
|
|
24
|
+
.replace(/[^\p{L}\p{N}\s]/gu, " ")
|
|
25
|
+
.replace(/\s+/g, " ")
|
|
26
|
+
.trim();
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Group signals by normalised summary, preserving first-seen order for both
|
|
30
|
+
* the groups and their members. Empty / whitespace-only summaries are kept
|
|
31
|
+
* under a stable empty key rather than dropped, so no signal id is lost from
|
|
32
|
+
* the consume set.
|
|
33
|
+
*/
|
|
34
|
+
export function groupSignalsBySummary(signals) {
|
|
35
|
+
const order = [];
|
|
36
|
+
const byKey = new Map();
|
|
37
|
+
for (const signal of signals) {
|
|
38
|
+
const key = normalizeSummary(signal.summary);
|
|
39
|
+
let group = byKey.get(key);
|
|
40
|
+
if (!group) {
|
|
41
|
+
group = { key, summary: signal.summary, members: [] };
|
|
42
|
+
byKey.set(key, group);
|
|
43
|
+
order.push(key);
|
|
44
|
+
}
|
|
45
|
+
group.members.push(signal);
|
|
46
|
+
}
|
|
47
|
+
return order.map((key) => byKey.get(key));
|
|
48
|
+
}
|
|
49
|
+
const CONF_RANK = {
|
|
50
|
+
high: 3,
|
|
51
|
+
medium: 2,
|
|
52
|
+
low: 1,
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Collapse near-duplicate lessons (identical normalised text) into one, summing
|
|
56
|
+
* `ev`, keeping the earliest `date`, the latest `last`, the max confidence, the
|
|
57
|
+
* strongest `kind` (constraint outranks a softer kind), and OR-ing
|
|
58
|
+
* `provisional` to `false` if any duplicate is active. Order is stable: the
|
|
59
|
+
* first occurrence's position is retained.
|
|
60
|
+
*
|
|
61
|
+
* Deterministic and lossless on `ev` — the summed evidence flows straight into
|
|
62
|
+
* the eviction score so a merged lesson is *harder* to evict, never easier.
|
|
63
|
+
*/
|
|
64
|
+
export function dedupeLessons(lessons) {
|
|
65
|
+
const order = [];
|
|
66
|
+
const byKey = new Map();
|
|
67
|
+
for (const lesson of lessons) {
|
|
68
|
+
const key = normalizeSummary(lesson.text);
|
|
69
|
+
const existing = byKey.get(key);
|
|
70
|
+
if (!existing) {
|
|
71
|
+
byKey.set(key, { ...lesson });
|
|
72
|
+
order.push(key);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
existing.ev += lesson.ev;
|
|
76
|
+
if (lesson.date < existing.date)
|
|
77
|
+
existing.date = lesson.date;
|
|
78
|
+
if (lesson.last > existing.last)
|
|
79
|
+
existing.last = lesson.last;
|
|
80
|
+
if (CONF_RANK[lesson.conf] > CONF_RANK[existing.conf]) {
|
|
81
|
+
existing.conf = lesson.conf;
|
|
82
|
+
}
|
|
83
|
+
if (lesson.kind === "constraint")
|
|
84
|
+
existing.kind = "constraint";
|
|
85
|
+
existing.provisional = existing.provisional && lesson.provisional;
|
|
86
|
+
}
|
|
87
|
+
return order.map((key) => byKey.get(key));
|
|
88
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feedback Learning Loop — lesson-store overview (FEEDBACK_LEARNING_LOOP_DESIGN.md §9 Phase 5).
|
|
3
|
+
*
|
|
4
|
+
* The pure, deterministic half of the `GET /api/feedback/lessons` dashboard
|
|
5
|
+
* surface. The route (coverage-excluded — it does the FS enumeration) hands
|
|
6
|
+
* each lesson file's raw contents to {@link summarizeLessonStore}, which parses
|
|
7
|
+
* the `## Lessons` section and reports the cap-utilisation metrics the
|
|
8
|
+
* "view/edit lessons and tune caps/threshold" settings page renders:
|
|
9
|
+
* byte size vs. cap, entry count vs. cap, active vs. provisional split, and
|
|
10
|
+
* whether the store is currently over either cap.
|
|
11
|
+
*
|
|
12
|
+
* Mirrors the §4 division of labour: the byte/entry accounting is mechanical
|
|
13
|
+
* (here, 100% covered), while the route owns only the FS read + JSON assembly.
|
|
14
|
+
*/
|
|
15
|
+
export interface LessonStoreSummary {
|
|
16
|
+
/** UTF-8 byte size of the on-disk `## Lessons` section (the cap unit, §6). */
|
|
17
|
+
bytes: number;
|
|
18
|
+
/** Per-scope byte cap. */
|
|
19
|
+
capBytes: number;
|
|
20
|
+
/** Total parsed lessons (active + provisional). */
|
|
21
|
+
entries: number;
|
|
22
|
+
/** Per-scope entry cap. */
|
|
23
|
+
maxEntries: number;
|
|
24
|
+
/** Injectable (promoted) lessons — the ones that actually reach a prompt. */
|
|
25
|
+
active: number;
|
|
26
|
+
/** Stored-but-not-yet-injected lessons awaiting corroboration (§4 step 4). */
|
|
27
|
+
provisional: number;
|
|
28
|
+
/** True when the file exceeds its byte cap or its entry cap. */
|
|
29
|
+
overCap: boolean;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Summarise one lesson store from its raw file contents. A file with no
|
|
33
|
+
* `## Lessons` section (or an empty one) reports zero entries — never throws,
|
|
34
|
+
* so a hand-edited or partially-written file degrades to "empty store" rather
|
|
35
|
+
* than breaking the overview. `bytes` measures the on-disk `## Lessons`
|
|
36
|
+
* section body — the §6 cap unit (`lessonsSectionByteLength` in
|
|
37
|
+
* lesson-format.ts) the eviction scorer and the nightly worksheet's
|
|
38
|
+
* `over_cap` enforce. Measuring the whole file here previously reported a
|
|
39
|
+
* permanently-stuck `overCap: true` in the band where the section fit the
|
|
40
|
+
* cap but frontmatter + heading overhead pushed the file past it — a state
|
|
41
|
+
* no enforcement actor would ever clear.
|
|
42
|
+
*/
|
|
43
|
+
export declare function summarizeLessonStore(fileMd: string, caps: {
|
|
44
|
+
capBytes: number;
|
|
45
|
+
maxEntries: number;
|
|
46
|
+
}): LessonStoreSummary;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feedback Learning Loop — lesson-store overview (FEEDBACK_LEARNING_LOOP_DESIGN.md §9 Phase 5).
|
|
3
|
+
*
|
|
4
|
+
* The pure, deterministic half of the `GET /api/feedback/lessons` dashboard
|
|
5
|
+
* surface. The route (coverage-excluded — it does the FS enumeration) hands
|
|
6
|
+
* each lesson file's raw contents to {@link summarizeLessonStore}, which parses
|
|
7
|
+
* the `## Lessons` section and reports the cap-utilisation metrics the
|
|
8
|
+
* "view/edit lessons and tune caps/threshold" settings page renders:
|
|
9
|
+
* byte size vs. cap, entry count vs. cap, active vs. provisional split, and
|
|
10
|
+
* whether the store is currently over either cap.
|
|
11
|
+
*
|
|
12
|
+
* Mirrors the §4 division of labour: the byte/entry accounting is mechanical
|
|
13
|
+
* (here, 100% covered), while the route owns only the FS read + JSON assembly.
|
|
14
|
+
*/
|
|
15
|
+
import { extractMarkdownSection, parseLessonsSection, } from "./lesson-format.js";
|
|
16
|
+
/**
|
|
17
|
+
* Summarise one lesson store from its raw file contents. A file with no
|
|
18
|
+
* `## Lessons` section (or an empty one) reports zero entries — never throws,
|
|
19
|
+
* so a hand-edited or partially-written file degrades to "empty store" rather
|
|
20
|
+
* than breaking the overview. `bytes` measures the on-disk `## Lessons`
|
|
21
|
+
* section body — the §6 cap unit (`lessonsSectionByteLength` in
|
|
22
|
+
* lesson-format.ts) the eviction scorer and the nightly worksheet's
|
|
23
|
+
* `over_cap` enforce. Measuring the whole file here previously reported a
|
|
24
|
+
* permanently-stuck `overCap: true` in the band where the section fit the
|
|
25
|
+
* cap but frontmatter + heading overhead pushed the file past it — a state
|
|
26
|
+
* no enforcement actor would ever clear.
|
|
27
|
+
*/
|
|
28
|
+
export function summarizeLessonStore(fileMd, caps) {
|
|
29
|
+
const sectionBody = extractMarkdownSection(fileMd, "Lessons");
|
|
30
|
+
const lessons = sectionBody ? parseLessonsSection(sectionBody) : [];
|
|
31
|
+
const provisional = lessons.filter((lesson) => lesson.provisional).length;
|
|
32
|
+
const bytes = sectionBody ? Buffer.byteLength(sectionBody, "utf-8") : 0;
|
|
33
|
+
return {
|
|
34
|
+
bytes,
|
|
35
|
+
capBytes: caps.capBytes,
|
|
36
|
+
entries: lessons.length,
|
|
37
|
+
maxEntries: caps.maxEntries,
|
|
38
|
+
active: lessons.length - provisional,
|
|
39
|
+
provisional,
|
|
40
|
+
overCap: bytes > caps.capBytes || lessons.length > caps.maxEntries,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feedback Learning Loop — promotion gate (FEEDBACK_LEARNING_LOOP_DESIGN.md §4 step 4).
|
|
3
|
+
*
|
|
4
|
+
* Decides whether a candidate lesson (one or more corroborating signals) is
|
|
5
|
+
* *active / injectable* or stays *provisional*. This is the deterministic
|
|
6
|
+
* "globally optimized, not single-shot" gate (requirement #4): the LLM groups
|
|
7
|
+
* signals by intent (semantic), but the promote/hold decision is pure code so
|
|
8
|
+
* the model never decides the threshold.
|
|
9
|
+
*
|
|
10
|
+
* Two hard rules kill the §3.5.1 sign-inversion failure mode at the gate:
|
|
11
|
+
* 1. `ignored` carries `valence='neutral'` and weight 0.25 — silence is weak
|
|
12
|
+
* corroboration, never disapproval, and can never *flip a lesson negative*.
|
|
13
|
+
* 2. `ignored` is **non-initiating**: a candidate made *only* of `ignored`
|
|
14
|
+
* signals never promotes, regardless of weighted sum. An ignore can
|
|
15
|
+
* strengthen an explicit/corrected lesson; it can never start one.
|
|
16
|
+
*/
|
|
17
|
+
import type { FeedbackSignalSource, FeedbackSignalValence } from "../../db/feedback-signals-store.js";
|
|
18
|
+
/** Minimal signal shape the gate scores — a projection of `feedback_signals`. */
|
|
19
|
+
export interface GateSignal {
|
|
20
|
+
source: FeedbackSignalSource;
|
|
21
|
+
valence: FeedbackSignalValence | null;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Per-signal evidence weight (§3.5.1 / §4 step 4):
|
|
25
|
+
* explicit | corrected = 1.0 · self_critique | replied | acted = 0.5 · ignored = 0.25
|
|
26
|
+
*
|
|
27
|
+
* Derived from `(source, valence)` only — `valence` already encodes the
|
|
28
|
+
* behavioral reaction (corrected→correction, ignored→neutral,
|
|
29
|
+
* replied/acted→positive), so the gate needs no `evidence_json` lookup.
|
|
30
|
+
* Checked in priority order: an authoritative directive (explicit source or a
|
|
31
|
+
* correction) outranks everything; only the *behavioral* `ignored` reaction
|
|
32
|
+
* (see {@link isIgnoredSignal}) drops to 0.25 — a neutral valence on an
|
|
33
|
+
* `explicit`/`self_critique` row is a deliberate signal, not silence, and keeps
|
|
34
|
+
* the 0.5 (or 1.0, for explicit) authoritative weight.
|
|
35
|
+
*/
|
|
36
|
+
export declare function signalWeight(signal: GateSignal): number;
|
|
37
|
+
/**
|
|
38
|
+
* `ignored` is the §3.5.1 behavioral notification-elapsed reaction: a
|
|
39
|
+
* `behavioral` signal carrying `valence='neutral'`. It drives the
|
|
40
|
+
* non-initiating / never-negative rule. A *neutral* valence on an `explicit`
|
|
41
|
+
* or `self_critique` row is NOT an ignore — it is an authoritative/deliberate
|
|
42
|
+
* signal that merely lacks a positive/negative tilt — so it must not inherit
|
|
43
|
+
* the 0.25 weight or the non-initiating treatment. Scoping the check to
|
|
44
|
+
* `behavioral` keeps this consistent with {@link signalWeight}, which already
|
|
45
|
+
* treats an explicit-neutral row as authoritative (1.0).
|
|
46
|
+
*/
|
|
47
|
+
export declare function isIgnoredSignal(signal: GateSignal): boolean;
|
|
48
|
+
/** An authoritative owner directive — promotes on first occurrence. */
|
|
49
|
+
export declare function isExplicitDirective(signal: GateSignal): boolean;
|
|
50
|
+
/** Weighted evidence sum across a candidate's contributing signals. */
|
|
51
|
+
export declare function computeWeightedEvidence(signals: ReadonlyArray<GateSignal>): number;
|
|
52
|
+
export type PromotionReason = "explicit-directive" | "evidence-threshold" | "below-threshold" | "ignored-non-initiating" | "no-signals";
|
|
53
|
+
export interface PromotionVerdict {
|
|
54
|
+
/** Active & injectable when true; provisional otherwise. */
|
|
55
|
+
promotable: boolean;
|
|
56
|
+
/** Stored-but-excluded-from-injection marker (`<!-- provisional -->`). */
|
|
57
|
+
provisional: boolean;
|
|
58
|
+
/** `high` if any explicit/corrected; `medium` at threshold; else `low`. */
|
|
59
|
+
conf: "high" | "medium" | "low";
|
|
60
|
+
weightedEv: number;
|
|
61
|
+
reason: PromotionReason;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Evaluate the promotion gate for a candidate's contributing signals.
|
|
65
|
+
*
|
|
66
|
+
* @param threshold weighted-evidence bar for behavioral/self_critique
|
|
67
|
+
* (`feedbackPromotionThreshold`, default 2).
|
|
68
|
+
*/
|
|
69
|
+
export declare function evaluatePromotion(signals: ReadonlyArray<GateSignal>, threshold: number): PromotionVerdict;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feedback Learning Loop — promotion gate (FEEDBACK_LEARNING_LOOP_DESIGN.md §4 step 4).
|
|
3
|
+
*
|
|
4
|
+
* Decides whether a candidate lesson (one or more corroborating signals) is
|
|
5
|
+
* *active / injectable* or stays *provisional*. This is the deterministic
|
|
6
|
+
* "globally optimized, not single-shot" gate (requirement #4): the LLM groups
|
|
7
|
+
* signals by intent (semantic), but the promote/hold decision is pure code so
|
|
8
|
+
* the model never decides the threshold.
|
|
9
|
+
*
|
|
10
|
+
* Two hard rules kill the §3.5.1 sign-inversion failure mode at the gate:
|
|
11
|
+
* 1. `ignored` carries `valence='neutral'` and weight 0.25 — silence is weak
|
|
12
|
+
* corroboration, never disapproval, and can never *flip a lesson negative*.
|
|
13
|
+
* 2. `ignored` is **non-initiating**: a candidate made *only* of `ignored`
|
|
14
|
+
* signals never promotes, regardless of weighted sum. An ignore can
|
|
15
|
+
* strengthen an explicit/corrected lesson; it can never start one.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Per-signal evidence weight (§3.5.1 / §4 step 4):
|
|
19
|
+
* explicit | corrected = 1.0 · self_critique | replied | acted = 0.5 · ignored = 0.25
|
|
20
|
+
*
|
|
21
|
+
* Derived from `(source, valence)` only — `valence` already encodes the
|
|
22
|
+
* behavioral reaction (corrected→correction, ignored→neutral,
|
|
23
|
+
* replied/acted→positive), so the gate needs no `evidence_json` lookup.
|
|
24
|
+
* Checked in priority order: an authoritative directive (explicit source or a
|
|
25
|
+
* correction) outranks everything; only the *behavioral* `ignored` reaction
|
|
26
|
+
* (see {@link isIgnoredSignal}) drops to 0.25 — a neutral valence on an
|
|
27
|
+
* `explicit`/`self_critique` row is a deliberate signal, not silence, and keeps
|
|
28
|
+
* the 0.5 (or 1.0, for explicit) authoritative weight.
|
|
29
|
+
*/
|
|
30
|
+
export function signalWeight(signal) {
|
|
31
|
+
if (signal.source === "explicit")
|
|
32
|
+
return 1.0;
|
|
33
|
+
if (signal.valence === "correction")
|
|
34
|
+
return 1.0;
|
|
35
|
+
if (isIgnoredSignal(signal))
|
|
36
|
+
return 0.25;
|
|
37
|
+
return 0.5;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* `ignored` is the §3.5.1 behavioral notification-elapsed reaction: a
|
|
41
|
+
* `behavioral` signal carrying `valence='neutral'`. It drives the
|
|
42
|
+
* non-initiating / never-negative rule. A *neutral* valence on an `explicit`
|
|
43
|
+
* or `self_critique` row is NOT an ignore — it is an authoritative/deliberate
|
|
44
|
+
* signal that merely lacks a positive/negative tilt — so it must not inherit
|
|
45
|
+
* the 0.25 weight or the non-initiating treatment. Scoping the check to
|
|
46
|
+
* `behavioral` keeps this consistent with {@link signalWeight}, which already
|
|
47
|
+
* treats an explicit-neutral row as authoritative (1.0).
|
|
48
|
+
*/
|
|
49
|
+
export function isIgnoredSignal(signal) {
|
|
50
|
+
return signal.source === "behavioral" && signal.valence === "neutral";
|
|
51
|
+
}
|
|
52
|
+
/** An authoritative owner directive — promotes on first occurrence. */
|
|
53
|
+
export function isExplicitDirective(signal) {
|
|
54
|
+
return signal.source === "explicit" || signal.valence === "correction";
|
|
55
|
+
}
|
|
56
|
+
/** Weighted evidence sum across a candidate's contributing signals. */
|
|
57
|
+
export function computeWeightedEvidence(signals) {
|
|
58
|
+
return signals.reduce((sum, signal) => sum + signalWeight(signal), 0);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Evaluate the promotion gate for a candidate's contributing signals.
|
|
62
|
+
*
|
|
63
|
+
* @param threshold weighted-evidence bar for behavioral/self_critique
|
|
64
|
+
* (`feedbackPromotionThreshold`, default 2).
|
|
65
|
+
*/
|
|
66
|
+
export function evaluatePromotion(signals, threshold) {
|
|
67
|
+
const weightedEv = computeWeightedEvidence(signals);
|
|
68
|
+
if (signals.length === 0) {
|
|
69
|
+
return {
|
|
70
|
+
promotable: false,
|
|
71
|
+
provisional: true,
|
|
72
|
+
conf: "low",
|
|
73
|
+
weightedEv,
|
|
74
|
+
reason: "no-signals",
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
// Rule 2: an ignored-only candidate is non-initiating — never promotes,
|
|
78
|
+
// regardless of weighted sum (two coincidental busy-morning ignores cannot
|
|
79
|
+
// teach "stop notifying about X").
|
|
80
|
+
if (signals.every(isIgnoredSignal)) {
|
|
81
|
+
return {
|
|
82
|
+
promotable: false,
|
|
83
|
+
provisional: true,
|
|
84
|
+
conf: "low",
|
|
85
|
+
weightedEv,
|
|
86
|
+
reason: "ignored-non-initiating",
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
// An explicit owner directive (or any correction) is authoritative →
|
|
90
|
+
// promote on first occurrence with high confidence.
|
|
91
|
+
if (signals.some(isExplicitDirective)) {
|
|
92
|
+
return {
|
|
93
|
+
promotable: true,
|
|
94
|
+
provisional: false,
|
|
95
|
+
conf: "high",
|
|
96
|
+
weightedEv,
|
|
97
|
+
reason: "explicit-directive",
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
// Behavioral / self_critique corroboration: promote at the weighted bar.
|
|
101
|
+
if (weightedEv >= threshold) {
|
|
102
|
+
return {
|
|
103
|
+
promotable: true,
|
|
104
|
+
provisional: false,
|
|
105
|
+
conf: "medium",
|
|
106
|
+
weightedEv,
|
|
107
|
+
reason: "evidence-threshold",
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
promotable: false,
|
|
112
|
+
provisional: true,
|
|
113
|
+
conf: "low",
|
|
114
|
+
weightedEv,
|
|
115
|
+
reason: "below-threshold",
|
|
116
|
+
};
|
|
117
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feedback Learning Loop — monthly re-generalization pre-step
|
|
3
|
+
* (FEEDBACK_LEARNING_LOOP_DESIGN.md §4 "Monthly re-generalization", Phase 5).
|
|
4
|
+
*
|
|
5
|
+
* The deterministic, daemon-side half of the *monthly* collapse. Where the
|
|
6
|
+
* nightly evening-review pre-step (`consolidation-prep.ts`) folds unconsumed
|
|
7
|
+
* *signals* into lessons, the monthly pass re-reads the *already-consolidated*
|
|
8
|
+
* lesson stores and surfaces them so the LLM can collapse several specific
|
|
9
|
+
* lessons that share a theme into one higher-level principle — e.g. three
|
|
10
|
+
* "shorter mail summary" / "shorter standup" / "shorter report" lessons → one
|
|
11
|
+
* `agent`-scope lesson "Default to terse, bulleted output." This is the engine
|
|
12
|
+
* that turns accumulated specifics into a small set of meaningful generalizations.
|
|
13
|
+
*
|
|
14
|
+
* Two layers, mirroring `consolidation-prep.ts`:
|
|
15
|
+
* - The dispatcher (coverage-excluded, FS-heavy) enumerates the lesson files
|
|
16
|
+
* on disk — the global `policies/agent-lessons.md` plus every per-agent
|
|
17
|
+
* `policies/agents/<slug>/lessons.md` — and reads their contents.
|
|
18
|
+
* - `buildRegeneralizationWorksheet(scopes, …)` — this pure markdown/XML
|
|
19
|
+
* composer turns those contents into a `<feedback_regeneralization>` block.
|
|
20
|
+
* Every output byte is a deterministic function of its inputs, so it stays
|
|
21
|
+
* I/O-free and 100% coverable.
|
|
22
|
+
*
|
|
23
|
+
* Unlike the evening worksheet, this pass carries **no signals and no consume
|
|
24
|
+
* ids** — it neither promotes nor consumes; it only ranks the existing lessons
|
|
25
|
+
* (lowest-scored first, the same eviction order Step 4 already uses) and flags
|
|
26
|
+
* staleness / over-cap so the LLM's collapse honours the same caps. A scope is
|
|
27
|
+
* surfaced only when it holds at least {@link MIN_LESSONS_FOR_REGENERALIZATION}
|
|
28
|
+
* *active* lessons — you need two to collapse one — and the whole block is
|
|
29
|
+
* omitted when no scope qualifies, so a sparse vault adds nothing to the
|
|
30
|
+
* monthly prompt.
|
|
31
|
+
*
|
|
32
|
+
* **Promotion-neutral by construction.** Only *active* (non-provisional)
|
|
33
|
+
* lessons are surfaced for collapse. Provisional lessons are awaiting
|
|
34
|
+
* corroboration and are owned exclusively by the nightly evening pass — the
|
|
35
|
+
* single promotion authority (`promotion-gate.ts`). Offering them here would
|
|
36
|
+
* let the LLM merge two provisional lessons into one active lesson, summing
|
|
37
|
+
* their `ev` past the threshold and bypassing the gate's
|
|
38
|
+
* `ignored`-only-never-promotes guard (§3.5.1) — the exact sign-inversion the
|
|
39
|
+
* gate exists to kill. They stay in the file untouched; the task-flow tells the
|
|
40
|
+
* LLM to preserve any provisional lesson byte-for-byte.
|
|
41
|
+
*/
|
|
42
|
+
import { type CanonicalScope } from "./scope-parser.js";
|
|
43
|
+
/** A scope needs at least this many *active* lessons before a collapse is possible. */
|
|
44
|
+
export declare const MIN_LESSONS_FOR_REGENERALIZATION = 2;
|
|
45
|
+
export interface RegeneralizationScopeInput {
|
|
46
|
+
/** `agent` (global) or `agent_slug` (per-agent) — the user scope is handled
|
|
47
|
+
* by the existing nightly user-profile consolidation, not re-generalised. */
|
|
48
|
+
scope: CanonicalScope;
|
|
49
|
+
/** Canonical store path (`policies/agent-lessons.md` / `policies/agents/<slug>/lessons.md`). */
|
|
50
|
+
storeFile: string;
|
|
51
|
+
/** Current lessons-store file contents. */
|
|
52
|
+
existingFileMd: string;
|
|
53
|
+
/** Byte/entry caps for the scope (§6). */
|
|
54
|
+
caps: {
|
|
55
|
+
capBytes: number;
|
|
56
|
+
maxEntries: number;
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export interface BuildRegeneralizationOptions {
|
|
60
|
+
nowIso: string;
|
|
61
|
+
recencyHalfLifeDays?: number;
|
|
62
|
+
/**
|
|
63
|
+
* Staleness horizon in days (`feedbackLessonStaleDays`, §4 step 7). A lesson
|
|
64
|
+
* whose `last=` predates `now − staleDays` and is not a `constraint` is
|
|
65
|
+
* flagged `stale="true"` so the LLM can drop it while collapsing. Omitted ⇒
|
|
66
|
+
* nothing is flagged stale.
|
|
67
|
+
*/
|
|
68
|
+
staleDays?: number;
|
|
69
|
+
}
|
|
70
|
+
export interface RegeneralizationResult {
|
|
71
|
+
/** `<feedback_regeneralization>…</…>` block for verbatim injection. */
|
|
72
|
+
block: string;
|
|
73
|
+
/** Number of scopes surfaced (each with ≥ MIN_LESSONS_FOR_REGENERALIZATION active lessons). */
|
|
74
|
+
scopeCount: number;
|
|
75
|
+
/** Total *active* lessons surfaced across all scopes (provisional excluded). */
|
|
76
|
+
lessonCount: number;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Compose the `<feedback_regeneralization>` block. Returns `null` when no scope
|
|
80
|
+
* holds at least {@link MIN_LESSONS_FOR_REGENERALIZATION} *active*
|
|
81
|
+
* (non-provisional) lessons — there is nothing to collapse, so the caller
|
|
82
|
+
* stamps nothing (no empty block in the prompt). Provisional lessons are
|
|
83
|
+
* excluded from the collapse set (see module header) but still counted in each
|
|
84
|
+
* scope's `current_entries` / `over_cap` so the cap status stays whole-file
|
|
85
|
+
* truthful. Scopes are emitted in input order.
|
|
86
|
+
*/
|
|
87
|
+
export declare function buildRegeneralizationWorksheet(scopes: ReadonlyArray<RegeneralizationScopeInput>, opts: BuildRegeneralizationOptions): RegeneralizationResult | null;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feedback Learning Loop — monthly re-generalization pre-step
|
|
3
|
+
* (FEEDBACK_LEARNING_LOOP_DESIGN.md §4 "Monthly re-generalization", Phase 5).
|
|
4
|
+
*
|
|
5
|
+
* The deterministic, daemon-side half of the *monthly* collapse. Where the
|
|
6
|
+
* nightly evening-review pre-step (`consolidation-prep.ts`) folds unconsumed
|
|
7
|
+
* *signals* into lessons, the monthly pass re-reads the *already-consolidated*
|
|
8
|
+
* lesson stores and surfaces them so the LLM can collapse several specific
|
|
9
|
+
* lessons that share a theme into one higher-level principle — e.g. three
|
|
10
|
+
* "shorter mail summary" / "shorter standup" / "shorter report" lessons → one
|
|
11
|
+
* `agent`-scope lesson "Default to terse, bulleted output." This is the engine
|
|
12
|
+
* that turns accumulated specifics into a small set of meaningful generalizations.
|
|
13
|
+
*
|
|
14
|
+
* Two layers, mirroring `consolidation-prep.ts`:
|
|
15
|
+
* - The dispatcher (coverage-excluded, FS-heavy) enumerates the lesson files
|
|
16
|
+
* on disk — the global `policies/agent-lessons.md` plus every per-agent
|
|
17
|
+
* `policies/agents/<slug>/lessons.md` — and reads their contents.
|
|
18
|
+
* - `buildRegeneralizationWorksheet(scopes, …)` — this pure markdown/XML
|
|
19
|
+
* composer turns those contents into a `<feedback_regeneralization>` block.
|
|
20
|
+
* Every output byte is a deterministic function of its inputs, so it stays
|
|
21
|
+
* I/O-free and 100% coverable.
|
|
22
|
+
*
|
|
23
|
+
* Unlike the evening worksheet, this pass carries **no signals and no consume
|
|
24
|
+
* ids** — it neither promotes nor consumes; it only ranks the existing lessons
|
|
25
|
+
* (lowest-scored first, the same eviction order Step 4 already uses) and flags
|
|
26
|
+
* staleness / over-cap so the LLM's collapse honours the same caps. A scope is
|
|
27
|
+
* surfaced only when it holds at least {@link MIN_LESSONS_FOR_REGENERALIZATION}
|
|
28
|
+
* *active* lessons — you need two to collapse one — and the whole block is
|
|
29
|
+
* omitted when no scope qualifies, so a sparse vault adds nothing to the
|
|
30
|
+
* monthly prompt.
|
|
31
|
+
*
|
|
32
|
+
* **Promotion-neutral by construction.** Only *active* (non-provisional)
|
|
33
|
+
* lessons are surfaced for collapse. Provisional lessons are awaiting
|
|
34
|
+
* corroboration and are owned exclusively by the nightly evening pass — the
|
|
35
|
+
* single promotion authority (`promotion-gate.ts`). Offering them here would
|
|
36
|
+
* let the LLM merge two provisional lessons into one active lesson, summing
|
|
37
|
+
* their `ev` past the threshold and bypassing the gate's
|
|
38
|
+
* `ignored`-only-never-promotes guard (§3.5.1) — the exact sign-inversion the
|
|
39
|
+
* gate exists to kill. They stay in the file untouched; the task-flow tells the
|
|
40
|
+
* LLM to preserve any provisional lesson byte-for-byte.
|
|
41
|
+
*/
|
|
42
|
+
import { extractMarkdownSection, parseLessonsSection, } from "./lesson-format.js";
|
|
43
|
+
import { scoreLesson, isLessonStale, DEFAULT_RECENCY_HALFLIFE_DAYS, } from "./eviction-scorer.js";
|
|
44
|
+
import { formatScope, scopeSectionSlug } from "./scope-parser.js";
|
|
45
|
+
/** A scope needs at least this many *active* lessons before a collapse is possible. */
|
|
46
|
+
export const MIN_LESSONS_FOR_REGENERALIZATION = 2;
|
|
47
|
+
function xmlEscape(value) {
|
|
48
|
+
return value
|
|
49
|
+
.replace(/&/g, "&")
|
|
50
|
+
.replace(/</g, "<")
|
|
51
|
+
.replace(/>/g, ">")
|
|
52
|
+
.replace(/"/g, """);
|
|
53
|
+
}
|
|
54
|
+
function round2(value) {
|
|
55
|
+
return (Math.round(value * 100) / 100).toFixed(2);
|
|
56
|
+
}
|
|
57
|
+
/** Collapse a one-line excerpt of a lesson for an XML text node.
|
|
58
|
+
* The clip strips a trailing lone high surrogate so cutting through an
|
|
59
|
+
* astral char (emoji) can't leave a U+FFFD in the worksheet. */
|
|
60
|
+
function inline(text, max = 300) {
|
|
61
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
62
|
+
const clipped = flat.length > max
|
|
63
|
+
? `${flat.slice(0, max - 1).replace(/[\uD800-\uDBFF]$/, "")}…`
|
|
64
|
+
: flat;
|
|
65
|
+
return xmlEscape(clipped);
|
|
66
|
+
}
|
|
67
|
+
function renderScope(input, sectionBody, activeLessons, totalEntries, opts, out) {
|
|
68
|
+
const label = formatScope(input.scope);
|
|
69
|
+
const section = scopeSectionSlug(input.scope);
|
|
70
|
+
const halfLife = opts.recencyHalfLifeDays ?? DEFAULT_RECENCY_HALFLIFE_DAYS;
|
|
71
|
+
// `current_bytes` / `current_entries` describe the on-disk `## Lessons`
|
|
72
|
+
// SECTION (active + provisional) — the §6 cap unit
|
|
73
|
+
// (`lessonsSectionByteLength` in lesson-format.ts), the same unit the
|
|
74
|
+
// nightly worksheet's `over_cap` and the eviction engine measure. The
|
|
75
|
+
// whole-file measure used previously disagreed with the nightly pass in a
|
|
76
|
+
// narrow band (frontmatter + `# heading` overhead), making the two
|
|
77
|
+
// worksheets contradict each other on the same store. `over_cap` covers
|
|
78
|
+
// the full entry set, not just the collapsible active subset — the LLM's
|
|
79
|
+
// Step-12 eviction targets the disk cap. The caller extracted
|
|
80
|
+
// `sectionBody` once during eligibility (a scope is only eligible when
|
|
81
|
+
// its `## Lessons` section parsed), so it is measured here verbatim.
|
|
82
|
+
const currentBytes = Buffer.byteLength(sectionBody, "utf-8");
|
|
83
|
+
const overCap = currentBytes > input.caps.capBytes || totalEntries > input.caps.maxEntries;
|
|
84
|
+
const provisionalHeld = totalEntries - activeLessons.length;
|
|
85
|
+
// Ascending score → rank 1 = lowest score = drop-first, the same convention
|
|
86
|
+
// the evening worksheet uses so the LLM reads both with one mental model.
|
|
87
|
+
const ranked = [...activeLessons].sort((a, b) => scoreLesson(a, opts.nowIso, undefined, halfLife) -
|
|
88
|
+
scoreLesson(b, opts.nowIso, undefined, halfLife));
|
|
89
|
+
out.push(` <scope label="${xmlEscape(label)}" store="${xmlEscape(input.storeFile)}" ` +
|
|
90
|
+
`section="${xmlEscape(section)}" ` +
|
|
91
|
+
`cap_bytes="${input.caps.capBytes}" max_entries="${input.caps.maxEntries}" ` +
|
|
92
|
+
`current_bytes="${currentBytes}" current_entries="${totalEntries}" ` +
|
|
93
|
+
`provisional_held="${provisionalHeld}" over_cap="${overCap}">`);
|
|
94
|
+
out.push(` <lessons note="active (non-provisional) lessons only, ranked by eviction ` +
|
|
95
|
+
`score (rank 1 = lowest, drop-first); cluster lessons that share a theme ` +
|
|
96
|
+
`and collapse each cluster into ONE higher-level principle; drop any lesson ` +
|
|
97
|
+
`marked stale="true" unless it joins a cluster; never collapse ` +
|
|
98
|
+
`across a contradiction; preserve any provisional lesson in the file ` +
|
|
99
|
+
`byte-for-byte — they await corroboration and are not yours to collapse or promote">`);
|
|
100
|
+
ranked.forEach((lesson, idx) => {
|
|
101
|
+
out.push(` <lesson rank="${idx + 1}" score="${round2(scoreLesson(lesson, opts.nowIso, undefined, halfLife))}" ev="${lesson.ev}" kind="${lesson.kind}" last="${lesson.last}" ` +
|
|
102
|
+
`provisional="${lesson.provisional}" ` +
|
|
103
|
+
`stale="${isLessonStale(lesson, opts.nowIso, opts.staleDays)}">` +
|
|
104
|
+
`${inline(lesson.text)}</lesson>`);
|
|
105
|
+
});
|
|
106
|
+
out.push(" </lessons>");
|
|
107
|
+
out.push(" </scope>");
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Compose the `<feedback_regeneralization>` block. Returns `null` when no scope
|
|
111
|
+
* holds at least {@link MIN_LESSONS_FOR_REGENERALIZATION} *active*
|
|
112
|
+
* (non-provisional) lessons — there is nothing to collapse, so the caller
|
|
113
|
+
* stamps nothing (no empty block in the prompt). Provisional lessons are
|
|
114
|
+
* excluded from the collapse set (see module header) but still counted in each
|
|
115
|
+
* scope's `current_entries` / `over_cap` so the cap status stays whole-file
|
|
116
|
+
* truthful. Scopes are emitted in input order.
|
|
117
|
+
*/
|
|
118
|
+
export function buildRegeneralizationWorksheet(scopes, opts) {
|
|
119
|
+
const eligible = [];
|
|
120
|
+
for (const input of scopes) {
|
|
121
|
+
// Single extraction per scope — `renderScope` reuses this body for its
|
|
122
|
+
// `current_bytes` measure instead of re-extracting (the old double
|
|
123
|
+
// extraction left renderScope with an unreachable missing-section arm).
|
|
124
|
+
const sectionBody = extractMarkdownSection(input.existingFileMd, "Lessons");
|
|
125
|
+
if (!sectionBody)
|
|
126
|
+
continue;
|
|
127
|
+
const allLessons = parseLessonsSection(sectionBody);
|
|
128
|
+
// Collapse the ACTIVE set only — provisional lessons are owned by the
|
|
129
|
+
// evening promotion gate (see module header); merging them here would
|
|
130
|
+
// bypass the `ignored`-only-never-promotes guard.
|
|
131
|
+
const active = allLessons.filter((lesson) => !lesson.provisional);
|
|
132
|
+
if (active.length >= MIN_LESSONS_FOR_REGENERALIZATION) {
|
|
133
|
+
eligible.push({ input, sectionBody, active, totalEntries: allLessons.length });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (eligible.length === 0)
|
|
137
|
+
return null;
|
|
138
|
+
const out = [];
|
|
139
|
+
out.push(`<feedback_regeneralization generated_at="${xmlEscape(opts.nowIso)}" ` +
|
|
140
|
+
`scopes="${eligible.length}">`);
|
|
141
|
+
let lessonCount = 0;
|
|
142
|
+
for (const { input, sectionBody, active, totalEntries } of eligible) {
|
|
143
|
+
renderScope(input, sectionBody, active, totalEntries, opts, out);
|
|
144
|
+
lessonCount += active.length;
|
|
145
|
+
}
|
|
146
|
+
out.push("</feedback_regeneralization>");
|
|
147
|
+
return {
|
|
148
|
+
block: out.join("\n"),
|
|
149
|
+
scopeCount: eligible.length,
|
|
150
|
+
lessonCount,
|
|
151
|
+
};
|
|
152
|
+
}
|