@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
|
@@ -51,7 +51,7 @@ export interface DmFreshnessAggregate {
|
|
|
51
51
|
* `agent_log_lag_minutes=0` by construction (the snapshot is built at
|
|
52
52
|
* dispatch time), so including them would drag the percentile toward 0
|
|
53
53
|
* and hide the cohort the plan §6 acceptance threshold targets
|
|
54
|
-
* ("p95 ≤ 60 — i.e. resumed turns are typically within an
|
|
54
|
+
* ("p95 ≤ 60 — i.e. resumed turns are typically within an activity_scan
|
|
55
55
|
* cadence of session start"). When `resumedTurns === 0`, both
|
|
56
56
|
* percentiles are 0 — there is no lag to report.
|
|
57
57
|
*/
|
|
@@ -224,7 +224,7 @@ function recordCoalescedObservation(db, params) {
|
|
|
224
224
|
// novelty_score populated. Without this, coalesced inserts only get
|
|
225
225
|
// summarized via the daemon-startup reclaim sweep — a row that lands
|
|
226
226
|
// mid-run sits at `summary_status='pending'` forever and the
|
|
227
|
-
//
|
|
227
|
+
// activity_scan skill is forced into the legacy fetch-on-doubt path.
|
|
228
228
|
if (inserted)
|
|
229
229
|
notifyObservationSummarizer(inserted.id);
|
|
230
230
|
return 1;
|
|
@@ -257,7 +257,7 @@ function recordCoalescedObservation(db, params) {
|
|
|
257
257
|
// summary describes obsolete content. Reset the summarizer-owned columns
|
|
258
258
|
// to mirror `recordObservation`'s UPSERT on payload change — without
|
|
259
259
|
// this, `summary_text` / `novelty_score` linger from the prior payload
|
|
260
|
-
// and the
|
|
260
|
+
// and the activity_scan skill consumes a stale summary as if it were
|
|
261
261
|
// current (`summary_status='done'` with `summaryStale=false`).
|
|
262
262
|
db.prepare(`UPDATE observations
|
|
263
263
|
SET change_type = ?,
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feedback Learning Loop — consolidation pre-step (FEEDBACK_LEARNING_LOOP_DESIGN.md §4).
|
|
3
|
+
*
|
|
4
|
+
* The daemon-side, deterministic half of Stage 2. On the evening-review tick it
|
|
5
|
+
* reads unconsumed `feedback_signals`, groups them by `(scope_type, scope_ref)`,
|
|
6
|
+
* pre-computes each candidate's weighted evidence + promotion verdict and each
|
|
7
|
+
* lessons file's eviction ranking + headroom, and emits a `<feedback_worksheet>`
|
|
8
|
+
* block — exactly as `<journal_skeleton>` / `harvestForGate` blocks are
|
|
9
|
+
* daemon-prepared today. The LLM step then does only the *semantic* work
|
|
10
|
+
* (intent-match merge, contradiction detection, phrasing) and writes via
|
|
11
|
+
* `PATCH /api/context/policies/agent-lessons`, then consumes the worksheet's ids.
|
|
12
|
+
*
|
|
13
|
+
* Two layers, mirroring `journal-skeleton-builder.ts`:
|
|
14
|
+
* - `gatherFeedbackWorksheetScopes(db, …)` — the single DB read (side-effect
|
|
15
|
+
* free); groups pending signals by scope. Cost scales with feedback volume,
|
|
16
|
+
* not agent count (the `idx_feedback_unconsumed` partial index).
|
|
17
|
+
* - `buildFeedbackWorksheet(scopes, …)` — pure markdown/XML composer. Every
|
|
18
|
+
* output byte is a deterministic function of its inputs; the caller supplies
|
|
19
|
+
* each lessons file's current contents so this stays I/O-free and 100%
|
|
20
|
+
* coverable.
|
|
21
|
+
*
|
|
22
|
+
* Phase 2 stored `user` + `agent`; Phase 4 added `agent:<slug>` (the evening-review
|
|
23
|
+
* pre-step now requests it). This module already rendered any lessons scope
|
|
24
|
+
* generically, so Phase 4 was wiring (`scopeTypes` + task-flow), not new logic here.
|
|
25
|
+
*/
|
|
26
|
+
import type Database from "better-sqlite3";
|
|
27
|
+
import { type FeedbackScopeType, type FeedbackSignalRow } from "../../db/feedback-signals-store.js";
|
|
28
|
+
import { type CanonicalScope } from "./scope-parser.js";
|
|
29
|
+
/** Fixed entry caps (§6 table) — config carries only the byte caps. */
|
|
30
|
+
export declare const GLOBAL_LESSON_ENTRY_CAP = 40;
|
|
31
|
+
export declare const PER_AGENT_LESSON_ENTRY_CAP = 20;
|
|
32
|
+
export interface WorksheetScopeGroup {
|
|
33
|
+
scope: CanonicalScope;
|
|
34
|
+
signals: FeedbackSignalRow[];
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Read unconsumed signals for the requested scope types and group them by
|
|
38
|
+
* canonical scope. Each scope type is queried independently (oldest-first
|
|
39
|
+
* within the type) so the per-pass row budget applies *per type*. A single
|
|
40
|
+
* global `LIMIT` over `created_at ASC` would let a backlog of unconsumed
|
|
41
|
+
* `agent_slug` rows occupy the oldest-N window and silently starve the
|
|
42
|
+
* `user`/`agent` scopes — the per-type fetch caps each scope type
|
|
43
|
+
* independently so a busy agent can't crowd out the others. Groups come back in
|
|
44
|
+
* `scopeTypes` order; rows whose `(scope_type, scope_ref)` can't be parsed
|
|
45
|
+
* (defensive — the route + behavioral sink only write valid pairs) are skipped
|
|
46
|
+
* so a bad row never breaks the pass.
|
|
47
|
+
*/
|
|
48
|
+
export declare function gatherFeedbackWorksheetScopes(db: Database.Database, opts: {
|
|
49
|
+
scopeTypes: ReadonlyArray<FeedbackScopeType>;
|
|
50
|
+
limit?: number;
|
|
51
|
+
}): WorksheetScopeGroup[];
|
|
52
|
+
/** Resolve per-scope byte/entry caps; `null` for raw (user) + unstored scopes. */
|
|
53
|
+
export declare function lessonCapsForScope(scope: CanonicalScope, byteCaps: {
|
|
54
|
+
global: number;
|
|
55
|
+
perAgent: number;
|
|
56
|
+
}): {
|
|
57
|
+
capBytes: number;
|
|
58
|
+
maxEntries: number;
|
|
59
|
+
} | null;
|
|
60
|
+
export interface WorksheetScopeInput {
|
|
61
|
+
scope: CanonicalScope;
|
|
62
|
+
signals: FeedbackSignalRow[];
|
|
63
|
+
/** Current lessons-store file contents (lessons scopes), else null. */
|
|
64
|
+
existingFileMd: string | null;
|
|
65
|
+
/** Byte/entry caps for lessons scopes; null for raw (user) scopes. */
|
|
66
|
+
caps: {
|
|
67
|
+
capBytes: number;
|
|
68
|
+
maxEntries: number;
|
|
69
|
+
} | null;
|
|
70
|
+
}
|
|
71
|
+
export interface BuildWorksheetOptions {
|
|
72
|
+
promotionThreshold: number;
|
|
73
|
+
nowIso: string;
|
|
74
|
+
recencyHalfLifeDays?: number;
|
|
75
|
+
/**
|
|
76
|
+
* Staleness horizon in days (`feedbackLessonStaleDays`, §4 step 7). An
|
|
77
|
+
* existing lesson whose `last=` predates `now − staleDays` and is not a
|
|
78
|
+
* `constraint` is flagged `stale="true"` so the LLM prunes it in the rebuild.
|
|
79
|
+
* Omitted ⇒ nothing is flagged stale (no time-based prune this pass).
|
|
80
|
+
*/
|
|
81
|
+
staleDays?: number;
|
|
82
|
+
}
|
|
83
|
+
export interface WorksheetResult {
|
|
84
|
+
/** `<feedback_worksheet>…</feedback_worksheet>` block for verbatim injection. */
|
|
85
|
+
block: string;
|
|
86
|
+
/** Every surfaced signal id — the exact consume set (§4 step 6). */
|
|
87
|
+
signalIds: number[];
|
|
88
|
+
scopeCount: number;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Compose the `<feedback_worksheet>` block. Returns `null` when there are no
|
|
92
|
+
* signals at all (the caller then stamps nothing — no empty block in the prompt).
|
|
93
|
+
*/
|
|
94
|
+
export declare function buildFeedbackWorksheet(scopes: ReadonlyArray<WorksheetScopeInput>, opts: BuildWorksheetOptions): WorksheetResult | null;
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feedback Learning Loop — consolidation pre-step (FEEDBACK_LEARNING_LOOP_DESIGN.md §4).
|
|
3
|
+
*
|
|
4
|
+
* The daemon-side, deterministic half of Stage 2. On the evening-review tick it
|
|
5
|
+
* reads unconsumed `feedback_signals`, groups them by `(scope_type, scope_ref)`,
|
|
6
|
+
* pre-computes each candidate's weighted evidence + promotion verdict and each
|
|
7
|
+
* lessons file's eviction ranking + headroom, and emits a `<feedback_worksheet>`
|
|
8
|
+
* block — exactly as `<journal_skeleton>` / `harvestForGate` blocks are
|
|
9
|
+
* daemon-prepared today. The LLM step then does only the *semantic* work
|
|
10
|
+
* (intent-match merge, contradiction detection, phrasing) and writes via
|
|
11
|
+
* `PATCH /api/context/policies/agent-lessons`, then consumes the worksheet's ids.
|
|
12
|
+
*
|
|
13
|
+
* Two layers, mirroring `journal-skeleton-builder.ts`:
|
|
14
|
+
* - `gatherFeedbackWorksheetScopes(db, …)` — the single DB read (side-effect
|
|
15
|
+
* free); groups pending signals by scope. Cost scales with feedback volume,
|
|
16
|
+
* not agent count (the `idx_feedback_unconsumed` partial index).
|
|
17
|
+
* - `buildFeedbackWorksheet(scopes, …)` — pure markdown/XML composer. Every
|
|
18
|
+
* output byte is a deterministic function of its inputs; the caller supplies
|
|
19
|
+
* each lessons file's current contents so this stays I/O-free and 100%
|
|
20
|
+
* coverable.
|
|
21
|
+
*
|
|
22
|
+
* Phase 2 stored `user` + `agent`; Phase 4 added `agent:<slug>` (the evening-review
|
|
23
|
+
* pre-step now requests it). This module already rendered any lessons scope
|
|
24
|
+
* generically, so Phase 4 was wiring (`scopeTypes` + task-flow), not new logic here.
|
|
25
|
+
*/
|
|
26
|
+
import { getPendingFeedbackSignals, } from "../../db/feedback-signals-store.js";
|
|
27
|
+
import { extractMarkdownSection, LESSON_KINDS, parseLessonsSection, } from "./lesson-format.js";
|
|
28
|
+
import { evaluatePromotion } from "./promotion-gate.js";
|
|
29
|
+
import { enforceCaps, scoreLesson, isLessonStale, DEFAULT_RECENCY_HALFLIFE_DAYS, } from "./eviction-scorer.js";
|
|
30
|
+
import { groupSignalsBySummary } from "./lesson-merge.js";
|
|
31
|
+
import { formatScope, parseScope, scopeKey, scopeSectionSlug, scopeStoreFile, } from "./scope-parser.js";
|
|
32
|
+
/** Fixed entry caps (§6 table) — config carries only the byte caps. */
|
|
33
|
+
export const GLOBAL_LESSON_ENTRY_CAP = 40;
|
|
34
|
+
export const PER_AGENT_LESSON_ENTRY_CAP = 20;
|
|
35
|
+
/** Default ceiling on signals pulled per pass (store caps the query at 500). */
|
|
36
|
+
const DEFAULT_SIGNAL_LIMIT = 400;
|
|
37
|
+
/**
|
|
38
|
+
* Read unconsumed signals for the requested scope types and group them by
|
|
39
|
+
* canonical scope. Each scope type is queried independently (oldest-first
|
|
40
|
+
* within the type) so the per-pass row budget applies *per type*. A single
|
|
41
|
+
* global `LIMIT` over `created_at ASC` would let a backlog of unconsumed
|
|
42
|
+
* `agent_slug` rows occupy the oldest-N window and silently starve the
|
|
43
|
+
* `user`/`agent` scopes — the per-type fetch caps each scope type
|
|
44
|
+
* independently so a busy agent can't crowd out the others. Groups come back in
|
|
45
|
+
* `scopeTypes` order; rows whose `(scope_type, scope_ref)` can't be parsed
|
|
46
|
+
* (defensive — the route + behavioral sink only write valid pairs) are skipped
|
|
47
|
+
* so a bad row never breaks the pass.
|
|
48
|
+
*/
|
|
49
|
+
export function gatherFeedbackWorksheetScopes(db, opts) {
|
|
50
|
+
const limit = opts.limit ?? DEFAULT_SIGNAL_LIMIT;
|
|
51
|
+
const order = [];
|
|
52
|
+
const byKey = new Map();
|
|
53
|
+
for (const scopeType of opts.scopeTypes) {
|
|
54
|
+
const rows = getPendingFeedbackSignals(db, { scopeType, limit });
|
|
55
|
+
for (const row of rows) {
|
|
56
|
+
const scope = parseScope(row.scope_type, row.scope_ref);
|
|
57
|
+
if (!scope)
|
|
58
|
+
continue;
|
|
59
|
+
const key = scopeKey(scope);
|
|
60
|
+
let group = byKey.get(key);
|
|
61
|
+
if (!group) {
|
|
62
|
+
group = { scope, signals: [] };
|
|
63
|
+
byKey.set(key, group);
|
|
64
|
+
order.push(key);
|
|
65
|
+
}
|
|
66
|
+
group.signals.push(row);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return order.map((key) => byKey.get(key));
|
|
70
|
+
}
|
|
71
|
+
/** Resolve per-scope byte/entry caps; `null` for raw (user) + unstored scopes. */
|
|
72
|
+
export function lessonCapsForScope(scope, byteCaps) {
|
|
73
|
+
if (scope.kind === "agent") {
|
|
74
|
+
return { capBytes: byteCaps.global, maxEntries: GLOBAL_LESSON_ENTRY_CAP };
|
|
75
|
+
}
|
|
76
|
+
if (scope.kind === "agent_slug") {
|
|
77
|
+
return {
|
|
78
|
+
capBytes: byteCaps.perAgent,
|
|
79
|
+
maxEntries: PER_AGENT_LESSON_ENTRY_CAP,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
function xmlEscape(value) {
|
|
85
|
+
return value
|
|
86
|
+
.replace(/&/g, "&")
|
|
87
|
+
.replace(/</g, "<")
|
|
88
|
+
.replace(/>/g, ">")
|
|
89
|
+
.replace(/"/g, """);
|
|
90
|
+
}
|
|
91
|
+
function round2(value) {
|
|
92
|
+
return (Math.round(value * 100) / 100).toFixed(2);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* `store=` attribute for a scope. Stored scopes (user/agent/agent_slug) resolve
|
|
96
|
+
* to a path; v2 scopes surfaced raw (channel/task/integration, not yet stored)
|
|
97
|
+
* render an empty string so the LLM treats them as advisory-only.
|
|
98
|
+
*/
|
|
99
|
+
function storeFileAttr(scope) {
|
|
100
|
+
return scopeStoreFile(scope) ?? "";
|
|
101
|
+
}
|
|
102
|
+
/** Collapse a one-line excerpt of a signal/lesson for an XML text node.
|
|
103
|
+
* The clip strips a trailing lone high surrogate so cutting through an
|
|
104
|
+
* astral char (emoji) can't leave a U+FFFD in the worksheet. */
|
|
105
|
+
function inline(text, max = 300) {
|
|
106
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
107
|
+
const clipped = flat.length > max
|
|
108
|
+
? `${flat.slice(0, max - 1).replace(/[\uD800-\uDBFF]$/, "")}…`
|
|
109
|
+
: flat;
|
|
110
|
+
return xmlEscape(clipped);
|
|
111
|
+
}
|
|
112
|
+
/** Authority ranking for picking a candidate's representative `src=` trailer. */
|
|
113
|
+
const SOURCE_AUTHORITY = {
|
|
114
|
+
explicit: 3,
|
|
115
|
+
self_critique: 2,
|
|
116
|
+
behavioral: 1,
|
|
117
|
+
};
|
|
118
|
+
/** The strongest source across a candidate's contributing signals. */
|
|
119
|
+
function dominantSource(rows) {
|
|
120
|
+
return rows.reduce((best, row) => SOURCE_AUTHORITY[row.source] > SOURCE_AUTHORITY[best] ? row.source : best, "behavioral");
|
|
121
|
+
}
|
|
122
|
+
/** Read a stated lesson `kind` out of a signal's `evidence_json` (the route
|
|
123
|
+
* stores an explicit/self_critique POST's `kind` there), tolerating malformed
|
|
124
|
+
* JSON. */
|
|
125
|
+
function evidenceKind(json) {
|
|
126
|
+
if (!json)
|
|
127
|
+
return null;
|
|
128
|
+
try {
|
|
129
|
+
const parsed = JSON.parse(json);
|
|
130
|
+
return typeof parsed?.kind === "string" && LESSON_KINDS.has(parsed.kind)
|
|
131
|
+
? parsed.kind
|
|
132
|
+
: null;
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Best-effort lesson `kind` for a candidate so the LLM doesn't have to guess
|
|
140
|
+
* the trailer it's told to copy "from the candidate" (task-flow Step 4a): an
|
|
141
|
+
* explicit/self_critique POST's stated `kind` wins, else a `correction`
|
|
142
|
+
* valence maps to `correction`, else `null` (the LLM infers from the prose).
|
|
143
|
+
*/
|
|
144
|
+
function candidateKind(rows) {
|
|
145
|
+
for (const row of rows) {
|
|
146
|
+
const kind = evidenceKind(row.evidence_json);
|
|
147
|
+
if (kind)
|
|
148
|
+
return kind;
|
|
149
|
+
}
|
|
150
|
+
return rows.some((row) => row.valence === "correction") ? "correction" : null;
|
|
151
|
+
}
|
|
152
|
+
function renderLessonsScope(input, opts, out) {
|
|
153
|
+
const label = formatScope(input.scope);
|
|
154
|
+
const storeFile = storeFileAttr(input.scope);
|
|
155
|
+
const section = scopeSectionSlug(input.scope);
|
|
156
|
+
const halfLife = opts.recencyHalfLifeDays ?? DEFAULT_RECENCY_HALFLIFE_DAYS;
|
|
157
|
+
const sectionBody = input.existingFileMd
|
|
158
|
+
? extractMarkdownSection(input.existingFileMd, "Lessons")
|
|
159
|
+
: null;
|
|
160
|
+
const existing = sectionBody ? parseLessonsSection(sectionBody) : [];
|
|
161
|
+
// `current_bytes` measures the on-disk `## Lessons` SECTION body — the
|
|
162
|
+
// §6 cap unit (`lessonsSectionByteLength` in lesson-format.ts), the same
|
|
163
|
+
// unit `enforceCaps` below derives `over_cap` from. Measuring the whole
|
|
164
|
+
// file here while `over_cap` measured the section produced a
|
|
165
|
+
// self-contradictory tag (and disagreed with the §6 unit the eviction
|
|
166
|
+
// engine actually controls).
|
|
167
|
+
const currentBytes = sectionBody
|
|
168
|
+
? Buffer.byteLength(sectionBody, "utf-8")
|
|
169
|
+
: 0;
|
|
170
|
+
// Eviction ranking: ascending score → rank 1 = evict-first. The plan
|
|
171
|
+
// (post-dedupe) tells the LLM whether the store is already over cap.
|
|
172
|
+
// Pass the same half-life the displayed `ranked` scores use so the plan
|
|
173
|
+
// and the ranking can never derive from two different scorings.
|
|
174
|
+
const plan = enforceCaps(existing, { maxBytes: input.caps.capBytes, maxEntries: input.caps.maxEntries }, opts.nowIso, { scopeLabel: label }, undefined, halfLife);
|
|
175
|
+
const ranked = [...existing].sort((a, b) => scoreLesson(a, opts.nowIso, undefined, halfLife) -
|
|
176
|
+
scoreLesson(b, opts.nowIso, undefined, halfLife));
|
|
177
|
+
out.push(` <scope label="${xmlEscape(label)}" store="${xmlEscape(storeFile)}" ` +
|
|
178
|
+
`section="${xmlEscape(section)}" mode="lessons" ` +
|
|
179
|
+
`cap_bytes="${input.caps.capBytes}" max_entries="${input.caps.maxEntries}" ` +
|
|
180
|
+
`current_bytes="${currentBytes}" current_entries="${existing.length}" ` +
|
|
181
|
+
`over_cap="${plan.evicted.length > 0}">`);
|
|
182
|
+
if (ranked.length > 0) {
|
|
183
|
+
out.push(` <existing_lessons note="ranked by eviction score; drop any lesson marked stale="true" unless a fresh candidate re-reinforces it; if the section still exceeds the cap after your edits, remove from rank 1 upward then append: ${xmlEscape("- [...N lower-signal lessons omitted — full history in feedback_signals]")}">`);
|
|
184
|
+
ranked.forEach((lesson, idx) => {
|
|
185
|
+
out.push(` <lesson rank="${idx + 1}" score="${round2(scoreLesson(lesson, opts.nowIso, undefined, halfLife))}" ev="${lesson.ev}" kind="${lesson.kind}" last="${lesson.last}" ` +
|
|
186
|
+
`provisional="${lesson.provisional}" ` +
|
|
187
|
+
`stale="${isLessonStale(lesson, opts.nowIso, opts.staleDays)}">` +
|
|
188
|
+
`${inline(lesson.text)}</lesson>`);
|
|
189
|
+
});
|
|
190
|
+
out.push(" </existing_lessons>");
|
|
191
|
+
}
|
|
192
|
+
renderCandidates(input.signals, opts, out, true);
|
|
193
|
+
out.push(" </scope>");
|
|
194
|
+
}
|
|
195
|
+
function renderRawScope(input, opts, out) {
|
|
196
|
+
const label = formatScope(input.scope);
|
|
197
|
+
const storeFile = storeFileAttr(input.scope);
|
|
198
|
+
const section = scopeSectionSlug(input.scope);
|
|
199
|
+
out.push(` <scope label="${xmlEscape(label)}" store="${xmlEscape(storeFile)}" ` +
|
|
200
|
+
`section="${xmlEscape(section)}" mode="raw">`);
|
|
201
|
+
renderCandidates(input.signals, opts, out, false);
|
|
202
|
+
out.push(" </scope>");
|
|
203
|
+
}
|
|
204
|
+
function renderCandidates(signals, opts, out, withVerdict) {
|
|
205
|
+
const groups = groupSignalsBySummary(signals.map((row) => ({ id: row.id, summary: row.summary, row })));
|
|
206
|
+
out.push(" <candidates>");
|
|
207
|
+
for (const group of groups) {
|
|
208
|
+
const ids = group.members.map((member) => member.id).join(",");
|
|
209
|
+
if (withVerdict) {
|
|
210
|
+
const memberRows = group.members.map((member) => member.row);
|
|
211
|
+
const verdict = evaluatePromotion(memberRows.map((row) => ({
|
|
212
|
+
source: row.source,
|
|
213
|
+
valence: row.valence,
|
|
214
|
+
})), opts.promotionThreshold);
|
|
215
|
+
const src = dominantSource(memberRows);
|
|
216
|
+
const kind = candidateKind(memberRows);
|
|
217
|
+
out.push(` <candidate signals="${group.members.length}" ` +
|
|
218
|
+
`weighted_ev="${round2(verdict.weightedEv)}" ` +
|
|
219
|
+
`decision="${verdict.promotable ? "promote" : "hold-provisional"}" ` +
|
|
220
|
+
`conf="${verdict.conf}" src="${src}"` +
|
|
221
|
+
(kind ? ` kind="${kind}"` : "") +
|
|
222
|
+
` reason="${verdict.reason}" ids="${ids}">` +
|
|
223
|
+
`${inline(group.summary)}</candidate>`);
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
out.push(` <candidate signals="${group.members.length}" ids="${ids}">` +
|
|
227
|
+
`${inline(group.summary)}</candidate>`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
out.push(" </candidates>");
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Compose the `<feedback_worksheet>` block. Returns `null` when there are no
|
|
234
|
+
* signals at all (the caller then stamps nothing — no empty block in the prompt).
|
|
235
|
+
*/
|
|
236
|
+
export function buildFeedbackWorksheet(scopes, opts) {
|
|
237
|
+
const signalIds = scopes.flatMap((scope) => scope.signals.map((signal) => signal.id));
|
|
238
|
+
if (signalIds.length === 0)
|
|
239
|
+
return null;
|
|
240
|
+
const out = [];
|
|
241
|
+
out.push(`<feedback_worksheet generated_at="${xmlEscape(opts.nowIso)}" ` +
|
|
242
|
+
`promotion_threshold="${opts.promotionThreshold}" scopes="${scopes.length}">`);
|
|
243
|
+
for (const scope of scopes) {
|
|
244
|
+
if (scope.caps) {
|
|
245
|
+
renderLessonsScope({ ...scope, caps: scope.caps }, opts, out);
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
renderRawScope(scope, opts, out);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
out.push(` <consume ids="${signalIds.join(",")}" />`);
|
|
252
|
+
out.push("</feedback_worksheet>");
|
|
253
|
+
return { block: out.join("\n"), signalIds, scopeCount: scopes.length };
|
|
254
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feedback Learning Loop — eviction scorer + cap enforcer (FEEDBACK_LEARNING_LOOP_DESIGN.md §6).
|
|
3
|
+
*
|
|
4
|
+
* A **new** pure-logic module (not `trimBulletEntries`, which is recency
|
|
5
|
+
* top-N with no notion of `ev`/`kind`; not `clearEntriesBefore`, which keys on
|
|
6
|
+
* the *leading* `[date]` not the trailer `last=`). It scores lessons and, when
|
|
7
|
+
* a file is over its per-scope byte/entry cap, evicts the lowest-scored first
|
|
8
|
+
* — provisional + stale go first — emitting an `[...N omitted]` marker.
|
|
9
|
+
*
|
|
10
|
+
* score = w_ev·log(ev+1) + w_recency·decay(last) + w_kind·importance(kind)
|
|
11
|
+
*
|
|
12
|
+
* where importance is `constraint > correction > do-more/do-less > preference`.
|
|
13
|
+
* Provisional lessons carry a fixed penalty so they sort below active peers of
|
|
14
|
+
* equal evidence. Near-duplicates are merged (their `ev` summed) *before*
|
|
15
|
+
* eviction is considered, so a merged lesson is harder to evict, never easier.
|
|
16
|
+
*/
|
|
17
|
+
import { type Lesson, type LessonKind } from "./lesson-format.js";
|
|
18
|
+
export interface EvictionWeights {
|
|
19
|
+
ev: number;
|
|
20
|
+
recency: number;
|
|
21
|
+
kind: number;
|
|
22
|
+
/** Subtracted from a provisional lesson's score so it evicts first. */
|
|
23
|
+
provisionalPenalty: number;
|
|
24
|
+
}
|
|
25
|
+
export declare const DEFAULT_EVICTION_WEIGHTS: EvictionWeights;
|
|
26
|
+
/** Half-life (days) of the recency decay term. */
|
|
27
|
+
export declare const DEFAULT_RECENCY_HALFLIFE_DAYS = 45;
|
|
28
|
+
/** `constraint` > `correction` > `do-more`/`do-less` > `preference`. */
|
|
29
|
+
export declare function kindImportance(kind: LessonKind): number;
|
|
30
|
+
/**
|
|
31
|
+
* Exponential recency decay in `[0, 1]`: `1` for a lesson reinforced today,
|
|
32
|
+
* `0.5` at one half-life, approaching `0` for ancient lessons. A future or
|
|
33
|
+
* unparseable `last` clamps to `1` (treated as fresh — never penalised for a
|
|
34
|
+
* clock/format quirk).
|
|
35
|
+
*/
|
|
36
|
+
export declare function recencyDecay(last: string, nowIso: string, halfLifeDays?: number): number;
|
|
37
|
+
/** Composite eviction score — higher means keep, lower means evict first. */
|
|
38
|
+
export declare function scoreLesson(lesson: Lesson, nowIso: string, weights?: EvictionWeights, halfLifeDays?: number): number;
|
|
39
|
+
/**
|
|
40
|
+
* §4 step 7 staleness test — a lesson is prunable for staleness when its `last`
|
|
41
|
+
* reinforcement predates `now − staleDays` and it is not a durable
|
|
42
|
+
* `constraint`. Shared single source of truth for both worksheet builders
|
|
43
|
+
* (the nightly `consolidation-prep` and the monthly `regeneralization-prep`)
|
|
44
|
+
* so the `stale="…"` flag they stamp can never drift apart.
|
|
45
|
+
*
|
|
46
|
+
* Semantics (kept byte-stable across the two prior local copies):
|
|
47
|
+
* - no horizon configured (`staleDays === undefined`) ⇒ never stale;
|
|
48
|
+
* - `kind=constraint` ⇒ never stale (durable);
|
|
49
|
+
* - an unparseable `last` (or `nowIso`) yields a `NaN` comparison, which is
|
|
50
|
+
* `false` — i.e. never prune on a clock/format quirk. Reuses {@link dateToMs}
|
|
51
|
+
* for the same `YYYY-MM-DD → epoch ms` parse the recency decay uses.
|
|
52
|
+
*/
|
|
53
|
+
export declare function isLessonStale(lesson: Lesson, nowIso: string, staleDays: number | undefined): boolean;
|
|
54
|
+
export interface CapConfig {
|
|
55
|
+
maxBytes: number;
|
|
56
|
+
maxEntries: number;
|
|
57
|
+
}
|
|
58
|
+
export interface EvictionPlan {
|
|
59
|
+
/** Lessons that survive, in eviction-score order (highest first). */
|
|
60
|
+
keep: Lesson[];
|
|
61
|
+
/** Lessons removed to satisfy the cap, lowest-scored first. */
|
|
62
|
+
evicted: Lesson[];
|
|
63
|
+
/** `[...N … omitted]` marker when anything was evicted, else `null`. */
|
|
64
|
+
omittedMarker: string | null;
|
|
65
|
+
/** Serialized byte length of the kept section incl. header + marker. */
|
|
66
|
+
bytes: number;
|
|
67
|
+
}
|
|
68
|
+
export declare function omittedMarker(count: number): string;
|
|
69
|
+
/**
|
|
70
|
+
* Dedupe, score, sort (highest first), then evict the lowest-scored lessons
|
|
71
|
+
* until the section fits both `maxEntries` and `maxBytes`. The byte cap is
|
|
72
|
+
* checked against the *serialized* section (header + bullets + marker) so the
|
|
73
|
+
* measured size matches what lands on disk.
|
|
74
|
+
*
|
|
75
|
+
* Always makes progress when over the byte cap with ≥1 lesson — even a single
|
|
76
|
+
* lesson longer than the cap is reduced to an empty kept set with the marker —
|
|
77
|
+
* so the loop terminates.
|
|
78
|
+
*/
|
|
79
|
+
export declare function enforceCaps(lessons: ReadonlyArray<Lesson>, cap: CapConfig, nowIso: string, opts: {
|
|
80
|
+
scopeLabel: string;
|
|
81
|
+
}, weights?: EvictionWeights, halfLifeDays?: number): EvictionPlan;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feedback Learning Loop — eviction scorer + cap enforcer (FEEDBACK_LEARNING_LOOP_DESIGN.md §6).
|
|
3
|
+
*
|
|
4
|
+
* A **new** pure-logic module (not `trimBulletEntries`, which is recency
|
|
5
|
+
* top-N with no notion of `ev`/`kind`; not `clearEntriesBefore`, which keys on
|
|
6
|
+
* the *leading* `[date]` not the trailer `last=`). It scores lessons and, when
|
|
7
|
+
* a file is over its per-scope byte/entry cap, evicts the lowest-scored first
|
|
8
|
+
* — provisional + stale go first — emitting an `[...N omitted]` marker.
|
|
9
|
+
*
|
|
10
|
+
* score = w_ev·log(ev+1) + w_recency·decay(last) + w_kind·importance(kind)
|
|
11
|
+
*
|
|
12
|
+
* where importance is `constraint > correction > do-more/do-less > preference`.
|
|
13
|
+
* Provisional lessons carry a fixed penalty so they sort below active peers of
|
|
14
|
+
* equal evidence. Near-duplicates are merged (their `ev` summed) *before*
|
|
15
|
+
* eviction is considered, so a merged lesson is harder to evict, never easier.
|
|
16
|
+
*/
|
|
17
|
+
import { formatLessonsSection, } from "./lesson-format.js";
|
|
18
|
+
import { dedupeLessons } from "./lesson-merge.js";
|
|
19
|
+
export const DEFAULT_EVICTION_WEIGHTS = {
|
|
20
|
+
ev: 1.0,
|
|
21
|
+
recency: 1.0,
|
|
22
|
+
kind: 0.75,
|
|
23
|
+
provisionalPenalty: 1.0,
|
|
24
|
+
};
|
|
25
|
+
/** Half-life (days) of the recency decay term. */
|
|
26
|
+
export const DEFAULT_RECENCY_HALFLIFE_DAYS = 45;
|
|
27
|
+
const KIND_IMPORTANCE = {
|
|
28
|
+
constraint: 4,
|
|
29
|
+
correction: 3,
|
|
30
|
+
"do-more": 2,
|
|
31
|
+
"do-less": 2,
|
|
32
|
+
preference: 1,
|
|
33
|
+
};
|
|
34
|
+
/** `constraint` > `correction` > `do-more`/`do-less` > `preference`. */
|
|
35
|
+
export function kindImportance(kind) {
|
|
36
|
+
return KIND_IMPORTANCE[kind];
|
|
37
|
+
}
|
|
38
|
+
function dateToMs(date) {
|
|
39
|
+
const ms = Date.parse(`${date.slice(0, 10)}T00:00:00Z`);
|
|
40
|
+
return Number.isFinite(ms) ? ms : NaN;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Exponential recency decay in `[0, 1]`: `1` for a lesson reinforced today,
|
|
44
|
+
* `0.5` at one half-life, approaching `0` for ancient lessons. A future or
|
|
45
|
+
* unparseable `last` clamps to `1` (treated as fresh — never penalised for a
|
|
46
|
+
* clock/format quirk).
|
|
47
|
+
*/
|
|
48
|
+
export function recencyDecay(last, nowIso, halfLifeDays = DEFAULT_RECENCY_HALFLIFE_DAYS) {
|
|
49
|
+
const lastMs = dateToMs(last);
|
|
50
|
+
const nowMs = Date.parse(nowIso);
|
|
51
|
+
if (!Number.isFinite(lastMs) || !Number.isFinite(nowMs))
|
|
52
|
+
return 1;
|
|
53
|
+
const ageDays = (nowMs - lastMs) / 86_400_000;
|
|
54
|
+
if (ageDays <= 0)
|
|
55
|
+
return 1;
|
|
56
|
+
return Math.pow(0.5, ageDays / halfLifeDays);
|
|
57
|
+
}
|
|
58
|
+
/** Composite eviction score — higher means keep, lower means evict first. */
|
|
59
|
+
export function scoreLesson(lesson, nowIso, weights = DEFAULT_EVICTION_WEIGHTS, halfLifeDays = DEFAULT_RECENCY_HALFLIFE_DAYS) {
|
|
60
|
+
const evTerm = weights.ev * Math.log(Math.max(lesson.ev, 0) + 1);
|
|
61
|
+
const recencyTerm = weights.recency * recencyDecay(lesson.last, nowIso, halfLifeDays);
|
|
62
|
+
const kindTerm = weights.kind * kindImportance(lesson.kind);
|
|
63
|
+
const penalty = lesson.provisional ? weights.provisionalPenalty : 0;
|
|
64
|
+
return evTerm + recencyTerm + kindTerm - penalty;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* §4 step 7 staleness test — a lesson is prunable for staleness when its `last`
|
|
68
|
+
* reinforcement predates `now − staleDays` and it is not a durable
|
|
69
|
+
* `constraint`. Shared single source of truth for both worksheet builders
|
|
70
|
+
* (the nightly `consolidation-prep` and the monthly `regeneralization-prep`)
|
|
71
|
+
* so the `stale="…"` flag they stamp can never drift apart.
|
|
72
|
+
*
|
|
73
|
+
* Semantics (kept byte-stable across the two prior local copies):
|
|
74
|
+
* - no horizon configured (`staleDays === undefined`) ⇒ never stale;
|
|
75
|
+
* - `kind=constraint` ⇒ never stale (durable);
|
|
76
|
+
* - an unparseable `last` (or `nowIso`) yields a `NaN` comparison, which is
|
|
77
|
+
* `false` — i.e. never prune on a clock/format quirk. Reuses {@link dateToMs}
|
|
78
|
+
* for the same `YYYY-MM-DD → epoch ms` parse the recency decay uses.
|
|
79
|
+
*/
|
|
80
|
+
export function isLessonStale(lesson, nowIso, staleDays) {
|
|
81
|
+
if (staleDays === undefined || lesson.kind === "constraint")
|
|
82
|
+
return false;
|
|
83
|
+
const lastMs = dateToMs(lesson.last);
|
|
84
|
+
const nowMs = Date.parse(nowIso);
|
|
85
|
+
return (nowMs - lastMs) / 86_400_000 > staleDays;
|
|
86
|
+
}
|
|
87
|
+
export function omittedMarker(count) {
|
|
88
|
+
return `- [...${count} lower-signal lessons omitted — full history in feedback_signals]`;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Dedupe, score, sort (highest first), then evict the lowest-scored lessons
|
|
92
|
+
* until the section fits both `maxEntries` and `maxBytes`. The byte cap is
|
|
93
|
+
* checked against the *serialized* section (header + bullets + marker) so the
|
|
94
|
+
* measured size matches what lands on disk.
|
|
95
|
+
*
|
|
96
|
+
* Always makes progress when over the byte cap with ≥1 lesson — even a single
|
|
97
|
+
* lesson longer than the cap is reduced to an empty kept set with the marker —
|
|
98
|
+
* so the loop terminates.
|
|
99
|
+
*/
|
|
100
|
+
export function enforceCaps(lessons, cap, nowIso, opts, weights = DEFAULT_EVICTION_WEIGHTS, halfLifeDays = DEFAULT_RECENCY_HALFLIFE_DAYS) {
|
|
101
|
+
const deduped = dedupeLessons(lessons);
|
|
102
|
+
const sorted = [...deduped].sort((a, b) => scoreLesson(b, nowIso, weights, halfLifeDays) -
|
|
103
|
+
scoreLesson(a, nowIso, weights, halfLifeDays));
|
|
104
|
+
const evicted = [];
|
|
105
|
+
let keep = sorted;
|
|
106
|
+
// Entry cap first — cheap, and shrinks the byte-cap work. Reverse the
|
|
107
|
+
// overflow slice (it comes off the descending-sorted array) so `evicted`
|
|
108
|
+
// honours its documented lowest-scored-first order; the byte-cap loop
|
|
109
|
+
// below already pushes lowest-first, so the combined array stays
|
|
110
|
+
// ascending by score.
|
|
111
|
+
if (keep.length > cap.maxEntries) {
|
|
112
|
+
evicted.push(...keep.slice(cap.maxEntries).reverse());
|
|
113
|
+
keep = keep.slice(0, cap.maxEntries);
|
|
114
|
+
}
|
|
115
|
+
const sectionOpts = {
|
|
116
|
+
scopeLabel: opts.scopeLabel,
|
|
117
|
+
capBytes: cap.maxBytes,
|
|
118
|
+
maxEntries: cap.maxEntries,
|
|
119
|
+
};
|
|
120
|
+
const measure = (lessonsToMeasure) => Buffer.byteLength(formatLessonsSection(lessonsToMeasure, {
|
|
121
|
+
...sectionOpts,
|
|
122
|
+
omittedMarker: evicted.length > 0 ? omittedMarker(evicted.length) : null,
|
|
123
|
+
}), "utf-8");
|
|
124
|
+
// Byte cap — drop lowest-scored (tail of the sorted array) until it fits.
|
|
125
|
+
while (keep.length > 0 && measure(keep) > cap.maxBytes) {
|
|
126
|
+
evicted.push(keep[keep.length - 1]);
|
|
127
|
+
keep = keep.slice(0, -1);
|
|
128
|
+
}
|
|
129
|
+
const marker = evicted.length > 0 ? omittedMarker(evicted.length) : null;
|
|
130
|
+
return {
|
|
131
|
+
keep,
|
|
132
|
+
evicted,
|
|
133
|
+
omittedMarker: marker,
|
|
134
|
+
bytes: Buffer.byteLength(formatLessonsSection(keep, { ...sectionOpts, omittedMarker: marker }), "utf-8"),
|
|
135
|
+
};
|
|
136
|
+
}
|