@aitne/daemon 0.1.9 → 0.1.10

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.
Files changed (65) hide show
  1. package/dist/api/env-writer.d.ts +1 -0
  2. package/dist/api/env-writer.js +9 -2
  3. package/dist/api/routes/agent-schedule.js +5 -1
  4. package/dist/api/routes/apple-calendar.js +4 -1
  5. package/dist/api/routes/calendar.js +12 -2
  6. package/dist/api/routes/context/path-resolve.js +6 -1
  7. package/dist/api/routes/context/permissions.js +9 -0
  8. package/dist/api/routes/dashboard/config.js +10 -0
  9. package/dist/api/routes/dashboard/oauth-google.js +5 -3
  10. package/dist/api/routes/feedback.d.ts +3 -0
  11. package/dist/api/routes/feedback.js +349 -0
  12. package/dist/api/routes/git.js +10 -3
  13. package/dist/api/routes/github.js +5 -1
  14. package/dist/api/routes/mcp.js +65 -13
  15. package/dist/api/server.js +3 -0
  16. package/dist/bootstrap/event-pipeline.js +1 -1
  17. package/dist/config.js +6 -0
  18. package/dist/core/backends/gemini-cli-core.js +13 -0
  19. package/dist/core/backends/plan-presets.js +8 -3
  20. package/dist/core/context-builder.js +149 -3
  21. package/dist/core/context-paths.d.ts +10 -0
  22. package/dist/core/context-paths.js +16 -0
  23. package/dist/core/daemon-api-cli.js +1 -1
  24. package/dist/core/dispatcher-message-handler.js +7 -0
  25. package/dist/core/dispatcher-scheduled-tasks.d.ts +41 -0
  26. package/dist/core/dispatcher-scheduled-tasks.js +267 -2
  27. package/dist/core/dispatcher.js +13 -1
  28. package/dist/core/feedback/consolidation-prep.d.ts +94 -0
  29. package/dist/core/feedback/consolidation-prep.js +242 -0
  30. package/dist/core/feedback/eviction-scorer.d.ts +81 -0
  31. package/dist/core/feedback/eviction-scorer.js +132 -0
  32. package/dist/core/feedback/lesson-format.d.ts +79 -0
  33. package/dist/core/feedback/lesson-format.js +194 -0
  34. package/dist/core/feedback/lesson-injection.d.ts +98 -0
  35. package/dist/core/feedback/lesson-injection.js +159 -0
  36. package/dist/core/feedback/lesson-merge.d.ts +51 -0
  37. package/dist/core/feedback/lesson-merge.js +88 -0
  38. package/dist/core/feedback/lesson-store-overview.d.ts +42 -0
  39. package/dist/core/feedback/lesson-store-overview.js +38 -0
  40. package/dist/core/feedback/promotion-gate.d.ts +69 -0
  41. package/dist/core/feedback/promotion-gate.js +117 -0
  42. package/dist/core/feedback/regeneralization-prep.d.ts +87 -0
  43. package/dist/core/feedback/regeneralization-prep.js +139 -0
  44. package/dist/core/feedback/scope-parser.d.ts +86 -0
  45. package/dist/core/feedback/scope-parser.js +141 -0
  46. package/dist/core/injection-policy.d.ts +82 -0
  47. package/dist/core/injection-policy.js +58 -0
  48. package/dist/core/signal-detector.d.ts +39 -1
  49. package/dist/core/signal-detector.js +277 -24
  50. package/dist/core/today-direct-writer.d.ts +59 -13
  51. package/dist/core/today-direct-writer.js +90 -13
  52. package/dist/core/wiki/wiki-fts.js +13 -6
  53. package/dist/db/feedback-signals-store.d.ts +77 -0
  54. package/dist/db/feedback-signals-store.js +144 -0
  55. package/dist/db/migrations.js +50 -0
  56. package/dist/db/schema.js +43 -6
  57. package/dist/safety/always-disallowed.d.ts +1 -1
  58. package/dist/safety/always-disallowed.js +39 -0
  59. package/dist/safety/risk-classifier.js +22 -7
  60. package/dist/services/browser-history/automation/egress-denylist.js +18 -2
  61. package/dist/services/browser-history/lifecycle/platform.js +44 -2
  62. package/dist/services/mcp/probe.js +30 -8
  63. package/dist/settings/runtime-settings.d.ts +8 -2
  64. package/dist/settings/runtime-settings.js +12 -0
  65. package/package.json +2 -2
