@modusensus/dsh-mneme 0.12.0 → 0.13.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/README.md +48 -19
- package/docs/CONFIGURATION.md +43 -4
- package/docs/DIRECTORIES.md +114 -0
- package/docs/DISTILL.md +64 -0
- package/docs/DSH-GO.md +97 -0
- package/docs/ENTITIES.md +5 -3
- package/docs/FORGET.md +77 -0
- package/docs/KB.md +110 -0
- package/docs/SLEEP.md +28 -3
- package/docs/STORAGE.md +23 -0
- package/lib/api-standalone.js +4 -1
- package/lib/api.js +581 -29
- package/lib/attachment-text.js +122 -0
- package/lib/choro-command.js +9 -2
- package/lib/client.js +3537 -1205
- package/lib/config.js +139 -1
- package/lib/continuity.js +7 -6
- package/lib/diary/auto.js +190 -0
- package/lib/diary/compose.js +440 -0
- package/lib/diary/dir.js +59 -0
- package/lib/diary/export.js +97 -0
- package/lib/diary/index.js +26 -0
- package/lib/diary/library.js +189 -0
- package/lib/diary/qq-inbox.js +142 -0
- package/lib/diary/rollover.js +61 -0
- package/lib/diary/session-provider.js +122 -0
- package/lib/dream/decisions.js +56 -5
- package/lib/dream/sleep.js +616 -15
- package/lib/dream.js +45 -13
- package/lib/dsh-go/host.js +162 -0
- package/lib/dsh-go/panel.js +2091 -0
- package/lib/dsh-go/text.js +342 -0
- package/lib/dsh-go/views/common.js +68 -0
- package/lib/dsh-go/views/entity.js +48 -0
- package/lib/dsh-go/views/memory.js +198 -0
- package/lib/dsh-go/views/root.js +30 -0
- package/lib/dsh-go/views/settings.js +398 -0
- package/lib/dsh-go/views/status.js +51 -0
- package/lib/embedding.js +42 -8
- package/lib/entities/extractor.js +36 -7
- package/lib/hot-memory.js +8 -9
- package/lib/index.js +92 -2
- package/lib/inject.js +189 -18
- package/lib/kb/index.js +309 -0
- package/lib/kb/routes.js +220 -0
- package/lib/kb/tools.js +148 -0
- package/lib/lang.js +87 -4
- package/lib/peak-hours.js +57 -2
- package/lib/semantic.js +347 -60
- package/lib/service.js +193 -66
- package/lib/settings.js +112 -14
- package/lib/store.js +347 -21
- package/lib/summarize.js +38 -11
- package/lib/tools.js +35 -4
- package/lib/user-settings.js +128 -32
- package/lib/vector-index.js +76 -6
- package/lib/wiki/cli-hint.js +90 -0
- package/lib/wiki/queries.js +116 -2
- package/lib/wiki/t1-jobs.js +232 -0
- package/lib/wiki/t1.js +7 -1
- package/package.json +25 -1
- package/src/api-standalone.js +4 -1
- package/src/api.js +581 -29
- package/src/attachment-text.js +122 -0
- package/src/choro-command.js +9 -2
- package/src/config.js +139 -1
- package/src/continuity.js +7 -6
- package/src/diary/auto.js +190 -0
- package/src/diary/compose.js +440 -0
- package/src/diary/dir.js +59 -0
- package/src/diary/export.js +97 -0
- package/src/diary/index.js +26 -0
- package/src/diary/library.js +189 -0
- package/src/diary/qq-inbox.js +142 -0
- package/src/diary/rollover.js +61 -0
- package/src/diary/session-provider.js +122 -0
- package/src/dream/decisions.js +56 -5
- package/src/dream/sleep.js +616 -15
- package/src/dream.js +45 -13
- package/src/dsh-go/host.js +162 -0
- package/src/dsh-go/panel.js +2091 -0
- package/src/dsh-go/text.js +342 -0
- package/src/dsh-go/views/common.js +68 -0
- package/src/dsh-go/views/entity.js +48 -0
- package/src/dsh-go/views/memory.js +198 -0
- package/src/dsh-go/views/root.js +30 -0
- package/src/dsh-go/views/settings.js +398 -0
- package/src/dsh-go/views/status.js +51 -0
- package/src/embedding.js +42 -8
- package/src/entities/extractor.js +36 -7
- package/src/hot-memory.js +8 -9
- package/src/index.js +92 -2
- package/src/inject.js +189 -18
- package/src/kb/index.js +309 -0
- package/src/kb/routes.js +220 -0
- package/src/kb/tools.js +148 -0
- package/src/lang.js +87 -4
- package/src/peak-hours.js +57 -2
- package/src/semantic.js +347 -60
- package/src/service.js +193 -66
- package/src/settings.js +112 -14
- package/src/store.js +347 -21
- package/src/summarize.js +38 -11
- package/src/tools.js +35 -4
- package/src/user-settings.js +128 -32
- package/src/vector-index.js +76 -6
- package/src/wiki/cli-hint.js +90 -0
- package/src/wiki/queries.js +116 -2
- package/src/wiki/t1-jobs.js +232 -0
- package/src/wiki/t1.js +7 -1
- package/test/api-routes-gaps.test.js +63 -0
- package/test/api.test.js +375 -4
- package/test/attachment-text.test.js +140 -0
- package/test/bm25-corpus-cache.test.js +145 -0
- package/test/capacity-warning.test.js +262 -0
- package/test/client.test.js +779 -154
- package/test/conflict-freeze.test.js +45 -1
- package/test/continuity.test.js +8 -1
- package/test/diary-library.test.js +187 -0
- package/test/diary.test.js +576 -0
- package/test/dream-maxtokens-fallback.test.js +3 -3
- package/test/dream.test.js +62 -1
- package/test/dsh-go-host.test.js +134 -0
- package/test/dsh-go-panel-actions.test.js +854 -0
- package/test/dsh-go-panel.test.js +251 -0
- package/test/embedding-ready.test.js +33 -3
- package/test/entities.test.js +73 -0
- package/test/forget-corrections.test.js +70 -0
- package/test/image-memory.test.js +171 -0
- package/test/issue-617-step1-query-and-fusion.test.js +361 -0
- package/test/kb-api-wiring.test.js +128 -0
- package/test/kb-collections.test.js +272 -0
- package/test/kb-routes.test.js +379 -0
- package/test/mint-source.test.js +26 -0
- package/test/peak-hours-observability.test.js +70 -0
- package/test/persona-api.test.js +3 -3
- package/test/persona-by-session.test.js +568 -0
- package/test/persona-settings.test.js +7 -7
- package/test/reflection-update-gate.test.js +129 -0
- package/test/reindex-backfill.test.js +334 -0
- package/test/reindex-scheduler.test.js +369 -0
- package/test/scope-candidates.test.js +249 -0
- package/test/sleep-action-set.test.js +14 -0
- package/test/sleep-heat.test.js +49 -0
- package/test/sleep.test.js +598 -0
- package/test/standalone-api.test.js +24 -0
- package/test/store.test.js +20 -0
- package/test/summarize.test.js +26 -0
- package/test/user-settings-segments.test.js +528 -0
- package/test/vector-index.test.js +120 -6
- package/test/wiki-api.test.js +355 -0
- package/test/wiki-cli-hint.test.js +118 -0
- package/test/wiki-queries.test.js +7 -0
- package/test/wiki-t1-jobs.test.js +125 -0
package/lib/index.js
CHANGED
|
@@ -17,6 +17,9 @@ import { createSummarizer } from "./summarize.js";
|
|
|
17
17
|
import { createSessionGate } from "./session-gate.js";
|
|
18
18
|
import { createDreamScheduler, describeStreamFailure } from "./dream.js";
|
|
19
19
|
import { createSleepScheduler, runSleep } from "./dream/sleep.js";
|
|
20
|
+
// #622 可观测性:错峰时段串解析失败是「静默未配置」(parsePeakSpec 契约不救错),
|
|
21
|
+
// 启动装配后对非空且解析失败的键出一条 warn——只补可见性,不改语义。
|
|
22
|
+
import { invalidPeakSpecs } from "./peak-hours.js";
|
|
20
23
|
import { createApi } from "./api.js";
|
|
21
24
|
import { createStandaloneApi } from "./api-standalone.js";
|
|
22
25
|
// #275 存储生命周期第一批:无损回收(手动入口,不挂启动路径)。
|
|
@@ -27,15 +30,23 @@ import { createChoroInject } from "./wiki/inject.js";
|
|
|
27
30
|
import { registerChoroCommand } from "./choro-command.js";
|
|
28
31
|
// 收尾档:面板重建按钮走 runChoroT0(P34;T1 不上 HTTP)。
|
|
29
32
|
import { runChoroT0 } from "./wiki/t0.js";
|
|
33
|
+
// T1 后台任务账(契约 §3.2):面板点「补语义正文」走 HTTP 起一个 spawn 子进程,
|
|
34
|
+
// 进程账不能住在 api.js(HTTP 面刻意不摸 child_process),装配点在这里。
|
|
35
|
+
import { createT1Jobs } from "./wiki/t1-jobs.js";
|
|
30
36
|
import { createSettings } from "./settings.js";
|
|
31
37
|
// 「Agent 眼中的你」自动事实(2026-11-06):逐卡事实清单 + dream 后自动维护轮。
|
|
32
38
|
import { createProfileFacts } from "./profile-facts.js";
|
|
39
|
+
// 知识库第二层(契约 §3.4):装配与开关无关(createKb 只建对象,读侧零 IO),
|
|
40
|
+
// 闸在路由与工具两层——这样「没开」与「没装配」在 HTTP 面上是两个可区分答案。
|
|
41
|
+
import { createKb } from "./kb/index.js";
|
|
33
42
|
import { createCommandManager } from "./commands.js";
|
|
34
43
|
import { createExecutionRecorder } from "./pipeline-runner.js";
|
|
35
44
|
import { registerMoRunCommand } from "./pipeline-command.js";
|
|
36
45
|
// M.O. 自动沉淀的触发侧(冷却 / 排队 / 兜底)。挖掘与落盘在 pipeline-nudge.js,
|
|
37
46
|
// 这里只管「什么时候跑」。两者分开的理由见 src/pipeline-nudge-auto.js 文件头。
|
|
38
47
|
import { createNudgeAutoRun } from "./pipeline-nudge-auto.js";
|
|
48
|
+
// QQ 日记本:合成管线(compose)与触发侧(auto)分开,理由见 src/diary/index.js 文件头。
|
|
49
|
+
import { createDiaryComposer, createDiaryAutoRun } from "./diary/index.js";
|
|
39
50
|
import { createVectorIndex } from "./vector-index.js";
|
|
40
51
|
// semantic(embedder/reranker/boot 回填)纯搬移至 src/semantic.js(PR2):宿主与
|
|
41
52
|
// daemon(dsh-mneme-serve,#363)共用同一套装配,原文件保留 barrel 出口。
|
|
@@ -243,6 +254,19 @@ export const apply = (ctx, config) => {
|
|
|
243
254
|
for (const [objKey, sub] of Object.entries(nestedFlags)) {
|
|
244
255
|
cfg[objKey] = { ...(cfg[objKey] ?? {}), ...sub };
|
|
245
256
|
}
|
|
257
|
+
// #622:错峰时段串(summarizePeakHours / dreamPeakHours)解析失败 = 静默未配置
|
|
258
|
+
// (parsePeakSpec 的契约:排程是省钱手段,绝不该因写错格式停掉蒸馏/巩固)。但
|
|
259
|
+
// 「不救错」不等于「不吭声」——配置写错的唯一症状是功能悄悄没生效(#424 的排查
|
|
260
|
+
// 只能靠「高峰期还在巩固」倒推)。这里对**非空**且解析为 null 的键各出一条 warn:
|
|
261
|
+
// 空串是合法的未配置,不告警(否则每个没配错峰的用户开机都被警告一遍)。helper
|
|
262
|
+
// 只转述 parsePeakSpec:等价类归一(PR #621,评审中)落地后,救得了的写法自然
|
|
263
|
+
// 不再走到这。行为零变化:只 log,不改 cfg。
|
|
264
|
+
for (const [key, spec] of Object.entries(invalidPeakSpecs(cfg))) {
|
|
265
|
+
ctx.logger?.warn?.(
|
|
266
|
+
`[dsh-mneme] ${key} "${spec}" 不是合法的高峰时段串,将按未配置处理(错峰不生效、全天候运行)。` +
|
|
267
|
+
`写法:09:00-18:00,多段用逗号分隔,可带星期前缀(mon-fri 08:00-12:00),支持跨零点;24:00 越界请写 00:00`
|
|
268
|
+
);
|
|
269
|
+
}
|
|
246
270
|
const sessionGate = createSessionGate(cfg);
|
|
247
271
|
|
|
248
272
|
// 失败表的启动期清理也放在装配之后(此前它在 createStore 之后、装配之前,写死
|
|
@@ -626,10 +650,17 @@ export const apply = (ctx, config) => {
|
|
|
626
650
|
disposers.push(createContinuityRescue(ctx, store, sessionGate));
|
|
627
651
|
}
|
|
628
652
|
|
|
653
|
+
// 知识库第二层(契约 §3.4):装配与开关无关(createKb 只建对象,读侧零 IO),闸在
|
|
654
|
+
// 路由与工具两层——这样「没开」(409)与「没装配」(503)在对外两面都是可区分的。
|
|
655
|
+
// 位置必须在 tools/api 两处消费点之前:cordis 的 inject 回调可能在任一之后才触发,
|
|
656
|
+
// 在消费点里现建就会造出第二份实例(两份缓存、两套 dispose)。
|
|
657
|
+
const kb = createKb({ store, embedder, cfg });
|
|
658
|
+
disposers.push(() => kb.dispose?.());
|
|
659
|
+
|
|
629
660
|
ctx.inject(["tools"], (toolsCtx) => {
|
|
630
661
|
// choro(M1):只读查询对象,chorographyEnabled 关着时工具压根不注册(闸在
|
|
631
662
|
// createTools 的 hiddenTools,与 organizeEnabled 同款反向语义)。
|
|
632
|
-
disposers.push(createTools(toolsCtx, service, cfg, embedder, sessionGate, createChoroQueries({ store })));
|
|
663
|
+
disposers.push(createTools(toolsCtx, service, cfg, embedder, sessionGate, createChoroQueries({ store }), kb));
|
|
633
664
|
});
|
|
634
665
|
|
|
635
666
|
// chorography M3:/choro 只读回执。与 /mo-run 同形(ctx.commands.register),
|
|
@@ -651,6 +682,15 @@ export const apply = (ctx, config) => {
|
|
|
651
682
|
const summarizer = createSummarizer(ctx, service, cfg, {}, sessionGate);
|
|
652
683
|
disposers.push(summarizer.dispose);
|
|
653
684
|
|
|
685
|
+
// QQ 日记本(v0.13,diaryEnabled):构造期零副作用(只闭包住依赖,不建目录、不读盘、
|
|
686
|
+
// 不调模型),所以位置可以放在这里——它必须早于下面的 createApi,面板的
|
|
687
|
+
// 「写一篇」按钮要转调同一个 composer;而自动触发侧在 webServer 块之后挂(同
|
|
688
|
+
// actionNudge 的 TDZ 理由)。关闸时 composer 是 null:路由回 409、座位不注册,
|
|
689
|
+
// 与「装配缺失」的 503 严格区分(#497 先例)。
|
|
690
|
+
const diary = cfg.diaryEnabled === true
|
|
691
|
+
? createDiaryComposer({ ctx, config: cfg, logger: ctx.logger, service, settings, memoryDir })
|
|
692
|
+
: null;
|
|
693
|
+
|
|
654
694
|
// #275 存储生命周期第一批:无损回收的手动入口。刻意不挂启动路径、不接定时器——
|
|
655
695
|
// 这一步是不可逆的内容丢弃,只由人显式触发(面板的维护卡、`dsh-mneme reclaim`)。
|
|
656
696
|
// 实例建在插件进程内,是为了让 CLI 也能走这条路:VACUUM 要排他锁,跟宿主抢锁的
|
|
@@ -676,17 +716,39 @@ export const apply = (ctx, config) => {
|
|
|
676
716
|
// 闸在路由/工具两层(关着 409 / 不注册),queries 本体装配与开关无关。
|
|
677
717
|
// 收尾档:choro 对象加两件 host 侧能力——embedder(向量惰性物化,P37)与
|
|
678
718
|
// runT0Apply(面板重建按钮的写路径,P34;T1 不上 HTTP,CLI 仍是它的入口)。
|
|
719
|
+
// T1 任务账随 choro 装配建立(与开关无关:开关在路由那侧判,没开时这些方法一次
|
|
720
|
+
// 都不会被调到,符合「503=装配缺件 / 409=用户没开」的分叉)。
|
|
721
|
+
const t1Jobs = createT1Jobs({ store, dbPath: join(memoryDir, "memory.db") });
|
|
722
|
+
disposers.push(() => t1Jobs.dispose());
|
|
679
723
|
const choro = {
|
|
680
724
|
...createChoroQueries({ store }),
|
|
681
725
|
embedder,
|
|
682
726
|
runT0Apply: ({ repoRoot }) => runChoroT0({ repoRoot, store, apply: true }),
|
|
727
|
+
startT1Job: (args) => t1Jobs.start(args),
|
|
728
|
+
t1JobStatus: (jobId) => t1Jobs.status(jobId),
|
|
683
729
|
};
|
|
730
|
+
// The semantic surface handed to the API is createSemantic()'s own return
|
|
731
|
+
// value, plus the vectorIndex it does not carry. Passing the bare literal
|
|
732
|
+
// `{ vectorIndex, reranker }` used to drop embeddingDiagnostics, so the
|
|
733
|
+
// panel's `data.embeddings` was permanently null on a real install — the
|
|
734
|
+
// typeof guard in api.js fell through every time, and no test noticed
|
|
735
|
+
// because none asserted on `data.embeddings`.
|
|
684
736
|
const api = createApi(ctx, service, settings, commands ?? {
|
|
685
737
|
add: () => { throw new Error("commands unavailable"); },
|
|
686
738
|
remove: () => false,
|
|
687
739
|
list: () => []
|
|
688
|
-
}, embedder, { vectorIndex, reranker }, cfg.apiToken, cfg, maintenance, extractProfileDraft, choro, profileFacts);
|
|
740
|
+
}, embedder, { ...semantic, vectorIndex, reranker }, cfg.apiToken, cfg, maintenance, extractProfileDraft, choro, profileFacts, kb, diary);
|
|
689
741
|
disposers.push(api.dispose);
|
|
742
|
+
|
|
743
|
+
// dsh-go 原生面板:只有宿主提供 dshGo 服务(dsh-go 手机 App)时才挂载。
|
|
744
|
+
// 动态 import 是刻意的——静态导入会让「没有桥就整块不加载」这句承诺落空。
|
|
745
|
+
// cfg.apiToken 一并交给面板:createApi 给 token / token-write 路由包了鉴权,
|
|
746
|
+
// 进程内调用不带令牌会被自家路由拦成 401,不为此在服务端开白名单。
|
|
747
|
+
ctx.inject(["dshGo"], (dshCtx) => {
|
|
748
|
+
import("./dsh-go/panel.js")
|
|
749
|
+
.then((panel) => panel.mountDshGoPanel(dshCtx, { apiToken: cfg.apiToken }))
|
|
750
|
+
.catch((error) => ctx.logger?.warn?.(`[dsh-mneme] dsh-go 面板未加载:${error?.message ?? error}`));
|
|
751
|
+
});
|
|
690
752
|
}
|
|
691
753
|
|
|
692
754
|
// M.O. 自动沉淀(v0.9.3):项目里重复走的那套动作,自动变成 memoryDir 里的一条
|
|
@@ -701,6 +763,34 @@ export const apply = (ctx, config) => {
|
|
|
701
763
|
disposers.push(createNudgeAutoRun({ ctx, memoryDir, logger: ctx.logger, sessionGate }));
|
|
702
764
|
}
|
|
703
765
|
|
|
766
|
+
// 日记的触发侧(v0.13):turn/end 跨天判定 + 启动补写。
|
|
767
|
+
//
|
|
768
|
+
// 启动补写为什么在这里而不是 webServer 块里:它不依赖面板,headless 宿主也该补。
|
|
769
|
+
// 补哪个项目只能靠状态文件里记过的项目根——宿主刚起来时没有任何会话,
|
|
770
|
+
// 而「上次是哪个项目、哪天写的」就在那儿。补写不 await:启动路径上吊住一次
|
|
771
|
+
// 模型调用,等于让整个插件加载等模型回来(本机 p90 五十秒级)。
|
|
772
|
+
if (diary) {
|
|
773
|
+
// diaryWriteEveryMinutes > 0 时额外起一根定时器(见 auto.js 的 tick):日记不该
|
|
774
|
+
// 取决于「谁先开口」,整天不说话也要有一篇。listProjects 用 knownProjects ——
|
|
775
|
+
// 定时器唤醒时没有会话,只能靠状态文件里记过的项目根。
|
|
776
|
+
const everyMinutes = Number(cfg.diaryWriteEveryMinutes) || 0;
|
|
777
|
+
const diaryAuto = createDiaryAutoRun({
|
|
778
|
+
ctx,
|
|
779
|
+
composer: diary,
|
|
780
|
+
logger: ctx.logger,
|
|
781
|
+
sessionGate,
|
|
782
|
+
everyMs: everyMinutes > 0 ? everyMinutes * 60 * 1000 : 0,
|
|
783
|
+
listProjects: () => diary.knownProjects()
|
|
784
|
+
});
|
|
785
|
+
disposers.push(diaryAuto);
|
|
786
|
+
const projects = diary.knownProjects();
|
|
787
|
+
if (projects.length) {
|
|
788
|
+
Promise.resolve(diaryAuto.catchUp(projects)).catch((error) => {
|
|
789
|
+
ctx.logger?.warn?.("[dsh-mneme] diary startup catch-up failed: " + String(error));
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
704
794
|
// Standalone external API (v0.7.12): plain node:http server for ecosystem
|
|
705
795
|
// integrations outside the DSH host. Persisted external_api settings win
|
|
706
796
|
// over the bundle config (enabled/port); the Bearer token lives in the same
|
package/lib/inject.js
CHANGED
|
@@ -44,13 +44,17 @@ function visibleSeqsOf(session) {
|
|
|
44
44
|
// 候选问的是同一件事——「当时到底想干什么」;这条取原话的路只该有一份实现。
|
|
45
45
|
// 复制一份的代价本仓库付过几次(src/lib 平价、snapshotEvents 兼容层),所以
|
|
46
46
|
// 这里只加 export,不改行为。
|
|
47
|
-
|
|
47
|
+
//
|
|
48
|
+
// #617:多带一个 seq——注入器要拿它判断「待认领票据是否比事件流更新」(见
|
|
49
|
+
// createInjector 的 pendingQueries)。seq 是宿主事件序号,取不到时为 null
|
|
50
|
+
// (老版本宿主 / 测试夹具),调用方按 null 参与相等比较,不做真假推断。
|
|
51
|
+
export function lastUserMessageInfo(ctx) {
|
|
48
52
|
try {
|
|
49
53
|
const session = ctx?.agent?.session;
|
|
50
54
|
// DSH ≥0.1.2-rc only exposes events via snapshotEvents(); older builds
|
|
51
55
|
// still have the .events property, so fall back to it.
|
|
52
56
|
const events = session?.snapshotEvents?.() ?? session?.events;
|
|
53
|
-
if (!Array.isArray(events) || events.length === 0) return "";
|
|
57
|
+
if (!Array.isArray(events) || events.length === 0) return { text: "", seq: null };
|
|
54
58
|
const visibleSeqs = visibleSeqsOf(session);
|
|
55
59
|
for (let i = events.length - 1; i >= 0; i--) {
|
|
56
60
|
const event = events[i];
|
|
@@ -61,16 +65,44 @@ export function lastUserQuery(ctx) {
|
|
|
61
65
|
if (kind !== undefined && kind !== "user") continue;
|
|
62
66
|
const parts = event.data?.content;
|
|
63
67
|
if (!Array.isArray(parts) || parts.length === 0) continue;
|
|
64
|
-
return
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
return {
|
|
69
|
+
text: safeSlice(
|
|
70
|
+
parts
|
|
71
|
+
.map((p) => (typeof p === "string" ? p : p?.text ?? ""))
|
|
72
|
+
.filter(Boolean)
|
|
73
|
+
.join("\n"),
|
|
74
|
+
500
|
|
75
|
+
),
|
|
76
|
+
seq: event?.seq ?? null
|
|
77
|
+
};
|
|
71
78
|
}
|
|
72
79
|
} catch { /* session internals unavailable: degrade to no query */ }
|
|
73
|
-
return "";
|
|
80
|
+
return { text: "", seq: null };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function lastUserQuery(ctx) {
|
|
84
|
+
return lastUserMessageInfo(ctx).text;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 认领消息 → 查询文本。#617 的补位票据就是这里算出来的字符串:判定口径与
|
|
88
|
+
// user/message 事件同源(content parts + source.kind),于是「插件注入的上下文」
|
|
89
|
+
// 这类非人类消息照样被跳过,票据不会拿插件自己的话当用户问题。
|
|
90
|
+
function inboxMessageQuery(message) {
|
|
91
|
+
try {
|
|
92
|
+
const parts = message?.content ?? message?.data?.content;
|
|
93
|
+
if (!Array.isArray(parts) || parts.length === 0) return "";
|
|
94
|
+
const kind = message?.source?.kind ?? message?.data?.source?.kind;
|
|
95
|
+
if (kind !== undefined && kind !== "user") return "";
|
|
96
|
+
return safeSlice(
|
|
97
|
+
parts
|
|
98
|
+
.map((p) => (typeof p === "string" ? p : p?.text ?? ""))
|
|
99
|
+
.filter(Boolean)
|
|
100
|
+
.join("\n"),
|
|
101
|
+
500
|
|
102
|
+
);
|
|
103
|
+
} catch {
|
|
104
|
+
return "";
|
|
105
|
+
}
|
|
74
106
|
}
|
|
75
107
|
|
|
76
108
|
// Hot-memory round extraction (v0.5.0 1.3): pairs each user/message with the
|
|
@@ -291,21 +323,117 @@ export function createInjector(ctx, service, settings, config, gate = null, sess
|
|
|
291
323
|
// first render after a new user message may still fall back to the rule-based
|
|
292
324
|
// pick; later assemblies in the same session reuse the cached vector. Bounded
|
|
293
325
|
// cache (cap 8, drop oldest) so a long session never grows it unbounded.
|
|
326
|
+
//
|
|
327
|
+
// #617 根因 B:旧实现用一个 lastPrefetched 字符串做去重,且它在 await 之前
|
|
328
|
+
// 就置位——同一条查询第一次失败(模型没下好、超时)之后**永不重试**,而那
|
|
329
|
+
// 恰好是最需要重试的一次。这里换成三件套:inflight Map 去重(渲染同一步会
|
|
330
|
+
// 被求值两次:assemble 一次、refreshContext 一次,同一条查询只该嵌一次)、
|
|
331
|
+
// 失败冷却 15s(失败可重试,但不许每次渲染都拿一次失败去撞模型)、返回
|
|
332
|
+
// Promise(调用方/测试能等到它,而不是靠魔法延时)。
|
|
294
333
|
const QUERY_VECTOR_CACHE_MAX = 8;
|
|
334
|
+
const PREFETCH_FAIL_COOLDOWN_MS = 15000;
|
|
295
335
|
const queryVectorCache = new Map();
|
|
296
|
-
|
|
336
|
+
const inflightPrefetches = new Map(); // query -> Promise<vector|null>
|
|
337
|
+
const prefetchFailedAt = new Map(); // query -> ms
|
|
297
338
|
|
|
298
339
|
function prefetchQueryVector(query) {
|
|
299
|
-
if (!query
|
|
300
|
-
|
|
301
|
-
|
|
340
|
+
if (!query) return null;
|
|
341
|
+
const cached = queryVectorCache.get(query);
|
|
342
|
+
if (cached) return Promise.resolve(cached);
|
|
343
|
+
const inflight = inflightPrefetches.get(query);
|
|
344
|
+
if (inflight) return inflight;
|
|
345
|
+
const failedAt = prefetchFailedAt.get(query);
|
|
346
|
+
if (failedAt !== undefined && Date.now() - failedAt < PREFETCH_FAIL_COOLDOWN_MS) return null;
|
|
347
|
+
const pending = service.embedQuery(query).then((vec) => {
|
|
302
348
|
if (Array.isArray(vec) && vec.length) {
|
|
303
349
|
queryVectorCache.set(query, vec);
|
|
304
350
|
if (queryVectorCache.size > QUERY_VECTOR_CACHE_MAX) {
|
|
305
351
|
queryVectorCache.delete(queryVectorCache.keys().next().value);
|
|
306
352
|
}
|
|
353
|
+
prefetchFailedAt.delete(query);
|
|
354
|
+
return vec;
|
|
307
355
|
}
|
|
308
|
-
|
|
356
|
+
prefetchFailedAt.set(query, Date.now());
|
|
357
|
+
return null;
|
|
358
|
+
}).catch(() => {
|
|
359
|
+
prefetchFailedAt.set(query, Date.now());
|
|
360
|
+
return null; /* prefetch is best-effort */
|
|
361
|
+
}).finally(() => {
|
|
362
|
+
inflightPrefetches.delete(query);
|
|
363
|
+
});
|
|
364
|
+
inflightPrefetches.set(query, pending);
|
|
365
|
+
return pending;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// #617 根因 A 的补位票据:宿主(agent-loop 0.2.x)在每个 step 的 pre-step 里
|
|
369
|
+
// 先 inbox.claim()(同步 emit agent/inbox/claimed),再 assemble(),而
|
|
370
|
+
// user/message 要到 step() 末尾(:865)才 append——注入的 text 回调在
|
|
371
|
+
// assemble / refreshContext 里求值,**永远比当轮用户输入早一拍**:Step 1 拿到
|
|
372
|
+
// "",第 N 轮的 Step 1 只能拿到第 N-1 轮的旧文本(实测 46 个渲染点错 3 次)。
|
|
373
|
+
// 票据在 claim 那一刻记下原文;渲染时若事件流里最后一条用户消息还不是它,
|
|
374
|
+
// 说明本轮 user/message 尚未落盘,用票据;一旦落盘,事件流才是权威(它对
|
|
375
|
+
// rewind / compact 的可见面判定更准,票据不看 surface)。
|
|
376
|
+
//
|
|
377
|
+
// 为什么只在 claimed 上记、不从 agent/inbox/spliced 推:claim 自己的两次
|
|
378
|
+
// mutate 都落 outcome:"canceled" 的 spliced(inserted 为空),拿 removedCount
|
|
379
|
+
// 当撤回信号会把每次正常认领误判成取消;claimed 只在真有消息被认领时触发,
|
|
380
|
+
// 是唯一无歧义的信号源。Cancel/Rewind 穿透因此也不存在——它们不产生 claimed。
|
|
381
|
+
const PENDING_QUERY_MAX = 32;
|
|
382
|
+
// 票据超过这个岁数就不再采信:一个 step 的 assemble 不可能跑这么久,更久说明
|
|
383
|
+
// 那条消息压根没落盘(认领后整轮被取消),票据已是过期证据。
|
|
384
|
+
const PENDING_QUERY_TTL_MS = 60000;
|
|
385
|
+
const pendingQueries = new Map(); // sessionId -> { text, at }
|
|
386
|
+
const offClaimed = typeof ctx?.on === "function"
|
|
387
|
+
? ctx.on("agent/inbox/claimed", (payload) => {
|
|
388
|
+
try {
|
|
389
|
+
const message = payload?.message;
|
|
390
|
+
const text = inboxMessageQuery(message);
|
|
391
|
+
if (!text) return;
|
|
392
|
+
const session = payload?.agent?.session;
|
|
393
|
+
const sessionId = session?.id;
|
|
394
|
+
if (typeof sessionId !== "string" || !sessionId) return;
|
|
395
|
+
// #539 无痕闸门必须挡在这里,不能只挡渲染侧:claim 在 pre-step 里同步触发,
|
|
396
|
+
// 早于 assemble;这条路径既写票据(票据会变成注入查询文本)又预取向量
|
|
397
|
+
// (embedQuery 直接 POST 外部 /embeddings),闸门命中时两者都是该会话数据的
|
|
398
|
+
// 外流。嵌入层自身没有任何会话维度判断,这里是唯一的拦截点。
|
|
399
|
+
if (sessionGate?.isOff(session)) return;
|
|
400
|
+
pendingQueries.delete(sessionId); // 重新插入 = 移到队尾
|
|
401
|
+
pendingQueries.set(sessionId, { text, at: Date.now() });
|
|
402
|
+
if (pendingQueries.size > PENDING_QUERY_MAX) {
|
|
403
|
+
pendingQueries.delete(pendingQueries.keys().next().value);
|
|
404
|
+
}
|
|
405
|
+
// 认领即预取:embed 在 assemble→step 之间就已经在跑,Step 1 的第二次
|
|
406
|
+
// 求值(refreshContext)有机会拿到向量。拿不到也无妨——查询文本已经在
|
|
407
|
+
// 手上,BM25 词法路(#198)能顶上。
|
|
408
|
+
prefetchQueryVector(text);
|
|
409
|
+
} catch { /* the ticket is best-effort; injection must never break */ }
|
|
410
|
+
})
|
|
411
|
+
: null;
|
|
412
|
+
|
|
413
|
+
// 当轮查询:票据优先于事件流(见上)。判据是**文本比对**而不是事件条数——
|
|
414
|
+
// 从 claim 到 assemble 之间还夹着 await tools.resolve(...),这期间任何插件
|
|
415
|
+
// 追加一个事件都会让条数增长,按条数判会把「消息还没落盘」误判成「已落盘」,
|
|
416
|
+
// 票据提前退休、Step 1 又退回空串(正是本 issue 要修的那一拍)。文本相等才是
|
|
417
|
+
// 「这条消息真的进事件流了」的直接证据;连续两轮同一句话时两者相等,取哪个都对。
|
|
418
|
+
function liveStepQuery(ctx) {
|
|
419
|
+
const info = lastUserMessageInfo(ctx);
|
|
420
|
+
let sessionId = null;
|
|
421
|
+
try {
|
|
422
|
+
const id = ctx?.agent?.session?.id;
|
|
423
|
+
if (typeof id === "string" && id) sessionId = id;
|
|
424
|
+
} catch { /* session internals unavailable */ }
|
|
425
|
+
if (!sessionId) return info.text;
|
|
426
|
+
const ticket = pendingQueries.get(sessionId);
|
|
427
|
+
if (!ticket) return info.text;
|
|
428
|
+
if (info.text === ticket.text) {
|
|
429
|
+
pendingQueries.delete(sessionId); // 已落盘,票据退休(顺带保证 Map 不被历史会话撑大)
|
|
430
|
+
return info.text;
|
|
431
|
+
}
|
|
432
|
+
if (Date.now() - ticket.at > PENDING_QUERY_TTL_MS) {
|
|
433
|
+
pendingQueries.delete(sessionId); // 过期证据,不采信
|
|
434
|
+
return info.text;
|
|
435
|
+
}
|
|
436
|
+
return ticket.text;
|
|
309
437
|
}
|
|
310
438
|
|
|
311
439
|
// 会话身份解析(v1.2 人格寻址用):读 session.header.agentPreset——冻结的
|
|
@@ -323,6 +451,39 @@ export function createInjector(ctx, service, settings, config, gate = null, sess
|
|
|
323
451
|
}
|
|
324
452
|
}
|
|
325
453
|
|
|
454
|
+
// 会话 id 解析(#606 绑会话卡用):session.id 在宿主里稳定可用(scope.js:97-103
|
|
455
|
+
// 就是靠它反查 workspace 的)。与 agentPreset 一样各自护住——两者来自同一个
|
|
456
|
+
// session 对象,但 accessor 抛错时不该互相拖累(一个取不到不影响另一个)。
|
|
457
|
+
// 为什么需要第二个键:DS Bot 里 agentPreset 恒为 "standard",按 bot 建卡分不开,
|
|
458
|
+
// 只有会话 id 能分开(实测见 issue #606)。
|
|
459
|
+
function sessionIdOf(ctx) {
|
|
460
|
+
try {
|
|
461
|
+
const id = ctx?.agent?.session?.id;
|
|
462
|
+
return typeof id === "string" && id.trim() ? id.trim() : null;
|
|
463
|
+
} catch {
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// 父会话 id 解析(子 agent 继承人格卡用):子代理会话的 header 里有 `parentSession`
|
|
469
|
+
// (实测 518 个子代理头恒带此字段,主会话没有)。⚠️ 与 `session-gate.js` 的口径不同:
|
|
470
|
+
// 那里刻意不用它判「是不是子代理」(宿主 fork 出的主会话分支同样带 parentSession),
|
|
471
|
+
// 这里只把它当「自己没建卡时该继承谁」的线索,fork 出的分支继承父卡同样是正确语义。
|
|
472
|
+
function parentSessionIdOf(ctx) {
|
|
473
|
+
try {
|
|
474
|
+
const session = ctx?.agent?.session;
|
|
475
|
+
const parent = session?.header?.parentSession;
|
|
476
|
+
return typeof parent === "string" && parent.trim() ? parent.trim() : null;
|
|
477
|
+
} catch {
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// 一次取全三个寻址键。分开取值、各自兜错:一个失败不该让另一个也丢。
|
|
483
|
+
function sessionIdentity(ctx) {
|
|
484
|
+
return { agentId: sessionAgentId(ctx), sessionId: sessionIdOf(ctx), parentSessionId: parentSessionIdOf(ctx) };
|
|
485
|
+
}
|
|
486
|
+
|
|
326
487
|
// User profile + rules: injected ahead of the memory block because they are
|
|
327
488
|
// always-relevant instructions the agent should follow every turn.
|
|
328
489
|
// 渲染整体搬到 src/user-settings.js(与面板预览共用一份,见那里的模块头注释)。
|
|
@@ -333,7 +494,9 @@ export function createInjector(ctx, service, settings, config, gate = null, sess
|
|
|
333
494
|
function renderUserSettings(ctx) {
|
|
334
495
|
// factsInject 是 profile-facts 域的读口(index.js 接线,面板预览路由传同一份):
|
|
335
496
|
// 开关与事实有无的判定都在 renderUserSettingsBlock 内部,「关」等于零读数。
|
|
336
|
-
|
|
497
|
+
const { agentId, sessionId, parentSessionId } = sessionIdentity(ctx);
|
|
498
|
+
return renderUserSettingsBlock({ settings, config, agentId, sessionId, parentSessionId, factsProvider: factsInject })
|
|
499
|
+
.text;
|
|
337
500
|
}
|
|
338
501
|
|
|
339
502
|
// #249 能力说明的系统提示段。order 150 = 插件指引段的既有惯例
|
|
@@ -360,10 +523,12 @@ export function createInjector(ctx, service, settings, config, gate = null, sess
|
|
|
360
523
|
text: (ctx) => {
|
|
361
524
|
if (sessionGate?.isOff(sessionOf(ctx))) return "";
|
|
362
525
|
// Bug4: pass the latest user query so injection prefers semantically
|
|
363
|
-
// relevant memories;
|
|
526
|
+
// relevant memories; liveStepQuery is best-effort (empty → legacy).
|
|
364
527
|
// The query vector is prefetched asynchronously (cached) because the
|
|
365
528
|
// render itself must stay synchronous.
|
|
366
|
-
|
|
529
|
+
// #617:liveStepQuery 而不是 lastUserQuery——宿主在 step 末尾才 append
|
|
530
|
+
// user/message,而注入在这之前求值,只看事件流会拿到上一轮的输入。
|
|
531
|
+
const query = liveStepQuery(ctx);
|
|
367
532
|
if (query) prefetchQueryVector(query);
|
|
368
533
|
const queryVector = queryVectorCache.get(query);
|
|
369
534
|
// v0.8.0 A3:scope 随请求解析(解析器恒返回对象,身份取不到时两维为
|
|
@@ -458,6 +623,12 @@ export function createInjector(ctx, service, settings, config, gate = null, sess
|
|
|
458
623
|
];
|
|
459
624
|
|
|
460
625
|
return () => {
|
|
626
|
+
// #617:认领票据是监听宿主事件的活句柄,卸载时必须成对摘掉,否则注入器重装
|
|
627
|
+
// (面板开关、宿主热重载)会留下一个继续写 Map 的孤儿监听。
|
|
628
|
+
offClaimed?.();
|
|
629
|
+
pendingQueries.clear();
|
|
630
|
+
inflightPrefetches.clear();
|
|
631
|
+
prefetchFailedAt.clear();
|
|
461
632
|
queryVectorCache.clear();
|
|
462
633
|
rotationHistory.clear();
|
|
463
634
|
injectionSnapshot = null; // 注入器卸载即失效:快照不得跨生命周期存留
|