@stardustlc/dsh-dream 0.8.0 → 0.9.0
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/CHANGELOG.md +14 -0
- package/README.en.md +8 -2
- package/README.md +8 -2
- package/docs/USAGE.en.md +8 -2
- package/docs/USAGE.md +9 -2
- package/docs/VALIDATION.md +1 -1
- package/docs/validation/0.8.1.md +18 -0
- package/docs/validation/0.9.0.md +11 -0
- package/lib/automatic-queue.d.ts +29 -0
- package/lib/automatic-queue.js +184 -0
- package/lib/automatic-state.d.ts +35 -0
- package/lib/automatic-state.js +14 -1
- package/lib/automatic.d.ts +11 -1
- package/lib/automatic.js +168 -82
- package/lib/bridge.js +3 -0
- package/lib/client.js +73 -15
- package/lib/config.d.ts +1 -0
- package/lib/config.js +1 -0
- package/lib/knowledge-store.d.ts +12 -1
- package/lib/knowledge-store.js +73 -1
- package/lib/memory.d.ts +36 -0
- package/lib/memory.js +35 -0
- package/lib/retrieval.d.ts +1 -0
- package/lib/retrieval.js +6 -0
- package/lib/web-actions.js +32 -0
- package/lib/web.d.ts +5 -0
- package/lib/web.js +11 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,20 @@
|
|
|
4
4
|
|
|
5
5
|
[历史英文记录](docs/CHANGELOG.en.md)
|
|
6
6
|
|
|
7
|
+
## 0.9.0 (2026-10-05)
|
|
8
|
+
|
|
9
|
+
- 安静 60 秒后整理,同项目/模型最多合并 4 轮;冷却与每日预算延后轮次,32 轮/24 小时的持久化队列支持重启恢复、取消保留和跨进程租约。
|
|
10
|
+
- 每个聊天独立设置「使用记忆」与「贡献记忆」,可跟随全局或单独开启/关闭;关闭贡献清理该聊天待处理片段,保留已有经验。
|
|
11
|
+
- 按项目和主题分组、勾选同范围无冲突候选,一次原子审阅最多 20 条。自动来源保留核对过的脱敏原话,方便审阅条件和例外。
|
|
12
|
+
- 事实默认 30 天待复核,支持明确到期时间;取回、使用反馈不续期。过期经验在界面显示待复核,自动上下文与规则预览暂时扣下。
|
|
13
|
+
- 不可变记忆快照与查询缓存按完整内容失效;同长度/同时间戳的文件中段变更、拒绝、到期及原子替换均不会继续使用旧结果。短摘要与来源目录可由事件重建。
|
|
14
|
+
- 简化中英面板,详细设置默认折叠,审阅页直接展示经验。Windows、官方宿主、浏览器与真实模型的验证范围见 [0.9.0 验收](docs/validation/0.9.0.md)。
|
|
15
|
+
|
|
16
|
+
## 0.8.1 (2026-10-04)
|
|
17
|
+
|
|
18
|
+
- 修复真实 DeepSeek 辅助整理因深度推理耗尽 1200 token、JSON 被截断而无法保存候选的问题。通过官方模型元数据,仅对明确支持 `off` 的模型关闭这条辅助调用的推理;保留当前主任务设置及其他提供商的默认行为。
|
|
19
|
+
- 模型元数据查询也受辅助整理的超时与取消约束;旧宿主或元数据不可用时保留原有调用方式。真实模型复测与限制见 [0.8.1 验收](docs/validation/0.8.1.md)。
|
|
20
|
+
|
|
7
21
|
## 0.8.0 (2026-10-04)
|
|
8
22
|
|
|
9
23
|
- 对照官方 0.2.1-alpha.1 源码接入会话事件、Agent Inbox 与动态上下文:符合条件的任务收尾自动整理候选,后续相关任务自动回用已采纳、有独立来源的经验,无需记住触发词。候选始终待审阅,日记和项目规则仍是独立操作。
|
package/README.en.md
CHANGED
|
@@ -2,15 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
[中文](README.md)
|
|
4
4
|
|
|
5
|
+

|
|
6
|
+
|
|
5
7
|
Collect candidate lessons from completed tasks, review them and reuse relevant memories. Manual reflections remain available.
|
|
6
8
|
|
|
7
|
-
[](https://www.npmjs.com/package/@stardustlc/dsh-dream) [](https://www.npmjs.com/package/@stardustlc/dsh-dream) [](https://www.npmjs.com/package/@stardustlc/dsh-dream)
|
|
8
10
|
|
|
9
11
|
## What it does
|
|
10
12
|
|
|
11
13
|
- Browse journals, retrieve lessons and inspect their sources.
|
|
12
14
|
- Collect candidates after tasks and retrieve accepted memories for relevant tasks, with separate switches.
|
|
15
|
+
- Consolidate after quiet time, retain turns during cooldown or quota limits, and control use and contribution per chat.
|
|
13
16
|
- Review lessons, resolve conflicts and record separate usage feedback.
|
|
17
|
+
- Review candidates by project/topic; pause expired facts until they are reviewed again.
|
|
14
18
|
- Preview AGENTS.md changes before applying them, with rollback support.
|
|
15
19
|
|
|
16
20
|
## Install
|
|
@@ -27,7 +31,9 @@ For the web version, replace `desktop` with `web`. Restart DSH after installatio
|
|
|
27
31
|
|
|
28
32
|
Use DSH normally. A completed turn qualifies when it contains an explicit preference/correction or at least two non-Dream tool calls. Within the configured budget, Dream creates candidates. Open Settings → Dream Journal to inspect their sources and accept them; relevant later tasks retrieve accepted memories automatically. You can still ask for a separate reflection and journal entry.
|
|
29
33
|
|
|
30
|
-
Automatic collection uses the current session's model and quota
|
|
34
|
+
Automatic collection uses the current session's model and quota. By default, it waits for 60 quiet seconds, merges up to four turns in the same project, and allows four calls per day at least ten minutes apart, with 1200 output tokens per call. Cooldown and quota limits defer turns; new input stops auxiliary work and retains unfinished excerpts. The bounded queue keeps up to 32 turns for 24 hours and survives restart. Ordinary chat is skipped.
|
|
35
|
+
|
|
36
|
+
New memories await review: inspect the source quote, conditions and exceptions before accepting them. Facts require review after 30 days by default; stable preferences have no default expiry. Retrieval is reference exposure, not renewed verification. Acceptance and project-rule changes remain separate actions. UI language follows DSH; journal and lesson content stays original.
|
|
31
37
|
|
|
32
38
|
## Requirements and configuration
|
|
33
39
|
|
package/README.md
CHANGED
|
@@ -2,15 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.en.md)
|
|
4
4
|
|
|
5
|
+

