@aitne/daemon 0.1.10 → 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.js +8 -5
- 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.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/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/context/permissions.js +3 -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 +48 -12
- package/dist/api/routes/dashboard/cost-approvals.js +66 -0
- package/dist/api/routes/dashboard/notifications.js +9 -9
- package/dist/api/routes/integrations/crud-patch.js +5 -1
- package/dist/api/routes/integrations-reconcile.js +2 -2
- 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 +9 -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 +245 -7
- 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 +26 -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 +25 -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 +39 -15
- 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 +62 -20
- 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 +1 -1
- package/dist/core/context-paths.js +1 -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 +49 -0
- 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 +17 -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 +63 -1
- package/dist/core/dispatcher-scheduled-tasks.js +213 -6
- 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 +284 -59
- package/dist/core/dm-freshness-metrics.d.ts +1 -1
- package/dist/core/drift-effects.js +2 -2
- package/dist/core/feedback/consolidation-prep.js +17 -5
- package/dist/core/feedback/eviction-scorer.js +6 -2
- package/dist/core/feedback/lesson-format.js +9 -4
- package/dist/core/feedback/lesson-injection.d.ts +1 -1
- package/dist/core/feedback/lesson-injection.js +17 -2
- package/dist/core/feedback/lesson-store-overview.d.ts +8 -4
- package/dist/core/feedback/lesson-store-overview.js +8 -4
- package/dist/core/feedback/regeneralization-prep.js +29 -16
- 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 +4 -4
- package/dist/core/injection-policy.js +4 -4
- 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 +12 -0
- package/dist/core/signal-detector.js +53 -9
- 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 +2 -2
- package/dist/core/today-direct-writer.js +1 -1
- 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/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/migrations.js +330 -0
- package/dist/db/observations.d.ts +2 -2
- package/dist/db/observations.js +3 -3
- package/dist/db/schema.js +217 -16
- 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/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 +75 -11
- 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 +16 -6
- 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/observations-batch.d.ts +1 -1
- package/dist/services/observations-batch.js +2 -2
- package/dist/settings/runtime-settings.d.ts +38 -11
- package/dist/settings/runtime-settings.js +203 -40
- package/dist/settings/settings-store.js +11 -3
- package/package.json +4 -4
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-Tuning Review Cycle — Measure stage (SELF_TUNING_REVIEW_CYCLE_DESIGN.md
|
|
3
|
+
* §3.1, Phase 1).
|
|
4
|
+
*
|
|
5
|
+
* The daemon-side, deterministic Measure step ($0 — LLM tokens buy judgment
|
|
6
|
+
* only, P1). On the weekly-review dispatch it computes SQL aggregates over
|
|
7
|
+
* `agent_actions`, `notification_log`, and the `runtime_state` self-tuning
|
|
8
|
+
* ledger for a 7-day window plus a 7-day-prior baseline (trend column), and
|
|
9
|
+
* renders one compact `<self_performance>` block — hard-capped at
|
|
10
|
+
* {@link SELF_PERFORMANCE_MAX_BYTES} — so the weekly review's "Metrics (agent
|
|
11
|
+
* side)" section copies daemon-computed facts instead of paying Sonnet prices
|
|
12
|
+
* to re-count them.
|
|
13
|
+
*
|
|
14
|
+
* Two layers, mirroring `consolidation-prep.ts`:
|
|
15
|
+
* - {@link gatherSelfPerformanceData} — the single DB read (side-effect
|
|
16
|
+
* free): per-`action_type` run/cost/duration aggregates (`agent_actions`
|
|
17
|
+
* has no process_key column; `action_type` carries the routine identity),
|
|
18
|
+
* the `routine.fetch_window` empty-run rate per integration (from the
|
|
19
|
+
* fan-out audit rows' `detail.prePass` payload the runner persists), the
|
|
20
|
+
* `activity_scan.gate` stage distribution (from `buildGateAuditDetail`'s
|
|
21
|
+
* historical per-tick rows), per-notification-type `user_reaction`
|
|
22
|
+
* breakdowns (the first reader of the column `signal-detector.ts`
|
|
23
|
+
* populates), and the `runtime_state.self_tuning:*` ledger.
|
|
24
|
+
* - {@link buildSelfPerformanceBlock} — pure renderer. Deterministic
|
|
25
|
+
* byte-capped output: per-section row budgets shrink one row at a time
|
|
26
|
+
* (largest section first) until the block fits the cap, and clipped rows
|
|
27
|
+
* surface as `omitted="N"` so truncation is never silent.
|
|
28
|
+
*
|
|
29
|
+
* Phase 1 carries no actuator: nothing here writes config, schedules, or
|
|
30
|
+
* lessons. Later phases (Recommend / Judge / Actuate) consume the same data
|
|
31
|
+
* shape; {@link SELF_TUNING_LEDGER_PREFIX} is exported so the Phase 3
|
|
32
|
+
* actuator writes the ledger keys this module already reads.
|
|
33
|
+
*/
|
|
34
|
+
import { formatSqliteDatetime } from "@aitne/shared";
|
|
35
|
+
import { extractMarkdownSection, parseLessonsSection, } from "./lesson-format.js";
|
|
36
|
+
/** Measurement window length; the baseline is the same span immediately prior. */
|
|
37
|
+
export const SELF_PERFORMANCE_WINDOW_DAYS = 7;
|
|
38
|
+
/** §3.1 — hard cap on the rendered `<self_performance>` block, in UTF-8 bytes. */
|
|
39
|
+
export const SELF_PERFORMANCE_MAX_BYTES = 1500;
|
|
40
|
+
/**
|
|
41
|
+
* §3.4 ledger key prefix. Phase 3's actuator writes
|
|
42
|
+
* `runtime_state.self_tuning:<key> = {prev, applied_at, baselineMetric, rule}`;
|
|
43
|
+
* Phase 1 already reads (and renders) whatever sits under the prefix so the
|
|
44
|
+
* weekly review sees applied changes the cycle they land.
|
|
45
|
+
*/
|
|
46
|
+
export const SELF_TUNING_LEDGER_PREFIX = "self_tuning:";
|
|
47
|
+
/** Fan-out audit rows carry the fetcher event's type as `action_type`. */
|
|
48
|
+
export const FETCH_WINDOW_ACTION_TYPE = "routine.fetch_window";
|
|
49
|
+
/** Per-tick gate audit rows (`buildGateAuditDetail` payload in `detail`). */
|
|
50
|
+
export const ACTIVITY_SCAN_GATE_ACTION_TYPE = "activity_scan.gate";
|
|
51
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
52
|
+
const SENT_STATUSES = new Set(["delivered", "batched"]);
|
|
53
|
+
const COMPLETED_PREPASS_STATUSES = new Set(["success", "partial"]);
|
|
54
|
+
const UNTYPED_NOTIFICATION = "(untyped)";
|
|
55
|
+
/** Tolerant JSON parse — a corrupt detail blob degrades to "no data", never a throw. */
|
|
56
|
+
function parseJson(raw) {
|
|
57
|
+
if (!raw)
|
|
58
|
+
return null;
|
|
59
|
+
try {
|
|
60
|
+
const parsed = JSON.parse(raw);
|
|
61
|
+
return typeof parsed === "object" && parsed !== null
|
|
62
|
+
? parsed
|
|
63
|
+
: null;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/** Median over a pre-sorted array; averages the two middle values on even counts. */
|
|
70
|
+
function median(sortedAscending) {
|
|
71
|
+
const n = sortedAscending.length;
|
|
72
|
+
if (n === 0)
|
|
73
|
+
return null;
|
|
74
|
+
const mid = Math.floor(n / 2);
|
|
75
|
+
return n % 2 === 1
|
|
76
|
+
? sortedAscending[mid]
|
|
77
|
+
: (sortedAscending[mid - 1] + sortedAscending[mid]) / 2;
|
|
78
|
+
}
|
|
79
|
+
function gatherActions(db, fromUtc, toUtc) {
|
|
80
|
+
const rows = db
|
|
81
|
+
.prepare(`SELECT action_type AS actionType, result,
|
|
82
|
+
cost_usd AS costUsd, duration_ms AS durationMs
|
|
83
|
+
FROM agent_actions
|
|
84
|
+
WHERE started_at >= ? AND started_at < ?`)
|
|
85
|
+
.all(fromUtc, toUtc);
|
|
86
|
+
const byType = new Map();
|
|
87
|
+
for (const row of rows) {
|
|
88
|
+
let agg = byType.get(row.actionType);
|
|
89
|
+
if (!agg) {
|
|
90
|
+
agg = {
|
|
91
|
+
actionType: row.actionType,
|
|
92
|
+
runs: 0,
|
|
93
|
+
success: 0,
|
|
94
|
+
partial: 0,
|
|
95
|
+
failed: 0,
|
|
96
|
+
skipped: 0,
|
|
97
|
+
costUsd: 0,
|
|
98
|
+
durations: [],
|
|
99
|
+
};
|
|
100
|
+
byType.set(row.actionType, agg);
|
|
101
|
+
}
|
|
102
|
+
agg.runs += 1;
|
|
103
|
+
// `in_progress` (transient) and NULL results count toward runs only.
|
|
104
|
+
if (row.result === "success")
|
|
105
|
+
agg.success += 1;
|
|
106
|
+
else if (row.result === "partial")
|
|
107
|
+
agg.partial += 1;
|
|
108
|
+
else if (row.result === "failed")
|
|
109
|
+
agg.failed += 1;
|
|
110
|
+
else if (row.result === "skipped")
|
|
111
|
+
agg.skipped += 1;
|
|
112
|
+
if (typeof row.costUsd === "number")
|
|
113
|
+
agg.costUsd += row.costUsd;
|
|
114
|
+
if (typeof row.durationMs === "number")
|
|
115
|
+
agg.durations.push(row.durationMs);
|
|
116
|
+
}
|
|
117
|
+
return [...byType.values()].map(({ durations, ...rest }) => {
|
|
118
|
+
const p50 = median([...durations].sort((a, b) => a - b));
|
|
119
|
+
return {
|
|
120
|
+
...rest,
|
|
121
|
+
p50DurationMs: p50 === null ? null : Math.round(p50),
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
function gatherFetchWindow(db, fromUtc, toUtc) {
|
|
126
|
+
const rows = db
|
|
127
|
+
.prepare(`SELECT detail FROM agent_actions
|
|
128
|
+
WHERE action_type = ? AND started_at >= ? AND started_at < ?
|
|
129
|
+
AND detail IS NOT NULL`)
|
|
130
|
+
.all(FETCH_WINDOW_ACTION_TYPE, fromUtc, toUtc);
|
|
131
|
+
const byKey = new Map();
|
|
132
|
+
for (const row of rows) {
|
|
133
|
+
const prePass = parseJson(row.detail)?.prePass;
|
|
134
|
+
if (typeof prePass !== "object" || prePass === null)
|
|
135
|
+
continue;
|
|
136
|
+
const integrationKey = prePass.integrationKey;
|
|
137
|
+
const status = prePass.status;
|
|
138
|
+
if (typeof integrationKey !== "string")
|
|
139
|
+
continue;
|
|
140
|
+
if (typeof status !== "string" || !COMPLETED_PREPASS_STATUSES.has(status)) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const fetched = typeof prePass.fetched === "number" ? prePass.fetched : 0;
|
|
144
|
+
const posted = typeof prePass.posted === "number" ? prePass.posted : 0;
|
|
145
|
+
let agg = byKey.get(integrationKey);
|
|
146
|
+
if (!agg) {
|
|
147
|
+
agg = { integrationKey, runs: 0, empty: 0 };
|
|
148
|
+
byKey.set(integrationKey, agg);
|
|
149
|
+
}
|
|
150
|
+
agg.runs += 1;
|
|
151
|
+
if (fetched === 0 && posted === 0)
|
|
152
|
+
agg.empty += 1;
|
|
153
|
+
}
|
|
154
|
+
return [...byKey.values()];
|
|
155
|
+
}
|
|
156
|
+
function gatherGate(db, fromUtc, toUtc) {
|
|
157
|
+
const rows = db
|
|
158
|
+
.prepare(`SELECT detail, result FROM agent_actions
|
|
159
|
+
WHERE action_type = ? AND started_at >= ? AND started_at < ?`)
|
|
160
|
+
.all(ACTIVITY_SCAN_GATE_ACTION_TYPE, fromUtc, toUtc);
|
|
161
|
+
const stats = {
|
|
162
|
+
ticks: rows.length,
|
|
163
|
+
stage0: 0,
|
|
164
|
+
stage2: 0,
|
|
165
|
+
stage3: 0,
|
|
166
|
+
stage3LowSignal: 0,
|
|
167
|
+
stage3LowSignalLowNovelty: 0,
|
|
168
|
+
};
|
|
169
|
+
for (const row of rows) {
|
|
170
|
+
const detail = parseJson(row.detail);
|
|
171
|
+
if (!detail)
|
|
172
|
+
continue; // corrupt/absent detail still counts as a tick
|
|
173
|
+
const stage = detail.stage_reached;
|
|
174
|
+
if (stage === "stage0_silent") {
|
|
175
|
+
stats.stage0 += 1;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
// The writer never emits a bare "stage2": a Stage-2 tick settles to
|
|
179
|
+
// either the silent alias "stage2_log_only" or an escalated "stage3"
|
|
180
|
+
// (see HourlyGateStats.stage2). Accept both spellings defensively.
|
|
181
|
+
if (stage === "stage2" || stage === "stage2_log_only") {
|
|
182
|
+
stats.stage2 += 1;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (stage !== "stage3")
|
|
186
|
+
continue;
|
|
187
|
+
// Min-observations-floor short-circuit: the gate verdict said stage3
|
|
188
|
+
// but no session ran (`resultOverride: "skipped"` in
|
|
189
|
+
// dispatcher-activity-scan.ts). Count as a tick only.
|
|
190
|
+
if (row.result === "skipped")
|
|
191
|
+
continue;
|
|
192
|
+
stats.stage3 += 1;
|
|
193
|
+
// R3 waste evidence — autonomous low-signal-fallback escalations only.
|
|
194
|
+
if (detail.gate_reason !== "low_signal_default" || detail.forced === true) {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
stats.stage3LowSignal += 1;
|
|
198
|
+
const snapshot = detail.signal_snapshot;
|
|
199
|
+
const novelty = typeof snapshot === "object" && snapshot !== null
|
|
200
|
+
? snapshot.maxNoveltyScore
|
|
201
|
+
: null;
|
|
202
|
+
// A null / missing novelty means nothing scored above the floor —
|
|
203
|
+
// that is the "≤ 1" waste case R3 measures, so it counts.
|
|
204
|
+
if (typeof novelty !== "number" || novelty <= 1) {
|
|
205
|
+
stats.stage3LowSignalLowNovelty += 1;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return stats;
|
|
209
|
+
}
|
|
210
|
+
function gatherNotifications(db, fromUtc, toUtc) {
|
|
211
|
+
const rows = db
|
|
212
|
+
.prepare(`SELECT notification_type AS notificationType,
|
|
213
|
+
user_reaction AS userReaction, status
|
|
214
|
+
FROM notification_log
|
|
215
|
+
WHERE created_at >= ? AND created_at < ?`)
|
|
216
|
+
.all(fromUtc, toUtc);
|
|
217
|
+
const byType = new Map();
|
|
218
|
+
for (const row of rows) {
|
|
219
|
+
if (row.status === null || !SENT_STATUSES.has(row.status))
|
|
220
|
+
continue;
|
|
221
|
+
const name = row.notificationType ?? UNTYPED_NOTIFICATION;
|
|
222
|
+
let agg = byType.get(name);
|
|
223
|
+
if (!agg) {
|
|
224
|
+
agg = {
|
|
225
|
+
notificationType: name,
|
|
226
|
+
sent: 0,
|
|
227
|
+
replied: 0,
|
|
228
|
+
acted: 0,
|
|
229
|
+
corrected: 0,
|
|
230
|
+
ignored: 0,
|
|
231
|
+
pending: 0,
|
|
232
|
+
};
|
|
233
|
+
byType.set(name, agg);
|
|
234
|
+
}
|
|
235
|
+
agg.sent += 1;
|
|
236
|
+
if (row.userReaction === "replied")
|
|
237
|
+
agg.replied += 1;
|
|
238
|
+
else if (row.userReaction === "acted")
|
|
239
|
+
agg.acted += 1;
|
|
240
|
+
else if (row.userReaction === "corrected")
|
|
241
|
+
agg.corrected += 1;
|
|
242
|
+
else if (row.userReaction === "ignored")
|
|
243
|
+
agg.ignored += 1;
|
|
244
|
+
}
|
|
245
|
+
for (const agg of byType.values()) {
|
|
246
|
+
// Unrecognised reaction values fall through to "no reaction yet".
|
|
247
|
+
agg.pending = Math.max(0, agg.sent - agg.replied - agg.acted - agg.corrected - agg.ignored);
|
|
248
|
+
}
|
|
249
|
+
return [...byType.values()];
|
|
250
|
+
}
|
|
251
|
+
function gatherLedger(db) {
|
|
252
|
+
const rows = db
|
|
253
|
+
.prepare(`SELECT key, value_json FROM runtime_state
|
|
254
|
+
WHERE key LIKE ? ORDER BY updated_at DESC, key ASC`)
|
|
255
|
+
.all(`${SELF_TUNING_LEDGER_PREFIX}%`);
|
|
256
|
+
const entries = [];
|
|
257
|
+
for (const row of rows) {
|
|
258
|
+
const value = parseJson(row.value_json);
|
|
259
|
+
if (!value)
|
|
260
|
+
continue; // a corrupt ledger blob never breaks the measure pass
|
|
261
|
+
entries.push({
|
|
262
|
+
key: row.key.slice(SELF_TUNING_LEDGER_PREFIX.length),
|
|
263
|
+
prev: value.prev,
|
|
264
|
+
appliedAt: typeof value.applied_at === "string" ? value.applied_at : null,
|
|
265
|
+
rule: typeof value.rule === "string" ? value.rule : null,
|
|
266
|
+
baselineMetric: value.baselineMetric,
|
|
267
|
+
...(typeof value.reverted_at === "string"
|
|
268
|
+
? { revertedAt: value.reverted_at }
|
|
269
|
+
: {}),
|
|
270
|
+
...(typeof value.verify_result === "string"
|
|
271
|
+
? { verifyResult: value.verify_result }
|
|
272
|
+
: {}),
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
return entries;
|
|
276
|
+
}
|
|
277
|
+
function gatherWindow(db, fromUtc, toUtc) {
|
|
278
|
+
return {
|
|
279
|
+
actions: gatherActions(db, fromUtc, toUtc),
|
|
280
|
+
fetchWindow: gatherFetchWindow(db, fromUtc, toUtc),
|
|
281
|
+
gate: gatherGate(db, fromUtc, toUtc),
|
|
282
|
+
notifications: gatherNotifications(db, fromUtc, toUtc),
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* The single DB read. Computes the current window `[now − windowDays, now)`
|
|
287
|
+
* and the baseline window `[now − 2·windowDays, now − windowDays)` over
|
|
288
|
+
* `agent_actions` / `notification_log` (both store SQLite UTC
|
|
289
|
+
* `YYYY-MM-DD HH:MM:SS` timestamps, so lexicographic comparison against
|
|
290
|
+
* `formatSqliteDatetime` cutoffs is exact), plus the self-tuning ledger.
|
|
291
|
+
*/
|
|
292
|
+
export function gatherSelfPerformanceData(db, opts) {
|
|
293
|
+
const windowDays = opts.windowDays ?? SELF_PERFORMANCE_WINDOW_DAYS;
|
|
294
|
+
const end = formatSqliteDatetime(opts.now);
|
|
295
|
+
const mid = formatSqliteDatetime(new Date(opts.now.getTime() - windowDays * DAY_MS));
|
|
296
|
+
const start = formatSqliteDatetime(new Date(opts.now.getTime() - 2 * windowDays * DAY_MS));
|
|
297
|
+
return {
|
|
298
|
+
windowDays,
|
|
299
|
+
current: gatherWindow(db, mid, end),
|
|
300
|
+
baseline: gatherWindow(db, start, mid),
|
|
301
|
+
ledger: gatherLedger(db),
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* §3.5 — summarise one lesson store's byte pressure from its raw file
|
|
306
|
+
* contents. Pure (the caller does the FS read); a file with no `## Lessons`
|
|
307
|
+
* section degrades to an empty store, never a throw. `medianEv` carries the
|
|
308
|
+
* evidence signal R5's Phase 2 rule keys on (utilization > 90% with median
|
|
309
|
+
* evidence ≤ 1), so the measurement lands one phase ahead of the rule.
|
|
310
|
+
*/
|
|
311
|
+
export function summarizeLessonStoreUtilization(scope, fileMd, capBytes) {
|
|
312
|
+
const sectionBody = extractMarkdownSection(fileMd, "Lessons");
|
|
313
|
+
const lessons = sectionBody ? parseLessonsSection(sectionBody) : [];
|
|
314
|
+
const bytes = sectionBody ? Buffer.byteLength(sectionBody, "utf-8") : 0;
|
|
315
|
+
const medianEv = median(lessons.map((lesson) => lesson.ev).sort((a, b) => a - b));
|
|
316
|
+
return { scope, bytes, capBytes, entries: lessons.length, medianEv };
|
|
317
|
+
}
|
|
318
|
+
// ── Renderer ────────────────────────────────────────────────────────────────
|
|
319
|
+
function xmlEscape(value) {
|
|
320
|
+
return value
|
|
321
|
+
.replace(/&/g, "&")
|
|
322
|
+
.replace(/</g, "<")
|
|
323
|
+
.replace(/>/g, ">")
|
|
324
|
+
.replace(/"/g, """);
|
|
325
|
+
}
|
|
326
|
+
/** Compact USD: at most 4 decimal places, no trailing zeros. */
|
|
327
|
+
function usd(value) {
|
|
328
|
+
return String(Math.round(value * 10000) / 10000);
|
|
329
|
+
}
|
|
330
|
+
/** Integer percent; callers guarantee `denominator > 0`. */
|
|
331
|
+
function pct(numerator, denominator) {
|
|
332
|
+
return `${Math.round((100 * numerator) / denominator)}%`;
|
|
333
|
+
}
|
|
334
|
+
/** One-line inline value for a ledger `prev=` attribute. */
|
|
335
|
+
function inlineValue(value) {
|
|
336
|
+
// JSON.stringify returns runtime `undefined` for `undefined` input even
|
|
337
|
+
// though its declared type is `string` — normalise to "null".
|
|
338
|
+
const json = JSON.stringify(value);
|
|
339
|
+
const raw = typeof value === "string" ? value : json === undefined ? "null" : json;
|
|
340
|
+
return xmlEscape(raw.length > 60 ? `${raw.slice(0, 59)}…` : raw);
|
|
341
|
+
}
|
|
342
|
+
const INITIAL_BUDGET = {
|
|
343
|
+
actions: 8,
|
|
344
|
+
fetchWindow: 8,
|
|
345
|
+
notifications: 6,
|
|
346
|
+
lessonStores: 6,
|
|
347
|
+
ledger: 3,
|
|
348
|
+
};
|
|
349
|
+
/** Drop order when two sections render the same row count (ties only). */
|
|
350
|
+
const SHRINK_TIE_ORDER = [
|
|
351
|
+
"notifications",
|
|
352
|
+
"lessonStores",
|
|
353
|
+
"actions",
|
|
354
|
+
"fetchWindow",
|
|
355
|
+
"ledger",
|
|
356
|
+
];
|
|
357
|
+
function findActionBaseline(baseline, actionType) {
|
|
358
|
+
return baseline.actions.find((entry) => entry.actionType === actionType);
|
|
359
|
+
}
|
|
360
|
+
function renderBlock(data, lessonStores, budget, generatedAt) {
|
|
361
|
+
const { current, baseline, ledger } = data;
|
|
362
|
+
const out = [];
|
|
363
|
+
out.push(`<self_performance generated_at="${xmlEscape(generatedAt)}" ` +
|
|
364
|
+
`window_days="${data.windowDays}" baseline="prior_${data.windowDays}d">`);
|
|
365
|
+
// Totals span ALL rows — they are what the weekly task-flow's
|
|
366
|
+
// "Metrics (agent side)" lines copy, so they must not depend on which
|
|
367
|
+
// per-type rows survived the byte budget below.
|
|
368
|
+
const sum = (window) => window.actions.reduce((acc, a) => ({
|
|
369
|
+
runs: acc.runs + a.runs,
|
|
370
|
+
failed: acc.failed + a.failed,
|
|
371
|
+
cost: acc.cost + a.costUsd,
|
|
372
|
+
}), { runs: 0, failed: 0, cost: 0 });
|
|
373
|
+
const cur = sum(current);
|
|
374
|
+
const prev = sum(baseline);
|
|
375
|
+
const notifSent = current.notifications.reduce((n, t) => n + t.sent, 0);
|
|
376
|
+
const notifIgnored = current.notifications.reduce((n, t) => n + t.ignored, 0);
|
|
377
|
+
const prevNotifSent = baseline.notifications.reduce((n, t) => n + t.sent, 0);
|
|
378
|
+
out.push(` <totals runs="${cur.runs}" failed="${cur.failed}" ` +
|
|
379
|
+
`cost_usd="${usd(cur.cost)}" prev_runs="${prev.runs}" ` +
|
|
380
|
+
`prev_failed="${prev.failed}" prev_cost_usd="${usd(prev.cost)}" ` +
|
|
381
|
+
`notif_sent="${notifSent}" notif_ignored="${notifIgnored}" ` +
|
|
382
|
+
`prev_notif_sent="${prevNotifSent}" />`);
|
|
383
|
+
const actions = [...current.actions].sort((a, b) => b.costUsd - a.costUsd ||
|
|
384
|
+
b.runs - a.runs ||
|
|
385
|
+
a.actionType.localeCompare(b.actionType));
|
|
386
|
+
const shownActions = actions.slice(0, budget.actions);
|
|
387
|
+
if (shownActions.length > 0) {
|
|
388
|
+
const omitted = actions.length - shownActions.length;
|
|
389
|
+
out.push(` <actions ranked="cost_desc"${omitted > 0 ? ` omitted="${omitted}"` : ""}>`);
|
|
390
|
+
for (const action of shownActions) {
|
|
391
|
+
const prevAction = findActionBaseline(baseline, action.actionType);
|
|
392
|
+
out.push(` <a t="${xmlEscape(action.actionType)}" runs="${action.runs}" ` +
|
|
393
|
+
`ok="${action.success}"` +
|
|
394
|
+
(action.partial > 0 ? ` part="${action.partial}"` : "") +
|
|
395
|
+
` fail="${action.failed}" skip="${action.skipped}" ` +
|
|
396
|
+
`cost_usd="${usd(action.costUsd)}"` +
|
|
397
|
+
(action.p50DurationMs !== null
|
|
398
|
+
? ` p50_ms="${action.p50DurationMs}"`
|
|
399
|
+
: "") +
|
|
400
|
+
` prev_runs="${prevAction?.runs ?? 0}" ` +
|
|
401
|
+
`prev_cost_usd="${usd(prevAction?.costUsd ?? 0)}" />`);
|
|
402
|
+
}
|
|
403
|
+
out.push(" </actions>");
|
|
404
|
+
}
|
|
405
|
+
const integrations = [...current.fetchWindow].sort((a, b) => b.runs - a.runs || a.integrationKey.localeCompare(b.integrationKey));
|
|
406
|
+
const shownIntegrations = integrations.slice(0, budget.fetchWindow);
|
|
407
|
+
if (shownIntegrations.length > 0) {
|
|
408
|
+
const omitted = integrations.length - shownIntegrations.length;
|
|
409
|
+
out.push(` <fetch_window_empty note="empty = completed pre-pass run, nothing fetched/posted"` +
|
|
410
|
+
`${omitted > 0 ? ` omitted="${omitted}"` : ""}>`);
|
|
411
|
+
for (const integration of shownIntegrations) {
|
|
412
|
+
const prevIntegration = baseline.fetchWindow.find((entry) => entry.integrationKey === integration.integrationKey);
|
|
413
|
+
out.push(` <i k="${xmlEscape(integration.integrationKey)}" ` +
|
|
414
|
+
`runs="${integration.runs}" empty="${integration.empty}" ` +
|
|
415
|
+
`rate="${pct(integration.empty, integration.runs)}"` +
|
|
416
|
+
(prevIntegration && prevIntegration.runs > 0
|
|
417
|
+
? ` prev_rate="${pct(prevIntegration.empty, prevIntegration.runs)}"`
|
|
418
|
+
: "") +
|
|
419
|
+
" />");
|
|
420
|
+
}
|
|
421
|
+
out.push(" </fetch_window_empty>");
|
|
422
|
+
}
|
|
423
|
+
if (current.gate.ticks > 0 || baseline.gate.ticks > 0) {
|
|
424
|
+
out.push(` <hourly_gate ticks="${current.gate.ticks}" ` +
|
|
425
|
+
`stage0="${current.gate.stage0}" stage2="${current.gate.stage2}" ` +
|
|
426
|
+
`stage3="${current.gate.stage3}" ` +
|
|
427
|
+
`stage3_low_signal="${current.gate.stage3LowSignal}" ` +
|
|
428
|
+
`stage3_low_signal_novelty_le1="${current.gate.stage3LowSignalLowNovelty}" ` +
|
|
429
|
+
`prev_ticks="${baseline.gate.ticks}" ` +
|
|
430
|
+
`prev_stage3="${baseline.gate.stage3}" />`);
|
|
431
|
+
}
|
|
432
|
+
const notifications = [...current.notifications].sort((a, b) => b.sent - a.sent || a.notificationType.localeCompare(b.notificationType));
|
|
433
|
+
const shownNotifications = notifications.slice(0, budget.notifications);
|
|
434
|
+
if (shownNotifications.length > 0) {
|
|
435
|
+
const omitted = notifications.length - shownNotifications.length;
|
|
436
|
+
out.push(` <notifications${omitted > 0 ? ` omitted="${omitted}"` : ""}>`);
|
|
437
|
+
for (const type of shownNotifications) {
|
|
438
|
+
out.push(` <n t="${xmlEscape(type.notificationType)}" sent="${type.sent}" ` +
|
|
439
|
+
`replied="${type.replied}" acted="${type.acted}" ` +
|
|
440
|
+
`corrected="${type.corrected}" ignored="${type.ignored}" ` +
|
|
441
|
+
`pending="${type.pending}" />`);
|
|
442
|
+
}
|
|
443
|
+
out.push(" </notifications>");
|
|
444
|
+
}
|
|
445
|
+
const stores = [...lessonStores].sort((a, b) => (b.capBytes > 0 ? b.bytes / b.capBytes : 0) -
|
|
446
|
+
(a.capBytes > 0 ? a.bytes / a.capBytes : 0) ||
|
|
447
|
+
a.scope.localeCompare(b.scope));
|
|
448
|
+
const shownStores = stores.slice(0, budget.lessonStores);
|
|
449
|
+
if (shownStores.length > 0) {
|
|
450
|
+
const omitted = stores.length - shownStores.length;
|
|
451
|
+
out.push(` <lesson_stores${omitted > 0 ? ` omitted="${omitted}"` : ""}>`);
|
|
452
|
+
for (const store of shownStores) {
|
|
453
|
+
out.push(` <s scope="${xmlEscape(store.scope)}" bytes="${store.bytes}" ` +
|
|
454
|
+
`cap="${store.capBytes}"` +
|
|
455
|
+
(store.capBytes > 0
|
|
456
|
+
? ` util="${pct(store.bytes, store.capBytes)}"`
|
|
457
|
+
: "") +
|
|
458
|
+
` entries="${store.entries}"` +
|
|
459
|
+
(store.medianEv !== null ? ` median_ev="${store.medianEv}"` : "") +
|
|
460
|
+
" />");
|
|
461
|
+
}
|
|
462
|
+
out.push(" </lesson_stores>");
|
|
463
|
+
}
|
|
464
|
+
const shownLedger = ledger.slice(0, budget.ledger);
|
|
465
|
+
if (shownLedger.length > 0) {
|
|
466
|
+
const omitted = ledger.length - shownLedger.length;
|
|
467
|
+
out.push(` <tuning_ledger${omitted > 0 ? ` omitted="${omitted}"` : ""}>`);
|
|
468
|
+
for (const entry of shownLedger) {
|
|
469
|
+
out.push(` <c key="${xmlEscape(entry.key)}" prev="${inlineValue(entry.prev)}"` +
|
|
470
|
+
(entry.appliedAt
|
|
471
|
+
? ` applied_at="${xmlEscape(entry.appliedAt)}"`
|
|
472
|
+
: "") +
|
|
473
|
+
(entry.rule ? ` rule="${xmlEscape(entry.rule)}"` : "") +
|
|
474
|
+
(entry.baselineMetric !== undefined && entry.baselineMetric !== null
|
|
475
|
+
? ` baseline="${inlineValue(entry.baselineMetric)}"`
|
|
476
|
+
: "") +
|
|
477
|
+
// §3.1 "measured effect" — the judge must see that a change was
|
|
478
|
+
// rolled back (key now in the 28d cool-down) or verified clean,
|
|
479
|
+
// not just that it was applied.
|
|
480
|
+
(entry.revertedAt
|
|
481
|
+
? ` reverted_at="${xmlEscape(entry.revertedAt)}"`
|
|
482
|
+
: "") +
|
|
483
|
+
(entry.verifyResult
|
|
484
|
+
? ` verified="${xmlEscape(entry.verifyResult)}"`
|
|
485
|
+
: "") +
|
|
486
|
+
" />");
|
|
487
|
+
}
|
|
488
|
+
out.push(" </tuning_ledger>");
|
|
489
|
+
}
|
|
490
|
+
out.push("</self_performance>");
|
|
491
|
+
return out.join("\n");
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Compose the `<self_performance>` block. Returns `null` when there is no
|
|
495
|
+
* telemetry at all (fresh install) so the caller stamps nothing — no empty
|
|
496
|
+
* block in the prompt. The byte cap is a hard guarantee: row budgets shrink
|
|
497
|
+
* until the block fits; in the (synthetic) case where even the skeleton
|
|
498
|
+
* exceeds the cap, a minimal self-closing element is emitted instead.
|
|
499
|
+
*/
|
|
500
|
+
export function buildSelfPerformanceBlock(data, opts) {
|
|
501
|
+
const hasData = data.current.actions.length > 0 ||
|
|
502
|
+
data.baseline.actions.length > 0 ||
|
|
503
|
+
data.current.notifications.length > 0 ||
|
|
504
|
+
data.baseline.notifications.length > 0 ||
|
|
505
|
+
data.current.gate.ticks > 0 ||
|
|
506
|
+
data.baseline.gate.ticks > 0 ||
|
|
507
|
+
data.ledger.length > 0;
|
|
508
|
+
if (!hasData)
|
|
509
|
+
return null;
|
|
510
|
+
const lessonStores = opts.lessonStores ?? [];
|
|
511
|
+
const maxBytes = opts.maxBytes ?? SELF_PERFORMANCE_MAX_BYTES;
|
|
512
|
+
const budget = { ...INITIAL_BUDGET };
|
|
513
|
+
const available = {
|
|
514
|
+
actions: data.current.actions.length,
|
|
515
|
+
fetchWindow: data.current.fetchWindow.length,
|
|
516
|
+
notifications: data.current.notifications.length,
|
|
517
|
+
lessonStores: lessonStores.length,
|
|
518
|
+
ledger: data.ledger.length,
|
|
519
|
+
};
|
|
520
|
+
const effectiveRows = (key) => Math.min(budget[key], available[key]);
|
|
521
|
+
let block = renderBlock(data, lessonStores, budget, opts.generatedAt);
|
|
522
|
+
while (Buffer.byteLength(block, "utf-8") > maxBytes) {
|
|
523
|
+
let target = null;
|
|
524
|
+
for (const key of SHRINK_TIE_ORDER) {
|
|
525
|
+
if (effectiveRows(key) === 0)
|
|
526
|
+
continue;
|
|
527
|
+
if (target === null || effectiveRows(key) > effectiveRows(target)) {
|
|
528
|
+
target = key;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
if (target === null) {
|
|
532
|
+
// Even the row-free skeleton exceeds the cap (only reachable with a
|
|
533
|
+
// tiny custom maxBytes) — degrade to the minimal stub element.
|
|
534
|
+
return (`<self_performance generated_at="${xmlEscape(opts.generatedAt)}" ` +
|
|
535
|
+
`window_days="${data.windowDays}" overflow="true" />`);
|
|
536
|
+
}
|
|
537
|
+
budget[target] = effectiveRows(target) - 1;
|
|
538
|
+
block = renderBlock(data, lessonStores, budget, opts.generatedAt);
|
|
539
|
+
}
|
|
540
|
+
return block;
|
|
541
|
+
}
|