@@ -44,11 +44,12 @@
44
44
  * does not own the manager's lifecycle.
45
45
  */
46
46
  import { EventPriority, createEvent, formatSqliteDatetime, getAgentDayDateStr, isBackendId, isKnowledgeImportEvent, isRoutineEvent, resolveProcessKey, } from "@aitne/shared";
47
- import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, } from "node:fs";
47
+ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync, } from "node:fs";
48
48
  import { join } from "node:path";
49
49
  import { randomUUID } from "node:crypto";
50
- import { CONTEXT_RELATIVE_PATHS } from "./context-paths.js";
50
+ import { CONTEXT_RELATIVE_PATHS, agentLessonsPath } from "./context-paths.js";
51
51
  import { getContextDir } from "../config.js";
52
+ import { ensureTodaySkeleton } from "./today-direct-writer.js";
52
53
  import { cleanupSessionWorkdir, ensureBackendMaterialized, } from "./workdir.js";
53
54
  import { readIntegrations, readIntegrationState } from "../db/integrations-store.js";
54
55
  import { refreshInterestsReflection } from "../services/browser-history/refresh-interests-reflection.js";
@@ -60,6 +61,10 @@ import { getRepository, getRepositoryByLocalPath, recordManagementInitDone, reco
60
61
  import { runRepositoryManagementInit, runRepositoryManagementScan, } from "./repository-management-docs.js";
61
62
  import { routineWindowKeyFromEvent } from "./routine-fetch-window-runner.js";
62
63
  import { routineHasWindows } from "./routine-windows.js";
64
+ import { buildFeedbackWorksheet, gatherFeedbackWorksheetScopes, lessonCapsForScope, GLOBAL_LESSON_ENTRY_CAP, PER_AGENT_LESSON_ENTRY_CAP, } from "./feedback/consolidation-prep.js";
65
+ import { buildRegeneralizationWorksheet, } from "./feedback/regeneralization-prep.js";
66
+ import { isSafeAgentSlug, scopeStoreFile, } from "./feedback/scope-parser.js";
67
+ import { feedbackRetentionCutoff, sweepConsumedFeedbackSignals, } from "../db/feedback-signals-store.js";
63
68
  import { morningRoutineRanToday } from "../bootstrap/schedule-helpers.js";
64
69
  import { resolveAgentId } from "./agents/agent-id-resolver.js";
65
70
  import { loadEffectiveDefinition } from "./agents/effective-definition.js";
@@ -227,6 +232,7 @@ export class ScheduledTaskRunner {
227
232
  morningRoutine;
228
233
  fetchWindowRunner;
229
234
  roadmapWriteLock;
235
+ todayWriteLock;
230
236
  writeTracker;
231
237
  getConfiguredServices;
232
238
  getActiveMailAccounts;
@@ -243,6 +249,7 @@ export class ScheduledTaskRunner {
243
249
  this.morningRoutine = deps.morningRoutine;
244
250
  this.fetchWindowRunner = deps.fetchWindowRunner;
245
251
  this.roadmapWriteLock = deps.roadmapWriteLock;
252
+ this.todayWriteLock = deps.todayWriteLock;
246
253
  this.writeTracker = deps.writeTracker;
247
254
  this.getConfiguredServices = deps.getConfiguredServices;
248
255
  this.getActiveMailAccounts = deps.getActiveMailAccounts;
@@ -1085,6 +1092,81 @@ export class ScheduledTaskRunner {
1085
1092
  };
1086
1093
  }
1087
1094
  }
1095
+ // Deterministic pre-step for `routine.today_refresh`. rotateDayFiles()
1096
+ // intentionally leaves today.md absent at the day boundary for the
1097
+ // morning routine to recreate; when that routine has not yet run (or
1098
+ // failed — e.g. a quota/budget death with no fallback), today.md is
1099
+ // missing and this section-only refresh would 404 on its PATCH and
1100
+ // fall back to budget-burning full-file PUTs against the strict
1101
+ // validateTodayContent schema (the "Refresh Today does nothing"
1102
+ // symptom). Guarantee the canonical skeleton exists (lock-aware,
1103
+ // absent-only) so the refresh session has a valid PATCH target; full
1104
+ // creation/repair stays the morning routine's job. See
1105
+ // ensureTodaySkeleton. Skipped when no today-write-lock was wired.
1106
+ if (this.todayWriteLock
1107
+ && isRoutineEvent(effectiveEvent)
1108
+ && effectiveEvent.routine === "today_refresh") {
1109
+ // Failure-isolated like the sibling pre-steps below: this seed is a
1110
+ // best-effort convenience so the refresh has a valid PATCH target.
1111
+ // A throw here (e.g. getContextDir / serializer-lock internals) must
1112
+ // NOT abort the whole today_refresh — that would reproduce the exact
1113
+ // "Refresh Today does nothing" symptom the skeleton was added to
1114
+ // prevent. The morning routine remains responsible for real repair.
1115
+ try {
1116
+ await ensureTodaySkeleton({
1117
+ contextDir: getContextDir(this.config, this.db),
1118
+ todayWriteLock: this.todayWriteLock,
1119
+ });
1120
+ }
1121
+ catch (err) {
1122
+ logger.warn({ err }, "Failed to seed today.md skeleton for today_refresh");
1123
+ }
1124
+ }
1125
+ // FEEDBACK_LEARNING_LOOP_DESIGN.md §4 — deterministic consolidation
1126
+ // pre-step for `routine.evening_review`. Runs synchronously BEFORE
1127
+ // `contextBuilder.build` so the `<feedback_worksheet>` (unconsumed
1128
+ // signals grouped by scope + per-candidate promotion verdicts +
1129
+ // eviction ranking + consume ids) lands in the review session's
1130
+ // context. Failure-isolated inside `prepareFeedbackWorksheet`: a throw
1131
+ // there is swallowed and the review still ships. Skipped when feedback
1132
+ // learning is off or no signals pend (no empty block in the prompt).
1133
+ if (isRoutineEvent(effectiveEvent)
1134
+ && effectiveEvent.routine === "evening_review") {
1135
+ const worksheetBlock = this.prepareFeedbackWorksheet();
1136
+ if (worksheetBlock) {
1137
+ effectiveEvent = {
1138
+ ...effectiveEvent,
1139
+ data: {
1140
+ ...effectiveEvent.data,
1141
+ feedbackWorksheetBlock: worksheetBlock,
1142
+ },
1143
+ };
1144
+ }
1145
+ }
1146
+ // FEEDBACK_LEARNING_LOOP_DESIGN.md §4 "Monthly re-generalization" /
1147
+ // Phase 5 — deterministic pre-step for `routine.monthly_review`. Re-reads
1148
+ // the consolidated lesson stores (global + per-agent) and stamps a
1149
+ // `<feedback_regeneralization>` block so the monthly session collapses
1150
+ // same-theme lessons into higher-level principles. Distinct from the
1151
+ // nightly evening-review pass: it carries no signals and consumes
1152
+ // nothing — it only surfaces existing lessons ranked for collapse.
1153
+ // Failure-isolated inside `prepareRegeneralizationWorksheet`; skipped
1154
+ // when feedback learning is off or no scope has enough lessons. Rides the
1155
+ // same `monthlyReviewEnabled` kill switch as the rest of the monthly
1156
+ // routine (the routine never dispatches while it is off).
1157
+ if (isRoutineEvent(effectiveEvent)
1158
+ && effectiveEvent.routine === "monthly_review") {
1159
+ const regeneralizationBlock = this.prepareRegeneralizationWorksheet();
1160
+ if (regeneralizationBlock) {
1161
+ effectiveEvent = {
1162
+ ...effectiveEvent,
1163
+ data: {
1164
+ ...effectiveEvent.data,
1165
+ regeneralizationBlock,
1166
+ },
1167
+ };
1168
+ }
1169
+ }
1088
1170
  // WEEKLY_INTERESTS_REFLECTION_PLAN.md §10.4 — deterministic
1089
1171
  // pre-hook for `routine.weekly_review`. Runs synchronously
1090
1172
  // BEFORE `contextBuilder.build` so the freshly-refreshed
@@ -1277,6 +1359,189 @@ export class ScheduledTaskRunner {
1277
1359
  this.appendWeeklyInterestsJournalLine(`interest reflection failed: ${message}`);
1278
1360
  }
1279
1361
  }
