lume-dsh-plugin 0.7.4 → 0.8.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.
Files changed (58) hide show
  1. package/CHANGELOG.md +460 -0
  2. package/README.md +245 -275
  3. package/lib/client.js +242 -181
  4. package/lib/core/card.js +11 -9
  5. package/lib/core/citations.js +235 -0
  6. package/lib/core/coverage.js +149 -0
  7. package/lib/core/dialogue-mining.js +49 -11
  8. package/lib/core/knowledge.js +165 -0
  9. package/lib/core/leak-detector.js +1 -3
  10. package/lib/core/ledger.js +118 -14
  11. package/lib/core/manifest.js +3 -1
  12. package/lib/core/memory-id.js +105 -0
  13. package/lib/core/metrics.js +270 -0
  14. package/lib/core/persona-limits.js +25 -0
  15. package/lib/core/scope.js +124 -0
  16. package/lib/core/signals.js +285 -5
  17. package/lib/core/task-memory.js +143 -0
  18. package/lib/core/text.js +45 -3
  19. package/lib/host/backfill.js +218 -0
  20. package/lib/host/bootstrap.js +130 -0
  21. package/lib/host/boundary.js +1 -3
  22. package/lib/host/clauses.js +180 -0
  23. package/lib/host/config.js +7 -0
  24. package/lib/host/diag.js +44 -7
  25. package/lib/host/distill-prompt.js +365 -0
  26. package/lib/host/distill.js +22 -348
  27. package/lib/host/extraction.js +11 -3
  28. package/lib/host/host-context.js +1 -0
  29. package/lib/host/host-events.js +100 -0
  30. package/lib/host/identity.js +12 -29
  31. package/lib/host/inbound.js +133 -0
  32. package/lib/host/injection.js +2 -6
  33. package/lib/host/llm-aux.js +130 -0
  34. package/lib/host/llm-route.js +3 -0
  35. package/lib/host/methods.js +180 -10
  36. package/lib/host/metrics-log.js +169 -0
  37. package/lib/host/notices.js +62 -0
  38. package/lib/host/project-access.js +210 -0
  39. package/lib/host/project.js +153 -2
  40. package/lib/host/prompt-blocks.js +113 -0
  41. package/lib/host/protocol.js +142 -18
  42. package/lib/host/reflection.js +28 -5
  43. package/lib/host/registry.js +0 -4
  44. package/lib/host/requirements-scan.js +108 -0
  45. package/lib/host/rpc-bridge.js +23 -4
  46. package/lib/host/rpc.js +1 -1
  47. package/lib/host/sections.js +48 -0
  48. package/lib/host/session-deps.js +27 -0
  49. package/lib/host/session-events.js +371 -0
  50. package/lib/host/session-runtime.js +17 -5
  51. package/lib/host/thinking.js +10 -1
  52. package/lib/host/tools.js +367 -0
  53. package/lib/host/triggers.js +25 -1
  54. package/lib/host/turn-boundary.js +116 -0
  55. package/lib/host/wiring.js +280 -0
  56. package/lib/host/workspace-map.js +81 -0
  57. package/lib/index.js +326 -877
  58. package/package.json +11 -5
package/lib/index.js CHANGED
@@ -25,26 +25,50 @@ import { makeRpcRoute } from "./host/rpc-bridge.js";
25
25
  import { FilePersonaStore, migrateLegacyState, PersonaStore } from "./host/store.js";
26
26
  import { IdentityStore, LUME_IDENTITY_SPEC, zodLike } from "./host/identity.js";
27
27
  import { PersonaRegistry } from "./host/registry.js";
28
- import { buildCorrectionPrompt, buildExtractionPrompt, extractNaming, isCoolingDown, isDuplicateFact, mergeNewFacts, parseCorrectionRule, parseFacts, resolveAuxRoute, shouldCaptureCorpus, shouldConsider, shouldConsiderCorrection } from "./host/extraction.js";
28
+ import { buildCorrectionPrompt, buildExtractionPrompt, extractNaming, isCoolingDown, isDuplicateFact, mergeNewFacts, parseCorrectionRule, parseFacts, resolveAuxRoute, shouldCaptureCorpus, shouldConsider, shouldConsiderCorrection, } from "./host/extraction.js";
29
29
  import { DistillJobRunner, DISTILL_ALGORITHM_VERSION, runDistill } from "./host/distill.js";
30
30
  import { jaccard } from "./core/retrieval.js";
31
31
  import { fnv1a32 } from "./core/sampling.js";
32
32
  import { detectLeak } from "./core/leak-detector.js";
33
- import { messageText } from "./core/text.js";
33
+ import { messageText, visibleText } from "./core/text.js";
34
+ import { formatWindows, recordReadArgs, recordResultText, unsupportedCitations } from "./core/citations.js";
34
35
  import { composeBoundary } from "./host/boundary.js";
35
36
  import { SessionRuntimeStore } from "./host/session-runtime.js";
36
37
  import { isCompactionCheckpoint } from "./host/compaction.js";
37
38
  import { LUME_REFLECTION_SPEC, ReflectionStore, buildReflectionPrompt, parseReflectionScore } from "./host/reflection.js";
38
39
  import { appendLumeLog } from "./host/diag.js";
39
- import { advancePhase, buildAlignmentCorrection, buildCasualDirective, buildCompactionNotice, buildInteractionDirective, buildLongSessionGuard, buildSessionAnchor, buildTaskPhaseDirective, buildToolFailureNotice, classifyInteraction, isUserAuthored, taskPhaseForMode } from "./host/protocol.js";
40
+ import { clearNotice, forceNotice, noticeOpen, noticeText, setNotice } from "./host/notices.js";
41
+ import { toolArgsOf, toolNameOf, toolTargetOf } from "./host/host-events.js";
42
+ import { advancePhase, buildAlignmentCorrection, buildCasualDirective, buildCompactionNotice, buildInteractionDirective, buildLongSessionGuard, buildSessionAnchor, buildTaskPhaseDirective, buildToolFailureNotice, classifyWithTrajectory, isUserAuthored, taskPhaseForMode, } from "./host/protocol.js";
43
+ import { DESIGN_SIGNAL_RE } from "./host/protocol.js";
40
44
  import { buildDocumentDirective, probeDocumentCapabilities } from "./host/documents.js";
41
45
  import { REASONING_MODEL_RE, TASK_SIGNAL_RE, selectStableThinkingProtocol } from "./host/thinking.js";
42
- import { normalizeChange, normalizeContract, normalizeHypothesis, normalizeProjectFact, projectKeyOf, renderChangeLedger, renderContract, renderHypotheses, renderProjectFacts } from "./core/ledger.js";
46
+ import { normalizeChange, normalizeContract, normalizeHypothesis, normalizeProjectFact, projectKeyOf, renderChangeLedger, renderContract, renderHypotheses, renderProjectFacts, } from "./core/ledger.js";
47
+ import { normalizeDesign, renderDesign, renderRequirements } from "./core/ledger.js";
43
48
  import { classifyTool, readResultSignals } from "./core/signals.js";
44
- import { buildContractMethodDirective, buildDocumentMethodDirective, buildImpactDirective, buildStructureHint, composeBlocks } from "./host/methods.js";
49
+ import { auditOpenQuestions, isRealVerifyCommand, summarizeToolChange, toolArtifactText, unrequestedChangeWords, } from "./core/signals.js";
50
+ import { recordSymbols, unsupportedClaims } from "./core/citations.js";
51
+ import { coverageRows, danglingSectionRefs, hasFigureRefs, pickRequirementCorpus, splitRequirementItems } from "./core/coverage.js";
52
+ /** 〔提问核对〕每会话上限(提问纪律的纠偏;比引用核对更敏感,限得更死)。 */
53
+ const QUESTION_AUDIT_MAX = 2;
54
+ /** 只把「文档类产物」当交付物收进覆盖核对(源码改动进去只会制造噪音)。 */
55
+ const DOC_ARTIFACT_RE = /\.(md|markdown|txt)$/i;
56
+ import { buildCarrierGapNotice, buildCitationDirective, buildClaimDirective, buildContractMethodDirective, buildDocumentMethodDirective, buildImpactDirective, buildQuestionAuditDirective, buildRequirementCoverageDirective, buildStructureHint, buildUnverifiedDeliveryNotice, composeBlocksDetailed, } from "./host/methods.js";
57
+ import { buildDesignMethodDirective, buildRequirementMethodDirective, buildDriftDirective } from "./host/methods.js";
45
58
  import { LUME_PROJECT_SPEC, ProjectStore } from "./host/project.js";
46
- import { DEFAULT_TRIGGER_THRESHOLDS, applyToolSignal, applyVerifyOutcome, cooldownOk, evaluateToolTrigger, evaluateTurnTrigger } from "./host/triggers.js";
47
- /** schemastery → domainTable 形参的桥接(与 identity.ts 同款)。 */
59
+ import { volatileBlocks } from "./host/prompt-blocks.js";
60
+ import { initStores } from "./host/bootstrap.js";
61
+ import { focusIdsFor } from "./host/clauses.js";
62
+ import { createMetricsLog } from "./host/metrics-log.js";
63
+ import { createAuxLlm } from "./host/llm-aux.js";
64
+ import { createLlmRouteCell } from "./host/llm-route.js";
65
+ import { createProjectAccess } from "./host/project-access.js";
66
+ import { installPromptSections } from "./host/sections.js";
67
+ import { registerLumeTools } from "./host/tools.js";
68
+ import { assembleBlockDeps, assembleSessionEventDeps, assembleToolDeps, startSessionBackfill } from "./host/wiring.js";
69
+ import { createSessionDisposedHandler, createSessionEventHandler } from "./host/session-events.js";
70
+ import { DEFAULT_TRIGGER_THRESHOLDS, applyToolSignal, applyVerifyOutcome, cooldownOk, evaluateToolTrigger, evaluateTurnTrigger, } from "./host/triggers.js";
71
+ /** schemastery → domainTable 形参的桥接(与 stores.identity().ts 同款)。 */
48
72
  const recordSchema = zodLike;
49
73
  /** 会话人设选择的持久层(键 = sessionId)。 */
