dsh-continual-evolve 0.15.13 → 0.15.15
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/README.md +6 -3
- package/README.zh.md +6 -3
- package/lib/extraction-ring.js +7 -1
- package/lib/extraction.d.ts +5 -4
- package/lib/extraction.js +2 -2
- package/lib/index.d.ts +0 -4
- package/lib/index.js +48 -32
- package/lib/plugin-log.d.ts +21 -0
- package/lib/plugin-log.js +42 -0
- package/lib/prompt-date.d.ts +33 -0
- package/lib/prompt-date.js +37 -0
- package/lib/retention.d.ts +23 -0
- package/lib/retention.js +34 -0
- package/lib/review.d.ts +4 -7
- package/lib/review.js +90 -50
- package/lib/store.d.ts +21 -1
- package/lib/store.js +22 -1
- package/lib/subagent-defs.js +15 -6
- package/lib/subagent.d.ts +16 -4
- package/lib/subagent.js +3 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ English | [中文](README.zh.md)
|
|
|
7
7
|
[](https://github.com/ZK-Andy/dsh-continual-evolve/actions/workflows/ci.yml)
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
[](package.json)
|
|
10
|
-
[]()
|
|
11
11
|
[]()
|
|
12
12
|
[]()
|
|
13
13
|
[]()
|
|
@@ -24,7 +24,7 @@ Reusable experience gathered in one session (user preferences, pitfalls, project
|
|
|
24
24
|
2. **Four tools, one writer** — `memory_write` (create/update/delete) is the explicit path for "remember …" / "forget …": every proposal passes the code gates (id shape, type enum, the feedback Why/How contract, secret screening carried over from v1, 64KB body cap, optional `corrects` links to the same-workspace active memory being superseded) inside one transaction. `memory_read` fetches bodies by id and searches by keyword — whitespace-separated terms are AND-combined, with literal substring matching for short terms and whenever the trigram index misses; the injected index carries hooks only. Each memory body served by id increments that memory's view counter (views + last viewed, the same presence heartbeat as skills). `skill_write` (create/update/delete in the project, plus an explicit move-to-global) sediments reusable procedures through the same discipline (kebab ids, secret screening, 64KB cap, no duplicate ids or bodies across scopes); every applied skill is materialized by the plugin as `<workspace>/.agents/skills/<id>/SKILL.md` — the database is the source of truth, the files are a read-only projection patrol restores on drift. `skill_read` fetches skill bodies by id and searches project plus global skills (the project shadows a same-named global); each body served by id increments that skill's view counter (views + last viewed), which the card shows as a presence heartbeat — viewed, not applied, so long-unviewed rows are removal candidates while pruning itself stays human. On hosts without the tools service the plugin degrades to "tell the user" phrasing and stays functional.
|
|
25
25
|
3. **Proposal-based extraction** — the trajectory-to-memory automation, audited in the `extraction_log`: every `agent/turn-stopping` schedules that session's boundary, with state kept **per session** (a run in flight keeps only its newest boundary — coalescing, not queueing; `compaction/start` and session close drain it) and **no idle timer**. A run reads the session increment since its cursor, feeds FTS-similar existing memories to one LLM call, and the model's structured proposal lands through the same gates; failures and rejections never advance the cursor. A cheap pre-gate (increment head + ownership triple-test, tiny verdict) runs first: refusals consume the increment as a model skip without the full call, and an unclear gate fails open to the full call — worst case degrades to the old single-call behavior, never to a stall. Full and gate usages ledger in separate `usage` / `cheap_usage` columns, and a gate that dies keeps its error head in the same row's `cheap_error` — a dead gate stays tellable from a gate that never existed. Internal agents, empty increments, turns without real user prose, and turns that already carried an explicit `memory_write` are mechanically skipped — and every skip is ledgered, consecutive quiet turns of one session as a single row carrying how many wake-ups it stands for.
|
|
26
26
|
4. **Autonomous content-quality loop** — the second background automation, independent from extraction by construction (own prompts, own daily budget, own `review_log` ledger, no cursor; only the store's write gates are shared): a cheap gate model call decides whether a memory/skill entry deserves a close look, and only approvals run the evolve pass that judges content quality (memories on home-ownership plus accuracy/usability/reusability/duplication/freshness, skills on home-ownership plus trigger precision/executability/duplication, where reuse is scope-aware: a project-partition skill is judged inside its project (in-project reuse) and the cross-project test applies to global-scope skills only — a repo-bound row in the global partition is the mis-homed case) and proposes the rewrite. Each of the four review jobs (memory and skill, gate and evolve) loads its own rubric from one subagent table — a single prompt cannot serve both, since a memory must survive having its repo name stripped while a project skill is supposed to be repo-bound. The same three home tests (user/environment vs repo-version subject, name-stripped sentence, repo facts live in ADR/docs) guard the extraction prompt, so repo-version specifics never enter as memories in the first place. Gate/leg failures ledger the error text instead of a bare code, along with the model they ran on and the tokens the failed call had already burned, and the output budget comes from the model's own advertised cap instead of a fixed number, so a thinking model is not cut off mid-verdict: a verdict that completed before a cut still counts, while a call that produced no verdict at all (route fault, budget cut) leaves the item queued instead of marking it reviewed. New writes are reviewed as they land, the existing stock drains through a bounded backlog queue, and untouched entries requeue after 30 days. Two hard guarantees bound the autonomy: delete never lands automatically (rewrite or quarantine only — quarantine hides, never deletes) and oversized rewrites degrade to quarantine. A daily cap (default 100 items, new > corrects-linked > stale > spot-check priority, the tail drops instead of rolling over) bounds the token spend, and every terminal path lands one `item-review` verdict row plus one daily rollup row (budget vs reviewed vs backlog vs dropped, tokens split by cheap/evolve purpose) for later replay. Ships in trial mode (`review.dryRun`, default on): verdicts ledgered, nothing applied — and a declined rewrite now rides the row's `proposed` column, because a trial has to show what it would write, not just why.
|
|
27
|
-
5. **Rollback is a ledger, not a mechanism** — every mutation lands an `extraction_log` row with full before/after snapshots; undo means rewriting the `before` values. Skill mutations land the same way in `skill_log`, review verdicts in `review_log`. A hygiene pass rides on every applied run: orphan FTS rows are dropped and secret-bearing rows are quarantined (hidden, never deleted; a quarantined skill's materialized file is removed so it stops loading). The read-only card surfaces quarantine anomalies.
|
|
27
|
+
5. **Rollback is a ledger, not a mechanism** — every mutation lands an `extraction_log` row with full before/after snapshots; undo means rewriting the `before` values. Skill mutations land the same way in `skill_log`, review verdicts in `review_log`. A hygiene pass rides on every applied run: orphan FTS rows are dropped and secret-bearing rows are quarantined (hidden, never deleted; a quarantined skill's materialized file is removed so it stops loading). The read-only card surfaces quarantine anomalies. Plugin diagnostics have a durable home of their own: a stock host attaches no exporter to its logger, so `plugin_log` is the only home — the host logger is not mirrored. All four ledgers rotate on one 30-day window, pruned by a day-throttled check that rides store readiness, log writes and the post-extraction hygiene pass.
|
|
28
28
|
6. **Read-only card** — mounted on the plugin's page in the official plugin manager (the `plugins.bundle.config` slot; hosts without it silently degrade to no card), shaped like the market's own settings card and aligned with ZCode's Settings → Memory viewer: a dropdown scope selector over the store's workspace partitions (opening on the host's current workspace), a search box, per-entry relative updated times, click-to-preview bodies (5 MiB cap), patrol warnings, and a skills section listing the workspace's project skills plus visible global skills (global rows badged, project shadowing global) with the same preview, each row showing its view count. Localized through the host's `locale/*.json` metadata channel; host theme tokens, the `Button` primitive, and zh/en copy that tracks the DSH language. The card is a request-time projection of the database and has no edit path.
|
|
29
29
|
|
|
30
30
|
Memory types follow the ZCode taxonomy: `user` (profile and environment facts), `feedback` (corrected or confirmed practices — bodies must carry **Why:** and **How to apply:** lines), `reference` (resource pointers). The extraction boundary is person- and environment-scoped knowledge only: decisions and trade-offs are repo-scoped by nature and belong to the repo's ADR route, never to memory.
|
|
@@ -95,9 +95,12 @@ Layout:
|
|
|
95
95
|
│ ├── store.ts # central SQLite store: DDL/WAL/FTS, reads, ledger, patrol, cursors
|
|
96
96
|
│ ├── store-apply.ts # the sole write gate: validate all, apply atomically, ledger
|
|
97
97
|
│ ├── token-usage.ts # single accounting entry: normalize provider usage, pure sums
|
|
98
|
+
│ ├── plugin-log.ts # the plugin's own log table (the host logger has no sink to write to)
|
|
99
|
+
│ ├── retention.ts # ledger retention: one 30-day window, day-throttled prune trigger
|
|
98
100
|
│ ├── llm-call.ts # the only transport: route, reasoning effort, output budget, failure facts
|
|
99
101
|
│ ├── subagent.ts # the loader every LLM call goes through (assemble → call → parse → account)
|
|
100
102
|
│ ├── subagent-defs.ts # every subagent job in one table: prompt, budget fallback, answer parser
|
|
103
|
+
│ ├── prompt-date.ts # the clock prompts judge dates by: the run's day + its instruction line
|
|
101
104
|
│ ├── chain.ts # responsibility chain: one step per link, order assembled by the caller
|
|
102
105
|
│ ├── review-candidates.ts # its queue: what is due (new > corrects-linked > stale)
|
|
103
106
|
│ ├── review.ts # autonomous quality loop: batch, budget, rollup, scheduler
|
|
@@ -120,7 +123,7 @@ Layout:
|
|
|
120
123
|
├── client/
|
|
121
124
|
│ └── client.js # hand-written client bundle: read-only card in the plugin manager
|
|
122
125
|
├── locale/ # host package metadata: localized display name + description (zh/en)
|
|
123
|
-
├── test/ # vitest suite (
|
|
126
|
+
├── test/ # vitest suite (29 files)
|
|
124
127
|
├── lib/ # build output (tsc)
|
|
125
128
|
├── docs/ # design.md (design) · FAQ.md (real-world pitfalls)
|
|
126
129
|
└── .agents/ # AI collaboration layer (AGENTS.md, skills, ADR notes)
|
package/README.zh.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
[](https://github.com/ZK-Andy/dsh-continual-evolve/actions/workflows/ci.yml)
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
[](package.json)
|
|
10
|
-
[]()
|
|
11
11
|
[]()
|
|
12
12
|
[]()
|
|
13
13
|
[]()
|
|
@@ -24,7 +24,7 @@ Agent 每个会话积累的可复用经验(用户偏好、踩坑教训、项
|
|
|
24
24
|
2. **四个工具,一个写者** —— `memory_write`(create/update/delete)承接显式的记住/忘掉:每条提案过代码门禁(id 规范、type 枚举、feedback 的 Why/How 契约、v1 移植的 secret 筛查、64KB 正文上限、可选 `corrects` 指向被替代的同工作区 active 记忆)后在一个事务内落盘。`memory_read` 按 id 取正文、按关键词检索——空格分隔的关键词取 AND,短词与 trigram 未命中时回退字面子串匹配;注入索引只有钩子。按 id 取到的每份记忆正文计一次查阅(与技能同纪律的存在心跳)。`skill_write`(项目内 create/update/delete + 显式搬到全局)沉淀可复用的操作规程,走同一套纪律(短横线 id、secret 筛查、64KB 上限、跨范围禁重名重文);每条落地的技能由插件代码物化为 `<workspace>/.agents/skills/<id>/SKILL.md`——库是唯一真相,文件是只读投影,漂移由巡检按库还原。`skill_read` 按 id 取技能正文、检索项目与全局技能(同名时项目遮蔽全局);按 id 取到的每份正文计一次查阅(查阅次数 + 上次查阅,卡片展示)。这是存在心跳——只证明被翻过,不证明有用:长期零查阅是删除候选,剪枝拍板仍需人工。宿主没有工具服务时降级为告诉用户措辞,注入不受影响。
|
|
25
25
|
3. **提案制提取** —— 轨迹到记忆的自动化,全程 `extraction_log` 可对账:每个 `agent/turn-stopping` 调度**该会话自己**的边界,状态按会话隔离(运行中只保留最新边界——合并而非排队;`compaction/start` 与会话收尾触发 drain),**没有空闲计时器**。一次运行读取该会话游标之后的轨迹增量,用 FTS 取相似既有记忆喂给一次 LLM 调用,模型的结构化提案过同一套门禁落盘;失败与被拒不推游标,下次触发重试。贵调用之前先过一道便宜预检门(增量前 4KB + 归属三测,小 verdict):拒了直接按 model-skip 消费游标,不烧全量 prompt;门不清不楚就透传给贵调用——最差退化成老单调用,永不 stall。贵用量与门用量分 `usage` / `cheap_usage` 两列记账;门自己挂了按 fail-open 透传,错误原文留在同一行的 `cheap_error`——门死了与门不存在可区分。内部 agent、空增量、无真实用户文本的轮次、当轮已有显式 `memory_write` 的轮次机械跳过——每次跳过都记账,同一会话连续的空转轮次合成一行并累计唤醒次数。
|
|
26
26
|
4. **内容质量自进化环** —— 第二条后台自动化,与提取在结构上独立(自有提示词、自有日预算、自有 `review_log` 账本、无游标;只共享库的写门):便宜门模型先判某条记忆/技能值不值得细看,批了才跑贵改写,按内容质量打分(记忆:归属 + 准确/可用/可复用/不重复/不过时五维;技能:归属 + 触发精度/可执行完备/不重复三维,其中「可复用」按分区判——项目分区技能只按该项目内跨任务可用判,跨项目可用只对全局分区技能判,绑定本仓库路径/门禁的项目技能不算缺陷)并给出改写提案。四个评审岗位(记忆/技能 × 门/昂贵门)从同一张子代理定义表各装自己的说明书——一份提示词服务不了两者:记忆要求抹掉仓库名仍成立,项目技能本来就该绑仓库。同一套归属三测(主语是用户/环境还是某仓库的某版机制、去名后还成立吗、仓库事实住 ADR/docs)也守在提取 prompt 门口,版本细节从源头就不进记忆;评审调用失败记错误原文,不记秃码,并连同它跑在哪个模型、已烧掉多少 token 一并落账;门自身预算取自模型自己广告的输出上限而非固定值——截断前已完成解析的 verdict 仍算数,而一条 verdict 都没产出的调用(路由故障、预算截断)不让条目记为已评审,而是留队重试。新写即审、存量走有界积压队列排空、长期未动 30 天回炉。两条硬保证框住自动化:删除永不自动落盘(只有改写与隔离——隔离隐藏不删),超幅改写自动转隔离。日上限默认 100 条(新写 > 被纠正牵连 > 陈旧 > 抽查,尾部丢弃不滚雪球)框住 token 开销;每条终态落一行 `item-review` verdict 行、每天合一行日 rollup(预算 vs 实审 vs 积压 vs 丢弃,token 按便宜/贵分账),方便以后复盘。默认试运行(`review.dryRun` 开):只记 verdict 不落盘,且未落盘的拟改稿全文随行进 `proposed` 列——试运行要看的是「改成什么样」,不只是「为什么改」。
|
|
27
|
-
5. **回滚靠账本,不靠机制** —— 每次变更落一条带 before/after 全文快照的 `extraction_log` 行;undo 就是取 before 重写。技能变更同样落 `skill_log`,评审 verdict 落 `review_log`。每次应用后随跑一次巡检:孤儿 FTS
|
|
27
|
+
5. **回滚靠账本,不靠机制** —— 每次变更落一条带 before/after 全文快照的 `extraction_log` 行;undo 就是取 before 重写。技能变更同样落 `skill_log`,评审 verdict 落 `review_log`。每次应用后随跑一次巡检:孤儿 FTS 行清除、含密钥条目隔离(隐藏,不物理删除;被隔离技能的物化文件一并撤下,使其不再被加载)。只读卡片显形巡检异常。插件诊断另有自己的 durable 家:宿主 logger 开箱没有 exporter,因此诊断只落 `plugin_log`,不再镜像宿主 logger;四张账本共用一个 30 天窗口裁剪,触发点是 store 就绪、写日志与提取后的巡检,按 UTC 日节流。
|
|
28
28
|
6. **只读卡片** —— 挂在官方插件管理的本 bundle 页面(`plugins.bundle.config` slot,老宿主自动降级为无卡片):形态对齐市场自家设置卡,内容对齐 ZCode 设置的记忆选项卡——下拉作用域选择器列出库里的工作区分区(默认选中宿主当前工作区)、搜索框、每条记忆的相对更新时间、点击行内展开正文预览(5 MiB 上限)、巡检异常警告,以及技能区(本工作区技能 + 可见的全局技能,全局行打标记,同名时项目遮蔽全局),预览同记忆,每行显示查阅次数。显示名与描述走宿主 `locale/*.json` 通道双语;宿主主题 token、`Button` 原语、中英文案跟随 DSH 语言。卡片是数据库的请求时投影,没有编辑路径。
|
|
29
29
|
|
|
30
30
|
记忆类型沿 ZCode 分类法:`user`(画像与环境事实)、`feedback`(被纠正/确认的做法——正文必带 **Why:** 与 **How to apply:**)、`reference`(资源指针)。提取边界只收"跟人与环境走的知识":决策与取舍天然属于仓库,走仓库的 ADR 路线,不进记忆库。
|
|
@@ -95,9 +95,12 @@ pnpm check:pack # 发布产物一致性(prepack 也会跑)
|
|
|
95
95
|
│ ├── store.ts # 中央 SQLite 库:DDL/WAL/FTS、读面、账本、巡检、游标
|
|
96
96
|
│ ├── store-apply.ts # 唯一写门:全量校验、事务原子应用、账本
|
|
97
97
|
│ ├── token-usage.ts # 统一记账口:归一各家用量、纯累加
|
|
98
|
+
│ ├── plugin-log.ts # 插件自有日志:只写库表(宿主 logger 无承接方)
|
|
99
|
+
│ ├── retention.ts # 账本保留期:30 天窗口 + 按日节流的裁剪触发
|
|
98
100
|
│ ├── llm-call.ts # 唯一传输层:路由、推理档位、输出预算、失败事实
|
|
99
101
|
│ ├── subagent.ts # 所有 LLM 调用经过的装载器(装配 → 调用 → 解析 → 记账)
|
|
100
102
|
│ ├── subagent-defs.ts # 子代理定义表:提示词、预算兜底、应答解析器,一处声明
|
|
103
|
+
│ ├── prompt-date.ts # 提示词的时钟:本轮基准日 + 那行「今天」指令
|
|
101
104
|
│ ├── chain.ts # 责任链:一环节一步,顺序由环境类组装
|
|
102
105
|
│ ├── review-candidates.ts # 评审队列:什么该审(新写 > 被纠正牵连 > 陈旧)
|
|
103
106
|
│ ├── review.ts # 内容质量自进化:批量、预算、日 rollup、调度
|
|
@@ -120,7 +123,7 @@ pnpm check:pack # 发布产物一致性(prepack 也会跑)
|
|
|
120
123
|
├── client/
|
|
121
124
|
│ └── client.js # 手写 client bundle:官方插件管理内只读卡片
|
|
122
125
|
├── locale/ # 宿主包元数据:显示名 + 描述双语(zh/en)
|
|
123
|
-
├── test/ # vitest 测试(
|
|
126
|
+
├── test/ # vitest 测试(29 个文件)
|
|
124
127
|
├── lib/ # 构建产物(tsc)
|
|
125
128
|
├── docs/ # design.md(设计)· FAQ.md(踩坑)
|
|
126
129
|
└── .agents/ # AI 协作层(AGENTS.md、技能、ADR 笔记)
|
package/lib/extraction-ring.js
CHANGED
|
@@ -12,6 +12,8 @@ import { runChain } from "./chain.js";
|
|
|
12
12
|
import { observedUsageOf } from "./llm-call.js";
|
|
13
13
|
import { boundaryCursorOf, eventsAfterCursor, evaluateSlice, seqOfCursor, serializeIncrement } from "./extraction-surface.js";
|
|
14
14
|
import { CHEAP_INCREMENT_MAX, cheapPrompt, extractionPrompt, manifestLineOf } from "./extraction-prompt.js";
|
|
15
|
+
import { todayUtc } from "./prompt-date.js";
|
|
16
|
+
import { pruneIfDue } from "./retention.js";
|
|
15
17
|
import { extractionGate, extractionMain } from "./subagent-defs.js";
|
|
16
18
|
import { cutFailureOf, subagentLoaderFor } from "./subagent.js";
|
|
17
19
|
/** Head length of a failed run's raw answer kept in the ledger `files` column. */
|
|
@@ -58,7 +60,7 @@ export async function runExtractionInner(deps, target, opts) {
|
|
|
58
60
|
// Narrowed once, here: the loader takes one concrete route and session.
|
|
59
61
|
const workspaceId = target.workspaceId;
|
|
60
62
|
const route = { provider: target.provider, model: target.model };
|
|
61
|
-
const call = { llm: deps.llm, route, sessionId: target.sessionId };
|
|
63
|
+
const call = { llm: deps.llm, route, sessionId: target.sessionId, today: opts.today ?? todayUtc() };
|
|
62
64
|
const cursor = store.cursor(workspaceId, target.sessionId);
|
|
63
65
|
let all;
|
|
64
66
|
try {
|
|
@@ -272,6 +274,10 @@ function proposeLink() {
|
|
|
272
274
|
// leakage quarantined — never a separate scheduler.
|
|
273
275
|
const patrol = store.patrol();
|
|
274
276
|
void patrol;
|
|
277
|
+
// Retention rides the same run: the ledgers only grow, and an
|
|
278
|
+
// applied extraction is what writes most of their rows. Day-throttled
|
|
279
|
+
// inside, so this is one state read on all but the day's first run.
|
|
280
|
+
pruneIfDue(store);
|
|
275
281
|
item.result = { status: "applied", applied: outcomes.filter((outcome) => outcome.ok).length, cursor: item.boundarySeq };
|
|
276
282
|
return "handled";
|
|
277
283
|
},
|
package/lib/extraction.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type SkipReason, type SurfaceReader } from "./extraction-surface.js";
|
|
2
2
|
import type { LedgerEntry, MemoryStore } from "./store.js";
|
|
3
3
|
import { type LlmRuntime, type RouteResolver } from "./llm-call.js";
|
|
4
|
+
import type { PluginLog } from "./plugin-log.js";
|
|
4
5
|
export type { LlmStream, LlmRuntime, RouteResolver } from "./llm-call.js";
|
|
5
6
|
/** Everything a run needs: the store, the session reader, and the LLM. */
|
|
6
7
|
export interface ExtractionDeps {
|
|
@@ -36,6 +37,8 @@ export interface RunOptions {
|
|
|
36
37
|
trigger: LedgerEntry["trigger"];
|
|
37
38
|
/** Character ceiling for the serialized increment (default 40000). */
|
|
38
39
|
incrementMaxChars?: number;
|
|
40
|
+
/** Baseline day `YYYY-MM-DD` (UTC) the prompts judge dates by; defaults to the run's wall clock. */
|
|
41
|
+
today?: string | undefined;
|
|
39
42
|
}
|
|
40
43
|
/**
|
|
41
44
|
* Run one extraction pass for a target. Never throws unless the ledger
|
|
@@ -48,10 +51,8 @@ export declare function runExtraction(deps: ExtractionDeps, target: ExtractionTa
|
|
|
48
51
|
/** The event wiring surface (duck-typed cordis context). */
|
|
49
52
|
export interface SchedulerHost {
|
|
50
53
|
on(event: string, handler: (...args: never[]) => void): unknown;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
warn(message: string): void;
|
|
54
|
-
};
|
|
54
|
+
/** The plugin's own log face: durable in the store, mirrored live. */
|
|
55
|
+
log: PluginLog;
|
|
55
56
|
}
|
|
56
57
|
/**
|
|
57
58
|
* Wire the extraction triggers onto the host context. Returns a disposer
|
package/lib/extraction.js
CHANGED
|
@@ -127,7 +127,7 @@ export function createExtractionScheduler(host, deps) {
|
|
|
127
127
|
catch {
|
|
128
128
|
// The ledger is down — the warn below is the last resort.
|
|
129
129
|
}
|
|
130
|
-
host.
|
|
130
|
+
host.log.warn("extraction-unexpected", `run failed unexpectedly: ${message}`, { sessionId });
|
|
131
131
|
}).finally(() => {
|
|
132
132
|
slot.running = false;
|
|
133
133
|
if (slot.pending !== null) {
|
|
@@ -186,7 +186,7 @@ export function createExtractionScheduler(host, deps) {
|
|
|
186
186
|
// its own reference and simply finds nothing left to drain.
|
|
187
187
|
slots.delete(id);
|
|
188
188
|
}));
|
|
189
|
-
host.
|
|
189
|
+
host.log.info("extraction-armed", "scheduler armed (per-session slots, turn-level, single-flight per session)");
|
|
190
190
|
return () => {
|
|
191
191
|
slots.clear();
|
|
192
192
|
for (const listener of listeners) {
|
package/lib/index.d.ts
CHANGED
|
@@ -43,10 +43,6 @@ interface HostContext {
|
|
|
43
43
|
text: string | ((context: SectionContext) => string);
|
|
44
44
|
}): void;
|
|
45
45
|
};
|
|
46
|
-
logger(name: string): {
|
|
47
|
-
info(message: string): void;
|
|
48
|
-
warn(message: string): void;
|
|
49
|
-
};
|
|
50
46
|
}
|
|
51
47
|
export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
52
48
|
/**
|
package/lib/index.js
CHANGED
|
@@ -38,6 +38,8 @@ import { registerMemoryTools } from "./memory-tools.js";
|
|
|
38
38
|
import { registerSkillTools } from "./skill-tools.js";
|
|
39
39
|
import { importWorkspaceMd } from "./import-md.js";
|
|
40
40
|
import { openMemoryStore } from "./store.js";
|
|
41
|
+
import { createPluginLog } from "./plugin-log.js";
|
|
42
|
+
import { pruneIfDue } from "./retention.js";
|
|
41
43
|
import { newestRegistryPath } from "./workspace-hint.js";
|
|
42
44
|
export const name = "continual-evolve";
|
|
43
45
|
export const inject = ["systemPrompt"];
|
|
@@ -117,23 +119,23 @@ function registerSchedulerDisposer(scoped, dispose) {
|
|
|
117
119
|
}
|
|
118
120
|
}
|
|
119
121
|
/** Register the memory tools through a nested inject (see memory-tools.ts). */
|
|
120
|
-
function mountToolsWhenAvailable(host, store, state) {
|
|
122
|
+
function mountToolsWhenAvailable(host, store, state, log) {
|
|
121
123
|
const nested = host;
|
|
122
124
|
if (typeof nested.inject !== "function") {
|
|
123
|
-
|
|
125
|
+
log.warn("tools-not-mounted", "host context exposes no inject — memory tools not registered (injection unaffected; explicit memory management falls back to telling the user)");
|
|
124
126
|
return;
|
|
125
127
|
}
|
|
126
128
|
nested.inject(["tools"], (scoped) => {
|
|
127
129
|
const tools = scoped.tools;
|
|
128
130
|
if (tools === undefined || typeof tools.register !== "function") {
|
|
129
|
-
|
|
131
|
+
log.warn("tools-empty", "tools service present but empty — memory tools not registered (injection unaffected)");
|
|
130
132
|
return;
|
|
131
133
|
}
|
|
132
134
|
registerMemoryTools(tools, store);
|
|
133
135
|
registerSkillTools(tools, store);
|
|
134
136
|
state.toolsAvailable = true;
|
|
135
|
-
|
|
136
|
-
|
|
137
|
+
log.info("memory-tools-registered", "memory_write and memory_read registered");
|
|
138
|
+
log.info("skill-tools-registered", "skill_write and skill_read registered");
|
|
137
139
|
});
|
|
138
140
|
}
|
|
139
141
|
/**
|
|
@@ -143,28 +145,29 @@ function mountToolsWhenAvailable(host, store, state) {
|
|
|
143
145
|
* without either service (or without `.on`) simply never arms — injection
|
|
144
146
|
* and the explicit tools remain functional.
|
|
145
147
|
*/
|
|
146
|
-
function mountExtractionWhenAvailable(host, store, config) {
|
|
147
|
-
const log = host.logger("continual-evolve");
|
|
148
|
+
function mountExtractionWhenAvailable(host, store, config, log) {
|
|
148
149
|
if (config.memoryIndex?.extraction === false) {
|
|
149
|
-
log.info("extraction disabled by config — injection and memory_write remain active");
|
|
150
|
+
log.info("extraction-disabled", "extraction disabled by config — injection and memory_write remain active");
|
|
150
151
|
return;
|
|
151
152
|
}
|
|
152
153
|
const nested = host;
|
|
153
154
|
const events = host;
|
|
154
|
-
|
|
155
|
-
|
|
155
|
+
const on = events.on;
|
|
156
|
+
if (typeof nested.inject !== "function" || typeof on !== "function") {
|
|
157
|
+
log.warn("extraction-not-armed", "host context exposes no inject/on — extraction not armed (injection and tools unaffected)");
|
|
156
158
|
return;
|
|
157
159
|
}
|
|
160
|
+
const schedulerHost = { on: (event, handler) => on.call(events, event, handler), log };
|
|
158
161
|
nested.inject(["sessionQuery", "llm"], (scoped) => {
|
|
159
162
|
const context = scoped;
|
|
160
163
|
const surface = context.sessionQuery;
|
|
161
164
|
const llm = (context.llm ?? context.get?.("llm"));
|
|
162
165
|
if (surface === undefined || typeof surface.readSurface !== "function") {
|
|
163
|
-
log.warn("sessionQuery service present but empty — extraction not armed (injection and tools unaffected)");
|
|
166
|
+
log.warn("extraction-no-surface", "sessionQuery service present but empty — extraction not armed (injection and tools unaffected)");
|
|
164
167
|
return;
|
|
165
168
|
}
|
|
166
169
|
const resolveRoute = routeResolverOf(scoped, context.get);
|
|
167
|
-
registerSchedulerDisposer(scoped, createExtractionScheduler(
|
|
170
|
+
registerSchedulerDisposer(scoped, createExtractionScheduler(schedulerHost, {
|
|
168
171
|
store, surface, llm,
|
|
169
172
|
...(resolveRoute === undefined ? {} : { resolveRoute }),
|
|
170
173
|
}));
|
|
@@ -176,23 +179,24 @@ function mountExtractionWhenAvailable(host, store, config) {
|
|
|
176
179
|
* increments), so it mounts independently from extraction. Own prompts, own
|
|
177
180
|
* budget, own ledger; the store's write gates are the only shared surface.
|
|
178
181
|
*/
|
|
179
|
-
function mountReviewWhenAvailable(host, store, config) {
|
|
180
|
-
const log = host.logger("continual-evolve");
|
|
182
|
+
function mountReviewWhenAvailable(host, store, config, log) {
|
|
181
183
|
if (config.review?.enabled?.get() === false) {
|
|
182
|
-
log.info("review disabled by config — extraction and tools unaffected");
|
|
184
|
+
log.info("review-disabled", "review disabled by config — extraction and tools unaffected");
|
|
183
185
|
return;
|
|
184
186
|
}
|
|
185
187
|
const nested = host;
|
|
186
188
|
const events = host;
|
|
187
|
-
|
|
188
|
-
|
|
189
|
+
const on = events.on;
|
|
190
|
+
if (typeof nested.inject !== "function" || typeof on !== "function") {
|
|
191
|
+
log.warn("review-not-armed", "host context exposes no inject/on — review not armed (injection and tools unaffected)");
|
|
189
192
|
return;
|
|
190
193
|
}
|
|
194
|
+
const schedulerHost = { on: (event, handler) => on.call(events, event, handler), log };
|
|
191
195
|
nested.inject(["llm"], (scoped) => {
|
|
192
196
|
const context = scoped;
|
|
193
197
|
const llm = (context.llm ?? context.get?.("llm"));
|
|
194
198
|
const resolveRoute = routeResolverOf(scoped, context.get);
|
|
195
|
-
registerSchedulerDisposer(scoped, createReviewScheduler(
|
|
199
|
+
registerSchedulerDisposer(scoped, createReviewScheduler(schedulerHost, {
|
|
196
200
|
store, llm,
|
|
197
201
|
...(resolveRoute === undefined ? {} : { resolveRoute }),
|
|
198
202
|
}, {
|
|
@@ -216,17 +220,17 @@ function mountReviewWhenAvailable(host, store, config) {
|
|
|
216
220
|
* `ctx.get` on every request (the FAQ #2 sibling-service rule — the service is
|
|
217
221
|
* optional, so it must not join the inject list, which would gate the mount).
|
|
218
222
|
*/
|
|
219
|
-
function mountCardWhenAvailable(host, store) {
|
|
223
|
+
function mountCardWhenAvailable(host, store, log) {
|
|
220
224
|
const nested = host;
|
|
221
225
|
if (typeof nested.inject !== "function") {
|
|
222
|
-
|
|
226
|
+
log.warn("card-not-mounted", "host context exposes no inject — memory card routes not mounted (memory section unaffected)");
|
|
223
227
|
return;
|
|
224
228
|
}
|
|
225
229
|
nested.inject(["webServer"], (scoped) => {
|
|
226
230
|
const context = scoped;
|
|
227
231
|
const webServer = context.webServer;
|
|
228
232
|
if (webServer === undefined) {
|
|
229
|
-
|
|
233
|
+
log.warn("card-empty-server", "webServer service present but empty — memory card routes not mounted (memory section unaffected)");
|
|
230
234
|
return;
|
|
231
235
|
}
|
|
232
236
|
mountCardRoutes(webServer, store, {
|
|
@@ -238,20 +242,32 @@ export function apply(host, config) {
|
|
|
238
242
|
if (config.memoryIndex?.enabled === false) {
|
|
239
243
|
return;
|
|
240
244
|
}
|
|
241
|
-
const log = host.logger("continual-evolve");
|
|
242
245
|
const state = { store: undefined, toolsAvailable: false };
|
|
246
|
+
// One log face for the whole plugin, writing to the store once it is open.
|
|
247
|
+
// Created before the store exists: the early mounts and the failure path
|
|
248
|
+
// below call it, and lines written before readiness are dropped.
|
|
249
|
+
const log = createPluginLog(() => state.store);
|
|
243
250
|
void openMemoryStore().then((store) => {
|
|
244
251
|
state.store = store;
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
252
|
+
// Retention has no scheduler: the day-throttled check rides mount and
|
|
253
|
+
// every log write, so the ledgers stay bounded in a long-lived process.
|
|
254
|
+
try {
|
|
255
|
+
pruneIfDue(store);
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
log.warn("prune-failed", `ledger retention skipped: ${error instanceof Error ? error.message : String(error)}`);
|
|
259
|
+
}
|
|
260
|
+
mountToolsWhenAvailable(host, store, state, log);
|
|
261
|
+
mountExtractionWhenAvailable(host, store, config, log);
|
|
262
|
+
mountReviewWhenAvailable(host, store, config, log);
|
|
248
263
|
if (config.memoryCard?.enabled !== false) {
|
|
249
|
-
mountCardWhenAvailable(host, store);
|
|
264
|
+
mountCardWhenAvailable(host, store, log);
|
|
250
265
|
}
|
|
251
|
-
}, (
|
|
252
|
-
// node:sqlite missing (old runtime) or the database unusable:
|
|
253
|
-
//
|
|
254
|
-
|
|
266
|
+
}, (_error) => {
|
|
267
|
+
// node:sqlite missing (old runtime) or the database unusable: degrade
|
|
268
|
+
// to a no-op plugin, never take the host down. Nothing is logged: the
|
|
269
|
+
// store is this plugin's only log home, and it is the thing that just
|
|
270
|
+
// failed — the host's logger has no sink to fall back on.
|
|
255
271
|
});
|
|
256
272
|
const frozen = createFrozenMemorySection();
|
|
257
273
|
host.systemPrompt.section({
|
|
@@ -272,7 +288,7 @@ export function apply(host, config) {
|
|
|
272
288
|
importWorkspaceMd(store, cwd);
|
|
273
289
|
}
|
|
274
290
|
catch (error) {
|
|
275
|
-
log.warn(`legacy memory import failed for ${cwd}: ${error instanceof Error ? error.message : String(error)}
|
|
291
|
+
log.warn("md-import-failed", `legacy memory import failed for ${cwd}: ${error instanceof Error ? error.message : String(error)}`, { workspaceId: cwd });
|
|
276
292
|
}
|
|
277
293
|
}
|
|
278
294
|
return frozen.textFor(context.agent, (agent) => memorySectionText(agent, {
|
|
@@ -283,6 +299,6 @@ export function apply(host, config) {
|
|
|
283
299
|
}));
|
|
284
300
|
},
|
|
285
301
|
});
|
|
286
|
-
log.info("continual-evolve memory section registered (central SQLite store, sole-writer gates)");
|
|
302
|
+
log.info("section-registered", "continual-evolve memory section registered (central SQLite store, sole-writer gates)");
|
|
287
303
|
}
|
|
288
304
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { MemoryStore } from "./store.js";
|
|
2
|
+
/** The partition a line belongs to, when the call site knows one. */
|
|
3
|
+
export interface LogWhere {
|
|
4
|
+
workspaceId?: string | undefined;
|
|
5
|
+
sessionId?: string | undefined;
|
|
6
|
+
}
|
|
7
|
+
/** The log face feature code holds: durable in the store, nothing else. */
|
|
8
|
+
export interface PluginLog {
|
|
9
|
+
info(event: string, message: string, where?: LogWhere): void;
|
|
10
|
+
warn(event: string, message: string, where?: LogWhere): void;
|
|
11
|
+
error(event: string, message: string, where?: LogWhere): void;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Bind a log face to one store accessor.
|
|
15
|
+
*
|
|
16
|
+
* @param storeOf - Reads the store each call: the sink outlives store startup,
|
|
17
|
+
* and a host whose store never opened has nowhere to write.
|
|
18
|
+
* @returns The log face feature code calls.
|
|
19
|
+
*/
|
|
20
|
+
export declare function createPluginLog(storeOf: () => MemoryStore | undefined): PluginLog;
|
|
21
|
+
//# sourceMappingURL=plugin-log.d.ts.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plugin's log face: one call writes one durable row.
|
|
3
|
+
*
|
|
4
|
+
* The host's logger is not a channel this plugin uses. A stock host attaches no
|
|
5
|
+
* exporter to it (docs/FAQ.md #4), so it can never be the home of a fact worth
|
|
6
|
+
* re-reading — and the sink belongs to the user's environment, which the plugin
|
|
7
|
+
* does not control. The store is the only home, which also means a diagnostic
|
|
8
|
+
* written while the store is unavailable is dropped: this face never throws,
|
|
9
|
+
* because a log line that can take a session down costs more than a lost line.
|
|
10
|
+
*/
|
|
11
|
+
import { pruneIfDue } from "./retention.js";
|
|
12
|
+
/**
|
|
13
|
+
* Bind a log face to one store accessor.
|
|
14
|
+
*
|
|
15
|
+
* @param storeOf - Reads the store each call: the sink outlives store startup,
|
|
16
|
+
* and a host whose store never opened has nowhere to write.
|
|
17
|
+
* @returns The log face feature code calls.
|
|
18
|
+
*/
|
|
19
|
+
export function createPluginLog(storeOf) {
|
|
20
|
+
const write = (level, event, message, where) => {
|
|
21
|
+
const store = storeOf();
|
|
22
|
+
if (store === undefined) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
store.logPlugin({ level, event, message, ...where });
|
|
27
|
+
// Retention rides the writes: the log is the one table written
|
|
28
|
+
// without a scheduler of its own, so it prunes itself.
|
|
29
|
+
pruneIfDue(store);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// Swallowed by design: the durable write and its prune are
|
|
33
|
+
// best-effort, and there is no second channel to fall back to.
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
return {
|
|
37
|
+
info: (event, message, where) => write("info", event, message, where),
|
|
38
|
+
warn: (event, message, where) => write("warn", event, message, where),
|
|
39
|
+
error: (event, message, where) => write("error", event, message, where),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=plugin-log.js.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The clock every prompt is anchored to: the run's baseline day and the line
|
|
3
|
+
* that hands it to the model.
|
|
4
|
+
*
|
|
5
|
+
* The model has no clock of its own. A direct LLM call carries an instruction
|
|
6
|
+
* and a user prompt and nothing else — no runtime context, no timestamps — so
|
|
7
|
+
* "is this date in the future?" is answered from the model's own prior unless
|
|
8
|
+
* the caller says otherwise. A run resolves its day once at the boundary and
|
|
9
|
+
* carries it as data, so a replay and a live run see the same prompts.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* The current UTC day `YYYY-MM-DD`.
|
|
13
|
+
*
|
|
14
|
+
* UTC is the plugin's unit: the review account's day uses the same value, so
|
|
15
|
+
* one run cannot straddle two dates.
|
|
16
|
+
*
|
|
17
|
+
* @returns Today's date in UTC.
|
|
18
|
+
*/
|
|
19
|
+
export declare function todayUtc(): string;
|
|
20
|
+
/**
|
|
21
|
+
* The date-baseline line appended to the instruction of a job that makes
|
|
22
|
+
* time-relative judgments.
|
|
23
|
+
*
|
|
24
|
+
* It names the day *and* says which side of it is a defect, because the rule
|
|
25
|
+
* being protected is "no future dates", not "never touch dates": the 2026-10-14
|
|
26
|
+
* trial produced 8 of 13 rewrite verdicts that stripped correct past dates as
|
|
27
|
+
* if they were future ones.
|
|
28
|
+
*
|
|
29
|
+
* @param today - The run's baseline day `YYYY-MM-DD` (UTC).
|
|
30
|
+
* @returns The instruction line to append to a system prompt.
|
|
31
|
+
*/
|
|
32
|
+
export declare function dateBaseline(today: string): string;
|
|
33
|
+
//# sourceMappingURL=prompt-date.d.ts.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The clock every prompt is anchored to: the run's baseline day and the line
|
|
3
|
+
* that hands it to the model.
|
|
4
|
+
*
|
|
5
|
+
* The model has no clock of its own. A direct LLM call carries an instruction
|
|
6
|
+
* and a user prompt and nothing else — no runtime context, no timestamps — so
|
|
7
|
+
* "is this date in the future?" is answered from the model's own prior unless
|
|
8
|
+
* the caller says otherwise. A run resolves its day once at the boundary and
|
|
9
|
+
* carries it as data, so a replay and a live run see the same prompts.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* The current UTC day `YYYY-MM-DD`.
|
|
13
|
+
*
|
|
14
|
+
* UTC is the plugin's unit: the review account's day uses the same value, so
|
|
15
|
+
* one run cannot straddle two dates.
|
|
16
|
+
*
|
|
17
|
+
* @returns Today's date in UTC.
|
|
18
|
+
*/
|
|
19
|
+
export function todayUtc() {
|
|
20
|
+
return new Date().toISOString().slice(0, 10);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The date-baseline line appended to the instruction of a job that makes
|
|
24
|
+
* time-relative judgments.
|
|
25
|
+
*
|
|
26
|
+
* It names the day *and* says which side of it is a defect, because the rule
|
|
27
|
+
* being protected is "no future dates", not "never touch dates": the 2026-10-14
|
|
28
|
+
* trial produced 8 of 13 rewrite verdicts that stripped correct past dates as
|
|
29
|
+
* if they were future ones.
|
|
30
|
+
*
|
|
31
|
+
* @param today - The run's baseline day `YYYY-MM-DD` (UTC).
|
|
32
|
+
* @returns The instruction line to append to a system prompt.
|
|
33
|
+
*/
|
|
34
|
+
export function dateBaseline(today) {
|
|
35
|
+
return `时间基准:今天是 ${today}(UTC)。正文里早于或等于今天的日期是按期发生的过去事实,不是缺陷、不要据此改写;只有晚于今天的日期才算「未来日期」错误。把相对日期(如"周四")折算成绝对日期时,以今天为参照。`;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=prompt-date.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How long the plugin keeps its own ledgers, and when it prunes them.
|
|
3
|
+
*
|
|
4
|
+
* `MemoryStore.pruneLedger` shipped with no production caller, so "the ledgers
|
|
5
|
+
* are pruned" was true of the API and false of the machine (2026-10-14 real
|
|
6
|
+
* store: 547 extraction rows in 9 days, 115 review rows in 3). Retention is a
|
|
7
|
+
* policy with a trigger, and the trigger is the part that was missing.
|
|
8
|
+
*
|
|
9
|
+
* The check is day-throttled and idempotent, so every writer can call it
|
|
10
|
+
* cheaply and a long-running process prunes without a scheduler of its own.
|
|
11
|
+
*/
|
|
12
|
+
import { type MemoryStore } from "./store.js";
|
|
13
|
+
/** Rows older than this many days are pruned. */
|
|
14
|
+
export declare const LEDGER_RETENTION_DAYS = 30;
|
|
15
|
+
/**
|
|
16
|
+
* Prune the ledgers and the plugin log when this UTC day has not been pruned.
|
|
17
|
+
*
|
|
18
|
+
* @param store - The store to prune.
|
|
19
|
+
* @param now - The instant to judge by (defaults to the wall clock; tests pass one).
|
|
20
|
+
* @returns Rows removed, or 0 when today's prune already ran.
|
|
21
|
+
*/
|
|
22
|
+
export declare function pruneIfDue(store: MemoryStore, now?: Date): number;
|
|
23
|
+
//# sourceMappingURL=retention.d.ts.map
|
package/lib/retention.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How long the plugin keeps its own ledgers, and when it prunes them.
|
|
3
|
+
*
|
|
4
|
+
* `MemoryStore.pruneLedger` shipped with no production caller, so "the ledgers
|
|
5
|
+
* are pruned" was true of the API and false of the machine (2026-10-14 real
|
|
6
|
+
* store: 547 extraction rows in 9 days, 115 review rows in 3). Retention is a
|
|
7
|
+
* policy with a trigger, and the trigger is the part that was missing.
|
|
8
|
+
*
|
|
9
|
+
* The check is day-throttled and idempotent, so every writer can call it
|
|
10
|
+
* cheaply and a long-running process prunes without a scheduler of its own.
|
|
11
|
+
*/
|
|
12
|
+
import { GLOBAL_WORKSPACE } from "./store.js";
|
|
13
|
+
/** Rows older than this many days are pruned. */
|
|
14
|
+
export const LEDGER_RETENTION_DAYS = 30;
|
|
15
|
+
/** The state key holding the UTC day of the last prune. */
|
|
16
|
+
const PRUNE_STATE_KEY = "prune:last";
|
|
17
|
+
/**
|
|
18
|
+
* Prune the ledgers and the plugin log when this UTC day has not been pruned.
|
|
19
|
+
*
|
|
20
|
+
* @param store - The store to prune.
|
|
21
|
+
* @param now - The instant to judge by (defaults to the wall clock; tests pass one).
|
|
22
|
+
* @returns Rows removed, or 0 when today's prune already ran.
|
|
23
|
+
*/
|
|
24
|
+
export function pruneIfDue(store, now = new Date()) {
|
|
25
|
+
const day = now.toISOString().slice(0, 10);
|
|
26
|
+
if (store.state(GLOBAL_WORKSPACE, PRUNE_STATE_KEY) === day) {
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
const cutoff = new Date(now.getTime() - LEDGER_RETENTION_DAYS * 24 * 60 * 60 * 1000).toISOString();
|
|
30
|
+
const removed = store.pruneLedger(cutoff);
|
|
31
|
+
store.setState(GLOBAL_WORKSPACE, PRUNE_STATE_KEY, day);
|
|
32
|
+
return removed;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=retention.js.map
|
package/lib/review.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type LlmRuntime, type RouteResolver } from "./llm-call.js";
|
|
2
|
+
import type { PluginLog } from "./plugin-log.js";
|
|
2
3
|
import type { MemoryStore } from "./store.js";
|
|
3
4
|
/** The LLM face review calls, aliased so each scheduler can name its own. */
|
|
4
5
|
export type ReviewLlm = LlmRuntime;
|
|
@@ -27,7 +28,7 @@ export interface ReviewCandidate {
|
|
|
27
28
|
scope?: "project" | "global" | undefined;
|
|
28
29
|
}
|
|
29
30
|
export interface ReviewRunOptions {
|
|
30
|
-
/** UTC day `YYYY-MM-DD` the hard cap accounts against. */
|
|
31
|
+
/** UTC day `YYYY-MM-DD` the hard cap accounts against — also the clock the prompts judge dates by. */
|
|
31
32
|
day: string;
|
|
32
33
|
/** Hard cap for the day (100). Candidates past it count as dropped. */
|
|
33
34
|
budget: number;
|
|
@@ -64,15 +65,11 @@ export declare const REVIEW_DEFAULT_STALE_DAYS = 30;
|
|
|
64
65
|
* run keeps going), exactly like extraction's fail-ledgered discipline.
|
|
65
66
|
*/
|
|
66
67
|
export declare function runReviewBatch(deps: ReviewDeps, candidates: readonly ReviewCandidate[], opts: ReviewRunOptions): Promise<ReviewRunSummary>;
|
|
67
|
-
/** Current UTC day `YYYY-MM-DD` for the hard-cap account. */
|
|
68
|
-
export declare function todayUtc(): string;
|
|
69
68
|
/** The event wiring surface (duck-typed cordis context, mirrors extraction). */
|
|
70
69
|
export interface ReviewSchedulerHost {
|
|
71
70
|
on(event: string, handler: (...args: never[]) => void): unknown;
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
warn(message: string): void;
|
|
75
|
-
};
|
|
71
|
+
/** The plugin's own log face: durable in the store, mirrored live. */
|
|
72
|
+
log: PluginLog;
|
|
76
73
|
}
|
|
77
74
|
/** The editable review settings, re-read on every drain. */
|
|
78
75
|
export interface ReviewSettings {
|
package/lib/review.js
CHANGED
|
@@ -20,8 +20,9 @@
|
|
|
20
20
|
import { runChain } from "./chain.js";
|
|
21
21
|
import { resolveCallRoute } from "./llm-call.js";
|
|
22
22
|
import { cwdOf } from "./memory-section.js";
|
|
23
|
+
import { todayUtc } from "./prompt-date.js";
|
|
23
24
|
import { collectCandidates } from "./review-candidates.js";
|
|
24
|
-
import { reviewChain } from "./review-ring.js";
|
|
25
|
+
import { failureHead, reviewChain } from "./review-ring.js";
|
|
25
26
|
import { subagentLoaderFor } from "./subagent.js";
|
|
26
27
|
import { zeroUsage } from "./token-usage.js";
|
|
27
28
|
/** Defaults for the review settings the host may override — and edit live. */
|
|
@@ -49,59 +50,85 @@ export async function runReviewBatch(deps, candidates, opts) {
|
|
|
49
50
|
const llm = deps.llm;
|
|
50
51
|
// The environment class: one loader and one chain for the whole run, in the
|
|
51
52
|
// order the ring declares. Without a route there is no ring to walk.
|
|
52
|
-
const ring = route === undefined || llm === undefined ? undefined : assembleRing({ llm, route, sessionId: opts.sessionId });
|
|
53
|
+
const ring = route === undefined || llm === undefined ? undefined : assembleRing({ llm, route, sessionId: opts.sessionId, today: opts.day });
|
|
53
54
|
let position = 0;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
55
|
+
// The run's account is written whatever happens to the loop: a fault that
|
|
56
|
+
// escapes one item (or the loop itself) is rethrown *after* the rollup is
|
|
57
|
+
// merged, so a dead run can never leave `reviewed` incremented with every
|
|
58
|
+
// other counter at zero and nothing saying why (2026-10-14 incident).
|
|
59
|
+
let escaped;
|
|
60
|
+
let failed = false;
|
|
61
|
+
try {
|
|
62
|
+
for (const candidate of batch) {
|
|
63
|
+
try {
|
|
64
|
+
const remaining = deps.store.consumeReviewBudget(opts.day, opts.budget);
|
|
65
|
+
if (remaining < 0) {
|
|
66
|
+
// Hard cap hit: this item and everything after it drops (low
|
|
67
|
+
// priority never rolls over — it simply waits for a future run).
|
|
68
|
+
summary.dropped += batch.length - position;
|
|
69
|
+
for (const item of batch.slice(position)) {
|
|
70
|
+
deps.store.logReview({
|
|
71
|
+
runId, workspaceId: item.workspaceId, itemKind: item.kind, itemId: item.id,
|
|
72
|
+
verdict: "skip", reason: "budget-exhausted", applied: 0,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
skipReasons.push("budget-exhausted");
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
summary.reviewed += 1;
|
|
79
|
+
if (candidate.priority === 0) {
|
|
80
|
+
newItems += 1;
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
requeued += 1;
|
|
84
|
+
}
|
|
85
|
+
if (route === undefined || ring === undefined) {
|
|
86
|
+
deps.store.logReview({
|
|
87
|
+
runId, workspaceId: candidate.workspaceId, itemKind: candidate.kind, itemId: candidate.id,
|
|
88
|
+
verdict: "skip", reason: "no-route", applied: 0,
|
|
89
|
+
});
|
|
90
|
+
skipReasons.push("no-route");
|
|
91
|
+
position += 1;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const item = {
|
|
95
|
+
deps, loader: ring.loader, route, runId, candidate, opts, summary, skipReasons,
|
|
96
|
+
gate: { usage: zeroUsage(), raw: undefined },
|
|
97
|
+
};
|
|
98
|
+
await runChain(ring.links, item);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
// A fault escaping the chain (a store write, a wiring bug) gets its
|
|
102
|
+
// own row carrying the error text and costs the batch only this
|
|
103
|
+
// item — the model-facing failures already degrade inside the ring.
|
|
61
104
|
deps.store.logReview({
|
|
62
|
-
runId, workspaceId:
|
|
63
|
-
verdict: "skip", reason:
|
|
105
|
+
runId, workspaceId: candidate.workspaceId, itemKind: candidate.kind, itemId: candidate.id,
|
|
106
|
+
verdict: "skip", reason: `review-failed: ${failureHead(error)}`,
|
|
107
|
+
model: route?.model, applied: 0,
|
|
64
108
|
});
|
|
109
|
+
skipReasons.push("review-failed");
|
|
65
110
|
}
|
|
66
|
-
skipReasons.push("budget-exhausted");
|
|
67
|
-
break;
|
|
68
|
-
}
|
|
69
|
-
summary.reviewed += 1;
|
|
70
|
-
if (candidate.priority === 0) {
|
|
71
|
-
newItems += 1;
|
|
72
|
-
}
|
|
73
|
-
else {
|
|
74
|
-
requeued += 1;
|
|
75
|
-
}
|
|
76
|
-
if (route === undefined || ring === undefined) {
|
|
77
|
-
deps.store.logReview({
|
|
78
|
-
runId, workspaceId: candidate.workspaceId, itemKind: candidate.kind, itemId: candidate.id,
|
|
79
|
-
verdict: "skip", reason: "no-route", applied: 0,
|
|
80
|
-
});
|
|
81
|
-
skipReasons.push("no-route");
|
|
82
111
|
position += 1;
|
|
83
|
-
continue;
|
|
84
112
|
}
|
|
85
|
-
const item = {
|
|
86
|
-
deps, loader: ring.loader, route, runId, candidate, opts, summary, skipReasons,
|
|
87
|
-
gate: { usage: zeroUsage(), raw: undefined },
|
|
88
|
-
};
|
|
89
|
-
await runChain(ring.links, item);
|
|
90
|
-
position += 1;
|
|
91
113
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
114
|
+
catch (error) {
|
|
115
|
+
failed = true;
|
|
116
|
+
escaped = error;
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
summary.backlog = Math.max(0, candidates.length - summary.reviewed - summary.dropped);
|
|
120
|
+
deps.store.mergeDaily(opts.day, opts.budget, {
|
|
121
|
+
newItems, requeued, approved: summary.approved, applied: summary.applied,
|
|
122
|
+
cheapToken: summary.cheapToken, evolveToken: summary.evolveToken,
|
|
123
|
+
backlog: summary.backlog, dropped: summary.dropped,
|
|
124
|
+
skipReasons: skipReasons.length > 0 ? skipReasons : undefined,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
if (failed) {
|
|
128
|
+
throw escaped;
|
|
129
|
+
}
|
|
99
130
|
return summary;
|
|
100
131
|
}
|
|
101
|
-
/** Current UTC day `YYYY-MM-DD` for the hard-cap account. */
|
|
102
|
-
export function todayUtc() {
|
|
103
|
-
return new Date().toISOString().slice(0, 10);
|
|
104
|
-
}
|
|
105
132
|
/** Assemble one run's ring (simple factory: one context, one bound loader, one chain). */
|
|
106
133
|
function assembleRing(call) {
|
|
107
134
|
const loader = subagentLoaderFor(call);
|
|
@@ -140,26 +167,39 @@ export function createReviewScheduler(host, deps, config) {
|
|
|
140
167
|
const workspace = triggerWorkspace ?? pendingWorkspace;
|
|
141
168
|
pendingWorkspace = undefined;
|
|
142
169
|
void (async () => {
|
|
170
|
+
const settings = config.read();
|
|
171
|
+
const day = todayUtc();
|
|
143
172
|
try {
|
|
144
|
-
const settings = config.read();
|
|
145
|
-
const day = todayUtc();
|
|
146
173
|
const daily = deps.store.getDaily(day);
|
|
147
174
|
const remaining = settings.dailyBudget - (daily?.reviewed ?? 0);
|
|
148
175
|
if (remaining <= 0) {
|
|
176
|
+
host.log.info("review-budget-spent", `drain skipped: daily budget spent (${daily?.reviewed ?? 0}/${settings.dailyBudget})`, { workspaceId: workspace });
|
|
149
177
|
return;
|
|
150
178
|
}
|
|
151
179
|
const candidates = collectCandidates(deps.store, { staleDays: settings.staleDays, limit: settings.batchSize * 2 });
|
|
152
180
|
const ordered = workspace === undefined
|
|
153
181
|
? candidates
|
|
154
182
|
: [...candidates.filter((c) => c.workspaceId === workspace), ...candidates.filter((c) => c.workspaceId !== workspace)];
|
|
155
|
-
await runReviewBatch(deps, ordered.slice(0, settings.batchSize), {
|
|
183
|
+
const summary = await runReviewBatch(deps, ordered.slice(0, settings.batchSize), {
|
|
156
184
|
day, budget: settings.dailyBudget, dryRun: settings.dryRun,
|
|
157
185
|
batchSize: settings.batchSize, staleDays: settings.staleDays,
|
|
158
186
|
provider: lastRoute?.provider, model: lastRoute?.model, sessionId: lastRoute?.sessionId,
|
|
159
187
|
});
|
|
188
|
+
host.log.info("review-drain", `reviewed ${summary.reviewed}, approved ${summary.approved}, ` +
|
|
189
|
+
`applied ${summary.applied}, dropped ${summary.dropped}, backlog ${summary.backlog}`, { workspaceId: workspace, sessionId: lastRoute?.sessionId });
|
|
160
190
|
}
|
|
161
191
|
catch (error) {
|
|
162
|
-
|
|
192
|
+
const head = failureHead(error);
|
|
193
|
+
host.log.warn("review-drain-failed", head, { workspaceId: workspace, sessionId: lastRoute?.sessionId });
|
|
194
|
+
// Two records on purpose: the log is the live channel, the day's
|
|
195
|
+
// rollup is the durable one every later reading sees. A drain that
|
|
196
|
+
// died must leave a trace in the ledger, not only in a log row.
|
|
197
|
+
try {
|
|
198
|
+
deps.store.mergeDaily(day, settings.dailyBudget, { skipReasons: [`drain-failed: ${head}`] });
|
|
199
|
+
}
|
|
200
|
+
catch (recordError) {
|
|
201
|
+
host.log.warn("review-drain-unledgered", failureHead(recordError), { workspaceId: workspace });
|
|
202
|
+
}
|
|
163
203
|
}
|
|
164
204
|
finally {
|
|
165
205
|
running = false;
|
|
@@ -189,7 +229,7 @@ export function createReviewScheduler(host, deps, config) {
|
|
|
189
229
|
drain(cwdOf(agent));
|
|
190
230
|
}));
|
|
191
231
|
const armed = config.read();
|
|
192
|
-
host.
|
|
232
|
+
host.log.info("review-armed", `scheduler armed (daily cap ${armed.dailyBudget}, dry-run ${armed.dryRun ? "on" : "off"})`);
|
|
193
233
|
return () => {
|
|
194
234
|
pendingWorkspace = undefined;
|
|
195
235
|
for (const listener of listeners) {
|
package/lib/store.d.ts
CHANGED
|
@@ -153,6 +153,21 @@ export interface LedgerEntry {
|
|
|
153
153
|
/** Applied/attempted mutations: `{id, action, before?, after?, sourceSeqs}`; a parse-failed run instead carries the raw answer's head. */
|
|
154
154
|
files?: unknown;
|
|
155
155
|
}
|
|
156
|
+
/** One plugin-log line: the level, a stable event slug, and the message. */
|
|
157
|
+
export interface PluginLogEntry {
|
|
158
|
+
level: "info" | "warn" | "error";
|
|
159
|
+
/**
|
|
160
|
+
* The event's stable name (`drain-failed`, `card-no-webserver` …), so the
|
|
161
|
+
* table can be counted and grepped instead of read line by line.
|
|
162
|
+
*/
|
|
163
|
+
event: string;
|
|
164
|
+
message: string;
|
|
165
|
+
/** The partition the line belongs to, when the call site knows one. */
|
|
166
|
+
workspaceId?: string | undefined;
|
|
167
|
+
sessionId?: string | undefined;
|
|
168
|
+
}
|
|
169
|
+
/** Longest message kept; a runaway error string must not become a table row. */
|
|
170
|
+
export declare const PLUGIN_LOG_MESSAGE_MAX = 2000;
|
|
156
171
|
/** Workspace rows the card's selector lists. */
|
|
157
172
|
export interface WorkspaceSummary {
|
|
158
173
|
workspaceId: string;
|
|
@@ -219,7 +234,12 @@ export interface MemoryStore {
|
|
|
219
234
|
mostRecentlyActiveWorkspace(): string | undefined;
|
|
220
235
|
/** Hygiene pass: orphan FTS cleanup + secret quarantine. */
|
|
221
236
|
patrol(): PatrolResult;
|
|
222
|
-
/**
|
|
237
|
+
/**
|
|
238
|
+
* Append one plugin-log line — the durable home for plugin diagnostics,
|
|
239
|
+
* because the host's logger has no exporter on a stock install.
|
|
240
|
+
*/
|
|
241
|
+
logPlugin(entry: PluginLogEntry): void;
|
|
242
|
+
/** Ledger and log rows older than the cutoff, removed (the unbounded tables). */
|
|
223
243
|
pruneLedger(beforeIso: string): number;
|
|
224
244
|
close(): void;
|
|
225
245
|
/** One active skill row from a partition, undefined when absent. */
|
package/lib/store.js
CHANGED
|
@@ -162,7 +162,22 @@ CREATE TABLE IF NOT EXISTS review_daily (
|
|
|
162
162
|
skip_top TEXT,
|
|
163
163
|
skip_counts TEXT
|
|
164
164
|
) STRICT;
|
|
165
|
+
-- The plugin's own log. A stock host attaches no exporter to its logger, so a
|
|
166
|
+
-- ctx.logger call leaves no trace anywhere (docs/FAQ.md #4) — diagnostics that
|
|
167
|
+
-- have no domain ledger row of their own land here instead. Pruned with the
|
|
168
|
+
-- ledgers; the event column is a stable slug, so runs can be counted and grepped.
|
|
169
|
+
CREATE TABLE IF NOT EXISTS plugin_log (
|
|
170
|
+
ts TEXT NOT NULL,
|
|
171
|
+
level TEXT NOT NULL CHECK (level IN ('info','warn','error')),
|
|
172
|
+
event TEXT NOT NULL,
|
|
173
|
+
message TEXT NOT NULL,
|
|
174
|
+
workspace_id TEXT,
|
|
175
|
+
session_id TEXT
|
|
176
|
+
) STRICT;
|
|
177
|
+
CREATE INDEX IF NOT EXISTS idx_plugin_log_ts ON plugin_log(ts);
|
|
165
178
|
`;
|
|
179
|
+
/** Longest message kept; a runaway error string must not become a table row. */
|
|
180
|
+
export const PLUGIN_LOG_MESSAGE_MAX = 2000;
|
|
166
181
|
/** Default database location: `$DSH_HOME/evolve/memory.db`, else `~/.dsh/…`. */
|
|
167
182
|
export function defaultStorePath() {
|
|
168
183
|
const home = process.env.DSH_HOME?.trim();
|
|
@@ -375,6 +390,8 @@ export async function openMemoryStore(explicitPath) {
|
|
|
375
390
|
reviewLast: database.prepare(`SELECT ts, applied FROM review_log WHERE workspace_id IS ? AND item_kind = ? AND item_id = ? AND NOT ${INCOMPLETE_REVIEW_SQL} ORDER BY rowid DESC LIMIT 1`),
|
|
376
391
|
reviewEvolved: database.prepare("SELECT COUNT(*) AS n FROM review_log WHERE workspace_id IS ? AND item_kind = ? AND item_id = ? AND applied = 1"),
|
|
377
392
|
reviewPrune: database.prepare("DELETE FROM review_log WHERE ts < ?"),
|
|
393
|
+
pluginLog: database.prepare("INSERT INTO plugin_log (ts, level, event, message, workspace_id, session_id) VALUES (?, ?, ?, ?, ?, ?)"),
|
|
394
|
+
pluginLogPrune: database.prepare("DELETE FROM plugin_log WHERE ts < ?"),
|
|
378
395
|
dailyGet: database.prepare("SELECT * FROM review_daily WHERE day = ?"),
|
|
379
396
|
dailyUpsert: database.prepare("INSERT INTO review_daily (day, budget, reviewed, new_items, requeued, approved, applied, cheap_token, evolve_token, backlog, dropped, skip_top, skip_counts) " +
|
|
380
397
|
"VALUES (?, ?, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, NULL) " +
|
|
@@ -794,11 +811,15 @@ export async function openMemoryStore(explicitPath) {
|
|
|
794
811
|
stmts.stateSet.run(GLOBAL_WORKSPACE, PATROL_STATE_KEY, JSON.stringify(result));
|
|
795
812
|
return result;
|
|
796
813
|
},
|
|
814
|
+
logPlugin(entry) {
|
|
815
|
+
stmts.pluginLog.run(new Date().toISOString(), entry.level, entry.event, entry.message.slice(0, PLUGIN_LOG_MESSAGE_MAX), entry.workspaceId ?? null, entry.sessionId ?? null);
|
|
816
|
+
},
|
|
797
817
|
pruneLedger(beforeIso) {
|
|
798
818
|
const memory = Number(stmts.prune.run(beforeIso).changes);
|
|
799
819
|
const skills = Number(stmts.skillPrune.run(beforeIso).changes);
|
|
800
820
|
const reviews = Number(stmts.reviewPrune.run(beforeIso).changes);
|
|
801
|
-
|
|
821
|
+
const logs = Number(stmts.pluginLogPrune.run(beforeIso).changes);
|
|
822
|
+
return memory + skills + reviews + logs;
|
|
802
823
|
},
|
|
803
824
|
close() {
|
|
804
825
|
database.close();
|
package/lib/subagent-defs.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { EXTRACTION_CHEAP_SYSTEM_PROMPT, EXTRACTION_SYSTEM_PROMPT, parseCheapAnswer, parseExtractionAnswer, } from "./extraction-prompt.js";
|
|
14
14
|
import { parseEvolveAnswer, parseGateAnswer, REVIEW_MEMORY_CHEAP_SYSTEM_PROMPT, REVIEW_MEMORY_SYSTEM_PROMPT, REVIEW_SKILL_CHEAP_SYSTEM_PROMPT, REVIEW_SKILL_SYSTEM_PROMPT, } from "./review-prompt.js";
|
|
15
|
+
import { dateBaseline } from "./prompt-date.js";
|
|
15
16
|
/**
|
|
16
17
|
* Output-budget fallbacks, used only for adapters that advertise no
|
|
17
18
|
* per-request cap of their own. A gate verdict is one JSON object plus the
|
|
@@ -21,45 +22,53 @@ import { parseEvolveAnswer, parseGateAnswer, REVIEW_MEMORY_CHEAP_SYSTEM_PROMPT,
|
|
|
21
22
|
export const GATE_MAX_TOKENS = 1024;
|
|
22
23
|
export const EVOLVE_MAX_TOKENS = 4096;
|
|
23
24
|
export const EXTRACTION_MAX_TOKENS = 4096;
|
|
25
|
+
/**
|
|
26
|
+
* Bind one instruction to the run's clock.
|
|
27
|
+
*
|
|
28
|
+
* Every job here reads item text that can name dates, and a direct call
|
|
29
|
+
* carries no clock of its own — so "today" is instruction, not data, and it
|
|
30
|
+
* arrives as the same appended line for every job in the ring.
|
|
31
|
+
*/
|
|
32
|
+
const withDateBaseline = (instruction) => (context) => `${instruction}\n\n${dateBaseline(context.today)}`;
|
|
24
33
|
/** The memory cheap gate: is this entry worth a close look at all? */
|
|
25
34
|
export const reviewGateMemory = {
|
|
26
35
|
id: "review-gate-memory",
|
|
27
|
-
system: REVIEW_MEMORY_CHEAP_SYSTEM_PROMPT,
|
|
36
|
+
system: withDateBaseline(REVIEW_MEMORY_CHEAP_SYSTEM_PROMPT),
|
|
28
37
|
maxTokens: GATE_MAX_TOKENS,
|
|
29
38
|
parse: (text) => parseGateAnswer(text),
|
|
30
39
|
};
|
|
31
40
|
/** The skill cheap gate: same question, scope-relative home rules. */
|
|
32
41
|
export const reviewGateSkill = {
|
|
33
42
|
id: "review-gate-skill",
|
|
34
|
-
system: REVIEW_SKILL_CHEAP_SYSTEM_PROMPT,
|
|
43
|
+
system: withDateBaseline(REVIEW_SKILL_CHEAP_SYSTEM_PROMPT),
|
|
35
44
|
maxTokens: GATE_MAX_TOKENS,
|
|
36
45
|
parse: (text) => parseGateAnswer(text),
|
|
37
46
|
};
|
|
38
47
|
/** The memory evolve pass: five-dimension judgment plus the proposed rewrite. */
|
|
39
48
|
export const reviewEvolveMemory = {
|
|
40
49
|
id: "review-evolve-memory",
|
|
41
|
-
system: REVIEW_MEMORY_SYSTEM_PROMPT,
|
|
50
|
+
system: withDateBaseline(REVIEW_MEMORY_SYSTEM_PROMPT),
|
|
42
51
|
maxTokens: EVOLVE_MAX_TOKENS,
|
|
43
52
|
parse: (text) => parseEvolveAnswer(text),
|
|
44
53
|
};
|
|
45
54
|
/** The skill evolve pass: scope-relative reuse, executability, duplication. */
|
|
46
55
|
export const reviewEvolveSkill = {
|
|
47
56
|
id: "review-evolve-skill",
|
|
48
|
-
system: REVIEW_SKILL_SYSTEM_PROMPT,
|
|
57
|
+
system: withDateBaseline(REVIEW_SKILL_SYSTEM_PROMPT),
|
|
49
58
|
maxTokens: EVOLVE_MAX_TOKENS,
|
|
50
59
|
parse: (text) => parseEvolveAnswer(text),
|
|
51
60
|
};
|
|
52
61
|
/** The extraction pre-gate: does this increment hold anything worth proposing? */
|
|
53
62
|
export const extractionGate = {
|
|
54
63
|
id: "extraction-gate",
|
|
55
|
-
system: EXTRACTION_CHEAP_SYSTEM_PROMPT,
|
|
64
|
+
system: withDateBaseline(EXTRACTION_CHEAP_SYSTEM_PROMPT),
|
|
56
65
|
maxTokens: GATE_MAX_TOKENS,
|
|
57
66
|
parse: (text) => parseCheapAnswer(text),
|
|
58
67
|
};
|
|
59
68
|
/** The extraction proposal call: the run's one expensive call. */
|
|
60
69
|
export const extractionMain = {
|
|
61
70
|
id: "extraction-main",
|
|
62
|
-
system: EXTRACTION_SYSTEM_PROMPT,
|
|
71
|
+
system: withDateBaseline(EXTRACTION_SYSTEM_PROMPT),
|
|
63
72
|
maxTokens: EXTRACTION_MAX_TOKENS,
|
|
64
73
|
parse: (text) => parseExtractionAnswer(text),
|
|
65
74
|
};
|
package/lib/subagent.d.ts
CHANGED
|
@@ -15,12 +15,22 @@
|
|
|
15
15
|
* across call sites, and a job that has no definition cannot be called.
|
|
16
16
|
*/
|
|
17
17
|
import { SubagentCallError, type LlmRuntime, type ModelSelection } from "./llm-call.js";
|
|
18
|
+
/** The per-call facts a definition's instruction may be built from. */
|
|
19
|
+
export interface SubagentContext {
|
|
20
|
+
/** The run's baseline day `YYYY-MM-DD` (UTC) — the clock time-relative judgments use. */
|
|
21
|
+
readonly today: string;
|
|
22
|
+
}
|
|
18
23
|
/** One subagent definition: everything that distinguishes one job from another. */
|
|
19
24
|
export interface SubagentDefinition<Answer> {
|
|
20
25
|
/** The job's name, for diagnostics; unique across the registry (asserted by test). */
|
|
21
26
|
readonly id: string;
|
|
22
|
-
/**
|
|
23
|
-
|
|
27
|
+
/**
|
|
28
|
+
* The job's entire instruction, sent as the system prompt — nothing is
|
|
29
|
+
* appended at the call site. The builder form is for jobs whose
|
|
30
|
+
* instruction depends on a per-call fact (the clock a rubric judges dates
|
|
31
|
+
* against); the loader resolves it once per call.
|
|
32
|
+
*/
|
|
33
|
+
readonly system: string | ((context: SubagentContext) => string);
|
|
24
34
|
/** Output budget fallback; the exact model's advertised cap wins when it has one. */
|
|
25
35
|
readonly maxTokens: number;
|
|
26
36
|
/**
|
|
@@ -30,11 +40,13 @@ export interface SubagentDefinition<Answer> {
|
|
|
30
40
|
*/
|
|
31
41
|
readonly parse: (text: string) => Answer;
|
|
32
42
|
}
|
|
33
|
-
/** What one call is bound to: the LLM service, the exact route, the session identity. */
|
|
43
|
+
/** What one call is bound to: the LLM service, the exact route, the session identity, the day. */
|
|
34
44
|
export interface SubagentCall {
|
|
35
45
|
llm: LlmRuntime | undefined;
|
|
36
46
|
route: ModelSelection;
|
|
37
47
|
sessionId: string | undefined;
|
|
48
|
+
/** The run's baseline day `YYYY-MM-DD` (UTC): the clock every instruction is anchored to. */
|
|
49
|
+
today: string;
|
|
38
50
|
}
|
|
39
51
|
/** One finished call: the parsed answer, the raw text, and the observed call facts. */
|
|
40
52
|
export interface SubagentAnswer<Answer> {
|
|
@@ -61,7 +73,7 @@ export interface SubagentLoader {
|
|
|
61
73
|
* Build the loader for one call context — the simple factory: the context
|
|
62
74
|
* produces the bound runner, the definitions table produces the strategies.
|
|
63
75
|
*
|
|
64
|
-
* @param call - The LLM service, the exact route,
|
|
76
|
+
* @param call - The LLM service, the exact route, the session identity, and the baseline day to send.
|
|
65
77
|
* @returns A loader whose `run` is the only LLM entry point a feature needs.
|
|
66
78
|
*/
|
|
67
79
|
export declare function subagentLoaderFor(call: SubagentCall): SubagentLoader;
|
package/lib/subagent.js
CHANGED
|
@@ -19,13 +19,14 @@ import { SubagentCallError, streamOnce } from "./llm-call.js";
|
|
|
19
19
|
* Build the loader for one call context — the simple factory: the context
|
|
20
20
|
* produces the bound runner, the definitions table produces the strategies.
|
|
21
21
|
*
|
|
22
|
-
* @param call - The LLM service, the exact route,
|
|
22
|
+
* @param call - The LLM service, the exact route, the session identity, and the baseline day to send.
|
|
23
23
|
* @returns A loader whose `run` is the only LLM entry point a feature needs.
|
|
24
24
|
*/
|
|
25
25
|
export function subagentLoaderFor(call) {
|
|
26
26
|
return {
|
|
27
27
|
async run(definition, prompt) {
|
|
28
|
-
const
|
|
28
|
+
const system = typeof definition.system === "function" ? definition.system({ today: call.today }) : definition.system;
|
|
29
|
+
const result = await streamOnce(call.llm, call.route, call.sessionId, system, prompt, definition.maxTokens);
|
|
29
30
|
return { answer: definition.parse(result.text), text: result.text, usage: result.usage, finish: result.finish };
|
|
30
31
|
},
|
|
31
32
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-continual-evolve",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.15",
|
|
4
4
|
"description": "ZCode-style workspace memory for DeepSeek Harness: one central SQLite store partitioned by workspace, index injection, and sole-writer gated writes.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|