@modusensus/dsh-mneme 0.9.0 → 0.9.2

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 (106) hide show
  1. package/README.md +82 -19
  2. package/bin/cli.mjs +69 -9
  3. package/docs/CONFIGURATION.md +48 -0
  4. package/docs/SEMANTIC.md +3 -3
  5. package/lib/api-standalone.js +78 -12
  6. package/lib/api.js +339 -59
  7. package/lib/candidates.js +454 -454
  8. package/lib/client.js +669 -256
  9. package/lib/config.js +50 -2
  10. package/lib/dream/sleep.js +35 -12
  11. package/lib/dream.js +86 -14
  12. package/lib/embedding.js +153 -22
  13. package/lib/index.js +32 -0
  14. package/lib/inject.js +34 -3
  15. package/lib/lang.js +124 -0
  16. package/lib/pipeline-activities.js +178 -0
  17. package/lib/pipeline-attribution.js +165 -0
  18. package/lib/pipeline-command.js +279 -0
  19. package/lib/pipeline-confirm.js +142 -0
  20. package/lib/pipeline-exec.js +174 -0
  21. package/lib/pipeline-nudge.js +1281 -0
  22. package/lib/pipeline-observations.js +181 -0
  23. package/lib/pipeline-observe.js +133 -0
  24. package/lib/pipeline-runner.js +346 -0
  25. package/lib/pipeline-runs.js +442 -0
  26. package/lib/pipeline.js +128 -7
  27. package/lib/scope-state.js +277 -0
  28. package/lib/semantic.js +66 -10
  29. package/lib/service.js +80 -3
  30. package/lib/settings.js +139 -1
  31. package/lib/store.js +216 -3
  32. package/lib/summarize.js +2 -3
  33. package/lib/tools.js +71 -0
  34. package/package.json +3 -3
  35. package/scripts/changelog-prep.mjs +62 -7
  36. package/scripts/check-sync.js +8 -6
  37. package/scripts/release-docs.mjs +204 -0
  38. package/scripts/release-notes.mjs +162 -0
  39. package/src/api-standalone.js +78 -12
  40. package/src/api.js +339 -59
  41. package/src/candidates.js +454 -454
  42. package/src/config.js +50 -2
  43. package/src/dream/sleep.js +35 -12
  44. package/src/dream.js +86 -14
  45. package/src/embedding.js +153 -22
  46. package/src/index.js +32 -0
  47. package/src/inject.js +34 -3
  48. package/src/lang.js +124 -0
  49. package/src/pipeline-activities.js +178 -0
  50. package/src/pipeline-attribution.js +165 -0
  51. package/src/pipeline-command.js +279 -0
  52. package/src/pipeline-confirm.js +142 -0
  53. package/src/pipeline-exec.js +174 -0
  54. package/src/pipeline-nudge.js +1281 -0
  55. package/src/pipeline-observations.js +181 -0
  56. package/src/pipeline-observe.js +133 -0
  57. package/src/pipeline-runner.js +346 -0
  58. package/src/pipeline-runs.js +442 -0
  59. package/src/pipeline.js +128 -7
  60. package/src/scope-state.js +277 -0
  61. package/src/semantic.js +66 -10
  62. package/src/service.js +80 -3
  63. package/src/settings.js +139 -1
  64. package/src/store.js +216 -3
  65. package/src/summarize.js +2 -3
  66. package/src/tools.js +71 -0
  67. package/test/api-hardening.test.js +173 -0
  68. package/test/api.test.js +210 -1
  69. package/test/changelog-prep.test.js +74 -8
  70. package/test/cli-enums.test.js +37 -0
  71. package/test/cli-flag-value.test.js +86 -0
  72. package/test/client.test.js +216 -6
  73. package/test/distill-tx-embed-gap.test.js +110 -0
  74. package/test/docs-config-keys.test.js +65 -0
  75. package/test/dream-maxtokens-fallback.test.js +132 -0
  76. package/test/embed-text-reasons.test.js +112 -0
  77. package/test/helpers/write-admission-samples.js +6 -2
  78. package/test/lib-smoke.test.js +20 -0
  79. package/test/persona-api.test.js +172 -0
  80. package/test/persona-flag.test.js +36 -0
  81. package/test/persona-inject.test.js +145 -0
  82. package/test/persona-settings.test.js +145 -0
  83. package/test/pipeline-activities.test.js +155 -0
  84. package/test/pipeline-attribution.test.js +175 -0
  85. package/test/pipeline-command.test.js +243 -0
  86. package/test/pipeline-confirm.test.js +148 -0
  87. package/test/pipeline-exec.test.js +172 -0
  88. package/test/pipeline-nudge.test.js +1036 -0
  89. package/test/pipeline-observations.test.js +157 -0
  90. package/test/pipeline-observe.test.js +127 -0
  91. package/test/pipeline-r1r2.test.js +149 -0
  92. package/test/pipeline-runner.test.js +442 -0
  93. package/test/pipeline-runs.test.js +767 -0
  94. package/test/pipeline.test.js +137 -6
  95. package/test/readme-claims.test.js +68 -0
  96. package/test/reindex-backfill.test.js +83 -4
  97. package/test/release-docs.test.js +130 -0
  98. package/test/release-notes.test.js +240 -0
  99. package/test/release-workflow.test.js +16 -0
  100. package/test/route-policy.test.js +113 -0
  101. package/test/scope-state.test.js +481 -0
  102. package/test/security-doc.test.js +139 -0
  103. package/test/sensitive-scan.test.js +44 -0
  104. package/test/tools-dto-schema-matrix.test.js +12 -3
  105. package/test/tools.test.js +6 -6
  106. package/test/workflow-guards.test.js +156 -0