50
74
  export const LUME_DOMAIN_SPEC = defineDomain({
@@ -146,105 +170,46 @@ function applyInner(ctx, config = {}) {
146
170
  }
147
171
  const defaultName = builtins[NONE_PERSONA] ? NONE_PERSONA : null;
148
172
  const legacyStatePath = join(assetsDir, "persona-state.json");
149
- // ── 存储就绪:会话选择域(必有)+ 身份域(失败降级为无档案功能)──
150
- let currentStore = null;
151
- let identity = null;
152
- const storesReady = (async () => {
153
- try {
154
- const domain = await ctx.storageDomain.open(LUME_DOMAIN_SPEC);
155
- ctx.effect(() => async () => {
156
- await domain.close();
157
- }, "lume: close state domain");
158
- const store = new PersonaStore(domain.table(SESSION_PERSONA_TABLE), { maxSessions: MAX_SESSIONS });
159
- const migrated = await migrateLegacyState(store, legacyStatePath);
160
- if (migrated)
161
- ctx.logger?.warn?.("lume: 已从 assets/persona-state.json 迁移旧的人设记忆");
162
- return store;
163
- }
164
- catch (error) {
165
- ctx.logger?.warn?.("lume: storageDomain 不可用,降级为 assets 文件存储", error);
166
- return new FilePersonaStore(legacyStatePath, { maxSessions: MAX_SESSIONS });
167
- }
168
- })();
169
- const identityReady = (async () => {
170
- try {
171
- const domain = await ctx.storageDomain.open(LUME_IDENTITY_SPEC);
172
- ctx.effect(() => async () => {
173
- await domain.close();
174
- }, "lume: close identity domain");
175
- return new IdentityStore({
176
- profile: domain.table("profile"),
177
- memory_facts: domain.table("memory_facts"),
178
- style_rules: domain.table("style_rules"),
179
- corpus_pins: domain.table("corpus_pins"),
180
- custom_personas: domain.table("custom_personas"),
181
- });
182
- }
183
- catch (error) {
184
- ctx.logger?.warn?.("lume: 身份域不可用,档案/记忆/自定义人设功能降级", error);
185
- return null;
186
- }
187
- })();
188
- void storesReady.then((store) => {
189
- currentStore = store;
190
- });
191
- void identityReady.then((store) => {
192
- identity = store;
173
+ // ── 存储与生命周期(会话/身份/反思/项目四域)── 实现见 host/bootstrap.ts
174
+ const stores = initStores({
175
+ ctx,
176
+ legacyStatePath,
177
+ maxSessions: MAX_SESSIONS,
178
+ projectMemoryOn: config.projectMemory ?? true,
179
+ migrateLegacyState,
180
+ personaDomainSpec: LUME_DOMAIN_SPEC,
181
+ sessionPersonaTable: SESSION_PERSONA_TABLE,
182
+ describeError,
193
183
  });
194
- // ── 反思域(会话结束后打分,失败降级为无反思功能)──
195
- let reflectionStore = null;
196
- const reflectionReady = (async () => {
197
- try {
198
- const domain = await ctx.storageDomain.open(LUME_REFLECTION_SPEC);
199
- ctx.effect(() => async () => { await domain.close(); }, "lume: close reflection domain");
200
- const store = new ReflectionStore(domain.table("logs"));
201
- const migrated = await store.migrateLegacy();
202
- if (migrated > 0)
203
- ctx.logger?.warn?.(`lume: 已迁移 ${migrated} 条旧版反思日志`);
204
- return store;
205
- }
206
- catch (error) {
207
- ctx.logger?.warn?.("lume: 反思域不可用,反思日志降级", error);
208
- return null;
209
- }
210
- })();
211
- void reflectionReady.then((s) => { reflectionStore = s; });
212
184
  // ── 项目域:任务契约 / 改动台账 / 假设台账 / 项目知识(失败降级为无载具功能)──
213
185
  const projectMemoryOn = config.projectMemory ?? true;
214
186
  const behaviorTriggersOn = config.behaviorTriggers ?? true;
187
+ /** 运行时度量开关(默认开):关掉只是不记录,不改变任何行为。 */
188
+ const metricsEnabled = config.metrics !== false;
215
189
  const triggerThresholds = {
216
190
  ...DEFAULT_TRIGGER_THRESHOLDS,
217
191
  inspectStreak: config.triggerInspectStreak ?? DEFAULT_TRIGGER_THRESHOLDS.inspectStreak,
218
192
  changeStreak: config.triggerChangeStreak ?? DEFAULT_TRIGGER_THRESHOLDS.changeStreak,
219
193
  deadPathFails: config.triggerDeadPathFails ?? DEFAULT_TRIGGER_THRESHOLDS.deadPathFails,
220
194
  };
221
- let project = null;
222
- const projectReady = (async () => {
223
- if (!projectMemoryOn)
224
- return null;
225
- try {
226
- const domain = await ctx.storageDomain.open(LUME_PROJECT_SPEC);
227
- ctx.effect(() => async () => { await domain.close(); }, "lume: close project domain");
228
- return new ProjectStore({
229
- contract: domain.table("contract"),
230
- ledger: domain.table("ledger"),
231
- hypotheses: domain.table("hypotheses"),
232
- facts: domain.table("facts"),
233
- });
234
- }
235
- catch (error) {
236
- ctx.logger?.warn?.("lume: 项目域不可用,任务契约/台账/项目知识降级", error);
237
- return null;
238
- }
239
- })();
240
- void projectReady.then((s) => { project = s; });
241
- const registry = new PersonaRegistry(builtins, () => identity);
242
- ctx.logger?.warn?.(`lume: 已加载(builtins=${Object.keys(builtins).join(",") || "空!"},assets=${assetsDir})`);
195
+ /** 请求是不是任务型(按形态决定方法块;提示装配与事件处理共用)。 */
196
+ function isTaskQuery(st) {
197
+ return TASK_SIGNAL_RE.test(st.intent?.text ?? st.userText ?? "");
198
+ }
199
+ /**
200
+ * fire-and-forget 的持久化:**失败必须留痕**。
201
+ *
202
+ * 现场教训(2026-09-23):一整批写入路径用 `void stores.projectReady.then(...)` 且没有 catch,
203
+ * 出问题时不报错也不入账(ledger 一直 undefined),六个功能静默失效很久才被发现。
204
+ */
205
+ const registry = new PersonaRegistry(builtins, () => stores.identity());
206
+ ctx.logger?.warn?.(`lume: 已加载(builtins=${Object.keys(builtins).join(",") || "空!"},assets=${assetsDir},能力=载具+触发器+设计pass+需求锚点)`);
243
207
  ctx.logger?.warn?.(`lume: llmRoute 初始化策略:agentDefaultModel → settings → 回退`);
244
208
  // ── 每会话运行时状态(内存,重启即弃,LRU 上限兜底)──
245
209
  const runtime = new SessionRuntimeStore();
246
210
  // ── 模型路由缓存(request/context,会话过程中由 agent-loop 更新)──
247
- let llmRoute = null;
211
+ // 共享可变单元(不是快照):会话事件在 request/context 里写它,读方都拿 .current
212
+ const llmRoute = createLlmRouteCell();
248
213
  // ── 主动解析默认模型:会话开始前蒸馏/提取也要能用 ──
249
214
  // request/context 事件只在对话路由变化时触发(delta event),静默状态下 llmRoute 恒为 null,
250
215
  // 导致蒸馏一开即报「模型路由不可用」。这里初始化即解析默认模型,后续仍被 request/context 覆盖。
@@ -258,8 +223,8 @@ function applyInner(ctx, config = {}) {
258
223
  if (agentDefaultModel) {
259
224
  const selection = agentDefaultModel.currentSelection?.();
260
225
  if (typeof selection?.provider === "string" && typeof selection?.model === "string") {
261
- llmRoute = { provider: selection.provider, model: selection.model };
262
- ctx.logger?.warn?.(`lume: llmRoute 从 agentDefaultModel 初始化 → ${llmRoute.provider}/${llmRoute.model}`);
226
+ llmRoute.current = { provider: selection.provider, model: selection.model };
227
+ ctx.logger?.warn?.(`lume: llmRoute 从 agentDefaultModel 初始化 → ${llmRoute.current.provider}/${llmRoute.current.model}`);
263
228
  return;
264
229
  }
265
230
  }
@@ -273,8 +238,8 @@ function applyInner(ctx, config = {}) {
273
238
  if (settings) {
274
239
  const raw = settings.get("agent-default-model");
275
240
  if (raw && typeof raw.provider === "string" && typeof raw.model === "string") {
276
- llmRoute = { provider: raw.provider, model: raw.model };
277
- ctx.logger?.warn?.(`lume: llmRoute 从 settings 初始化 → ${llmRoute.provider}/${llmRoute.model}`);
241
+ llmRoute.current = { provider: raw.provider, model: raw.model };
242
+ ctx.logger?.warn?.(`lume: llmRoute 从 settings 初始化 → ${llmRoute.current.provider}/${llmRoute.current.model}`);
278
243
  return;
279
244
  }
280
245
  }
@@ -284,98 +249,21 @@ function applyInner(ctx, config = {}) {
284
249
  }
285
250
  ctx.logger?.warn?.("lume: llmRoute 初始化失败 — 蒸馏/提取在对话前不可用");
286
251
  })();
287
- /** 小模型单次调用(提取/蒸馏等辅助功能用);路由由调用方解析后传入,不可用时返回 null。signal 中止时抛错。
288
- * 组装时保留全部块(text + reasoning),蒸馏解析需要完整的模型输出——
289
- * 推理型模型可能把 JSON 拆在 reasoning 块尾部,只取 text 会拿到半成品。
290
- * 蒸馏类调用传完整控制参数:reasoningEffort=low(复述风模型常吃 4000+ token 复述指令,低推理显著缩短)、
291
- * temperature=0(稳定)。模型不支持低推理时会抛 UNSUPPORTED_REASONING_EFFORT,捕获降级重试(去掉 effort 重发)。 */
292
- async function callLlm(route, system, userText, maxTokens, signal) {
293
- if (!route)
294
- return null;
295
- const llm = ctx.get("llm");
296
- if (!llm)
297
- return null;
298
- try {
299
- const messages = [
300
- createUserMessage({
301
- content: [{ type: "text", text: userText }],
302
- source: { kind: "plugin", plugin: "lume" },
303
- }),
304
- ];
305
- const assembler = new BlockAssembler();
306
- try {
307
- for await (const chunk of llm.stream({ provider: route.provider, model: route.model, messages, system, maxTokens, reasoningEffort: ReasoningEffortId("low"), temperature: 0, ...(signal ? { signal } : {}) })) {
308
- assembler.push(chunk);
309
- }
310
- // 错误经流内 finish chunk 传输(不 throw)——检查 finish.kind === "error"
311
- if (assembler.finish.kind === "error") {
312
- const code = assembler.finish.failure?.code;
313
- if (code !== "UNSUPPORTED_REASONING_EFFORT")
314
- throw new Error(String(assembler.finish.failure?.message ?? "unnamed stream error"));
315
- // 不支持 effort:降级无 effort 重发
316
- const assembler2 = new BlockAssembler();
317
- for await (const chunk of llm.stream({ provider: route.provider, model: route.model, messages, system, maxTokens, temperature: 0, ...(signal ? { signal } : {}) })) {
318
- assembler2.push(chunk);
319
- }
320
- if (assembler2.finish.kind === "error") {
321
- throw new Error(String(assembler2.finish.failure?.message ?? "unnamed stream error"));
322
- }
323
- return assembler2
324
- .blocks()
325
- .map((block) => {
326
- const text = block?.text;
327
- return typeof text === "string" ? text : "";
328
- })
329
- .filter((text) => text.length > 0)
330
- .join(" ")
331
- .trim();
332
- }
333
- }
334
- catch (error) {
335
- // throw 形态的错误:非 UNSUPPORTED 直接抛;是则降级重试
336
- if (error?.code !== "UNSUPPORTED_REASONING_EFFORT")
337
- throw error;
338
- const assembler2 = new BlockAssembler();
339
- for await (const chunk of llm.stream({ provider: route.provider, model: route.model, messages, system, maxTokens, temperature: 0, ...(signal ? { signal } : {}) })) {
340
- assembler2.push(chunk);
341
- }
342
- if (assembler2.finish.kind === "error") {
343
- throw new Error(String(assembler2.finish.failure?.message ?? "unnamed stream error"));
344
- }
345
- return assembler2
346
- .blocks()
347
- .map((block) => {
348
- const text = block?.text;
349
- return typeof text === "string" ? text : "";
350
- })
351
- .filter((text) => text.length > 0)
352
- .join(" ")
353
- .trim();
354
- }
355
- const allBlocks = assembler
356
- .blocks()
357
- .map((block) => {
358
- const text = block?.text;
359
- return typeof text === "string" ? text : "";
360
- })
361
- .filter((text) => text.length > 0);
362
- // 诊断探针:完整输出落盘(含 max-tokens 截断标记;追加,一次失败可看全程)
363
- try {
364
- const existing = readFileSync(LLM_DUMP_PATH, "utf8");
365
- const dumps = existing ? JSON.parse(existing) : [];
366
- dumps.push({ ts: Date.now(), route: `${route.provider}/${route.model}`, maxTokens, finish: assembler.finish, blocks: allBlocks.map((t) => t.slice(0, 6000)) });
367
- writeFileSync(LLM_DUMP_PATH, JSON.stringify(dumps, null, 2), "utf8");
368
- }
369
- catch { /* 诊断失败不阻断 */ }
370
- return allBlocks.join(" ").trim();
371
- }
372
- catch (error) {
373
- if (signal?.aborted)
374
- throw error; // 用户取消:向上抛,任务状态走 cancelled
375
- ctx.logger?.warn?.("lume: 小模型调用失败,本轮跳过", error);
376
- return null;
377
- }
378
- }
252
+ // ── 辅助模型调用(实现见 host/llm-aux.ts)──
253
+ const { callLlm } = createAuxLlm({ ctx, llmRoute: () => llmRoute.current, llmDumpPath: LLM_DUMP_PATH });
254
+ // ── 载具与项目知识的读取入口(实现见 host/project-access.ts)──
255
+ const projectAccess = createProjectAccess({
256
+ ctx,
257
+ config,
258
+ runtime,
259
+ stores,
260
+ projectTask: stores.projectTask,
261
+ normalizeProjectFact,
262
+ isRealVerifyCommand,
263
+ jaccard,
264
+ projectKeyOf,
265
+ });
266
+ const { projectKeyFor, commandSummary, flushPendingFacts, settleVerification, saveSessionMemory, taskMemoriesOf, contractOf, changesOf, hypothesesOf, factsOf, requirementsOf, designOf, needsDesignPass, structureToolName, } = projectAccess;
379
267
  /** 被动提取:三道门 → 小模型 → 合并落盘。按会话串行,失败静默。 */
380
268
  function scheduleExtraction(sid, st) {
381
269
  if (st.extracting) {
@@ -396,7 +284,7 @@ function applyInner(ctx, config = {}) {
396
284
  st.userText = "";
397
285
  st.assistantText = "";
398
286
  try {
399
- if (!extractionEnabled || !identity)
287
+ if (!extractionEnabled || !stores.identity())
400
288
  return;
401
289
  const personaName = st.lastInjected;
402
290
  if (!personaName || !userText)
@@ -405,14 +293,17 @@ function applyInner(ctx, config = {}) {
405
293
  // 一条风格约定写回 style_rules(Jaccard 相似自动替换,不堆叠)。冷却与
406
294
  // 记忆提取共用,避免同一轮双模型调用。
407
295
  if (shouldConsiderCorrection(userText) && !isCoolingDown(st.lastExtractionAt, Date.now(), cooldownMs)) {
408
- const route = resolveAuxRoute(extractionRouteOverride, llmRoute);
296
+ const route = resolveAuxRoute(extractionRouteOverride, llmRoute.current);
409
297
  if (route) {
410
- const prompt = buildCorrectionPrompt(userText, assistantText, identity.getStyleRules(personaName).map((r) => r.rule));
298
+ const prompt = buildCorrectionPrompt(userText, assistantText, stores
299
+ .identity()
300
+ .getStyleRules(personaName)
301
+ .map((r) => r.rule));
411
302
  const output = await callLlm(route, prompt.system, prompt.userText, 400);
412
303
  const rule = output === null ? null : parseCorrectionRule(output);
413
304
  if (rule) {
414
305
  st.lastExtractionAt = Date.now();
415
- await identity.addStyleRule(personaName, rule, (a, b) => jaccard(a, b) >= 0.6);
306
+ await stores.identity().addStyleRule(personaName, rule, (a, b) => jaccard(a, b) >= 0.6);
416
307
  ctx.logger?.warn?.(`lume: 纠偏捕获 → ${personaName}: ${rule}`);
417
308
  }
418
309
  }
@@ -420,7 +311,9 @@ function applyInner(ctx, config = {}) {
420
311
  // 通道 B:语料摘录——用户认可上一轮回复「像本人」时,把真实对话对
421
312
  // 摘录进 corpus_pins(注入时并入采样池,让语气随真实使用收敛)。
422
313
  if (shouldCaptureCorpus(userText) && pinCandidate && pinCandidate.assistant) {
423
- const written = await identity.addCorpusPin(personaName, { user: pinCandidate.user, assistant: pinCandidate.assistant, at: Date.now() }, (a, b) => jaccard(a, b) >= 0.8);
314
+ const written = await stores
315
+ .identity()
316
+ .addCorpusPin(personaName, { user: pinCandidate.user, assistant: pinCandidate.assistant, at: Date.now() }, (a, b) => jaccard(a, b) >= 0.8);
424
317
  if (written)
425
318
  ctx.logger?.warn?.(`lume: 语料摘录 → ${personaName}: ${pinCandidate.assistant.slice(0, 40)}`);
426
319
  }
@@ -429,26 +322,26 @@ function applyInner(ctx, config = {}) {
429
322
  return;
430
323
  if (isCoolingDown(st.lastExtractionAt, Date.now(), cooldownMs))
431
324
  return;
432
- const existing = identity.getMemory(personaName);
325
+ const existing = stores.identity().getMemory(personaName);
433
326
  if (isDuplicateFact(userText, existing))
434
327
  return;
435
328
  const prompt = buildExtractionPrompt(userText, assistantText, existing.map((f) => f.text));
436
- const output = await callLlm(resolveAuxRoute(extractionRouteOverride, llmRoute), prompt.system, prompt.userText, 800);
329
+ const output = await callLlm(resolveAuxRoute(extractionRouteOverride, llmRoute.current), prompt.system, prompt.userText, 800);
437
330
  if (output === null) {
438
331
  ctx.logger?.warn?.(`lume: 反思跳过(${sid})模型无输出`);
439
332
  return;
440
333
  }
441
334
  st.lastExtractionAt = Date.now();
442
- const fresh = mergeNewFacts(parseFacts(output), identity.getMemory(personaName));
335
+ const fresh = mergeNewFacts(parseFacts(output), stores.identity().getMemory(personaName));
443
336
  for (const fact of fresh) {
444
- const written = await identity.addMemory(personaName, fact, (candidate, all) => isDuplicateFact(candidate, all));
337
+ const written = await stores.identity().addMemory(personaName, fact, (candidate, all) => isDuplicateFact(candidate, all));
445
338
  if (written)
446
339
  ctx.logger?.warn?.(`lume: 提取记忆 → ${personaName}: ${fact}`);
447
340
  }
448
341
  // 取名类事实同步身份档案:下拉显示档案名 + 【你是谁】段生效
449
342
  const named = extractNaming(fresh);
450
343
  if (named) {
451
- await identity.setProfileName(personaName, named);
344
+ await stores.identity().setProfileName(personaName, named);
452
345
  ctx.logger?.warn?.(`lume: 人设 ${personaName} 被命名为「${named}」`);
453
346
  }
454
347
  }
@@ -458,13 +351,14 @@ function applyInner(ctx, config = {}) {
458
351
  }
459
352
  /** 蒸馏任务 Runner:素材文本 → 角色卡(契约+语料)。路由可配专用档(distillProvider/Model),默认跟随主对话。 */
460
353
  const distillRunner = new DistillJobRunner({
461
- route: () => resolveAuxRoute(distillRouteOverride, llmRoute),
354
+ route: () => resolveAuxRoute(distillRouteOverride, llmRoute.current),
462
355
  call: (route, system, userText, maxTokens, signal) => callLlm(route, system, userText, maxTokens, signal),
463
356
  logger: ctx.logger,
464
357
  });
465
358
  // 版本迁移:有本地原始素材的旧角色在后台自动重蒸馏;只替换基础契约/语料,
466
359
  // 身份名、记忆、习得风格与 corpus pins 均留在独立表中,不参与覆盖。
467
- void identityReady.then(async (store) => {
360
+ void stores.identityReady
361
+ .then(async (store) => {
468
362
  if (!store)
469
363
  return;
470
364
  for (const [personaName, oldCard] of Object.entries(store.listCustomPersonas())) {
@@ -472,7 +366,7 @@ function applyInner(ctx, config = {}) {
472
366
  continue;
473
367
  try {
474
368
  const upgraded = await runDistill({
475
- route: () => resolveAuxRoute(distillRouteOverride, llmRoute),
369
+ route: () => resolveAuxRoute(distillRouteOverride, llmRoute.current),
476
370
  call: (route, system, userText, maxTokens, signal) => callLlm(route, system, userText, maxTokens, signal),
477
371
  logger: ctx.logger,
478
372
  }, { text: oldCard.distillSource, hint: oldCard.distillHint });
@@ -492,591 +386,12 @@ function applyInner(ctx, config = {}) {
492
386
  ctx.logger?.warn?.(`lume: 角色卡 ${personaName} 后台升级失败,保留旧卡`, error);
493
387
  }
494
388
  }
495
- });
389
+ })
390
+ .catch((error) => ctx.logger?.warn?.(`lume: 旧角色后台重蒸馏失败:${describeError(error)}`));
496
391
  // ── 会话事件:路由缓存 + 轮次缓冲 + 提取调度 + 清理 ──
497
- ctx.effect(() => ctx.on("session/event", (session, event) => {
498
- const sid = String(session.id);
499
- const st = runtime.get(sid);
500
- switch (event.type) {
501
- case "request/context": {
502
- // 路由缓存的真正来源:agent-loop 在路由变化时 append 的 request/context
503
- // ({provider, model, contextWindow})。request/header 的载荷是 {header,
504
- // reason},拿不到 provider/model——v0.3.0 一直监听错了事件,提取从未跑通。
505
- const data = event.data;
506
- if (typeof data?.provider === "string" && typeof data?.model === "string") {
507
- llmRoute = { provider: data.provider, model: data.model };
508
- ctx.logger?.warn?.(`lume: request/context 更新 llmRoute → ${llmRoute.provider}/${llmRoute.model}`);
509
- }
510
- else {
511
- ctx.logger?.warn?.("lume: request/context 未携带 provider/model,保留 llmRoute", data);
512
- }
513
- break;
514
- }
515
- case "user/message": {
516
- // 压缩检查点:宿主把被压缩的历史替换成一条摘要消息,必须与真实
517
- // 用户消息区分——否则摘要会被当成「用户当前说的话」,污染协议
518
- // 路由所依赖的 lastQuery 与对话缓冲。这是兜底识别:同一轮里
519
- // compaction/summary 通常先到且带规模,不要把那条覆盖成无规模的。
520
- if (isCompactionCheckpoint(event.data)) {
521
- if (!st.compaction || st.compaction.turnIndex !== st.turnIndex) {
522
- st.compaction = { turnIndex: st.turnIndex, shadowedItems: 0, tokens: 0 };
523
- }
524
- appendLumeLog(`[${sid}] 检测到上下文压缩检查点(第 ${st.turnIndex} 轮)`);
525
- break;
526
- }
527
- // 只有真实用户消息能定义本轮意图。宿主快照(@deepseek-ai/dsh-system-prompt)、
528
- // 工作区指令(agent-instructions)、技能目录(skill-catalog)都经这条通道投递,
529
- // 曾被当成用户发言:覆盖真实请求,并把模式从「执行」冲成「问答」。
530
- if (!isUserAuthored(event.data))
531
- break;
532
- const text = messageText(event.data);
533
- if (text) {
534
- const normalized = text.trim().replace(/\s+/g, " ").slice(0, 240);
535
- const explicitCorrection = /不是这个意思|不是我说的|你理解错|答非所问|听不懂|我说的是|我指的是|不对|错了|别这样|重新来/i.test(text);
536
- const repeatedRequest = normalized.length >= 5 && st.recentUserQueries.includes(normalized);
537
- st.userText = text;
538
- st.alignmentCorrection = explicitCorrection
539
- ? buildAlignmentCorrection("user-correction")
540
- : repeatedRequest
541
- ? buildAlignmentCorrection("repeated-request")
542
- : null;
543
- st.recentUserQueries.push(normalized);
544
- if (st.recentUserQueries.length > 5)
545
- st.recentUserQueries.shift();
546
- st.recentTurns.push(`用户: ${text.slice(0, 300)}`);
547
- if (st.recentTurns.length > 12)
548
- st.recentTurns.shift();
549
- // 模式/阶段的冻结统一由 resolveIntent(组装时读会话权威历史)负责,
550
- // 这里只记账:两处都写会让「同一条消息」被判定为不同轮而反复重算。
551
- }
552
- break;
553
- }
554
- case "assistant/message": {
555
- const text = messageText(event.data?.message);
556
- if (text) {
557
- st.assistantText = text;
558
- st.recentTurns.push(`助手: ${text.slice(0, 300)}`);
559
- if (st.recentTurns.length > 12)
560
- st.recentTurns.shift();
561
- }
562
- break;
563
- }
564
- case "tool/call": {
565
- st.toolCalls++;
566
- if (st.interactionMode === "execute")
567
- st.taskPhase = advancePhase(st.taskPhase, "execute");
568
- // 行为类别在调用阶段记账(连击),成败到结果阶段才结算。
569
- st.toolKind = classifyTool(event.data?.name);
570
- applyToolSignal(st.triggerCounters, st.toolKind, null);
571
- // 自动改动台账:mutate 类工具一被调用就先记一条——实测模型几乎不会主动调 lume_change
572
- // (4 个会话里 0 次),而 edit/write 每次会话几十次。载具必须由插件自己落账,
573
- // 否则「改动台账」永远空着(这正是上一版没生效的地方)。
574
- if (projectMemoryOn && st.toolKind === "mutate") {
575
- const target = targetPathFromToolEvent(event.data);
576
- if (target) {
577
- const toolName = String(event.data?.name ?? "tool");
578
- void projectReady.then((store) => store?.upsertChange(sid, { target, change: `(自动)由 ${toolName} 修改`, why: "", verify: "", status: "done", at: Date.now() }));
579
- }
580
- }
581
- break;
582
- }
583
- case "tool/result": {
584
- const data = event.data;
585
- const resultText = messageText(data?.message);
586
- const explicitError = Boolean(data?.error) || /失败|报错|错误|exception|traceback|timed out|permission denied|unknown|not started/i.test(resultText);
587
- const unknownResult = /结果未知|outcome unknown|tool_not_started|tool_outcome_unknown/i.test(resultText);
588
- if (unknownResult)
589
- st.toolUnknown++;
590
- else if (explicitError)
591
- st.toolFailures++;
592
- else
593
- st.toolSuccesses++;
594
- if (st.interactionMode === "execute")
595
- st.taskPhase = advancePhase(st.taskPhase, unknownResult || explicitError ? "diagnose" : "verify");
596
- // 行为信号 → 计数器 → 触发器提醒。提醒只在这一步之后可见(尾部快照),
597
- // 且每类触发器有轮级冷却:提示一多就变噪音,模型会学会忽略。
598
- const signals = readResultSignals(resultText, explicitError);
599
- applyVerifyOutcome(st.triggerCounters, st.toolKind, signals);
600
- if (behaviorTriggersOn) {
601
- const fire = evaluateToolTrigger(st.triggerCounters, {
602
- turnIndex: st.turnIndex,
603
- isTask: isTaskQuery(st),
604
- diagnosing: st.interactionMode === "diagnosis",
605
- hasContract: contractOf(sid) !== null,
606
- unverifiedChanges: changesOf(sid).filter((item) => item.status !== "verified" && item.status !== "skipped").length,
607
- hypothesesTouched: st.hypothesesTouched,
608
- }, triggerThresholds);
609
- if (fire && cooldownOk(st.triggerFiredAt[fire.id], st.turnIndex)) {
610
- st.triggerFiredAt[fire.id] = st.turnIndex;
611
- st.triggerNudge = fire.text;
612
- ctx.logger?.warn?.(`lume: [${sid}] 行为触发器 ${fire.id}(steps=${st.triggerCounters.steps},inspect=${st.triggerCounters.inspectStreak},mutate=${st.triggerCounters.mutateStreak},verifyFail=${st.triggerCounters.verifyFailStreak})`);
613
- // 环境性死路自动落成项目知识:下次会话不必重踩。
614
- if (fire.id === "dead-path" && signals.env && project) {
615
- const key = projectKeyFor(sid, session);
616
- if (key)
617
- void project.addFact(key, normalizeProjectFact({ kind: "deadend", text: `本环境验证受阻(${st.triggerCounters.verifyFailStreak} 次连续失败,环境/依赖类):换降级阶梯,不要重复同一命令` }, Date.now()), (candidate, existing) => existing.some((fact) => fact.text === candidate));
618
- }
619
- }
620
- }
621
- break;
622
- }
623
- case "compaction/summary": {
624
- // 压缩由宿主 preset 在隔离域执行(Lume 无法接管该服务),但事件在
625
- // 会话总线上可见。记录规模,供下一轮注入「摘要不是完整历史」的重锚。
626
- const data = event.data;
627
- const shadowedItems = Array.isArray(data?.shadowedSeqs) ? data.shadowedSeqs.length : 0;
628
- const tokens = typeof data?.shadowedTokenCount === "number" ? data.shadowedTokenCount : 0;
629
- st.compaction = { turnIndex: st.turnIndex, shadowedItems, tokens };
630
- appendLumeLog(`[${sid}] 压缩完成:替换 ${shadowedItems} 项历史(~${tokens} tokens),下一轮注入状态重锚`);
631
- break;
632
- }
633
- case "turn/end": {
634
- st.turnIndex++;
635
- // 切换窗口的消耗只发生在轮边界(渲染函数只读状态,不再就地清零):
636
- // 同一步里 prompt 会被构建多次,若在渲染里消耗窗口,第二次构建就会
637
- // 丢掉接班招呼——那是「注入随构建次数漂移」,正是本版要消灭的东西。
638
- st.switchGreetingPending = false;
639
- if (st.switchTurn !== null && st.turnIndex - st.switchTurn >= boundaryTurns)
640
- st.switchTurn = null;
641
- // ── 行为触发器(轮边界)──
642
- // 连击按轮清零:新一轮是新请求,上一轮的「撒网」不该继续累加;死路连击
643
- // 跨轮保留(同一环境不可用是会话级事实)。上轮的提醒到这里失效。
644
- st.triggerCounters.inspectStreak = 0;
645
- st.triggerCounters.mutateStreak = 0;
646
- st.triggerNudge = null;
647
- st.turnNudge = null;
648
- st.hypothesesTouched = false;
649
- if (behaviorTriggersOn && project) {
650
- const fire = evaluateTurnTrigger({
651
- turnIndex: st.turnIndex,
652
- hasContract: contractOf(sid) !== null,
653
- compactionTurn: st.compaction?.turnIndex ?? null,
654
- lastDriftTurn: st.lastDriftTurn,
655
- counters: st.triggerCounters,
656
- knowledgePrompted: st.knowledgePrompted,
657
- }, triggerThresholds);
658
- if (fire && cooldownOk(st.triggerFiredAt[fire.id], st.turnIndex)) {
659
- st.triggerFiredAt[fire.id] = st.turnIndex;
660
- if (fire.id === "criteria-drift") {
661
- // 契约对账用「交付口径」渲染原始判据:防判据随进展漂移。
662
- st.lastDriftTurn = st.turnIndex;
663
- st.turnNudge = renderContract(contractOf(sid), true);
664
- }
665
- else {
666
- st.knowledgePrompted = true;
667
- st.turnNudge = fire.text;
668
- }
669
- ctx.logger?.warn?.(`lume: [${sid}] 轮触发器 ${fire.id}(turn=${st.turnIndex})`);
670
- }
671
- }
672
- // 低成本会话内纠偏:只处理明确的错误/失败信号,且要求连续轮次用户请求相同。
673
- const failed = /失败|报错|错误|exception|traceback|cannot|unable|permission denied|timed out|找不到|不存在/i.test(st.assistantText);
674
- const queryKey = st.userText.trim().replace(/\s+/g, " ").slice(0, 240);
675
- if (failed && queryKey && queryKey === st.lastFailureQuery)
676
- st.failureStreak++;
677
- else if (failed && queryKey) {
678
- st.lastFailureQuery = queryKey;
679
- st.failureStreak = 1;
680
- }
681
- else if (!failed) {
682
- st.failureStreak = 0;
683
- st.lastFailureQuery = null;
684
- st.protocolCorrection = null;
685
- }
686
- if (st.failureStreak >= 2)
687
- st.protocolCorrection = "检测到相同请求连续失败:先定位根因并记录已排除假设,再选择不同方案;不要重复同一调用。";
688
- const claimsVerification = /验证|测试|构建|检查|确认生效|实际结果|已通过|未验证|无法验证/i.test(st.assistantText);
689
- st.postTurnReview = st.interactionMode === "execute" && st.assistantText && !claimsVerification
690
- ? "〔上轮交付复核〕上一轮执行回复没有给出可见的验证证据。本轮若继续处理同一任务,先确认上轮变更是否真实生效,再继续扩大范围。"
691
- : null;
692
- if (st.interactionMode === "execute")
693
- st.taskPhase = advancePhase(st.taskPhase, st.toolFailures > 0 || st.toolUnknown > 0 ? "diagnose" : claimsVerification ? "deliver" : "verify");
694
- // 即时对齐只影响当前轮;下一轮重新根据用户消息判断,避免纠偏条款滞留。
695
- st.alignmentCorrection = null;
696
- // 风格泄漏检测挂在 turn/end(该事件已被窗口机制验证可靠;assistant/message
697
- // 的投递在实测中不可靠)。切换完成后逐轮检查回复是否残留旧人设签名词,
698
- // 窗口已关仍检出 → 重开窗口 + 升级播报;一轮干净回复自动解除升级。
699
- if (st.prevSignatures.length > 0 && st.lastInjected !== undefined && st.assistantText) {
700
- const report = detectLeak(st.assistantText, st.prevSignatures);
701
- const inWindow = st.switchTurn !== null && st.turnIndex - st.switchTurn < boundaryTurns;
702
- if (report.leaked && !inWindow) {
703
- st.switchTurn = st.turnIndex;
704
- st.leakEscalated = true;
705
- ctx.logger?.warn?.(`lume: [${sid}] 检测到旧人设风格泄漏(${report.hits.map((h) => `${h.word}×${h.count}`).join("、")}),重新注入升级版切换播报`);
706
- }
707
- else if (!report.leaked) {
708
- st.leakEscalated = false;
709
- }
710
- }
711
- scheduleExtraction(sid, st);
712
- break;
713
- }
714
- default:
715
- // 诊断:压缩事件是否经由 session/event 总线投递(宿主按 session
716
- // 所属上下文收集监听者,隔离域里发出的日志事件可能不经过这里)。
717
- if (typeof event.type === "string" && /compact/i.test(event.type)) {
718
- appendLumeLog(`[${sid}] 收到未处理的压缩事件类型 ${event.type}`);
719
- }
720
- break;
721
- }
722
- }), "lume: session events");
723
- ctx.effect(() => ctx.on("session/disposed", (session) => {
724
- const sid = String(session.id);
725
- const st = runtime.get(sid);
726
- const turns = [...st.recentTurns];
727
- runtime.delete(sid);
728
- // 任务载具是会话态:任务结束即无意义,清掉避免无界增长(项目知识在另一张表,不受影响)。
729
- void projectReady.then((store) => store?.clearSession(sid));
730
- // 反思日志:会话结束后空闲时间跑一次小模型,零用户感知 token。
731
- // 历史不够长(< 4 条消息)或路由不可用时静默跳过。
732
- if (reflectionEnabled && turns.length < 4)
733
- ctx.logger?.warn?.(`lume: 反思跳过(${sid})历史不足:${turns.length} < 4 条消息`);
734
- if (reflectionEnabled && turns.length >= 4) {
735
- void (async () => {
736
- const store = await reflectionReady;
737
- if (!store) {
738
- ctx.logger?.warn?.(`lume: 反思跳过(${sid})reflection 域不可用`);
739
- return;
740
- }
741
- if (!store)
742
- return;
743
- const route = resolveAuxRoute({}, llmRoute);
744
- if (!route) {
745
- ctx.logger?.warn?.(`lume: 反思跳过(${sid})无可用小模型路由`);
746
- return;
747
- }
748
- if (!route)
749
- return;
750
- const prompt = buildReflectionPrompt(turns);
751
- const output = await callLlm(route, prompt.system, prompt.userText, 800);
752
- if (output === null)
753
- return;
754
- const score = parseReflectionScore(output);
755
- if (!score) {
756
- ctx.logger?.warn?.(`lume: 反思跳过(${sid})评分解析失败`);
757
- return;
758
- }
759
- await store.log(sid, score);
760
- ctx.logger?.warn?.(`lume: 反思日志 ${sid} context=${score.context} planning=${score.planning} verification=${score.verification} review=${score.review} diagnosis=${score.diagnosis}「${score.note}」`);
761
- })();
762
- }
763
- }), "lume: session disposal");
392
+ ctx.effect(() => ctx.on("session/event", (session, event) => sessionEventHandler(session, event)), "lume: session events");
393
+ ctx.effect(() => ctx.on("session/disposed", (session) => sessionDisposedHandler(session)), "lume: session disposal");
764
394
  // ── 载具与项目知识的读取入口(事件处理器 / 工具 / 注入三处共用)──
765
- /** 项目键:优先取会话工作目录(跨会话共享同一仓库的知识)。 */
766
- function projectKeyFor(sid, source) {
767
- const st = runtime.get(sid);
768
- if (st.projectKey)
769
- return st.projectKey;
770
- // 三种调用来源:提示词 context({agent:{session}})、工具 exec({agent:{session}})、
771
- // 会话事件(session 本身)。统一取到 session 再读 cwd。
772
- const session = source?.agent?.session ?? source?.session ?? source;
773
- const cwd = String(session?.cwd || st.cwd || "");
774
- const key = projectKeyOf(cwd);
775
- // 只有拿到真实工作目录才缓存:否则一次无 cwd 的调用会把 "unknown" 固化下来。
776
- if (cwd && key)
777
- st.projectKey = key;
778
- return st.projectKey ?? key;
779
- }
780
- /** 从工具入参里取目标路径(自动改动台账用):兼容常见字段名,取不到返回 null——宁可少记,不要记错。 */
781
- function targetPathFromToolEvent(data) {
782
- const args = data?.args ?? data?.input ?? data?.parameters ?? data?.arguments;
783
- if (!args || typeof args !== "object")
784
- return null;
785
- for (const key of ["path", "file_path", "filePath", "file", "filename", "target", "notebook_path"]) {
786
- const value = args[key];
787
- if (typeof value === "string" && value.trim())
788
- return value.trim().slice(0, 120);
789
- }
790
- return null;
791
- }
792
- function isTaskQuery(st) {
793
- return TASK_SIGNAL_RE.test(st.intent?.text ?? st.userText ?? "");
794
- }
795
- function contractOf(sid) {
796
- return project?.getContract(sid) ?? null;
797
- }
798
- function changesOf(sid) {
799
- return project?.getChanges(sid) ?? [];
800
- }
801
- function hypothesesOf(sid) {
802
- return project?.getHypotheses(sid) ?? [];
803
- }
804
- function factsOf(sid, context) {
805
- const projectKey = projectKeyFor(sid, context);
806
- return project && projectKey ? project.getFacts(projectKey) : [];
807
- }
808
- /** 环境里是否有符号级结构分析工具:有就让模型用它替代通篇 read。 */
809
- function structureToolName(context) {
810
- try {
811
- const schemas = ctx.get("tools")?.schemas?.(context?.agent);
812
- if (!Array.isArray(schemas))
813
- return null;
814
- for (const schema of schemas) {
815
- const name = String(schema?.name ?? "");
816
- if (/analy|tree|symbol|lsp|reference|code_map|outline/i.test(name))
817
- return name;
818
- }
819
- return null;
820
- }
821
- catch {
822
- return null;
823
- }
824
- }
825
- /**
826
- * 任务载具块:契约 / 改动台账 / 假设台账 / 项目知识 + 方法块 + 触发器提醒。
827
- *
828
- * 全部落在尾部快照(易变层)——这正是「载具」现在才做得起的原因:0.6.2 之前每步
829
- * 注入一份会变的状态等于每步作废整段前缀,而快照只在内容变化时才付费(实测 58 步
830
- * 只产生 9 条快照)。顺序上把「此刻最该做的一件事」(触发器提醒)放在最后。
831
- */
832
- function carrierBlocks(sid, context, st, query, mode) {
833
- if (!projectMemoryOn)
834
- return [];
835
- const isTask = mode !== "question" || TASK_SIGNAL_RE.test(query);
836
- const contract = contractOf(sid);
837
- const changes = changesOf(sid);
838
- const docDirective = buildDocumentDirective({ query, capabilities: probeDocumentCapabilities(ctx.get("tools"), context?.agent) });
839
- return [
840
- // 契约:有就回显(交付轮切成对账口径),没有且是任务轮就先教它写一份。
841
- { text: renderContract(contract, st.taskPhase === "deliver") },
842
- { text: !contract && isTask ? buildContractMethodDirective() : null, droppable: true },
843
- // 台账与假设:存在就回显——让模型「看见」自己的计划,而不是记在脑子里。
844
- { text: changes.length > 0 ? renderChangeLedger(changes) : null },
845
- { text: renderHypotheses(hypothesesOf(sid)) },
846
- // 项目知识:只在与项目相关的轮次出现(闲聊不该背仓库事实)。
847
- { text: isTask ? renderProjectFacts(factsOf(sid, context)) : null, droppable: true },
848
- // 方法块:按任务形态出现;文档方法论只在判定为文档任务时出现。
849
- { text: isTask && mode !== "question" ? buildImpactDirective() : null, droppable: true },
850
- { text: docDirective ? buildDocumentMethodDirective() : null },
851
- { text: mode === "execute" || mode === "diagnosis" ? buildStructureHint(structureToolName(context)) : null, droppable: true },
852
- { text: st.turnNudge },
853
- { text: st.triggerNudge },
854
- ];
855
- }
856
- // ── 模型可调用工具(主写入通道)──
857
- // 工具 output schema 的 const 语义要求成功值恒为 { ok: true };失败一律抛错交由框架呈现。
858
- // as const 让 defineTool 从字面量推断 O,三个工具共用同一份成功形状。
859
- const OK_OUTPUT_SCHEMA = {
860
- type: "object",
861
- additionalProperties: false,
862
- properties: { ok: { type: "boolean", const: true, required: true } },
863
- };
864
- function dutyPersona(exec) {
865
- const sid = exec?.agent?.session?.id;
866
- const st = sid !== undefined ? runtime.get(String(sid)) : undefined;
867
- return st?.lastInjected ?? defaultName;
868
- }
869
- ctx.effect(() => {
870
- ctx.tools.register(defineTool({
871
- name: "lume_remember",
872
- description: "记住关于用户或你们关系的持久事实(偏好、习惯、背景、称呼)。仅当信息明确值得长期记住时调用;每次一条,40 字以内。不要记录工作内容、代码或项目机密。",
873
- parameters: {
874
- text: { type: "string", required: true, description: "要长期记住的事实,第三人称陈述句,≤40 字" },
875
- },
876
- output: { schema: OK_OUTPUT_SCHEMA, render: () => [{ type: "text", text: "已保存" }] },
877
- execute: async (args, exec) => {
878
- if (!identity)
879
- throw new Error("lume identity store is unavailable");
880
- const personaName = dutyPersona(exec);
881
- if (!personaName)
882
- throw new Error("lume_remember requires an active persona (当前没有当值人设)");
883
- await identity.addMemory(personaName, String(args.text), isDuplicateFact);
884
- return { ok: true };
885
- },
886
- }));
887
- ctx.tools.register(defineTool({
888
- name: "lume_update_style",
889
- description: "把用户对你说话方式的新要求固化为长期风格约定(如「少用 emoji」「自称改成XX」)。仅当用户明确提出风格/语气要求时调用,每条一句话。",
890
- parameters: {
891
- rule: { type: "string", required: true, description: "风格约定,一句话祈使句" },
892
- },
893
- output: { schema: OK_OUTPUT_SCHEMA, render: () => [{ type: "text", text: "已保存" }] },
894
- execute: async (args, exec) => {
895
- if (!identity)
896
- throw new Error("lume identity store is unavailable");
897
- const personaName = dutyPersona(exec);
898
- if (!personaName)
899
- throw new Error("lume_update_style requires an active persona (当前没有当值人设)");
900
- await identity.addStyleRule(personaName, String(args.rule), (a, b) => jaccard(a, b) >= 0.6);
901
- return { ok: true };
902
- },
903
- }));
904
- ctx.tools.register(defineTool({
905
- name: "lume_create_persona",
906
- description: "创建一个全新的自定义人设。仅当用户明确想新建人设时使用:先在对话中访谈收集(人设的名字、性格、说话方式、对用户的称呼),收集完整后再调用本工具保存,并告知用户保存成功。",
907
- parameters: {
908
- name: { type: "string", required: true, description: "人设英文键名,小写字母开头,≤32 字符(如 tsundere)" },
909
- displayName: { type: "string", required: true, description: "界面显示名(如「傲娇」)" },
910
- description: { type: "string", required: true, description: "一句话简介" },
911
- promptText: { type: "string", required: true, description: "完整风格契约:称呼/emoji/语气词/节奏/立场,与内置契约同构" },
912
- },
913
- output: { schema: OK_OUTPUT_SCHEMA, render: () => [{ type: "text", text: "已保存" }] },
914
- execute: async (args) => {
915
- if (!identity)
916
- throw new Error("lume identity store is unavailable");
917
- await identity.setCustomPersona(String(args.name), {
918
- displayName: String(args.displayName),
919
- description: String(args.description ?? ""),
920
- promptText: String(args.promptText),
921
- createdAt: Date.now(),
922
- });
923
- return { ok: true };
924
- },
925
- }));
926
- // ── 任务载具工具(第二组写入通道)──
927
- // 与人格工具一样是「模型主动调用、零额外 LLM 调用」,区别在写入对象:契约/台账/假设
928
- // 属于当前任务(会话态),项目知识按工作目录跨会话累积。列表类参数统一用字符串
929
- // 分隔(分号或换行),不引入数组 schema——省 schema token,也少一层校验风险。
930
- const splitList = (value) => String(value ?? "")
931
- .split(/[;;\n]/)
932
- .map((item) => item.trim())
933
- .filter(Boolean);
934
- ctx.effect(() => {
935
- ctx.tools.register(defineTool({
936
- name: "lume_contract",
937
- description: "写下或更新本任务的任务契约(需求量化的落点):目标、范围、数量、完成判据、非目标、待确认。任务型请求开工前调用一次;探索后回填实际数量;之后只传变化的字段即可(局部更新)。",
938
- parameters: {
939
- goal: { type: "string", description: "目标:一句话、可观察的结果" },
940
- scope: { type: "string", description: "范围:路径/模块/章节,分号或换行分隔" },
941
- expectCount: { type: "number", required: true, description: "预计数量(探索前先估)" },
942
- actualCount: { type: "number", description: "实际数量(探索后回填)" },
943
- criteria: { type: "string", description: "完成判据:可执行、可核对,分号或换行分隔" },
944
- nonGoals: { type: "string", description: "非目标:明确不动的东西,分号分隔" },
945
- open: { type: "string", description: "待确认:只列真正阻塞的(≤2 个),分号分隔" },
946
- },
947
- output: { schema: OK_OUTPUT_SCHEMA, render: () => [{ type: "text", text: "已记录任务契约" }] },
948
- execute: async (args, exec) => {
949
- if (!project)
950
- throw new Error("lume project store is unavailable");
951
- const sid = String(exec?.agent?.session?.id ?? "");
952
- if (!sid)
953
- throw new Error("lume_contract requires an active session");
954
- const st = runtime.get(sid);
955
- const normalized = normalizeContract({
956
- goal: args.goal,
957
- scope: splitList(args.scope),
958
- expectCount: args.expectCount,
959
- actualCount: args.actualCount,
960
- criteria: splitList(args.criteria),
961
- nonGoals: splitList(args.nonGoals),
962
- open: splitList(args.open),
963
- }, Date.now(), st.turnIndex);
964
- const existing = project.getContract(sid);
965
- if (existing) {
966
- // 局部更新:未传的字段保持原值(回填数量时不该把判据清空)。
967
- const patch = {};
968
- if (args.goal !== undefined)
969
- patch.goal = normalized.goal;
970
- if (args.scope !== undefined)
971
- patch.scope = normalized.scope;
972
- if (args.expectCount !== undefined)
973
- patch.expectCount = normalized.expectCount;
974
- if (args.actualCount !== undefined)
975
- patch.actualCount = normalized.actualCount;
976
- if (args.criteria !== undefined)
977
- patch.criteria = normalized.criteria;
978
- if (args.nonGoals !== undefined)
979
- patch.nonGoals = normalized.nonGoals;
980
- if (args.open !== undefined)
981
- patch.open = normalized.open;
982
- await project.patchContract(sid, patch);
983
- }
984
- else {
985
- if (!normalized.goal)
986
- throw new Error("lume_contract requires a goal on first write");
987
- await project.setContract(sid, normalized);
988
- }
989
- return { ok: true };
990
- },
991
- }));
992
- ctx.tools.register(defineTool({
993
- name: "lume_change",
994
- description: "改动台账:记录/更新一处将要改或已改的位置(文件/符号/文档章节 → 改什么 → 怎么验 → 状态)。动手前先列计划项,改完推进状态;只推进状态时可只传 target + status。文档任务用章节名当 target,形成分节记账。",
995
- parameters: {
996
- target: { type: "string", required: true, description: "目标位置:文件路径 / 符号 / 文档章节" },
997
- change: { type: "string", description: "改什么(一句话)" },
998
- why: { type: "string", description: "为什么改(对齐契约的哪一条)" },
999
- verify: { type: "string", description: "怎么验(命令 / 回读 / 对照)" },
1000
- status: { type: "string", description: "planned | done | verified | skipped" },
1001
- },
1002
- output: { schema: OK_OUTPUT_SCHEMA, render: () => [{ type: "text", text: "已更新改动台账" }] },
1003
- execute: async (args, exec) => {
1004
- if (!project)
1005
- throw new Error("lume project store is unavailable");
1006
- const sid = String(exec?.agent?.session?.id ?? "");
1007
- if (!sid)
1008
- throw new Error("lume_change requires an active session");
1009
- const target = String(args.target ?? "").trim();
1010
- if (!target)
1011
- throw new Error("lume_change requires a target");
1012
- const status = args.status;
1013
- const allowed = status === "planned" || status === "done" || status === "verified" || status === "skipped" ? status : undefined;
1014
- if (args.change === undefined && allowed !== undefined) {
1015
- const hit = await project.setChangeStatus(sid, target, allowed);
1016
- if (!hit)
1017
- throw new Error(`lume_change: no ledger entry for ${target}`);
1018
- return { ok: true };
1019
- }
1020
- const item = normalizeChange({ target, change: args.change, why: args.why, verify: args.verify, status: allowed }, Date.now());
1021
- if (!item)
1022
- throw new Error("lume_change requires target and change");
1023
- await project.upsertChange(sid, item);
1024
- return { ok: true };
1025
- },
1026
- }));
1027
- ctx.tools.register(defineTool({
1028
- name: "lume_hypothesis",
1029
- description: "假设台账:记录一条正在验证的假设及其证据与状态(open/testing/confirmed/excluded)。排查类任务里每验证一次就更新状态;已排除的假设不要再重复尝试。",
1030
- parameters: {
1031
- text: { type: "string", required: true, description: "假设内容,一句话" },
1032
- evidence: { type: "string", description: "支持或推翻它的观察(含命令输出/时间戳摘要)" },
1033
- status: { type: "string", description: "open | testing | confirmed | excluded" },
1034
- },
1035
- output: { schema: OK_OUTPUT_SCHEMA, render: () => [{ type: "text", text: "已更新假设台账" }] },
1036
- execute: async (args, exec) => {
1037
- if (!project)
1038
- throw new Error("lume project store is unavailable");
1039
- const sid = String(exec?.agent?.session?.id ?? "");
1040
- if (!sid)
1041
- throw new Error("lume_hypothesis requires an active session");
1042
- const item = normalizeHypothesis({ text: args.text, evidence: args.evidence, status: args.status }, Date.now());
1043
- if (!item)
1044
- throw new Error("lume_hypothesis requires text");
1045
- await project.upsertHypothesis(sid, item);
1046
- runtime.get(sid).hypothesesTouched = true;
1047
- return { ok: true };
1048
- },
1049
- }));
1050
- ctx.tools.register(defineTool({
1051
- name: "lume_project_note",
1052
- description: "记录一条**稳定的项目事实**(按工作目录跨会话累积):构建/测试命令、模块数据流、仓库约定、或一条死路(试过但行不通的做法)。只记可复用、已验证的事实,不要记一次性进展。",
1053
- parameters: {
1054
- kind: { type: "string", required: true, description: "build | test | module | convention | deadend" },
1055
- text: { type: "string", required: true, description: "事实本身,一句话,≤200 字" },
1056
- },
1057
- output: { schema: OK_OUTPUT_SCHEMA, render: () => [{ type: "text", text: "已记入项目知识" }] },
1058
- execute: async (args, exec) => {
1059
- if (!project)
1060
- throw new Error("lume project store is unavailable");
1061
- const sid = String(exec?.agent?.session?.id ?? "");
1062
- if (!sid)
1063
- throw new Error("lume_project_note requires an active session");
1064
- const fact = normalizeProjectFact({ kind: args.kind, text: args.text }, Date.now());
1065
- if (!fact)
1066
- throw new Error("lume_project_note requires text");
1067
- const projectKey = projectKeyFor(sid, { agent: exec?.agent });
1068
- if (!projectKey) {
1069
- // 拿不到工作目录:不写跨会话表——写一次就会把不同项目的知识串进同一个键(现场事故:facts 的键曾是 "unknown")
1070
- ctx.logger?.warn?.(`lume: [${sid}] 项目知识未落盘(无法确定工作目录)`);
1071
- return { ok: true };
1072
- }
1073
- await project.addFact(projectKey, fact, (candidate, existing) => existing.some((entry) => jaccard(entry.text, candidate) >= 0.7));
1074
- return { ok: true };
1075
- },
1076
- }));
1077
- }, "lume: carrier tools");
1078
- }, "lume: persona tools");
1079
- // ── 人设五段式注入 + 切换播报 ──
1080
395
  /**
1081
396
  * 权威意图解析:与宿主会话历史对账,而不是只信事件缓存。
1082
397
  *
@@ -1085,7 +400,7 @@ function applyInner(ctx, config = {}) {
1085
400
  * 读会话投影出的消息(`deriveMessages`,宿主侧带增量缓存),取最后一条真实用户
1086
401
  * 消息作为本轮意图;messageId 未变时零成本短路,保证一轮内只冻结一次。
1087
402
  */
1088
- function resolveIntent(context, st) {
403
+ function resolveIntent(sid, context, st) {
1089
404
  const session = context?.agent?.session;
1090
405
  if (typeof session?.cwd === "string" && session.cwd)
1091
406
  st.cwd = session.cwd;
@@ -1110,7 +425,20 @@ function applyInner(ctx, config = {}) {
1110
425
  messageId = `text:${st.userText.slice(0, 120)}`;
1111
426
  }
1112
427
  if (text !== null && (st.intent === null || st.intent.messageId !== messageId)) {
1113
- const mode = classifyInteraction(text);
428
+ // 路由从「这一句话的词」升到「最近几轮轨迹的证据」(0.8.x):纠正当场按被纠正前那句话重算、
429
+ // 在途任务对承接式追问有粘性、任务型轨迹上不因一句没有动词的话掉回问答。
430
+ // 路由是唯一「判错就全盘错」的决策,所以判定结论(模式 + 命中判据 + 证据来源)一并落进度量。
431
+ // recentUserQueries 里**已经包含这一句**(user/message 事件先到):轨迹证据必须把它剔掉,
432
+ // 否则「最近三轮有两轮任务型」会被当前句自己凑够——那是自证,不是证据。
433
+ const normalizedQuery = text.trim().replace(/\s+/g, " ").slice(0, 240);
434
+ const decision = classifyWithTrajectory({
435
+ text,
436
+ recentUserTexts: st.recentUserQueries.filter((item) => item !== normalizedQuery),
437
+ prevMode: st.interactionMode,
438
+ prevPhase: st.taskPhase,
439
+ hadMutations: st.triggerCounters.mutations > 0,
440
+ });
441
+ const mode = decision.mode;
1114
442
  st.intent = { turnIndex: st.turnIndex, messageId, text };
1115
443
  st.lastQuery = text;
1116
444
  st.interactionMode = mode;
@@ -1119,18 +447,189 @@ function applyInner(ctx, config = {}) {
1119
447
  st.toolSuccesses = 0;
1120
448
  st.toolFailures = 0;
1121
449
  st.toolUnknown = 0;
450
+ metricsLog.record({
451
+ kind: "route",
452
+ at: Date.now(),
453
+ sid,
454
+ turn: st.turnIndex,
455
+ mode: decision.mode,
456
+ matched: decision.matched,
457
+ source: decision.source,
458
+ excerpt: text.trim().replace(/\s+/g, " ").slice(0, 120),
459
+ ...(decision.evidence ? { evidence: decision.evidence.trim().replace(/\s+/g, " ").slice(0, 120) } : {}),
460
+ });
1122
461
  }
1123
462
  return { text: st.intent?.text ?? st.lastQuery ?? "", mode: st.interactionMode };
1124
463
  }
464
+ /**
465
+ * 本步的注入文本,按「会话恒定 / 每步易变」分成四段。
466
+ *
467
+ * 恒定段必须在一个会话内逐字节不变——它排在消息序列最前面,变一个字就作废它
468
+ * 后面的全部内容(工具定义、结构化输出、**整段对话历史**)。所以恒定段只允许
469
+ * 依赖会话级状态:当值人设、契约正文、身份名、模型能力。任何按 query / 轮次 /
470
+ * 阶段变化的内容都归易变段。
471
+ */
472
+ /** 提示块装配的依赖:块表在 host/prompt-blocks.ts,这里只负责接线。 */
473
+ /** 会话事件处理器的依赖(处理器本体在 host/session-events.ts,这里只接线)。 */
474
+ /** disposed 处理器复用同一批依赖。 */
475
+ // ── 运行时度量(0.8.x)──
476
+ // 放在装配最前面:它是后面所有优化的**靶子**——没有度量,「路由改准了没有 / 条款加权有没有用」
477
+ // 都只能靠感觉(这正是上一轮改动没能证明自己变聪明的原因)。
478
+ const metricsLog = createMetricsLog({ enabled: metricsEnabled });
479
+ // 回读磁盘尾部:跨会话趋势(纠正率是升还是降)比单个会话更有信息量。
480
+ metricsLog.loadFromDisk();
481
+ // ── 依赖装配:三个 deps 对象集中在 host/wiring.ts(无状态函数由它自己 import)──
482
+ // 这里只传「index 才有的运行期状态」:ctx / 存储句柄 / 会话态 / 模型路由 / 项目访问入口 / 配置开关。
483
+ const wiring = {
484
+ ctx,
485
+ runtime,
486
+ stores: {
487
+ identity: () => stores.identity(),
488
+ project: () => stores.project(),
489
+ reflectionReady: stores.reflectionReady,
490
+ },
491
+ access: {
492
+ contractOf,
493
+ changesOf,
494
+ designOf,
495
+ requirementsOf,
496
+ hypothesesOf,
497
+ factsOf,
498
+ projectKeyFor,
499
+ flushPendingFacts,
500
+ settleVerification,
501
+ saveSessionMemory,
502
+ taskMemoriesOf,
503
+ needsDesignPass,
504
+ structureToolName,
505
+ },
506
+ projectTask: stores.projectTask,
507
+ llmRoute,
508
+ callLlm,
509
+ defaultName,
510
+ isTaskQuery,
511
+ scheduleExtraction,
512
+ triggerThresholds,
513
+ boundaryTurns,
514
+ taskSignalRe: TASK_SIGNAL_RE,
515
+ docArtifactRe: DOC_ARTIFACT_RE,
516
+ probeCaps: (context) => probeDocumentCapabilities(structureToolName(context), context),
517
+ reflectionFeedback: () => stores.reflectionStore()?.getFeedback() ?? null,
518
+ projectMemoryOn,
519
+ behaviorTriggersOn,
520
+ reflectionEnabled,
521
+ metrics: {
522
+ record: (record) => metricsLog.record(record),
523
+ summary: (scope) => metricsLog.summaryText(scope),
524
+ health: (sid) => metricsLog.health(sid),
525
+ },
526
+ };
527
+ // 会话补蒸馏:把最近 7 天的会话(**含已经撑满、聊不动的那些**)榨成跨会话知识。
528
+ // 为什么必须在这里做:上下文撑满 → 宿主压缩会失败(现场:compaction/end error=context overflow)→
529
+ // 会话再也产不出事件 → 期间没沉淀的知识会永久丢;但会话记录还躺在硬盘上。
530
+ // 分片执行(每片一个会话)以免阻塞宿主同进程的事件循环;幂等来自 addFact 的相似度去重。
531
+ ctx.effect(() => {
532
+ let stopBackfill = null;
533
+ void stores.projectReady
534
+ .then((store) => {
535
+ if (!store)
536
+ return;
537
+ stopBackfill = startSessionBackfill({
538
+ log: (message) => {
539
+ // 日志同时走宿主 logger:appendLumeLog 依赖 DSH_HOME,宿主进程里可能没有 →
540
+ // 补蒸馏会“跑了却没有任何日志”(现场踩过:重启后零沉淀零日志)。
541
+ appendLumeLog(message);
542
+ ctx.logger?.warn?.(`lume: ${message}`);
543
+ },
544
+ addFact: (key, fact) => store.addFact(key, fact, isDuplicateFact),
545
+ });
546
+ })
547
+ .catch((error) => ctx.logger?.warn?.(`lume: 会话补蒸馏启动失败:${describeError(error)}`));
548
+ return () => stopBackfill?.();
549
+ });
550
+ const sessionEventDeps = assembleSessionEventDeps(wiring);
551
+ const toolDeps = assembleToolDeps(wiring);
552
+ const blockDeps = assembleBlockDeps(wiring);
553
+ /**
554
+ * 每轮落两条度量:一条**状态快照**(效能判定的基线:契约/设计/台账/假设/计数器)
555
+ * 与一条**块装配**(预算压力 + 本轮加权了哪三条条款)。
556
+ *
557
+ * 同一步里提示词会被构建多次,所以状态快照按**步**去重(步键 = 轮号 + 本轮工具调用数,见下)——
558
+ * 否则长轮次里只有轮首那一条,效能判定就没有分辨率(真机实测:跑了十几分钟只有 3 条)。
559
+ * 块装配不按步去重:它记的是「这一步实际注入了什么」,逐步都要。
560
+ */
561
+ const metricsTurnSeen = new Map();
562
+ function recordTurnMetrics(sid, st, query, mode, composed) {
563
+ if (!metricsLog.enabled)
564
+ return;
565
+ const changes = changesOf(sid);
566
+ const unverified = changes.filter((item) => item.status !== "verified" && item.status !== "skipped").length;
567
+ const health = metricsLog.health(sid);
568
+ const state = {
569
+ hasContract: contractOf(sid) !== null,
570
+ unverifiedChanges: unverified,
571
+ // 纠正闭环:本模式被纠正过的次数与最后一次的轮号(与装配侧同一份数据,见 host/clauses.ts)
572
+ correctionModes: health.correctionsByMode,
573
+ lastCorrectionTurnByMode: health.lastCorrectionTurnByMode,
574
+ };
575
+ metricsLog.record({
576
+ kind: "blocks",
577
+ at: Date.now(),
578
+ sid,
579
+ turn: st.turnIndex,
580
+ mode,
581
+ kept: composed.kept,
582
+ dropped: composed.dropped,
583
+ chars: composed.chars,
584
+ budget: composed.budget,
585
+ focus: focusIdsFor(st, mode, query, state),
586
+ });
587
+ // 按**步**(而不是按轮)去重:一条长轮次里如果只记轮首快照,效能判定就没有分辨率
588
+ // (真机实测:一个跑了十几分钟的轮次只有 3 条记录)。步键取「轮号 + 本轮工具调用数」——
589
+ // 有工具动作才可能变,纯生成步骤不会灌水。
590
+ const stepKey = `${st.turnIndex}:${st.toolCalls}`;
591
+ if (metricsTurnSeen.get(sid) === stepKey)
592
+ return;
593
+ if (metricsTurnSeen.size > 400)
594
+ metricsTurnSeen.clear();
595
+ metricsTurnSeen.set(sid, stepKey);
596
+ metricsLog.record({
597
+ kind: "state",
598
+ at: Date.now(),
599
+ sid,
600
+ turn: st.turnIndex,
601
+ counters: { ...st.triggerCounters },
602
+ hasContract: state.hasContract,
603
+ designs: designOf(sid).length,
604
+ changes: changes.length,
605
+ unverified,
606
+ verified: changes.filter((item) => item.status === "verified").length,
607
+ hypotheses: hypothesesOf(sid).length,
608
+ });
609
+ }
610
+ const sessionDisposedHandler = createSessionDisposedHandler(sessionEventDeps);
611
+ const sessionEventHandler = createSessionEventHandler(sessionEventDeps);
612
+ /** 工具注册的依赖(工具定义本体在 host/tools.ts)。 */
613
+ // 工具定义本体在 host/tools.ts(调用点必须在 deps 声明之后)
614
+ registerLumeTools(toolDeps);
1125
615
  function computeTurn(sid, context) {
1126
616
  const st = runtime.get(sid);
1127
617
  // 意图取自会话权威历史(resolveIntent),不再读事件缓存里的最新文本:
1128
618
  // 后者会被注入消息覆盖,且首步可能还带着上一轮的值,导致模式在轮内漂移。
1129
- const { text: query, mode } = resolveIntent(context, st);
619
+ const { text: query, mode } = resolveIntent(sid, context, st);
620
+ // 路由自校:本会话已被用户纠正两次以上 → 顶一句带具体数字的提醒(上限 2 次防噪音)。
621
+ // 这是度量**回灌**到行为的那一环:只有记录没有回流,度量就只是日志。
622
+ const routeHealth = metricsLog.health(sid);
623
+ if (routeHealth.corrections >= 2 && noticeOpen(st, "metrics")) {
624
+ const modes = Object.entries(routeHealth.correctionsByMode)
625
+ .map(([name, count]) => `${name} ${count}`)
626
+ .join(" · ");
627
+ setNotice(st, "metrics", `〔路由自校〕本会话已有 ${routeHealth.corrections} 次判定被用户纠正${modes ? `(落在:${modes})` : ""}。这一轮先复述你理解的真实请求再动手;判不准就问一句,不要按上一轮的模式惯性继续。`);
628
+ }
1130
629
  // 协议正文按模型能力冻结(不随 query 切变体):切变体会让系统提示词每轮改写,
1131
630
  // 代价远超省下的几百 token。见 selectStableThinkingProtocol。
1132
631
  const thinkingStable = selectStableThinkingProtocol({
1133
- isReasoningModel: typeof llmRoute?.model === "string" && REASONING_MODEL_RE.test(llmRoute.model),
632
+ isReasoningModel: typeof llmRoute.current?.model === "string" && REASONING_MODEL_RE.test(llmRoute.current.model),
1134
633
  }).trim();
1135
634
  // 易变的任务指令:路由、阶段、闲聊声明、长会话护栏、目标锚点、即时对齐、
1136
635
  // 交付复核、压缩重锚、文档能力指引、失败纠偏、反思提醒——全部每步可变。
@@ -1139,24 +638,16 @@ function applyInner(ctx, config = {}) {
1139
638
  // 记录工作目录:工具 exec / 会话事件里可能拿不到 cwd,项目键靠这里缓存兜住(实测项目知识曾落到 unknown)
1140
639
  if (typeof context?.agent?.session?.cwd === "string" && context.agent.session.cwd)
1141
640
  st.cwd = context.agent.session.cwd;
1142
- const thinkingRuntime = composeBlocks([
1143
- { text: buildInteractionDirective(mode) },
1144
- { text: buildTaskPhaseDirective(st.taskPhase) },
1145
- { text: buildCasualDirective(TASK_SIGNAL_RE.test(query)) },
1146
- { text: buildLongSessionGuard(st.turnIndex) },
1147
- { text: buildSessionAnchor(st.turnIndex, mode, query, st.recentTurns) },
1148
- { text: st.alignmentCorrection },
1149
- { text: st.postTurnReview },
1150
- { text: st.compaction ? buildCompactionNotice(st.compaction, st.turnIndex) : null },
1151
- { text: st.protocolCorrection },
1152
- { text: reflectionStore?.getFeedback() ?? null, droppable: true },
1153
- ...carrierBlocks(sid, context, st, query, mode),
1154
- ]);
641
+ // cwd 到手就补落盘暂存的项目知识(这条路径是"cwd 后到"的主要补写时机)
642
+ flushPendingFacts(sid, context);
643
+ const composed = composeBlocksDetailed(volatileBlocks(blockDeps, { sid, context, st, query, mode }));
644
+ recordTurnMetrics(sid, st, query, mode, composed);
645
+ const thinkingRuntime = composed.text;
1155
646
  // 会话选择尚未就绪(启动竞态):只出任务协议,人设段留空——与旧实现一致,
1156
647
  // 也避免把「尚未选择」误记成一次人设切换。
1157
- if (!currentStore)
648
+ if (!stores.currentStore())
1158
649
  return { thinkingStable, thinkingRuntime, personaStable: "", personaData: "", boundary: "" };
1159
- const selected = currentStore.get(sid);
650
+ const selected = stores.currentStore().get(sid);
1160
651
  const personaName = selected ?? defaultName;
1161
652
  const previous = st.lastInjected;
1162
653
  if (previous !== undefined && previous !== personaName) {
@@ -1182,9 +673,9 @@ function applyInner(ctx, config = {}) {
1182
673
  const personaStable = buildPersonaContractSection({ persona, profileName });
1183
674
  const personaData = buildPersonaRuntimeSection({
1184
675
  persona,
1185
- memories: personaName ? identity?.getMemory(personaName) ?? [] : [],
1186
- styleRules: personaName ? identity?.getStyleRules(personaName) ?? [] : [],
1187
- corpusPins: personaName ? identity?.getCorpusPins(personaName) ?? [] : [],
676
+ memories: personaName ? (stores.identity()?.getMemory(personaName) ?? []) : [],
677
+ styleRules: personaName ? (stores.identity()?.getStyleRules(personaName) ?? []) : [],
678
+ corpusPins: personaName ? (stores.identity()?.getCorpusPins(personaName) ?? []) : [],
1188
679
  query,
1189
680
  turnIndex: st.turnIndex,
1190
681
  sessionKey: sid,
@@ -1210,9 +701,7 @@ function applyInner(ctx, config = {}) {
1210
701
  if (layeredOn)
1211
702
  return part === "thinking" ? turn.thinkingStable : turn.personaStable;
1212
703
  const stable = part === "thinking" ? turn.thinkingStable : turn.personaStable;
1213
- const dynamic = part === "thinking"
1214
- ? turn.thinkingRuntime
1215
- : [turn.personaData, turn.boundary].filter(Boolean).join("\n\n");
704
+ const dynamic = part === "thinking" ? turn.thinkingRuntime : [turn.personaData, turn.boundary].filter(Boolean).join("\n\n");
1216
705
  return [stable, dynamic].filter(Boolean).join("\n\n");
1217
706
  }
1218
707
  /** 易变段取文:任务指令 / 人设数据 / 切换播报三段各自成 context,由宿主按 order 归并成一条尾部快照。 */
@@ -1228,38 +717,40 @@ function applyInner(ctx, config = {}) {
1228
717
  return builtins;
1229
718
  },
1230
719
  get store() {
1231
- return currentStore;
720
+ return stores.currentStore();
1232
721
  },
1233
722
  get registry() {
1234
723
  return registry;
1235
724
  },
1236
725
  get identity() {
1237
- return identity;
726
+ return stores.identity();
1238
727
  },
1239
728
  get distill() {
1240
729
  return distillRunner;
1241
730
  },
1242
731
  getProjectState(sessionId) {
1243
732
  // 诊断视图:任务载具 + 项目知识(供排查"模型到底看到了什么")。
1244
- if (!project)
733
+ if (!stores.project())
1245
734
  return null;
1246
735
  const st = runtime.get(sessionId);
1247
736
  return {
1248
737
  projectKey: st.projectKey,
1249
- contract: project.getContract(sessionId),
1250
- changes: project.getChanges(sessionId),
1251
- hypotheses: project.getHypotheses(sessionId),
1252
- facts: st.projectKey ? project.getFacts(st.projectKey) : [],
738
+ contract: stores.project().getContract(sessionId),
739
+ changes: stores.project().getChanges(sessionId),
740
+ hypotheses: stores.project().getHypotheses(sessionId),
741
+ facts: st.projectKey ? stores.project().getFacts(st.projectKey) : [],
742
+ design: stores.project() ? stores.project().getDesign(sessionId) : [],
743
+ requirements: stores.project() ? stores.project().getRequirements(sessionId) : [],
1253
744
  triggers: { ...st.triggerCounters, fired: st.triggerFiredAt },
1254
745
  };
1255
746
  },
1256
747
  async clearProjectFacts(sessionId) {
1257
- if (!project)
748
+ if (!stores.project())
1258
749
  return false;
1259
750
  const st = runtime.get(sessionId);
1260
751
  if (!st.projectKey)
1261
752
  return false;
1262
- await project.clearFacts(st.projectKey);
753
+ await stores.project().clearFacts(st.projectKey);
1263
754
  return true;
1264
755
  },
1265
756
  });
@@ -1267,66 +758,25 @@ function applyInner(ctx, config = {}) {
1267
758
  // 挪到 apply 的最后注册:它只服务客户端菜单(人设列表/蒸馏/管理),
1268
759
  // 绝不该挡住人设段、工具与易变段的注册(0.7.1 的教训:这里抛错 → 整段 apply 中断 → 界面看不到人设)。
1269
760
  // 注册本身见下方 registerRpcChannel()。
1270
- // ── 系统提示词段落 ──
1271
- ctx.effect(() => ctx.systemPrompt.section({
1272
- name: LUME_PERSONA_SECTION,
1273
- order: personaOrder,
1274
- text: (context) => {
1275
- const sid = context.agent?.session?.id ?? context.agent?.id;
1276
- return sid ? systemSectionText(String(sid), context, "persona") : "";
1277
- },
1278
- }), "lume.persona-section()");
1279
- // 易变段走 runtime-context 通道:宿主把它渲染成对话尾部的一条快照消息(文案自带
1280
- // "supersedes earlier runtime-context snapshots",新快照取代旧快照,不堆叠进历史),
1281
- // 因此它的每一次变化只花自己那几百 token,不作废前面的任何前缀。
1282
- // 宿主不支持该 API 时 layeredOn=false,易变段已并回 system 段(见 systemSectionText)。
1283
- if (layeredOn) {
1284
- const dynamicContexts = [
761
+ // ── 系统提示词段与易变段注册(三条通道的用意见 host/sections.ts)──
762
+ installPromptSections({
763
+ ctx,
764
+ layeredOn,
765
+ personaSection: LUME_PERSONA_SECTION,
766
+ personaOrder,
767
+ thinkingSection: LUME_THINKING_SECTION,
768
+ thinkingOrder: LUME_THINKING_ORDER,
769
+ contexts: [
1285
770
  { name: LUME_RUNTIME_CONTEXT, order: LUME_RUNTIME_ORDER, part: "thinking" },
1286
771
  { name: LUME_PERSONA_RUNTIME_CONTEXT, order: LUME_PERSONA_RUNTIME_ORDER, part: "persona" },
1287
772
  { name: LUME_BOUNDARY_CONTEXT, order: LUME_BOUNDARY_ORDER, part: "boundary" },
1288
- ];
1289
- for (const entry of dynamicContexts) {
1290
- ctx.effect(() => ctx.systemPrompt.context({
1291
- name: entry.name,
1292
- order: entry.order,
1293
- text: (context) => {
1294
- const sid = context.agent?.session?.id ?? context.agent?.id;
1295
- return sid ? runtimeContextText(String(sid), context, entry.part) : "";
1296
- },
1297
- }), `lume.runtime-context(${entry.name})`);
1298
- }
1299
- }
1300
- ctx.effect(() => ctx.systemPrompt.section({
1301
- name: LUME_THINKING_SECTION,
1302
- order: LUME_THINKING_ORDER,
1303
- text: (context) => {
1304
- const sid = context.agent?.session?.id ?? context.agent?.id;
1305
- return sid ? systemSectionText(String(sid), context, "thinking") : "";
1306
- },
1307
- }), "lume.thinking-section()");
1308
- // 工具失败提示走 runtime-context 通道:宿主把它渲染成对话尾部的一条消息,
1309
- // 而不是拼进 system 串。system 串只要变化就会写一条新的 request/header,
1310
- // 既在界面上多出一行「系统提示词」,也让前缀缓存从系统提示词处整段失效。
1311
- // `systemPrompt.context` 是宿主较新版本才有的 API,缺失时静默跳过(旧宿主下
1312
- // 只是失去这条提示,不影响其余功能)。
1313
- ctx.effect(() => {
1314
- if (typeof ctx.systemPrompt?.context !== "function") {
1315
- ctx.logger?.warn?.("lume: 当前宿主不支持 systemPrompt.context,工具失败提示已跳过(不影响其余功能)");
1316
- return;
1317
- }
1318
- return ctx.systemPrompt.context({
1319
- name: LUME_TOOL_NOTICE_CONTEXT,
1320
- order: LUME_TOOL_NOTICE_ORDER,
1321
- text: (context) => {
1322
- const sid = context.agent?.session?.id ?? context.agent?.id;
1323
- const st = sid ? runtime.get(String(sid)) : null;
1324
- if (!st)
1325
- return "";
1326
- return buildToolFailureNotice({ failures: st.toolFailures, unknown: st.toolUnknown }) ?? "";
1327
- },
1328
- });
1329
- }, "lume.tool-notice-context()");
773
+ ],
774
+ systemSectionText,
775
+ runtimeContextText,
776
+ toolNoticeContext: { name: LUME_TOOL_NOTICE_CONTEXT, order: LUME_TOOL_NOTICE_ORDER },
777
+ runtime,
778
+ buildToolFailureNotice,
779
+ });
1330
780
  registerRpcChannel(ctx);
1331
781
  /**
1332
782
  * 注册客户端 RPC 通道(`/lume`)。放在最后 + 独立 try/catch:
@@ -1341,8 +791,7 @@ function applyInner(ctx, config = {}) {
1341
791
  */
1342
792
  function registerRpcChannel(scope) {
1343
793
  const dispatch = async (endpoint, payload) => {
1344
- currentStore ??= await storesReady;
1345
- identity ??= await identityReady;
794
+ await stores.ensureReady();
1346
795
  return handleEndpoint(endpoint, payload);
1347
796
  };
1348
797
  // 依赖组合与宿主自带 dsh-api-gateway 一致(["connection", "webServer"])。