1362
+ /**
1363
+ * FEEDBACK_LEARNING_LOOP_DESIGN.md §4 — the deterministic consolidation
1364
+ * pre-step. Reads unconsumed `feedback_signals` (user + agent + `agent:<slug>`
1365
+ * scope as of Phase 4), reads each lessons store's current contents, and
1366
+ * composes the `<feedback_worksheet>` block via the pure `core/feedback/*`
1367
+ * modules. Returns the block string, or `null` when feedback learning is off,
1368
+ * nothing pends, or anything throws — so the caller simply skips stamping and
1369
+ * the evening review proceeds unchanged.
1370
+ *
1371
+ * The DB read + per-scope file read live here (the FS-/DB-heavy dispatcher
1372
+ * is coverage-excluded); the byte-deterministic worksheet composition lives
1373
+ * in `buildFeedbackWorksheet`, which is 100% unit-tested.
1374
+ */
1375
+ prepareFeedbackWorksheet() {
1376
+ if (this.config.feedbackLearningEnabled === false)
1377
+ return null;
1378
+ try {
1379
+ // Nightly retention close-out (§4 step 6 / §6): drop signal rows that
1380
+ // were consumed past the retention horizon so the raw table stays
1381
+ // bounded. Only touches already-consumed rows — an unconsolidated
1382
+ // signal is never lost to retention. Runs once per Evening Review.
1383
+ // Guarded against a missing/NaN retention knob so a config gap degrades
1384
+ // to "skip the sweep", not "throw and abandon the whole consolidation".
1385
+ // The guard + cutoff math live in the pure, 100%-covered
1386
+ // `feedbackRetentionCutoff` helper (returns null → skip).
1387
+ const retentionCutoff = feedbackRetentionCutoff(this.config.feedbackSignalRetentionDays, Date.now());
1388
+ if (retentionCutoff) {
1389
+ sweepConsumedFeedbackSignals(this.db, retentionCutoff);
1390
+ }
1391
+ const groups = gatherFeedbackWorksheetScopes(this.db, {
1392
+ // Phase 4 adds `agent_slug` — per-agent (`agent:<slug>`) signals captured
1393
+ // by the explicit route + behavioral sink now fold into
1394
+ // `policies/agents/<slug>/lessons.md`. Per-scope-type fetch (not a global
1395
+ // LIMIT) keeps an `agent_slug` backlog from starving user/agent.
1396
+ scopeTypes: ["user", "agent", "agent_slug"],
1397
+ });
1398
+ if (groups.length === 0)
1399
+ return null;
1400
+ const contextDir = getContextDir(this.config, this.db);
1401
+ const byteCaps = {
1402
+ global: this.config.feedbackLessonMaxBytesGlobal,
1403
+ perAgent: this.config.feedbackLessonMaxBytesPerAgent,
1404
+ };
1405
+ const scopes = [];
1406
+ for (const group of groups) {
1407
+ const caps = lessonCapsForScope(group.scope, byteCaps);
1408
+ let existingFileMd = null;
1409
+ if (caps) {
1410
+ const rel = scopeStoreFile(group.scope);
1411
+ if (rel) {
1412
+ const full = join(contextDir, rel);
1413
+ // Per-scope read isolation (FEEDBACK_LEARNING_LOOP_DESIGN.md §11
1414
+ // v1.9 nuance #3, now closed). A *missing* file is the normal
1415
+ // first-write case (existingFileMd stays null → the LLM PUTs a
1416
+ // fresh store); but a *present-but-unreadable* file (EACCES, a
1417
+ // mid-write truncation) must NOT forfeit the whole night's
1418
+ // consolidation. Skip just this scope: its signals stay unconsumed
1419
+ // and retry next Evening Review, the file is left untouched, and the
1420
+ // user/agent scopes that read cleanly still consolidate. Crucially
1421
+ // we do NOT fall back to existingFileMd=null on a read error — that
1422
+ // would make the LLM PUT a fresh file and destroy the existing
1423
+ // lessons.
1424
+ try {
1425
+ existingFileMd = existsSync(full)
1426
+ ? readFileSync(full, "utf-8")
1427
+ : null;
1428
+ }
1429
+ catch (err) {
1430
+ logger.warn({ err, path: rel }, "Skipping feedback scope — lessons file present but unreadable; its signals will retry next Evening Review");
1431
+ continue;
1432
+ }
1433
+ }
1434
+ }
1435
+ scopes.push({
1436
+ scope: group.scope,
1437
+ signals: group.signals,
1438
+ existingFileMd,
1439
+ caps,
1440
+ });
1441
+ }
1442
+ if (scopes.length === 0)
1443
+ return null;
1444
+ const result = buildFeedbackWorksheet(scopes, {
1445
+ promotionThreshold: this.config.feedbackPromotionThreshold,
1446
+ nowIso: new Date().toISOString(),
1447
+ staleDays: this.config.feedbackLessonStaleDays,
1448
+ });
1449
+ return result?.block ?? null;
1450
+ }
1451
+ catch (err) {
1452
+ logger.warn({ err }, "Failed to prepare feedback worksheet");
1453
+ return null;
1454
+ }
1455
+ }
1456
+ /**
1457
+ * FEEDBACK_LEARNING_LOOP_DESIGN.md §4 "Monthly re-generalization" / Phase 5 —
1458
+ * the deterministic monthly pre-step. Enumerates the consolidated lesson
1459
+ * stores on disk (the global `policies/agent-lessons.md` plus every per-agent
1460
+ * `policies/agents/<slug>/lessons.md`), reads their contents, and composes a
1461
+ * `<feedback_regeneralization>` block via the pure
1462
+ * `buildRegeneralizationWorksheet`. Returns the block, or `null` when feedback
1463
+ * learning is off, no store holds enough lessons to collapse, or anything
1464
+ * throws — so the caller simply skips stamping and the monthly review
1465
+ * proceeds unchanged.
1466
+ *
1467
+ * The FS enumeration lives here (the dispatcher is coverage-excluded); the
1468
+ * byte-deterministic composition lives in `buildRegeneralizationWorksheet`,
1469
+ * which is 100% unit-tested.
1470
+ */
1471
+ prepareRegeneralizationWorksheet() {
1472
+ if (this.config.feedbackLearningEnabled === false)
1473
+ return null;
1474
+ try {
1475
+ const contextDir = getContextDir(this.config, this.db);
1476
+ const scopes = [];
1477
+ // Per-file read isolation (FEEDBACK_LEARNING_LOOP_DESIGN.md §11 v1.9
1478
+ // nuance #3, now closed): one present-but-unreadable lessons file must
1479
+ // skip only that scope, not abandon the whole monthly collapse. A scope
1480
+ // that fails to read is simply not surfaced for re-generalization this
1481
+ // month; the file is left untouched.
1482
+ const readScopeFile = (rel) => {
1483
+ const full = join(contextDir, rel);
1484
+ if (!existsSync(full))
1485
+ return null;
1486
+ try {
1487
+ return readFileSync(full, "utf-8");
1488
+ }
1489
+ catch (err) {
1490
+ logger.warn({ err, path: rel }, "Skipping re-generalization scope — lessons file present but unreadable");
1491
+ return null;
1492
+ }
1493
+ };
1494
+ // Global `agent`-scope store.
1495
+ const globalRel = CONTEXT_RELATIVE_PATHS.agentLessons;
1496
+ const globalMd = readScopeFile(globalRel);
1497
+ if (globalMd !== null) {
1498
+ scopes.push({
1499
+ scope: { kind: "agent" },
1500
+ storeFile: globalRel,
1501
+ existingFileMd: globalMd,
1502
+ caps: {
1503
+ capBytes: this.config.feedbackLessonMaxBytesGlobal,
1504
+ maxEntries: GLOBAL_LESSON_ENTRY_CAP,
1505
+ },
1506
+ });
1507
+ }
1508
+ // Per-agent `agent:<slug>` stores under `policies/agents/<slug>/lessons.md`.
1509
+ // The slug is the directory name; the same `isSafeAgentSlug` guard the
1510
+ // inject + consolidate sides apply rejects any unsafe directory name so a
1511
+ // hand-created `..`-style dir never reaches the worksheet.
1512
+ const agentsDir = join(contextDir, "policies", "agents");
1513
+ if (existsSync(agentsDir)) {
1514
+ for (const entry of readdirSync(agentsDir, { withFileTypes: true })) {
1515
+ if (!entry.isDirectory() || !isSafeAgentSlug(entry.name))
1516
+ continue;
1517
+ const rel = agentLessonsPath(entry.name);
1518
+ const md = readScopeFile(rel);
1519
+ if (md === null)
1520
+ continue;
1521
+ scopes.push({
1522
+ scope: { kind: "agent_slug", ref: entry.name },
1523
+ storeFile: rel,
1524
+ existingFileMd: md,
1525
+ caps: {
1526
+ capBytes: this.config.feedbackLessonMaxBytesPerAgent,
1527
+ maxEntries: PER_AGENT_LESSON_ENTRY_CAP,
1528
+ },
1529
+ });
1530
+ }
1531
+ }
1532
+ if (scopes.length === 0)
1533
+ return null;
1534
+ const result = buildRegeneralizationWorksheet(scopes, {
1535
+ nowIso: new Date().toISOString(),
1536
+ staleDays: this.config.feedbackLessonStaleDays,
1537
+ });
1538
+ return result?.block ?? null;
1539
+ }
1540
+ catch (err) {
1541
+ logger.warn({ err }, "Failed to prepare feedback re-generalization worksheet");
1542
+ return null;
1543
+ }
1544
+ }
1280
1545
  /**
1281
1546
  * Append a single bullet under `## Weekly interests reflection`
1282
1547
  * inside `context/journal/agent.md`, creating the section (and the
@@ -587,6 +587,7 @@ export class EventDispatcher {
587
587
  morningRoutine: this.morningRoutine,
588
588
  fetchWindowRunner: this.fetchWindowRunner,
589
589
  roadmapWriteLock: this.roadmapWriteLock,
590
+ todayWriteLock: this.todayWriteLock,
590
591
  writeTracker: this.writeTracker,
591
592
  getConfiguredServices: () => this.getConfiguredServices(),
592
593
  getActiveMailAccounts: () => this.getActiveMailAccounts(),
@@ -803,10 +804,21 @@ export class EventDispatcher {
803
804
  const scheduleRowId = isScheduledEvent(event) && event.scheduleId !== undefined
804
805
  ? event.scheduleId
805
806
  : null;
806
- this.agentExecutionTracker.begin(event.correlationId, resolution, {
807
+ // `begin` opens the rollup row AND returns the resolved Agent slug (or null
808
+ // for a firing that resolves to no Agent — reactive DMs, legacy tasks).
809
+ const agentId = this.agentExecutionTracker.begin(event.correlationId, resolution, {
807
810
  scheduleRowId,
808
811
  trigger: this.resolveExecutionTrigger(event, taskContext),
809
812
  });
813
+ // FEEDBACK_LEARNING_LOOP_DESIGN.md §5 Phase 4 — thread the resolved slug onto
814
+ // the event so `ContextBuilder` can inject this Agent's own
815
+ // `policies/agents/<slug>/lessons.md` (scope `agent:<slug>`). This runs in
816
+ // `dispatchSafe` before `dispatch(event)`, so the stamp is visible to the
817
+ // builder downstream; the morning routine propagates it to Stage A via the
818
+ // `{...parent.data}` spread in `composeStageAEvent`. No-op for unbound runs.
819
+ if (agentId !== null) {
820
+ event.data.agentId = agentId;
821
+ }
810
822
  }
811
823
  /** Classify an execution's trigger for the rollup row (§5.2). */
812
824
  resolveExecutionTrigger(event, taskContext) {
@@ -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;