package/lib/embedding.js CHANGED
@@ -31,10 +31,32 @@ function embeddingsUrl(baseUrl) {
31
31
  * Call the embeddings API for one text. Resolves to a Float64 array, or null
32
32
  * when the provider is not configured, the call fails, or the response is
33
33
  * unusable. Never throws: failures degrade to keyword search.
34
+ *
35
+ * The plain null return cannot tell the caller WHY — "model not loaded",
36
+ * "endpoint 404", "timeout" and "input rejected" all look identical, so the
37
+ * receipt ledger used to flatten them into one
38
+ * `embedding-request-returned-no-vector`. Use `embedTextDetailed` when the
39
+ * reason matters; this wrapper keeps the old contract for existing callers.
40
+ */
41
+ export async function embedText(cfg, text) {
42
+ const out = await embedTextDetailed(cfg, text);
43
+ return out.vector;
44
+ }
45
+
46
+ /**
47
+ * Same call, but the failure mode is distinguishable: returns
48
+ * { vector: number[]|null, reason: null|string } where reason is one of
49
+ * config-incomplete | request-failed:<err> | http-<status> |
50
+ * response-not-json | response-no-vector | empty-text
51
+ * Never throws. `reason: null` means success.
34
52
  */
35
- export async function embedText({ baseUrl, apiKey, model }, text) {
53
+ export async function embedTextDetailed({ baseUrl, apiKey, model }, text) {
36
54
  const url = embeddingsUrl(baseUrl);
37
- if (!url || !apiKey || !model || !text) return null;
55
+ if (!url || !apiKey || !model || !text) {
56
+ // 空输入不是端点故障:区分开,否则「text 为空」会被误报成网络问题。
57
+ const missing = !url ? "baseUrl" : !apiKey ? "apiKey" : !model ? "model" : "text";
58
+ return { vector: null, reason: `config-incomplete:${missing}` };
59
+ }
38
60
  let res;
39
61
  try {
40
62
  res = await fetch(url, {
@@ -46,18 +68,33 @@ export async function embedText({ baseUrl, apiKey, model }, text) {
46
68
  body: JSON.stringify({ model, input: String(text).slice(0, 8000) }),
47
69
  signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS)
48
70
  });
49
- } catch {
50
- return null;
71
+ } catch (error) {
72
+ // 超时是 AbortSignal 抛的 TimeoutError;网络层错误(DNS/拒连)是 TypeError。
73
+ // 两者都走 request-failed,但带上类型,日志里能分清「慢」和「不通」。
74
+ const kind = error?.name === "TimeoutError" || error?.name === "AbortError" ? "timeout" : "network";
75
+ return { vector: null, reason: `request-failed:${kind}:${String(error?.message ?? error).slice(0, 200)}` };
76
+ }
77
+ if (!res.ok) {
78
+ // 状态码本身就是最有用的诊断:401/403 是钥匙、404 是路径、429 是限流、
79
+ // 5xx 是服务端。文本截断且不含请求体,日志安全。
80
+ let detail = "";
81
+ try {
82
+ const t = await res.text();
83
+ detail = String(t).slice(0, 200);
84
+ } catch { /* body unreadable, status alone is already informative */ }
85
+ return { vector: null, reason: `http-${res.status}${detail ? `:${detail}` : ""}` };
51
86
  }
52
- if (!res.ok) return null;
53
87
  let body;
54
88
  try {
55
89
  body = await res.json();
56
90
  } catch {
57
- return null;
91
+ return { vector: null, reason: "response-not-json" };
58
92
  }
59
93
  const vec = body?.data?.[0]?.embedding;
60
- return Array.isArray(vec) && vec.length ? Array.from(vec) : null;
94
+ if (!Array.isArray(vec) || !vec.length) {
95
+ return { vector: null, reason: "response-no-vector" };
96
+ }
97
+ return { vector: Array.from(vec), reason: null };
61
98
  }
62
99
 
63
100
  /**
@@ -69,9 +106,51 @@ export async function embedText({ baseUrl, apiKey, model }, text) {
69
106
  * successful embed the model that produced the vectors is recorded, so the
70
107
  * index can detect drift and the auto-reindex backfill knows what to rebuild.
71
108
  */
109
+ /**
110
+ * Bounded receipt ring for embedding attempts.
111
+ *
112
+ * Why a ring instead of llm_audit_logs: a full backfill is thousands of
113
+ * attempts, and one audit row each would bury the LLM calls that table exists
114
+ * to trace. What the user actually needs in order to answer "is the vector
115
+ * layer silently dead right now?" is the recent window plus running totals —
116
+ * which costs nothing and cannot flood.
117
+ */
118
+ const RECEIPT_LIMIT = 200;
119
+
120
+ function createReceiptLedger() {
121
+ const recent = [];
122
+ const totals = { ok: 0, failed: 0, skipped: 0 };
123
+ let lastOkAt = null;
124
+ let lastFailAt = null;
125
+ let lastFailReason = null;
126
+ return {
127
+ record(entry) {
128
+ recent.push(entry);
129
+ if (recent.length > RECEIPT_LIMIT) recent.shift();
130
+ if (entry.status in totals) totals[entry.status]++;
131
+ if (entry.status === "ok") lastOkAt = entry.at;
132
+ else if (entry.status === "failed") {
133
+ lastFailAt = entry.at;
134
+ lastFailReason = entry.reason ?? null;
135
+ }
136
+ },
137
+ snapshot() {
138
+ return {
139
+ totals: { ...totals },
140
+ lastOkAt,
141
+ lastFailAt,
142
+ lastFailReason,
143
+ recent: recent.slice(-RECEIPT_LIMIT)
144
+ };
145
+ }
146
+ };
147
+ }
148
+
72
149
  export function createEmbedder({ store, settings, logger, vectorIndex }) {
73
150
  // Dimension of the most recent successful embed, exposed for fingerprinting.
74
151
  let _dimension = 0;
152
+ // Per-write embed receipts (see createReceiptLedger above).
153
+ const ledger = createReceiptLedger();
75
154
 
76
155
  /** Record the producing model fingerprint in vector_meta (best-effort). */
77
156
  function markModel(cfg, dimension) {
@@ -85,19 +164,45 @@ export function createEmbedder({ store, settings, logger, vectorIndex }) {
85
164
  // ready/configured 两个 getter 共用,避免以后改一处漏一处。
86
165
  const configComplete = (cfg) => !!(cfg?.enabled && cfg.baseUrl && cfg.apiKey && cfg.model);
87
166
 
167
+ /**
168
+ * Embed one memory and store the vector. Never throws — a flaky endpoint
169
+ * must not break memory writes — but it now REPORTS what happened instead
170
+ * of returning undefined on both success and failure. Callers that ignore
171
+ * the result are unaffected; callers that care get a receipt.
172
+ *
173
+ * @returns {Promise<{status:"ok"|"failed"|"skipped", id:string,
174
+ * at:string, reason?:string, dim?:number, model?:string}>}
175
+ */
88
176
  async function embedFor(id, title, content) {
177
+ const at = new Date().toISOString();
89
178
  const cfg = settings.getVectorConfig();
90
- if (!configComplete(cfg)) return;
179
+ if (!configComplete(cfg)) {
180
+ const entry = { status: "skipped", id, at, reason: "vector-config-incomplete" };
181
+ ledger.record(entry);
182
+ return entry;
183
+ }
91
184
  const text = [title, content].filter(Boolean).join("\n");
92
- const vector = await embedText(cfg, text);
93
- if (vector) {
94
- store.setEmbedding(id, vector);
95
- _dimension = vector.length;
96
- // Bug3: record which model produced the current vectors so the index can
97
- // detect drift and skip a redundant backfill when nothing changed.
98
- markModel(cfg, vector.length);
99
- logger?.info?.(`[dsh-mneme] embedded memory ${id} (dim=${vector.length})`);
185
+ const detailed = await embedTextDetailed(cfg, text);
186
+ if (detailed.reason) {
187
+ // 回执带上可区分的失败模式(http-404 / request-failed:timeout /
188
+ // config-incomplete:model / response-no-vector ...),不再把「模型没配」
189
+ // 「请求挂了」「响应没有向量」压成同一个词。状态页与日志按 reason 分诊。
190
+ const entry = { status: "failed", id, at, reason: detailed.reason };
191
+ ledger.record(entry);
192
+ logger?.warn?.(`[dsh-mneme] embed memory ${id} failed: ${detailed.reason}`);
193
+ return entry;
100
194
  }
195
+ const vector = detailed.vector;
196
+ const model = modelHashOf(cfg.model);
197
+ store.setEmbedding(id, vector, model);
198
+ _dimension = vector.length;
199
+ // Bug3: record which model produced the current vectors so the index can
200
+ // detect drift and skip a redundant backfill when nothing changed.
201
+ markModel(cfg, vector.length);
202
+ const entry = { status: "ok", id, at, dim: vector.length, model };
203
+ ledger.record(entry);
204
+ logger?.info?.(`[dsh-mneme] embedded memory ${id} (dim=${vector.length})`);
205
+ return entry;
101
206
  }
102
207
 
103
208
  return {
@@ -120,10 +225,26 @@ export function createEmbedder({ store, settings, logger, vectorIndex }) {
120
225
  get configured() {
121
226
  return configComplete(settings.getVectorConfig());
122
227
  },
123
- /** Fire-and-forget re-embed of a memory after any write. */
228
+ /**
229
+ * Re-embed a memory after any write. Still non-blocking — the caller does
230
+ * not have to await it — but it now RETURNS the receipt promise so a
231
+ * caller that wants to know can. Previously this returned undefined and
232
+ * swallowed the rejection, so "did this write get a vector?" had no answer
233
+ * anywhere in the process. Swallowing still applies to the rejection
234
+ * itself: a vector failure must never break the memory write.
235
+ */
124
236
  schedule(memory) {
125
- if (!memory?.id) return;
126
- embedFor(memory.id, memory.title, memory.content).catch(() => {});
237
+ if (!memory?.id) {
238
+ return Promise.resolve({ status: "skipped", id: null, at: new Date().toISOString(), reason: "no-id" });
239
+ }
240
+ return embedFor(memory.id, memory.title, memory.content).catch((error) => ({
241
+ status: "failed", id: memory.id, at: new Date().toISOString(), reason: String(error)
242
+ }));
243
+ },
244
+
245
+ /** Recent embed receipts + running totals (see createReceiptLedger). */
246
+ embedReceipts() {
247
+ return ledger.snapshot();
127
248
  },
128
249
 
129
250
  /** Embed one text and return its vector (null on failure/disabled). */
@@ -155,17 +276,27 @@ export function createEmbedder({ store, settings, logger, vectorIndex }) {
155
276
  return _dimension || undefined;
156
277
  },
157
278
 
158
- /** Batch re-index rows still missing an embedding. */
279
+ /**
280
+ * Batch re-index rows whose vector is missing OR came from another model.
281
+ * Uses needsEmbeddingFor (not needsEmbedding) so a model switch actually
282
+ * re-embeds the old vectors instead of leaving them eligible for "has a
283
+ * vector" forever while they poison similarity ranking.
284
+ */
159
285
  async reindexMissing(limit = 50) {
160
286
  const cfg = settings.getVectorConfig();
161
287
  if (!cfg?.enabled) return { indexed: 0, skipped: 0 };
162
- const rows = store.needsEmbedding(limit);
288
+ const model = modelHashOf(cfg.model);
289
+ // Fall back to the plain selector for stores that predate the
290
+ // provenance column (older hosts, test doubles).
291
+ const rows = typeof store.needsEmbeddingFor === "function"
292
+ ? store.needsEmbeddingFor(model, limit)
293
+ : store.needsEmbedding(limit);
163
294
  let indexed = 0;
164
295
  for (const row of rows) {
165
296
  const text = [row.title, row.content].filter(Boolean).join("\n");
166
297
  const vector = await embedText(cfg, text);
167
298
  if (vector) {
168
- store.setEmbedding(row.id, vector);
299
+ store.setEmbedding(row.id, vector, model);
169
300
  _dimension = vector.length;
170
301
  indexed++;
171
302
  }
package/lib/index.js CHANGED
@@ -22,6 +22,8 @@ import { createStandaloneApi } from "./api-standalone.js";
22
22
  import { createMaintenance } from "./maintenance.js";
23
23
  import { createSettings } from "./settings.js";
24
24
  import { createCommandManager } from "./commands.js";
25
+ import { createExecutionRecorder } from "./pipeline-runner.js";
26
+ import { registerMoRunCommand } from "./pipeline-command.js";
25
27
  import { createVectorIndex } from "./vector-index.js";
26
28
  // semantic(embedder/reranker/boot 回填)纯搬移至 src/semantic.js(PR2):宿主与
27
29
  // daemon(dsh-mneme-serve,#363)共用同一套装配,原文件保留 barrel 出口。
@@ -388,11 +390,40 @@ export const apply = (ctx, config) => {
388
390
  // Custom commands: register persisted commands into the DSH command registry
389
391
  // on boot; add/remove re-register live through the API.
390
392
  let commands = null;
393
+ // M.O. 执行记账器:默认 null(旗标关),开了才装配。
394
+ let recorder = null;
391
395
  if (ctx.commands) {
392
396
  commands = createCommandManager({ ctx, settings, logger: ctx.logger, language: langOf(cfg) });
393
397
  commands.sync();
394
398
  }
395
399
 
400
+ // M.O. 执行记账(v0.9.0 第二批):`/mo-run` 显式开一次执行窗口,窗口内的命令执行
401
+ // 才可能被记成一条 run。挂的是 `session/event`——本仓已有三处消费者(continuity /
402
+ // summarize / api),不是新引入的宿主依赖。
403
+ //
404
+ // 为什么必须显式开窗:工作流的 trigger 是人话,拿它去猜「用户这句话是不是要开始走
405
+ // 某条流程」只能猜错,而猜错会把一条从没跑过的流程记成跑过。所以窗口边界由人的
406
+ // 显式动作划,**没开窗时一个字都不记**。
407
+ //
408
+ // 门控:默认关(actionRunEnabled),并且它挂热路径,轻量档也关。
409
+ if (cfg.actionRunEnabled === true) {
410
+ recorder = createExecutionRecorder({ ctx, logger: ctx.logger });
411
+ const disposeMoRun = registerMoRunCommand({
412
+ ctx,
413
+ recorder,
414
+ enabled: true,
415
+ language: () => langOf(cfg)
416
+ });
417
+ if (disposeMoRun) {
418
+ disposers.push(disposeMoRun);
419
+ } else {
420
+ // 注册不上(宿主没有命令注册表)时别留一个只开着窗口却没人能操作的记录器。
421
+ recorder.dispose();
422
+ recorder = null;
423
+ ctx.logger?.warn?.("[dsh-mneme] actionRunEnabled=true 但宿主没有命令注册表,M.O. 记账未启用");
424
+ }
425
+ }
426
+
396
427
  // Dream scheduler: automatic consolidation + summary runs, triggered by
397
428
  // store growth. Writes through the service fire the dream hook, which asks
398
429
  // the scheduler to (re)schedule a run once absolute and since-last-run
@@ -582,6 +613,7 @@ export const apply = (ctx, config) => {
582
613
  if (typeof dispose === "function") dispose();
583
614
  }
584
615
  commands?.dispose();
616
+ recorder?.dispose();
585
617
  if (dream) await dream.dispose();
586
618
  if (sleep) sleep.dispose();
587
619
  store.close();
package/lib/inject.js CHANGED
@@ -306,13 +306,44 @@ export function createInjector(ctx, service, settings, config, gate = null) {
306
306
  }).catch(() => { /* prefetch is best-effort */ });
307
307
  }
308
308
 
309
+ // 会话身份解析(v1.2 人格寻址用):读 session.header.agentPreset——冻结的
310
+ // SessionHeader(issue #17 身份契约)。⚠️ 不能先读 requestHeader():那返回
311
+ // EpochHeader(请求级头,首请求后为真),会把 session.header 遮蔽掉,身份取空
312
+ // (scope.js 同坑,本地实测踩过)。任何失败静默 → null(落 default 卡)。
313
+ function sessionAgentId(ctx) {
314
+ try {
315
+ const session = ctx?.agent?.session;
316
+ const header = session?.header ?? null;
317
+ const preset = header?.agentPreset;
318
+ return typeof preset === "string" && preset.trim() ? preset.trim() : null;
319
+ } catch {
320
+ return null;
321
+ }
322
+ }
323
+
309
324
  // User profile + rules: injected ahead of the memory block because they are
310
325
  // always-relevant instructions the agent should follow every turn.
311
- function renderUserSettings() {
326
+ // 人格段(beta):双闸门(config.personaEnabled && 卡.enabled)开时渲染在最前
327
+ // ——设计稿⑥:特化指令(人格)先出,画像与规则后出、后写覆盖占上风,rules 恒
328
+ // 最硬。v1.2 多卡:按会话身份取 byAgent 卡,身份缺失/无卡/卡关落 default。
329
+ // 任一闸关着时本函数输出与历史版本逐字节一致(老用户零变化)。人格段非空即
330
+ // 触发整块渲染(与 profile 同权)。
331
+ function renderUserSettings(ctx) {
312
332
  const profile = settings.getProfile().trim();
313
333
  const rules = settings.getRules();
314
- if (!profile && !rules.length) return "";
334
+ const persona = config?.personaEnabled === true
335
+ ? settings.getPersonaFor(sessionAgentId(ctx))
336
+ : {};
337
+ const personaLines = persona?.enabled === true
338
+ ? [
339
+ ...(typeof persona.name === "string" && persona.name.trim() ? [STR.personaNameLine[language](persona.name.trim())] : []),
340
+ ...(typeof persona.personality === "string" && persona.personality.trim() ? [STR.personaPersonalityLine[language](persona.personality.trim())] : []),
341
+ ...(typeof persona.replyStyle === "string" && persona.replyStyle.trim() ? [STR.personaReplyStyleLine[language](persona.replyStyle.trim())] : [])
342
+ ]
343
+ : [];
344
+ if (!profile && !rules.length && !personaLines.length) return "";
315
345
  const lines = [STR.userSettingsHeader[language]];
346
+ lines.push(...personaLines);
316
347
  if (profile) lines.push(STR.profileLine[language](profile));
317
348
  for (const rule of rules) lines.push(STR.ruleLine[language](rule));
318
349
  return escapePromptVars(lines.join("\n"));
@@ -418,7 +449,7 @@ export function createInjector(ctx, service, settings, config, gate = null) {
418
449
  ctx.systemPrompt.context({
419
450
  name: "user-settings",
420
451
  order: 85,
421
- text: renderUserSettings
452
+ text: (ctx) => renderUserSettings(ctx)
422
453
  })
423
454
  ];
424
455
 
package/lib/lang.js CHANGED
@@ -122,6 +122,20 @@ export const STR = {
122
122
  zh: (rule) => `- 规则:${rule}`,
123
123
  en: (rule) => `- Rule: ${rule}`
124
124
  },
125
+ // 人格定义页(beta):人格段行(renderUserSettings 拼装,双闸门开时渲染)。
126
+ // 设计稿⑥:人格段在 profile/rules 之前——特化指令后出、通用底座压轴,rules 恒最硬。
127
+ personaNameLine: {
128
+ zh: (name) => `- 你的人格名字:${name}`,
129
+ en: (name) => `- Your persona name: ${name}`
130
+ },
131
+ personaPersonalityLine: {
132
+ zh: (text) => `- 人格性格设定:${text}`,
133
+ en: (text) => `- Persona personality: ${text}`
134
+ },
135
+ personaReplyStyleLine: {
136
+ zh: (text) => `- 回复要求:${text}`,
137
+ en: (text) => `- Reply style: ${text}`
138
+ },
125
139
 
126
140
  // --- summarize.js:蒸馏转录标签(进入 LLM 上下文) --------------------------
127
141
  transcriptUser: { zh: (t) => `用户:${t}`, en: (t) => `User: ${t}` },
@@ -223,6 +237,116 @@ export const STR = {
223
237
  en: (name) => `Failed to submit command ${name}; see logs`
224
238
  },
225
239
 
240
+ // --- pipeline-command.js:/mo-run 的开窗命令 -----------------------------------
241
+ // 回执是这个功能唯一的对外窗口(面板侧还没做),所以每一条都要说清「发生了什么、
242
+ // 为什么」——尤其失败时不许含糊成「完成」。
243
+ moRunDescription: {
244
+ zh: "M.O. 记账:显式开一次执行窗口,之后跑过的命令才可能被记下来",
245
+ en: "M.O. ledger: explicitly open an execution window; only commands run inside it can be recorded"
246
+ },
247
+ moRunInputHint: {
248
+ zh: "start <工作流 id> | stop | confirm <做了的步骤> | status",
249
+ en: "start <pipeline id> | stop | confirm <steps that ran> | status"
250
+ },
251
+ moRunUsage: {
252
+ zh: "用法:/mo-run start <工作流 id> → 走一遍 → /mo-run stop → /mo-run confirm <做了的步骤 id>\n\n停止时会给出一份待确认清单(含本次观察到的业务动作);**确认之后才写账**。\n确认写法:`/mo-run confirm P6 V2` 表示这两步做了;`V2:-` 表示没做(记 skipped),`V2:?` 表示不确定(不写)。\n未提到的步骤什么都不写——「没确认」不等于「没发生」。\n\n它不会自己判断「你是不是在走某条流程」,也不会自己判断「哪几步走了」:这两件事都会猜错。",
253
+ en: "Usage: /mo-run start <pipeline id> → walk through it → /mo-run stop → /mo-run confirm <step ids that ran>\n\nStop gives you a checklist (with the business actions observed); **the ledger is only written after you confirm**.\nConfirm syntax: `/mo-run confirm P6 V2` means both ran; `V2:-` means not done (recorded as skipped), `V2:?` means unsure (not written).\nSteps you do not mention are written as nothing — unconfirmed is not the same as not-happened.\n\nIt never guesses whether you are walking a workflow, nor which steps ran: both guesses would be wrong."
254
+ },
255
+ moRunStart: {
256
+ zh: {
257
+ ok: (id, steps) => `已开窗:${id}(${steps} 步)。走完用 /mo-run stop 结账——它会给你一份待确认清单,**确认之后才写账**。`,
258
+ alreadyOpen: (id) => `这个会话已经开着窗口了:${id}。先用 /mo-run stop 结账,再开新的。`,
259
+ notFound: (ids) => `找不到这条工作流。可用:${ids}`,
260
+ noneAvailable: "(当前项目里没有读到任何工作流)",
261
+ unreadable: (detail) => `工作流读不出来:${detail}`,
262
+ noProject: "拿不到这个会话的工作目录,无法定位工作流。",
263
+ noSession: "拿不到会话身份,无法记账。",
264
+ failed: (reason) => `开窗失败:${reason}`
265
+ },
266
+ en: {
267
+ ok: (id, steps) => `Window opened: ${id} (${steps} steps). When done, run /mo-run stop — it gives you a checklist, and **the ledger is only written after you confirm**.`,
268
+ alreadyOpen: (id) => `This session already has an open window: ${id}. Run /mo-run stop first.`,
269
+ notFound: (ids) => `No such workflow. Available: ${ids}`,
270
+ noneAvailable: "(no workflow found in this project)",
271
+ unreadable: (detail) => `Workflow unreadable: ${detail}`,
272
+ noProject: "Cannot determine this session's working directory, so the workflow cannot be located.",
273
+ noSession: "No session identity available; nothing can be recorded.",
274
+ failed: (reason) => `Failed to open window: ${reason}`
275
+ }
276
+ },
277
+ moRunStop: {
278
+ zh: {
279
+ ok: (id, steps) => `已结账并落盘:${id},记下 ${steps} 步。`,
280
+ awaiting: (id, steps) => `已结账:${id}(${steps} 步)。**还没有写账**——请确认哪几步真的走了:`,
281
+ confirmHow: (sample) =>
282
+ `\n用 /mo-run confirm ${sample} 列出「做了」的步骤;要标「没做」就写 ${String(sample).split(" ")[0]}:-\n未提到的步骤什么都不写(「没确认」不等于「没发生」)。`,
283
+ notCommands: (n) => `${n} 次调用不是命令执行器(读文件、检索这类),与工作流步骤无关。`,
284
+ truncated: "⚠️ 事件缓冲到上限被截断,这次记账可能不完整。",
285
+ noWindow: "这个会话没有开着窗口,没什么可结账的。",
286
+ activities: (n) => `本次观察到 ${n} 种业务动作(只留动作与旗标,位置参数略去):`,
287
+ shellNoise: (n) => `另有 ${n} 段是 shell 语法(赋值、管道、输出),不是工作流的一步。`,
288
+ writeFailed: (detail) => `写盘失败:${detail}`,
289
+ failed: (reason) => `结账失败:${reason}`
290
+ },
291
+ en: {
292
+ ok: (id, steps) => `Closed and persisted: ${id}, ${steps} step(s) recorded.`,
293
+ awaiting: (id, steps) => `Closed: ${id} (${steps} steps). **Nothing persisted yet** — confirm which steps really ran:`,
294
+ confirmHow: (sample) =>
295
+ `\nUse /mo-run confirm ${sample} to list the steps that ran; mark one as not-done with ${String(sample).split(" ")[0]}:-\nSteps you do not mention are written as nothing (unconfirmed is not the same as not-happened).`,
296
+ notCommands: (n) => `${n} call(s) were not command executors (file reads, searches) and are irrelevant to workflow steps.`,
297
+ truncated: "⚠️ Event buffer hit its cap and was truncated; this record may be incomplete.",
298
+ noWindow: "No window is open for this session, so there is nothing to close.",
299
+ activities: (n) => `${n} kind(s) of business action observed this pass (actions and flags only; positional arguments omitted):`,
300
+ shellNoise: (n) => `Another ${n} segment(s) were shell syntax (assignments, pipes, output), not steps of a workflow.`,
301
+ writeFailed: (detail) => `Failed to write: ${detail}`,
302
+ failed: (reason) => `Failed to close: ${reason}`
303
+ }
304
+ },
305
+ moRunConfirm: {
306
+ zh: {
307
+ ok: (id, steps) => `已记入账本:${id},${steps} 步。`,
308
+ notDone: (list) => `记为「这次没做」(skipped):${list}`,
309
+ unsure: (list) => `你说不确定,没有写进账:${list}`,
310
+ unanswered: (list) => `没提到的步骤没有写进账:${list}`,
311
+ nothingToConfirm: "没有待确认的清单。先 /mo-run start <工作流 id>,走一遍,再 /mo-run stop。",
312
+ mismatch: (n, rows) => `⚠️ 有 ${n} 步你说做了、但声明的命令这次一次都没出现(是声明该改、还是本来不必敲?由你判):\n${rows}`,
313
+ obsSaved: (file) => `本次观察已旁挂:${file}`, noAnswers: (notDone, unsure, unanswered) =>
314
+ `一条都没确认,**没有落盘**。你说「没做」的 ${notDone} 步、不确定的 ${unsure} 步、没提到的 ${unanswered} 步。\n确认「做了」的步骤才会写账:/mo-run confirm <步骤 id>`,
315
+ badValue: (stepId, value) => `${stepId} 的回答「${value}」不合法。只认「做了」(或不写标记)/「没做」写作 -/「不确定」写作 ?`,
316
+ unknownStep: (stepId) => `没有这一步:${stepId}`,
317
+ writeFailed: (detail) => `写盘失败:${detail}`,
318
+ failed: (reason) => `确认失败:${reason}`
319
+ },
320
+ en: {
321
+ ok: (id, steps) => `Recorded: ${id}, ${steps} step(s).`,
322
+ notDone: (list) => `Marked as not done this pass (skipped): ${list}`,
323
+ unsure: (list) => `You were unsure; not written to the ledger: ${list}`,
324
+ unanswered: (list) => `Steps you did not mention were not written to the ledger: ${list}`,
325
+ nothingToConfirm: "Nothing to confirm. Run /mo-run start <pipeline id>, walk through it, then /mo-run stop.",
326
+ mismatch: (n, rows) => `⚠️ ${n} step(s) you marked done never showed their declared command this pass (stale declaration, or simply not needed? your call):\n${rows}`,
327
+ obsSaved: (file) => `Observations side-written to: ${file}`, noAnswers: (notDone, unsure, unanswered) =>
328
+ `Nothing was confirmed; **nothing was persisted**. Not-done: ${notDone}, unsure: ${unsure}, unmentioned: ${unanswered}.\nOnly steps confirmed as done are recorded: /mo-run confirm <step id>`,
329
+ badValue: (stepId, value) => `Answer "${value}" for ${stepId} is invalid. Use done (no marker) / - for not-done / ? for unsure.`,
330
+ unknownStep: (stepId) => `No such step: ${stepId}`,
331
+ writeFailed: (detail) => `Failed to write: ${detail}`,
332
+ failed: (reason) => `Failed to confirm: ${reason}`
333
+ }
334
+ },
335
+ moRunStatus: {
336
+ zh: {
337
+ closed: "当前没有开着的执行窗口。用 /mo-run start <工作流 id> 开一个。",
338
+ awaiting: (id, steps) => `等你在确认:${id}(${steps} 步)。用 /mo-run confirm <做了的步骤 id> 写账。`,
339
+ open: (s, recorded) => `窗口开着:${s.pipelineId}\n开始于 ${s.startedAt}\n已缓冲 ${s.bufferedEvents} 条事件,识别出 ${recorded} 步${s.truncated ? "(缓冲已截断)" : ""}\n认不出 ${s.unattributed},待确认 ${s.ambiguous}`,
340
+ none: "(还没有)"
341
+ },
342
+ en: {
343
+ closed: "No execution window is open. Open one with /mo-run start <pipeline id>.",
344
+ awaiting: (id, steps) => `Waiting for your confirmation: ${id} (${steps} steps). Write the ledger with /mo-run confirm <step ids that ran>.`,
345
+ open: (s, recorded) => `Window open: ${s.pipelineId}\nStarted ${s.startedAt}\n${s.bufferedEvents} event(s) buffered, ${recorded} step(s) recognized${s.truncated ? " (buffer truncated)" : ""}\nUnattributed ${s.unattributed}, ambiguous ${s.ambiguous}`,
346
+ none: "(none yet)"
347
+ }
348
+ },
349
+
226
350
  // --- mirror.js:镜像文件标签(渲染随实例语言;解析两种语言都认) ----------------
227
351
  mirrorLabel: {
228
352
  zh: { type: "类型", importance: "重要性", tags: "标签", updated: "更新时间", source: "来源", scope: "作用域", sensitivity: "敏感度" },