|
|
6
|
+
|
|
5
7
|
自动整理任务中的候选经验,审阅后在相关任务中回用;也可手动复盘并保存梦境日记。
|
|
6
8
|
|
|
7
|
-
[](https://www.npmjs.com/package/@stardustlc/dsh-dream) [](https://www.npmjs.com/package/@stardustlc/dsh-dream) [](https://www.npmjs.com/package/@stardustlc/dsh-dream)
|
|
8
10
|
|
|
9
11
|
## 功能
|
|
10
12
|
|
|
11
13
|
- 浏览梦境日记、检索经验并查看关联来源。
|
|
12
14
|
- 完成任务后自动提取候选,相关新任务自动取回已采纳经验;两个开关独立控制。
|
|
15
|
+
- 安静后合并整理,冷却和额度不足时保留待处理轮次;可分别控制每个聊天的使用与贡献。
|
|
13
16
|
- 直接审阅、处理冲突,记录独立的使用反馈。
|
|
17
|
+
- 按项目和主题批量审阅,事实到期暂停回用,复核后恢复。
|
|
14
18
|
- 预览规则差异后写入 AGENTS.md,并支持回滚。
|
|
15
19
|
|
|
16
20
|
## 安装
|
|
@@ -27,7 +31,9 @@ dsh plugin --profile desktop add @stardustlc/dsh-dream
|
|
|
27
31
|
|
|
28
32
|
正常使用 DSH 即可:完成的一轮任务出现明确偏好、纠正,或至少两次非 Dream 工具调用时,会在预算内整理候选。在「设置 → 梦境日记」查看待审阅提示,核对来源后采纳;后续相关任务自动回用。也可说:“回顾最近的任务,记录值得保留的经验。”单独生成日记。
|
|
29
33
|
|
|
30
|
-
|
|
34
|
+
自动整理使用当前会话的模型和额度,默认安静 60 秒后开始,同项目最多合并 4 轮,每天最多 4 次、间隔 10 分钟,单次最多 1200 输出 token。冷却和额度不足时轮次进入等待队列;新输入会暂停整理并保留未完成片段。队列最多保留 32 轮、24 小时,重启后可继续。普通闲聊跳过。
|
|
35
|
+
|
|
36
|
+
新经验始终先待审阅:展开核对原话、条件与例外,再采纳。事实默认 30 天后待复核;稳定偏好没有默认到期时间。取回只表示提供参考,不会冒充重新核验。它不自动采纳,也不写项目规则;规则写入仍需预览和应用。中英文界面跟随 DSH 语言,日记与经验正文保留原文。
|
|
31
37
|
|
|
32
38
|
## 依赖与配置
|
|
33
39
|
|
package/docs/USAGE.en.md
CHANGED
|
@@ -17,13 +17,19 @@ Restart the web service afterwards. The dream journal lives at `~/.dsh/.dsh-drea
|
|
|
17
17
|
|
|
18
18
|
Use DSH normally: a completed human root turn qualifies after an explicit preference/correction or at least two non-Dream tool calls with a visible assistant summary. Dream observes new events only; it does not scan old history, subagents or unsuccessful turns.
|
|
19
19
|
|
|
20
|
-
Settings → Dream Journal
|
|
20
|
+
Settings → Dream Journal → Journal shows collection/retrieval defaults, today's count, the latest reason and a review link. Expand “Chat controls and consolidation limits” to independently override use and contribution for an observed chat. Disabling contribution removes that chat's pending excerpts, preserving existing lessons. Overrides take priority over global defaults and persist in `journalDir/automatic/state.json`.
|
|
21
|
+
|
|
22
|
+
From 0.9.0, collection waits for 60 quiet seconds and combines up to four turns with the same project/provider/model. Cooldown and quota limits defer inputs in a persistent queue (32 turns, 24 hours). Overflow removes the oldest unleased turn; expiry and overflow are visible. New input pauses auxiliary work and retains unfinished inputs. Manual consolidation skips quiet time but respects active tasks, cooldown and daily quota. Restart waits for fresh quiet time before resuming.
|
|
23
|
+
|
|
24
|
+
Filter candidate lessons by project/topic, expand original masked quotes, conditions and exceptions, then select up to 20 conflict-free candidates for atomic acceptance/rejection. All selected lessons must share scope and topic; any changed revision prevents the whole write. Resolve conflicts and review stale lessons individually.
|
|
25
|
+
|
|
26
|
+
Facts pause after 30 days since review/validation by default; explicit `reviewAfter` dates are supported. Stable preferences have no default expiry. Due lessons are held from automatic context and rule previews until reviewed again. Retrieval and feedback record exposure/use, never renewed verification or a later deadline.
|
|
21
27
|
|
|
22
28
|
Defaults: four auxiliary calls per day, ten minutes apart, 12000 total system-plus-JSON characters, 1200 output tokens, a 30-second timeout and at most three candidates per call. The request uses the current session's provider/model and quota and can incur charges. Only limited masked visible messages are sent; reasoning, system prompts and tool arguments/results are excluded. New input, disabling collection or unloading cancels active collection. Attempts consume budget even on failure, with no Dream-level retry; host adapter retry policy is unchanged.
|
|
23
29
|
|
|
24
30
|
Automatic retrieval makes no auxiliary model call. It supplies at most three accepted, independently sourced, task/project/platform-matching lessons in 3000 characters of dynamic context, preserving conditions and exceptions. Candidates, stale, disputed and rejected lessons are excluded. Version-bound lessons remain held because dependency versions are unknown here; use explicit `dream_context` with an actually verified package/version. Automatic features do not save journals, accept candidates or edit AGENTS.md. Verified on official 0.2.0-rc.2 and 0.2.1-alpha.1; hosts missing these interfaces keep manual tools and skills.
|
|
25
31
|
|
|
26
|
-
Configuration keys are `autoCollect`, `autoRetrieve`, `autoMaxCallsPerDay` (1–20), `autoCooldownMs` (1000–3600000), `autoMinToolCalls` (1–50), `autoMaxInputChars` (1000–20000), `autoMaxOutputTokens` (200–2000) and `autoTimeoutMs` (1000–60000). Add them under the `dream` row's `config` in your profile's `cordis.patch.yml`.
|
|
32
|
+
Configuration keys are `autoCollect`, `autoRetrieve`, `autoMaxCallsPerDay` (1–20), `autoCooldownMs` (1000–3600000), `autoIdleMs` (0–3600000, default 60000), `autoMinToolCalls` (1–50), `autoMaxInputChars` (1000–20000), `autoMaxOutputTokens` (200–2000) and `autoTimeoutMs` (1000–60000). Add them under the `dream` row's `config` in your profile's `cordis.patch.yml`. Automatic state stores bounded masked pending excerpts for up to 24 hours, along with settings, budgets and reference counts. Verified source quotes remain plaintext evidence with the lesson; hidden reasoning and tool bodies are excluded.
|
|
27
33
|
|
|
28
34
|
## Journal and lesson panel
|
|
29
35
|
|
package/docs/USAGE.md
CHANGED
|
@@ -24,7 +24,13 @@ dsh plugin --profile web remove @stardustlc/dsh-dream
|
|
|
24
24
|
|
|
25
25
|
安装后正常完成任务即可,不需要记住「做梦」触发词。仅观察新产生的真人主会话事件:完成轮次出现明确偏好/纠正,或至少两次非 Dream 工具调用且有可见助手总结时,才会在预算内调用当前会话模型整理。它不扫描旧会话、子代理或失败轮次。
|
|
26
26
|
|
|
27
|
-
「设置 →
|
|
27
|
+
「设置 → 梦境日记 → 日记」提供自动整理、自动取回的默认开关、今日次数、最近原因和待审阅入口。展开「聊天设置与整理预算」,可针对已观察到的聊天分别控制「使用记忆」与「贡献记忆」;独立设置优先于全局默认。关闭贡献清理该聊天尚未整理的片段,已有经验仍保留。控制值保存在 `journalDir/automatic/state.json`,重启后继续保留。
|
|
28
|
+
|
|
29
|
+
0.9.0 起默认等待 60 秒安静时间,同项目、提供商和模型最多合并 4 轮进行一次整理。冷却和当日预算用完时保留轮次等待恢复,队列最多 32 轮、24 小时;容量不足先移除最早未被处理的轮次,过期片段清理并显示计数。新输入暂停辅助调用并保留未完成轮次。可以点「现在整理待处理轮次」跳过安静时间,但不能绕过主任务优先、冷却或每日预算。重启后会等待新的安静时间再继续,不扫描旧会话历史。
|
|
30
|
+
|
|
31
|
+
「经验」页按项目与主题筛选待核实记录。展开来源核对脱敏原话、适用条件及例外,再勾选无冲突候选进行批量采纳/驳回,最多 20 条;必须同项目、范围和主题,任一版本已变化会整体拒绝写入。冲突和待复核经验请逐条处理。
|
|
32
|
+
|
|
33
|
+
事实默认在最近审阅/核验 30 天后暂停回用;明确的 `reviewAfter` 也可指定期限。稳定偏好没有默认期限。到期显示「已到复核时间」,不会注入或用于规则预览;核对当前事实后重新采纳才续期。「最近取回」只记录提供参考的时间,使用反馈和反复检索都不延长有效期。
|
|
28
34
|
|
|
29
35
|
默认每天最多 4 次辅助调用、间隔 10 分钟、输入总长最多 12000 字符、输出最多 1200 token、30 秒超时,每次最多 3 条候选。输入仅包含脱敏后的有限可见消息;不包含隐藏推理、系统提示或工具参数/结果。该请求使用 DSH 当前模型和额度,可能产生费用;新输入、关闭整理或卸载会取消进行中的请求。尝试前扣预算,失败不返还,也不自动重试;宿主 adapter 的重试策略保持原样。
|
|
30
36
|
|
|
@@ -118,6 +124,7 @@ dream_review { action: "accept", lessonId: "lesson-…", expectedRevision: 1 }
|
|
|
118
124
|
autoRetrieve: true # 自动取回已采纳经验;页面开关可覆盖
|
|
119
125
|
autoMaxCallsPerDay: 4 # 辅助调用次数(1-20),跨宿主共享此日记目录预算
|
|
120
126
|
autoCooldownMs: 600000 # 两次尝试的间隔(1000-3600000)
|
|
127
|
+
autoIdleMs: 60000 # 安静时间(0-3600000);0 仅建议隔离验证使用
|
|
121
128
|
autoMinToolCalls: 2 # 非 Dream 工具次数门槛(1-50);明确偏好/纠正可无需工具
|
|
122
129
|
autoMaxInputChars: 12000 # 辅助请求 system + JSON 字符预算(1000-20000)
|
|
123
130
|
autoMaxOutputTokens: 1200 # 输出预算(200-2000)
|
|
@@ -128,7 +135,7 @@ dream_review { action: "accept", lessonId: "lesson-…", expectedRevision: 1 }
|
|
|
128
135
|
|
|
129
136
|
梦原料入梦前自动脱敏:sk 密钥、GitHub/Groq/Slack 令牌、AWS 密钥、JWT、`password/token/api_key` 赋值、超长高熵串都会被打成 `[已脱敏·类型]`。M1 的经验 / 证据 / 错误 / 桥接写盘路径在 `maskSecrets` 开启时同样先过 `mask.ts`,面板只读路由返回的文本也会再做一次脱敏。不需要时可在配置里设 `maskSecrets: false`。
|
|
130
137
|
|
|
131
|
-
自动整理和自动取回始终脱敏,不受 `maskSecrets: false`
|
|
138
|
+
自动整理和自动取回始终脱敏,不受 `maskSecrets: false` 影响。脱敏不能保证识别所有个人信息;敏感项目可关闭自动整理或只关闭该聊天的贡献。自动状态保留预算、哈希化会话游标、开关、取回次数以及最多 24 小时的有限脱敏待处理片段;不会保存隐藏推理或工具正文。已核对的来源原话随经验作为明文证据保存。关闭贡献不删除已有经验。
|
|
132
139
|
|
|
133
140
|
## 权限与数据
|
|
134
141
|
|
package/docs/VALIDATION.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# dsh-dream 验证记录
|
|
2
2
|
|
|
3
|
-
[0.
|
|
3
|
+
[0.9.0 队列与记忆审阅验收](validation/0.9.0.md) · [0.8.1 真实模型验收](validation/0.8.1.md) · [0.8.0 自动整理与回用验收](validation/0.8.0.md)。
|
|
4
4
|
|
|
5
5
|
本页整理原 README 的历史验证说明,保留当时的版本、日期与范围。自动测试、启动检查、浏览器操作和真实服务验收分别记录,不能相互替代。更详细的版本验收文件仍保留在仓库中。
|
|
6
6
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Dream 0.8.1 acceptance / 验收
|
|
2
|
+
|
|
3
|
+
## Observed failure
|
|
4
|
+
|
|
5
|
+
With the official 0.2.1-alpha.1 DeepSeek adapter and `deepseek-flash`, a short invented CSV preference with an encoding exception consumed all 1200 output tokens. The adapter returned `max-tokens` after reasoning and incomplete JSON. Dream correctly discarded it, but the useful candidate was lost.
|
|
6
|
+
|
|
7
|
+
## Change
|
|
8
|
+
|
|
9
|
+
The auxiliary extraction checks the official `llm.resolveModelInfo(provider, model, signal)` capability. If this exact model advertises the `off` reasoning effort, only the auxiliary call requests it. Unknown models, unavailable metadata and legacy hosts retain their provider default. Metadata resolution and streaming share the existing timeout and abort signal. No retries, larger budgets or main-agent settings were added.
|
|
10
|
+
|
|
11
|
+
## Validation
|
|
12
|
+
|
|
13
|
+
- Windows: 304 tests, 303 passed, one optional stress test skipped. Regression cases cover complete extraction within the short budget, unsupported/missing metadata, and metadata calls that ignore abort.
|
|
14
|
+
- Real official SDK/provider with the local existing API credential: the invented CSV preference was saved as one unreviewed, project-scoped candidate with its exact source and UTF-8 exception; a second case without reusable information saved nothing. Both finished with `stop`; project AGENTS.md and the credential file remained unchanged.
|
|
15
|
+
- The quality check uses invented session events, not a claim that those events were emitted by a human's live session. The previous 0.8.0 acceptance separately exercised complete official RC2 and Alpha AgentLoops using a scripted adapter.
|
|
16
|
+
- The user's native Desktop process and one main window were observed. The window was minimized, so it was not restored while the user watched a video. Native visual/interaction acceptance remains untested.
|
|
17
|
+
|
|
18
|
+
Local receipts: `E:/deepseek/.harness-validation/dream-auto-20261003/real-model-before-081.json`, `real-model-verification.json`, `automatic-081-tests.log`.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# 0.9.0 验收(2026-10-05)
|
|
2
|
+
|
|
3
|
+
- Windows / Node 24:320 项测试,319 通过,1 项可选并发压力测试默认跳过。覆盖队列合并、额度延后、取消与重启、跨进程预算/死租约恢复、过期清理、聊天独立开关、原子审阅、幂等重试及来源映射。
|
|
4
|
+
- 快照:完整内容哈希;固定文件大小与时间戳的中段修改、事件损坏、原子替换、拒绝与反馈均重新计算。不可变缓存不能被调用方修改。时钟到期无需文件变化即可暂停事实取回。
|
|
5
|
+
- 官方 DSH 0.2.1-alpha.1 隔离 Web 宿主:正常任务在队列中等待、安静后产生候选;浏览器展开条件/例外/来源、勾选两条候选、原子采纳成功。后续实际官方 AgentLoop 收到已采纳记忆;每个聊天的使用/贡献独立保存。
|
|
6
|
+
- 浏览器:中文/英文切换,新控制文案跟随语言且原文不翻译;390×844 窄屏没有页面横向溢出,实际点击待复核成功;预算与聊天详细设置默认折叠,经验页不被自动设置遮挡。
|
|
7
|
+
- 真实模型:官方 DeepSeek SDK / deepseek-flash,限定输入输出、支持时关闭辅助推理;有条件的 CSV 偏好保存候选,无长期结论的输入返回空列表。另一次真实官方 AgentLoop 正常生成两条待审阅候选。测试输入均为人工构造演示,不使用个人会话;本地密钥未复制到磁盘,项目规则保持原样。
|
|
8
|
+
|
|
9
|
+
测试产物保存在本地 `.harness-validation/dream-memory-20261005/`。安装与发布单独检查包内容、完整性和官方 profile 加载;GitHub CI 负责 Linux 构建与测试。
|
|
10
|
+
|
|
11
|
+
Web 操作验收覆盖共享客户端和官方宿主接口,不等同于原生桌面窗口操作。本轮没有切换或重启用户正在使用的桌面窗口。网络/model 故障仍可能使整理失败;失败消耗本次预算,Dream 不无限重试。队列到期或超出容量会清理待处理片段并显示计数。
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ResolvedDreamConfig } from './config.js';
|
|
2
|
+
import { type PendingMemory } from './automatic-state.js';
|
|
3
|
+
/** One event-driven timer, persistent bounded inputs, cross-process call leases. */
|
|
4
|
+
export declare class MemoryQueue {
|
|
5
|
+
private timer?;
|
|
6
|
+
private work;
|
|
7
|
+
private running;
|
|
8
|
+
private closed;
|
|
9
|
+
private quietUntil;
|
|
10
|
+
private controller?;
|
|
11
|
+
private cfg;
|
|
12
|
+
private now;
|
|
13
|
+
private canRun;
|
|
14
|
+
private generate;
|
|
15
|
+
private onProblem;
|
|
16
|
+
constructor(cfg: ResolvedDreamConfig, now: () => number, canRun: () => boolean, generate: (rows: PendingMemory[], signal: AbortSignal) => Promise<{
|
|
17
|
+
reason: string;
|
|
18
|
+
saved: number;
|
|
19
|
+
}>, onProblem?: (reason: string) => void);
|
|
20
|
+
touch(): void;
|
|
21
|
+
cancel(): void;
|
|
22
|
+
stop(): void;
|
|
23
|
+
whenIdle(): Promise<void>;
|
|
24
|
+
enqueue(row: PendingMemory): void;
|
|
25
|
+
flush(): void;
|
|
26
|
+
kick(): void;
|
|
27
|
+
private enabled;
|
|
28
|
+
private drain;
|
|
29
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { budgetDay, readAutomaticState, updateAutomaticState } from './automatic-state.js';
|
|
3
|
+
const TTL = 86400000;
|
|
4
|
+
function alive(pid) { try {
|
|
5
|
+
process.kill(pid, 0);
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
catch (e) {
|
|
9
|
+
return e.code !== 'ESRCH';
|
|
10
|
+
} }
|
|
11
|
+
/** One event-driven timer, persistent bounded inputs, cross-process call leases. */
|
|
12
|
+
export class MemoryQueue {
|
|
13
|
+
timer;
|
|
14
|
+
work = Promise.resolve();
|
|
15
|
+
running = false;
|
|
16
|
+
closed = false;
|
|
17
|
+
quietUntil;
|
|
18
|
+
controller;
|
|
19
|
+
cfg;
|
|
20
|
+
now;
|
|
21
|
+
canRun;
|
|
22
|
+
generate;
|
|
23
|
+
onProblem;
|
|
24
|
+
constructor(cfg, now, canRun, generate, onProblem = () => { }) {
|
|
25
|
+
this.cfg = cfg;
|
|
26
|
+
this.now = now;
|
|
27
|
+
this.canRun = canRun;
|
|
28
|
+
this.generate = generate;
|
|
29
|
+
this.onProblem = onProblem;
|
|
30
|
+
this.quietUntil = now() + cfg.autoIdleMs;
|
|
31
|
+
}
|
|
32
|
+
touch() { this.quietUntil = this.now() + this.cfg.autoIdleMs; this.controller?.abort(); this.kick(); }
|
|
33
|
+
cancel() { this.controller?.abort(); this.kick(); }
|
|
34
|
+
stop() { this.closed = true; clearTimeout(this.timer); this.controller?.abort(); }
|
|
35
|
+
async whenIdle() { await this.work; }
|
|
36
|
+
enqueue(row) {
|
|
37
|
+
updateAutomaticState(this.cfg, state => {
|
|
38
|
+
const cursor = createHash('sha256').update(row.sessionId).digest('hex').slice(0, 24);
|
|
39
|
+
if ((state.cursors[cursor] ?? -1) >= row.endSeq)
|
|
40
|
+
return;
|
|
41
|
+
if (Object.keys(state.cursors).length >= 128 && state.cursors[cursor] === undefined)
|
|
42
|
+
delete state.cursors[Object.keys(state.cursors)[0]];
|
|
43
|
+
state.cursors[cursor] = row.endSeq;
|
|
44
|
+
state.pending ??= [];
|
|
45
|
+
if (state.pending.some(p => p.key === row.key))
|
|
46
|
+
return;
|
|
47
|
+
if (state.pending.length >= 32) {
|
|
48
|
+
const replace = state.pending.findIndex(p => !p.lease);
|
|
49
|
+
state.overflow = (state.overflow ?? 0) + 1;
|
|
50
|
+
if (replace < 0)
|
|
51
|
+
return;
|
|
52
|
+
state.pending.splice(replace, 1);
|
|
53
|
+
}
|
|
54
|
+
state.pending.push(row);
|
|
55
|
+
const reason = state.day === budgetDay(this.now()) && state.calls >= this.cfg.autoMaxCallsPerDay ? 'daily-budget'
|
|
56
|
+
: state.lastAttemptAt !== undefined && this.now() < state.lastAttemptAt + this.cfg.autoCooldownMs ? 'cooldown' : 'queued';
|
|
57
|
+
if (state.last?.reason !== 'recording')
|
|
58
|
+
state.last = { at: this.now(), reason, saved: 0 };
|
|
59
|
+
}, this.now());
|
|
60
|
+
this.kick();
|
|
61
|
+
}
|
|
62
|
+
flush() {
|
|
63
|
+
this.quietUntil = 0;
|
|
64
|
+
updateAutomaticState(this.cfg, s => { for (const p of s.pending ?? [])
|
|
65
|
+
p.readyAt = this.now(); }, this.now());
|
|
66
|
+
this.kick();
|
|
67
|
+
}
|
|
68
|
+
kick() {
|
|
69
|
+
clearTimeout(this.timer);
|
|
70
|
+
if (this.closed || this.running)
|
|
71
|
+
return;
|
|
72
|
+
try {
|
|
73
|
+
let state = readAutomaticState(this.cfg, this.now());
|
|
74
|
+
const now = this.now();
|
|
75
|
+
if (state.pending?.some(p => now - p.createdAt >= TTL)) {
|
|
76
|
+
updateAutomaticState(this.cfg, s => {
|
|
77
|
+
const expired = (s.pending ?? []).filter(p => now - p.createdAt >= TTL);
|
|
78
|
+
s.expired = (s.expired ?? 0) + expired.length;
|
|
79
|
+
s.pending = (s.pending ?? []).filter(p => now - p.createdAt < TTL);
|
|
80
|
+
}, now);
|
|
81
|
+
state = readAutomaticState(this.cfg, now);
|
|
82
|
+
}
|
|
83
|
+
if (!state.pending?.length)
|
|
84
|
+
return;
|
|
85
|
+
const expiresAt = Math.min(...state.pending.map(p => p.createdAt + TTL));
|
|
86
|
+
const eligible = state.pending.filter(p => this.enabled(p, state));
|
|
87
|
+
const paused = !eligible.length || !this.canRun();
|
|
88
|
+
let at = paused ? expiresAt : Math.min(...eligible.map(p => Math.max(p.readyAt, p.lease && alive(p.lease.pid) ? p.lease.until : 0)));
|
|
89
|
+
at = Math.max(at, this.quietUntil, (state.lastAttemptAt ?? 0) + this.cfg.autoCooldownMs);
|
|
90
|
+
if (state.day === budgetDay(now) && state.calls >= this.cfg.autoMaxCallsPerDay) {
|
|
91
|
+
const next = new Date(now);
|
|
92
|
+
next.setHours(24, 0, 0, 0);
|
|
93
|
+
at = Math.max(at, next.getTime());
|
|
94
|
+
}
|
|
95
|
+
if (!paused && at <= now) {
|
|
96
|
+
this.running = true;
|
|
97
|
+
this.work = this.work.then(() => this.drain()).catch(e => this.onProblem(e.message === 'automatic-storage-busy' ? e.message : 'automatic-storage-error')).finally(() => { this.running = false; this.kick(); });
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
this.timer = setTimeout(() => { this.kick(); }, Math.max(1, Math.min(at - now, expiresAt - now, 2147483647)));
|
|
101
|
+
this.timer.unref?.();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
this.onProblem(error.message === 'automatic-storage-busy' ? error.message : 'automatic-state-invalid');
|
|
106
|
+
if (error.message === 'automatic-storage-busy') {
|
|
107
|
+
this.timer = setTimeout(() => this.kick(), 1000);
|
|
108
|
+
this.timer.unref?.();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
enabled(row, state) {
|
|
113
|
+
const key = createHash('sha256').update(row.sessionId).digest('hex').slice(0, 24);
|
|
114
|
+
return state.policies?.[key]?.contribute ?? state.enabled ?? this.cfg.autoCollect;
|
|
115
|
+
}
|
|
116
|
+
async drain() {
|
|
117
|
+
if (this.closed || !this.canRun())
|
|
118
|
+
return;
|
|
119
|
+
const now = this.now(), job = randomUUID();
|
|
120
|
+
const rows = updateAutomaticState(this.cfg, state => {
|
|
121
|
+
const all = state.pending ?? [];
|
|
122
|
+
state.expired = (state.expired ?? 0) + all.filter(p => now - p.createdAt >= TTL).length;
|
|
123
|
+
state.pending = all.filter(p => now - p.createdAt < TTL);
|
|
124
|
+
if (state.day !== budgetDay(now)) {
|
|
125
|
+
state.day = budgetDay(now);
|
|
126
|
+
state.calls = 0;
|
|
127
|
+
}
|
|
128
|
+
if (state.calls >= this.cfg.autoMaxCallsPerDay || now < (state.lastAttemptAt ?? 0) + this.cfg.autoCooldownMs || now < this.quietUntil)
|
|
129
|
+
return [];
|
|
130
|
+
const due = state.pending.filter(p => this.enabled(p, state) && p.readyAt <= now && (!p.lease || p.lease.until <= now || !alive(p.lease.pid)));
|
|
131
|
+
const first = due[0];
|
|
132
|
+
if (!first)
|
|
133
|
+
return [];
|
|
134
|
+
const selected = [];
|
|
135
|
+
let chars = 0;
|
|
136
|
+
for (const p of due.filter(p => p.cwd === first.cwd && p.provider === first.provider && p.model === first.model)) {
|
|
137
|
+
const size = JSON.stringify(p.sources.map(({ seq, role, text }) => ({ seq, role, text }))).length;
|
|
138
|
+
if (selected.length && chars + size > this.cfg.autoMaxInputChars - 1000)
|
|
139
|
+
continue;
|
|
140
|
+
selected.push(p);
|
|
141
|
+
chars += size;
|
|
142
|
+
if (selected.length === 4)
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
for (const p of selected)
|
|
146
|
+
p.lease = { job, pid: process.pid, until: now + this.cfg.autoTimeoutMs + 5000 };
|
|
147
|
+
state.calls++;
|
|
148
|
+
state.lastAttemptAt = now;
|
|
149
|
+
state.last = { at: now, reason: 'recording', saved: 0, job };
|
|
150
|
+
return selected;
|
|
151
|
+
}, now);
|
|
152
|
+
if (!rows.length)
|
|
153
|
+
return;
|
|
154
|
+
const controller = new AbortController();
|
|
155
|
+
this.controller = controller;
|
|
156
|
+
const timer = setTimeout(() => controller.abort(new Error('automatic-timeout')), this.cfg.autoTimeoutMs);
|
|
157
|
+
timer.unref?.();
|
|
158
|
+
let result = { reason: 'model-error', saved: 0 };
|
|
159
|
+
try {
|
|
160
|
+
result = await this.generate(rows, controller.signal);
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
result.reason = controller.signal.aborted ? (controller.signal.reason?.message === 'automatic-timeout' ? 'timeout' : 'cancelled') : 'model-error';
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
clearTimeout(timer);
|
|
167
|
+
this.controller = undefined;
|
|
168
|
+
updateAutomaticState(this.cfg, state => {
|
|
169
|
+
state.pending = (state.pending ?? []).filter(p => {
|
|
170
|
+
if (p.lease?.job !== job)
|
|
171
|
+
return true;
|
|
172
|
+
if (result.reason === 'cancelled') {
|
|
173
|
+
delete p.lease;
|
|
174
|
+
p.readyAt = this.now() + this.cfg.autoIdleMs;
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
return false;
|
|
178
|
+
});
|
|
179
|
+
if (state.last?.job === job)
|
|
180
|
+
state.last = { at: this.now(), ...result, job };
|
|
181
|
+
}, this.now());
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
package/lib/automatic-state.d.ts
CHANGED
|
@@ -1,4 +1,31 @@
|
|
|
1
1
|
import type { ResolvedDreamConfig } from './config.js';
|
|
2
|
+
export interface PendingMemory {
|
|
3
|
+
key: string;
|
|
4
|
+
sessionId: string;
|
|
5
|
+
cwd: string;
|
|
6
|
+
endSeq: number;
|
|
7
|
+
createdAt: number;
|
|
8
|
+
readyAt: number;
|
|
9
|
+
provider: string;
|
|
10
|
+
model: string;
|
|
11
|
+
sources: Array<{
|
|
12
|
+
seq: number;
|
|
13
|
+
role: 'user' | 'assistant';
|
|
14
|
+
text: string;
|
|
15
|
+
hash: string;
|
|
16
|
+
}>;
|
|
17
|
+
lease?: {
|
|
18
|
+
job: string;
|
|
19
|
+
pid: number;
|
|
20
|
+
until: number;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export interface ChatMemoryPolicy {
|
|
24
|
+
sessionId: string;
|
|
25
|
+
cwd?: string;
|
|
26
|
+
use?: boolean;
|
|
27
|
+
contribute?: boolean;
|
|
28
|
+
}
|
|
2
29
|
export interface AutomaticState {
|
|
3
30
|
version: 1;
|
|
4
31
|
day: string;
|
|
@@ -7,6 +34,14 @@ export interface AutomaticState {
|
|
|
7
34
|
retrievalEnabled?: boolean;
|
|
8
35
|
lastAttemptAt?: number;
|
|
9
36
|
cursors: Record<string, number>;
|
|
37
|
+
pending?: PendingMemory[];
|
|
38
|
+
policies?: Record<string, ChatMemoryPolicy>;
|
|
39
|
+
expired?: number;
|
|
40
|
+
overflow?: number;
|
|
41
|
+
retrieved?: Record<string, {
|
|
42
|
+
at: number;
|
|
43
|
+
count: number;
|
|
44
|
+
}>;
|
|
10
45
|
last?: {
|
|
11
46
|
at: number;
|
|
12
47
|
reason: string;
|
package/lib/automatic-state.js
CHANGED
|
@@ -10,7 +10,7 @@ export function readAutomaticState(cfg, now = Date.now()) {
|
|
|
10
10
|
const file = join(cfg.journalDir, 'automatic', 'state.json');
|
|
11
11
|
if (!existsSync(file))
|
|
12
12
|
return { version: 1, day: budgetDay(now), calls: 0, cursors: {} };
|
|
13
|
-
if (statSync(file).size >
|
|
13
|
+
if (statSync(file).size > 2 * 1024 * 1024)
|
|
14
14
|
throw new Error('automatic-state-invalid');
|
|
15
15
|
const raw = JSON.parse(readFileSync(file, 'utf8'));
|
|
16
16
|
if (raw?.version !== 1 || typeof raw.day !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(raw.day)
|
|
@@ -24,6 +24,19 @@ export function readAutomaticState(cfg, now = Date.now()) {
|
|
|
24
24
|
|| (raw.last !== undefined && (!Number.isSafeInteger(raw.last.at) || typeof raw.last.reason !== 'string' || raw.last.reason.length > 80 || !Number.isSafeInteger(raw.last.saved) || raw.last.saved < 0 || (raw.last.job !== undefined && typeof raw.last.job !== 'string')))) {
|
|
25
25
|
throw new Error('automatic-state-invalid');
|
|
26
26
|
}
|
|
27
|
+
const text = (v, max) => typeof v === 'string' && v.length > 0 && v.length <= max;
|
|
28
|
+
const integer = (v) => Number.isSafeInteger(v) && Number(v) >= 0;
|
|
29
|
+
if ((raw.pending !== undefined && (!Array.isArray(raw.pending) || raw.pending.length > 32 || raw.pending.some((p) => !p || !text(p.key, 64) || !text(p.sessionId, 200) || !text(p.cwd, 2000) || !integer(p.endSeq)
|
|
30
|
+
|| !integer(p.createdAt) || !integer(p.readyAt) || !text(p.provider, 200) || !text(p.model, 200)
|
|
31
|
+
|| !Array.isArray(p.sources) || p.sources.length < 1 || p.sources.length > 6 || p.sources.some((s) => !s || !integer(s.seq) || !['user', 'assistant'].includes(s.role) || !text(s.text, 1800) || !/^[a-f0-9]{64}$/.test(s.hash))
|
|
32
|
+
|| (p.lease !== undefined && (!text(p.lease.job, 64) || !integer(p.lease.pid) || p.lease.pid < 1 || !integer(p.lease.until))))))
|
|
33
|
+
|| (raw.policies !== undefined && (!raw.policies || Array.isArray(raw.policies) || typeof raw.policies !== 'object'
|
|
34
|
+
|| Object.keys(raw.policies).length > 128 || Object.entries(raw.policies).some(([key, p]) => !/^[a-f0-9]{24}$/.test(key) || !p || !text(p.sessionId, 200) || (p.cwd !== undefined && !text(p.cwd, 2000))
|
|
35
|
+
|| ['use', 'contribute'].some(k => p[k] !== undefined && typeof p[k] !== 'boolean'))))
|
|
36
|
+
|| ['expired', 'overflow'].some(k => raw[k] !== undefined && !integer(raw[k]))
|
|
37
|
+
|| (raw.retrieved !== undefined && (!raw.retrieved || Array.isArray(raw.retrieved) || typeof raw.retrieved !== 'object'
|
|
38
|
+
|| Object.keys(raw.retrieved).length > 2000 || Object.entries(raw.retrieved).some(([key, v]) => !text(key, 200) || !v || !integer(v.at) || !integer(v.count)))))
|
|
39
|
+
throw new Error('automatic-state-invalid');
|
|
27
40
|
return raw;
|
|
28
41
|
}
|
|
29
42
|
/** No waiting on a live lock: observing a turn must never stall its owner. */
|
package/lib/automatic.d.ts
CHANGED
|
@@ -8,7 +8,11 @@ export declare class AutomaticDream {
|
|
|
8
8
|
private queries;
|
|
9
9
|
private queue;
|
|
10
10
|
private pending;
|
|
11
|
-
private
|
|
11
|
+
private scheduler?;
|
|
12
|
+
private active;
|
|
13
|
+
private sessions;
|
|
14
|
+
private contextCache;
|
|
15
|
+
private retrievalStamp;
|
|
12
16
|
private closed;
|
|
13
17
|
private ready;
|
|
14
18
|
private host;
|
|
@@ -27,8 +31,14 @@ export declare class AutomaticDream {
|
|
|
27
31
|
enabled?: boolean;
|
|
28
32
|
retrievalEnabled?: boolean;
|
|
29
33
|
}): void;
|
|
34
|
+
controlSession(id: string, changes: {
|
|
35
|
+
use?: boolean | null;
|
|
36
|
+
contribute?: boolean | null;
|
|
37
|
+
}): void;
|
|
38
|
+
flush(): void;
|
|
30
39
|
observe(session: any, event: any): void;
|
|
31
40
|
private process;
|
|
41
|
+
private generateBatch;
|
|
32
42
|
private generate;
|
|
33
43
|
private parse;
|
|
34
44
|
context(agent: any): string;
|