@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.
- package/dist/api/env-writer.d.ts +1 -0
- package/dist/api/env-writer.js +9 -2
- package/dist/api/routes/agent-schedule.js +5 -1
- package/dist/api/routes/apple-calendar.js +4 -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 +9 -0
- package/dist/api/routes/dashboard/config.js +10 -0
- 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/mcp.js +65 -13
- package/dist/api/server.js +3 -0
- package/dist/bootstrap/event-pipeline.js +1 -1
- package/dist/config.js +6 -0
- package/dist/core/backends/gemini-cli-core.js +13 -0
- package/dist/core/backends/plan-presets.js +8 -3
- package/dist/core/context-builder.js +149 -3
- package/dist/core/context-paths.d.ts +10 -0
- package/dist/core/context-paths.js +16 -0
- package/dist/core/daemon-api-cli.js +1 -1
- package/dist/core/dispatcher-message-handler.js +7 -0
- package/dist/core/dispatcher-scheduled-tasks.d.ts +41 -0
- package/dist/core/dispatcher-scheduled-tasks.js +267 -2
- package/dist/core/dispatcher.js +13 -1
- package/dist/core/feedback/consolidation-prep.d.ts +94 -0
- package/dist/core/feedback/consolidation-prep.js +242 -0
- package/dist/core/feedback/eviction-scorer.d.ts +81 -0
- package/dist/core/feedback/eviction-scorer.js +132 -0
- package/dist/core/feedback/lesson-format.d.ts +79 -0
- package/dist/core/feedback/lesson-format.js +194 -0
- package/dist/core/feedback/lesson-injection.d.ts +98 -0
- package/dist/core/feedback/lesson-injection.js +159 -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 +42 -0
- package/dist/core/feedback/lesson-store-overview.js +38 -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 +139 -0
- package/dist/core/feedback/scope-parser.d.ts +86 -0
- package/dist/core/feedback/scope-parser.js +141 -0
- package/dist/core/injection-policy.d.ts +82 -0
- package/dist/core/injection-policy.js +58 -0
- package/dist/core/signal-detector.d.ts +39 -1
- package/dist/core/signal-detector.js +277 -24
- package/dist/core/today-direct-writer.d.ts +59 -13
- package/dist/core/today-direct-writer.js +90 -13
- package/dist/core/wiki/wiki-fts.js +13 -6
- package/dist/db/feedback-signals-store.d.ts +77 -0
- package/dist/db/feedback-signals-store.js +144 -0
- package/dist/db/migrations.js +50 -0
- package/dist/db/schema.js +43 -6
- package/dist/safety/always-disallowed.d.ts +1 -1
- package/dist/safety/always-disallowed.js +39 -0
- package/dist/safety/risk-classifier.js +22 -7
- package/dist/services/browser-history/automation/egress-denylist.js +18 -2
- package/dist/services/browser-history/lifecycle/platform.js +44 -2
- package/dist/services/mcp/probe.js +30 -8
- package/dist/settings/runtime-settings.d.ts +8 -2
- package/dist/settings/runtime-settings.js +12 -0
- 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
|
package/dist/core/dispatcher.js
CHANGED
|
@@ -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
|
-
|
|
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;
|