engine7 7.1.106 → 7.1.107

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -530,6 +530,10 @@ function formatForLlm(i) {
530
530
  function formatLog(i) {
531
531
  return toWall(i ?? /* @__PURE__ */ new Date());
532
532
  }
533
+ function formatShort(i) {
534
+ const p = wallParts(i ?? /* @__PURE__ */ new Date());
535
+ return `${p.mo}/${p.d} ${p2(p.hh)}:${p2(p.mm)}`;
536
+ }
533
537
  function warnOnce(callSite, shape, sample) {
534
538
  const key = `${callSite}:${shape}`;
535
539
  if (warned.has(key)) return;
@@ -20570,7 +20574,7 @@ CREATE TABLE IF NOT EXISTS watermarks (
20570
20574
  blocks.push(block);
20571
20575
  count++;
20572
20576
  }
20573
- head = head.replace(/entry_count: \d+/, `entry_count: ${count}`).replace(/last_appended_at: '[^']*'/, `last_appended_at: '${(/* @__PURE__ */ new Date()).toISOString()}'`);
20577
+ head = head.replace(/entry_count: \d+/, `entry_count: ${count}`).replace(/last_appended_at: '[^']*'/, `last_appended_at: '${formatMdTimestamp()}'`);
20574
20578
  writeFileSync17(file2, head + "\n\n" + blocks.join("\n\n") + "\n", "utf8");
20575
20579
  }
20576
20580
  /**
@@ -20990,7 +20994,7 @@ function calcDurationDays(startDate, endDate) {
20990
20994
  const ms = Date.parse(endDate + "T00:00:00Z") - Date.parse(startDate + "T00:00:00Z");
20991
20995
  return Number.isNaN(ms) ? null : Math.round(ms / 864e5);
20992
20996
  }
20993
- function cellDateCst(cell) {
20997
+ function cellDateKey(cell) {
20994
20998
  return todayKey(cell.startTs);
20995
20999
  }
20996
21000
  async function extractForesights(cell, ownerId, provider, model) {
@@ -21019,7 +21023,7 @@ async function extractForesights(cell, ownerId, provider, model) {
21019
21023
  }
21020
21024
  }
21021
21025
  if (!raw) return [];
21022
- const startFallback = cellDateCst(cell);
21026
+ const startFallback = cellDateKey(cell);
21023
21027
  const out = [];
21024
21028
  for (const r of raw) {
21025
21029
  if (!r || typeof r !== "object") continue;
@@ -21060,7 +21064,7 @@ var init_foresight = __esm({
21060
21064
  });
21061
21065
 
21062
21066
  // src/mem7/core/pipeline.ts
21063
- function dateCst(tsMs) {
21067
+ function dateKey(tsMs) {
21064
21068
  return todayKey(tsMs);
21065
21069
  }
21066
21070
  async function runMem7Pipeline(args) {
@@ -21095,7 +21099,7 @@ async function runMem7Pipeline(args) {
21095
21099
  store.saveTail(sessionId, tail);
21096
21100
  for (const cell of cells) {
21097
21101
  const userSenders = [...new Set(cell.messages.filter((m) => m.role === "user").map((m) => m.senderId))];
21098
- const dateStr = dateCst(cell.startTs);
21102
+ const dateStr = dateKey(cell.startTs);
21099
21103
  const cellTimeIso = new Date(cell.startTs).toISOString();
21100
21104
  let episodeOut = null;
21101
21105
  let factsOut = [];
@@ -21268,11 +21272,18 @@ function parseProactiveConfig(raw) {
21268
21272
  console.warn(`[proactive] model "${modelRef}" \u975E\u6CD5\uFF08${err.message}\uFF09\u2014\u2014 \u6539\u7528\u4E3B\u6A21\u578B`);
21269
21273
  }
21270
21274
  }
21275
+ const num = (v, dflt) => {
21276
+ const n = typeof v === "number" ? v : Number(v);
21277
+ return Number.isFinite(n) && n >= 0 ? n : dflt;
21278
+ };
21271
21279
  return {
21272
21280
  enabled: obj.enabled === true || obj.enabled === "yes" || obj.enabled === 1,
21273
21281
  modelRef: parsed ? `${parsed.providerId}/${parsed.modelId}` : "",
21274
21282
  providerId: parsed?.providerId ?? "",
21275
- modelId: parsed?.modelId ?? ""
21283
+ modelId: parsed?.modelId ?? "",
21284
+ // 默认 3 天 = 翀哥的"以周为单位";staleHours 默认 36 = 与旧行为一致
21285
+ leadGateDays: num(obj.leadGateDays, 3),
21286
+ staleHours: num(obj.staleHours, 36)
21276
21287
  };
21277
21288
  }
21278
21289
  function proactiveDbPath(workspace) {
@@ -21636,6 +21647,8 @@ function rowToAction(r) {
21636
21647
  createdAt: r.created_at,
21637
21648
  completedAt: r.completed_at ?? void 0,
21638
21649
  cancelReason: r.cancel_reason ?? void 0,
21650
+ injectedAt: r.injected_at ?? void 0,
21651
+ note: r.note ?? void 0,
21639
21652
  provenance: r.provenance ? JSON.parse(r.provenance) : void 0
21640
21653
  };
21641
21654
  }
@@ -21685,6 +21698,8 @@ CREATE TABLE IF NOT EXISTS actions (
21685
21698
  created_at TEXT NOT NULL,
21686
21699
  completed_at TEXT,
21687
21700
  cancel_reason TEXT,
21701
+ injected_at TEXT,
21702
+ note TEXT,
21688
21703
  provenance TEXT
21689
21704
  );
21690
21705
  CREATE INDEX IF NOT EXISTS idx_actions_due ON actions(status, scheduled_time);
@@ -21724,6 +21739,8 @@ CREATE INDEX IF NOT EXISTS idx_proactive_decisions_created ON proactive_decision
21724
21739
  this.db.exec(SCHEMA10);
21725
21740
  const actionCols = new Set(this.db.prepare("PRAGMA table_info(actions)").all().map((r) => r.name));
21726
21741
  if (!actionCols.has("provenance")) this.db.exec("ALTER TABLE actions ADD COLUMN provenance TEXT");
21742
+ if (!actionCols.has("injected_at")) this.db.exec("ALTER TABLE actions ADD COLUMN injected_at TEXT");
21743
+ if (!actionCols.has("note")) this.db.exec("ALTER TABLE actions ADD COLUMN note TEXT");
21727
21744
  const decisionCols = new Set(this.db.prepare("PRAGMA table_info(proactive_decisions)").all().map((r) => r.name));
21728
21745
  if (!decisionCols.has("coverage_matched")) this.db.exec("ALTER TABLE proactive_decisions ADD COLUMN coverage_matched INTEGER NOT NULL DEFAULT 0");
21729
21746
  this.migrateNakedLocalTimes();
@@ -21987,16 +22004,51 @@ CREATE INDEX IF NOT EXISTS idx_proactive_decisions_created ON proactive_decision
21987
22004
  ).run(reason, nowIso2, ...intentIds);
21988
22005
  return Number(res.changes);
21989
22006
  }
22007
+ /**
22008
+ * 改状态。**只更新显式传入的列**——此前是「任一字段有值就把两列都写」,
22009
+ * 未传的那列被写成 NULL:sweep 的 R2 复检只传 cancelReason,会把已存在的
22010
+ * completed_at 清空(0925 修)。
22011
+ */
21990
22012
  updateActionStatus(id, status, extra) {
21991
- if (extra?.completedAt !== void 0 || extra?.cancelReason !== void 0) {
21992
- this.db.prepare("UPDATE actions SET status=?, completed_at=?, cancel_reason=? WHERE id=?").run(status, extra?.completedAt ?? null, extra?.cancelReason ?? null, id);
21993
- } else {
21994
- this.db.prepare("UPDATE actions SET status=? WHERE id=?").run(status, id);
22013
+ const sets = ["status=?"];
22014
+ const vals = [status];
22015
+ if (extra?.completedAt !== void 0) {
22016
+ sets.push("completed_at=?");
22017
+ vals.push(extra.completedAt);
22018
+ }
22019
+ if (extra?.cancelReason !== void 0) {
22020
+ sets.push("cancel_reason=?");
22021
+ vals.push(extra.cancelReason);
21995
22022
  }
22023
+ if (extra?.note !== void 0) {
22024
+ sets.push("note=?");
22025
+ vals.push(extra.note);
22026
+ }
22027
+ vals.push(id);
22028
+ this.db.prepare(`UPDATE actions SET ${sets.join(", ")} WHERE id=?`).run(...vals);
21996
22029
  }
21997
- /** sweep 注入前标记(防重复注入;复刻 due→推送的原子语义) */
21998
- markActionReminded(id) {
21999
- this.db.prepare(`UPDATE actions SET status='executing' WHERE id=? AND status='queued'`).run(id);
22030
+ /** sweep 注入前标记(防重复注入;复刻 due→推送的原子语义)。
22031
+ * 0925:同时记 injected_at —— 收割机判"超时"必须拿**注入时刻**,不能拿排期/创建时刻。 */
22032
+ markActionReminded(id, injectedAt) {
22033
+ this.db.prepare(`UPDATE actions SET status='executing', injected_at=? WHERE id=? AND status='queued'`).run(injectedAt, id);
22034
+ }
22035
+ /**
22036
+ * 注入失败回滚(0925):submitMessage 被 dispatcher guard 拒了、或主 session 不在——
22037
+ * 此时 action 已被标 executing 却从没进过 agent 的视野,就是一条"谁也没见过"的僵尸。
22038
+ * 退回去重排,下一轮 sweep 还能再试。返回回滚条数。
22039
+ */
22040
+ rollbackReminded(ids) {
22041
+ if (ids.length === 0) return 0;
22042
+ const ph = ids.map(() => "?").join(",");
22043
+ const res = this.db.prepare(
22044
+ `UPDATE actions SET status='queued', injected_at=NULL WHERE status='executing' AND id IN (${ph})`
22045
+ ).run(...ids);
22046
+ return Number(res.changes);
22047
+ }
22048
+ /** 某状态的条数(对账告警用:executing 只增不减 = 闭环断了) */
22049
+ countByStatus(status) {
22050
+ const r = this.db.prepare("SELECT COUNT(*) AS c FROM actions WHERE status=?").get(status);
22051
+ return Number(r?.c ?? 0);
22000
22052
  }
22001
22053
  /**
22002
22054
  * 0915:executing 超时收割。sweep 注入时标 executing,agent 处理后标终态;
@@ -22005,10 +22057,12 @@ CREATE INDEX IF NOT EXISTS idx_proactive_decisions_created ON proactive_decision
22005
22057
  * scheduled_time 或 created_at 距今超 staleHours 的 executing → cancelled。返回收割条数。
22006
22058
  */
22007
22059
  reapStaleExecuting(nowIso2, staleHours = 36) {
22060
+ if (!(staleHours > 0)) return 0;
22008
22061
  const cutoff = new Date(Date.parse(nowIso2) - staleHours * 36e5).toISOString();
22009
22062
  const res = this.db.prepare(
22010
- `UPDATE actions SET status='cancelled', completed_at=?, cancel_reason=? WHERE status='executing' AND (scheduled_time < ? OR created_at < ?)`
22011
- ).run(nowIso2, `executing \u8D85\u65F6\u6536\u5272\uFF08\u6CE8\u5165\u540E ${staleHours}h \u672A\u95ED\u73AF\uFF09`, cutoff, cutoff);
22063
+ `UPDATE actions SET status='cancelled', completed_at=?, cancel_reason=?
22064
+ WHERE status='executing' AND COALESCE(injected_at, created_at) < ?`
22065
+ ).run(nowIso2, `\u672A\u6536\u5230\u56DE\u6267\uFF08\u6CE8\u5165\u540E ${staleHours}h \u65E0\u95ED\u73AF\u4FE1\u53F7\uFF1B\u539F\u56E0\u672A\u77E5\uFF09`, cutoff);
22012
22066
  return Number(res.changes);
22013
22067
  }
22014
22068
  /**
@@ -22578,12 +22632,17 @@ ${D.intent_node_example}
22578
22632
  \`\`\`
22579
22633
 
22580
22634
  **Intent fields:**
22581
- - \`suggested_time\`: When to surface/execute this intent
22582
- - \`expiry\`: When this intent is no longer relevant
22635
+ - \`suggested_time\`: When to surface/execute this intent\uFF08\u89C1\u4E0B\u65B9\u65F6\u95F4\u683C\u5F0F\uFF09
22636
+ - \`expiry\`: When this intent is no longer relevant\uFF08\u89C1\u4E0B\u65B9\u65F6\u95F4\u683C\u5F0F\uFF09
22583
22637
  - \`priority\`: "low", "medium", "high", or "urgent"
22584
22638
  - \`status\`: "pending", "action_scheduled", or "resolved"
22585
22639
  - \`related_time_node\`: Link to a TIME node for urgency tracking
22586
- - \`pattern_source\`: The concept ID that triggered this intent (for pattern-based intents)`;
22640
+ - \`pattern_source\`: The concept ID that triggered this intent (for pattern-based intents)
22641
+
22642
+ **Time field format (applies to every *_time field below):**
22643
+ - Write **local wall time with NO timezone suffix** \u2014 \`"2026-09-21T09:00:00"\` means 9am in ${tz()}. This is the only accepted form.
22644
+ - **Never append \`Z\` or \`+08:00\`.** A suffixed value is read as an absolute instant, so \`"2026-09-21T09:00:00Z"\` lands at 5pm ${tz()}, not 9am \u2014 and it is logged as a contract violation.
22645
+ - Anchor on the \`## Current Time\` line when there is one, and convert relative phrasing ("\u660E\u65E99\u70B9") into that local clock.`;
22587
22646
  const SECTION_INTENTS_PATTERNS = `
22588
22647
  ## Pattern-Based Intents
22589
22648
 
@@ -22970,7 +23029,7 @@ ${contextText}
22970
23029
 
22971
23030
  ## Current Time
22972
23031
 
22973
- ${args.now.toISOString()}
23032
+ ${formatForLlm(args.now)} \u2190 use THIS as "now" (local wall time, NOT UTC)
22974
23033
 
22975
23034
  ## Existing Commitments (calendar / current work state)
22976
23035
 
@@ -23077,6 +23136,7 @@ var DEFAULT_DOMAIN, registryCache, DEFAULT_SECTION_ORDER, MODE_PROMPTS2, OPPORTU
23077
23136
  var init_prompts = __esm({
23078
23137
  "src/memory/proactive/prompts.ts"() {
23079
23138
  "use strict";
23139
+ init_clock();
23080
23140
  DEFAULT_DOMAIN = {
23081
23141
  domain_description: "\u4F60\u4E0E TA \u5171\u540C\u7684\u751F\u6D3B\u2014\u2014TA \u7684\u5DE5\u4F5C\u4E0E\u9879\u76EE\u3001\u5BB6\u4EBA\u670B\u53CB\u7684\u5F80\u6765\u3001\u5065\u5EB7\u3001\u5B66\u4E60\u6210\u957F\u548C\u65E5\u5E38\u4E8B\u52A1",
23082
23142
  event_type_desc: "\u5BF9\u8BDD\u4E2D\u53D1\u751F\u7684\u4E00\u4EF6\u4E8B\u2014\u2014TA \u8BF4\u7684\u8BDD\u3001\u4E0E\u4EBA\u7684\u4E92\u52A8\u3001\u63D0\u5230\u7684\u884C\u7A0B\u6216\u8BA1\u5212",
@@ -23098,12 +23158,12 @@ var init_prompts = __esm({
23098
23158
  time_node_example: JSON.stringify({
23099
23159
  op: "ADD_NODE",
23100
23160
  node_type: "time",
23101
- data: { id: "t-example", title: "\u4F5C\u54C1\u63D0\u4EA4\u622A\u6B62", scheduled_time: "2026-09-21T23:59:00Z", recurrence: null, urgency_window_hours: 24 }
23161
+ data: { id: "t-example", title: "\u4F5C\u54C1\u63D0\u4EA4\u622A\u6B62", scheduled_time: "2026-09-21T23:59:00", recurrence: null, urgency_window_hours: 24 }
23102
23162
  }, null, 2),
23103
23163
  intent_node_example: JSON.stringify({
23104
23164
  op: "ADD_NODE",
23105
23165
  node_type: "intent",
23106
- data: { intent_id: "i-example", title: "\u63D0\u9192 TA \u786E\u8BA4\u4F5C\u54C1\u63D0\u4EA4\u72B6\u6001", description: "\u63D0\u4EA4 9/21 \u622A\u6B62\uFF0C\u63D0\u524D\u786E\u8BA4\u72B6\u6001", status: "pending", priority: "high", suggested_time: "2026-09-18T09:00:00Z", expiry: "2026-09-21T23:59:00Z" }
23166
+ data: { intent_id: "i-example", title: "\u63D0\u9192 TA \u786E\u8BA4\u4F5C\u54C1\u63D0\u4EA4\u72B6\u6001", description: "\u63D0\u4EA4 9/21 \u622A\u6B62\uFF0C\u63D0\u524D\u786E\u8BA4\u72B6\u6001", status: "pending", priority: "high", suggested_time: "2026-09-18T09:00:00", expiry: "2026-09-21T23:59:00" }
23107
23167
  }, null, 2),
23108
23168
  pattern_intent_examples: [
23109
23169
  "**\u6A21\u5F0F**\uFF1ATA \u6BCF\u5468\u4E09\u665A\u6709\u56FA\u5B9A\u5B89\u6392\uFF08\u6982\u5FF5 c-ta-schedule\uFF0Cstrength 0.7\uFF09",
@@ -23236,7 +23296,7 @@ Return valid JSON with this structure:
23236
23296
  {
23237
23297
  "title": "Short action title (max 50 chars)",
23238
23298
  "description": "Detailed description of what to do",
23239
- "scheduled_time": "ISO 8601 datetime when to execute",
23299
+ "scheduled_time": "Local wall time, NO timezone suffix - '2026-09-21T09:00:00' means 9am ${tz()}",
23240
23300
  "estimated_duration_minutes": 30,
23241
23301
  "urgency": "low|medium|high|urgent"
23242
23302
  }
@@ -23312,7 +23372,7 @@ If deadline has no time context, fall back to normal timing rules.
23312
23372
  });
23313
23373
 
23314
23374
  // src/memory/proactive/calendar-bridge.ts
23315
- function toBeijingParts(iso) {
23375
+ function toCalendarWallParts(iso) {
23316
23376
  const b = wallParts(iso);
23317
23377
  const dateStr = `${b.mo}/${b.d}`;
23318
23378
  const timeExact = `${String(b.hh).padStart(2, "0")}:${String(b.mm).padStart(2, "0")}`;
@@ -23325,13 +23385,19 @@ function parseCalendarDateMs(dateStr, now) {
23325
23385
  const day = parseInt(parts[1], 10);
23326
23386
  if (!Number.isFinite(month) || !Number.isFinite(day)) return null;
23327
23387
  let year = wallParts(now).y;
23328
- let candidate = new Date(year, month - 1, day).getTime();
23388
+ let candidate = wallMidnightMs(year, month, day);
23389
+ if (candidate === null) return null;
23329
23390
  if (candidate < now.getTime() - 7 * 864e5) {
23330
23391
  year += 1;
23331
- candidate = new Date(year, month - 1, day).getTime();
23392
+ candidate = wallMidnightMs(year, month, day);
23332
23393
  }
23333
23394
  return candidate;
23334
23395
  }
23396
+ function wallMidnightMs(year, month, day) {
23397
+ const p22 = (n) => String(n).padStart(2, "0");
23398
+ const ms = Date.parse(parseWall(`${year}-${p22(month)}-${p22(day)}T00:00:00`));
23399
+ return Number.isNaN(ms) ? null : ms;
23400
+ }
23335
23401
  function injectCalendarEvents(store, calDb, days = 30, now = /* @__PURE__ */ new Date()) {
23336
23402
  const cutoffMs = now.getTime() + days * 864e5;
23337
23403
  const rows = listEventsForInjection(calDb, 50);
@@ -23538,6 +23604,16 @@ function enqueueOpportunity(persist, opp, anchor, planningContext, previousTitle
23538
23604
  console.log(`[proactive] opportunity enqueued (${anchor.label}): ${opp.title}`);
23539
23605
  return true;
23540
23606
  }
23607
+ function isProfilePrediction(text) {
23608
+ return PROFILE_RE.test(text);
23609
+ }
23610
+ function capForesightEntries(entries2, today, openCap = 12, upcomingCap = 8) {
23611
+ const isOpen = (e) => e.start <= today;
23612
+ const sorted = entries2.slice().sort((a, b) => Number(isOpen(b)) - Number(isOpen(a)) || (isOpen(a) ? a.end.localeCompare(b.end) : a.start.localeCompare(b.start)));
23613
+ const open = sorted.filter(isOpen).slice(0, openCap);
23614
+ const upcoming = sorted.filter((e) => !isOpen(e)).slice(0, upcomingCap);
23615
+ return [...open, ...upcoming];
23616
+ }
23541
23617
  function foresightMatchKey(text) {
23542
23618
  return text.replace(/\n+/g, " ").slice(0, FORESIGHT_TEXT_MAX).replace(/\s+/g, "");
23543
23619
  }
@@ -23587,7 +23663,16 @@ function parseForesightEntries(everosRoot, now, userId) {
23587
23663
  const key = text.replace(/\s+/g, "");
23588
23664
  if (seen.has(key)) continue;
23589
23665
  seen.add(key);
23590
- out.push({ start, end, text: text.replace(/\n+/g, " ").slice(0, FORESIGHT_TEXT_MAX), evidence: evidence.replace(/\n+/g, " ").slice(0, 80), ownerId: blockOwner });
23666
+ const textFlat = text.replace(/\n+/g, " ");
23667
+ out.push({
23668
+ start,
23669
+ end,
23670
+ text: textFlat.slice(0, FORESIGHT_TEXT_MAX),
23671
+ evidence: evidence.replace(/\n+/g, " ").slice(0, 80),
23672
+ ownerId: blockOwner,
23673
+ // 画像型在这里就标出来,扫描路径据此剔除(见 isProfilePrediction 的注释)
23674
+ kind: isProfilePrediction(textFlat) ? "profile" : "event"
23675
+ });
23591
23676
  }
23592
23677
  }
23593
23678
  return out;
@@ -23641,7 +23726,10 @@ async function runOpportunityScan(deps) {
23641
23726
  return ms !== null && ms >= now.getTime() - 864e5 && ms <= now.getTime() + 31 * 864e5;
23642
23727
  }).sort((a, b) => (parseCalendarDateMs(a.date, now) ?? 0) - (parseCalendarDateMs(b.date, now) ?? 0)).slice(0, 30).map((c) => `- [calendar #${c.id}] ${c.date} ${c.time} ${c.title}`);
23643
23728
  const userPrompt = [
23644
- `\u5F53\u524D\u65F6\u95F4\uFF1A${now.toISOString()}`,
23729
+ // 这一行必须与上面的 calendar 行同坐标:calendar 的 date/time 是**配置时区**墙上时间,
23730
+ // 而这里此前给的是 UTC Z —— 同一个 prompt 两套坐标、且都没标注,模型无从判断"现在"。
23731
+ `\u5F53\u524D\u65F6\u95F4\uFF1A${formatForLlm(now)}`,
23732
+ `\uFF08\u4E0B\u9762 calendar \u884C\u7684 date/time \u4E5F\u662F ${tz()} \u672C\u5730\u65F6\u523B\uFF0C\u4E0E\u4E4B\u540C\u4E00\u5750\u6807\uFF09`,
23645
23733
  "",
23646
23734
  "## \u6302\u7740\u7684 intent\uFF08\u672A\u5B8C\u7ED3\uFF09",
23647
23735
  activeIntents.length > 0 ? activeIntents.join("\n") : "\uFF08\u65E0\uFF09",
@@ -23649,7 +23737,8 @@ async function runOpportunityScan(deps) {
23649
23737
  "## \u672A\u6765\u65E5\u7A0B\uFF08\u672A\u6765 30 \u5929 calendar\uFF09",
23650
23738
  calendarLines.length > 0 ? calendarLines.join("\n") : "\uFF08\u65E0\uFF09",
23651
23739
  "",
23652
- "## \u9884\u6D4B\uFF08everos foresight\u2014\u2014\u6B64\u524D\u5BF9\u8BDD\u4E2D LLM \u9884\u6D4B\u7684\u672A\u6765\u4E8B\u4EF6\uFF0C\u5E26\u65F6\u95F4\u7A97\u53E3\uFF1B\u662F\u9884\u6D4B\u4E0D\u662F\u4E8B\u5B9E\uFF09",
23740
+ // 0919 删轨 B 后源头已是 mem7(945be93c 漏改的措辞)
23741
+ "## \u9884\u6D4B\uFF08mem7 foresight\u2014\u2014\u6B64\u524D\u5BF9\u8BDD\u4E2D LLM \u9884\u6D4B\u7684\u672A\u6765\u4E8B\u4EF6\uFF0C\u5E26\u65F6\u95F4\u7A97\u53E3\uFF1B\u662F\u9884\u6D4B\u4E0D\u662F\u4E8B\u5B9E\uFF09",
23653
23742
  (deps.foresightLines ?? []).length > 0 ? (deps.foresightLines ?? []).join("\n") : "\uFF08\u65E0\uFF09",
23654
23743
  "",
23655
23744
  "## \u8FD1\u671F\u4E8B\u4EF6\uFF08\u6700\u65B0 15 \u6761\uFF09",
@@ -23729,7 +23818,7 @@ async function runOpportunityScan(deps) {
23729
23818
  }
23730
23819
  return enqueued;
23731
23820
  }
23732
- var OPPORTUNITY_SCAN_INTERVAL_MS, foresightCache, FORESIGHT_CACHE_TTL_MS, FORESIGHT_TEXT_MAX, FS_MATCH_STOP;
23821
+ var OPPORTUNITY_SCAN_INTERVAL_MS, PROFILE_RE, foresightCache, FORESIGHT_CACHE_TTL_MS, FORESIGHT_TEXT_MAX, FS_MATCH_STOP;
23733
23822
  var init_opportunity = __esm({
23734
23823
  "src/memory/proactive/opportunity.ts"() {
23735
23824
  "use strict";
@@ -23739,6 +23828,7 @@ var init_opportunity = __esm({
23739
23828
  init_recall_score();
23740
23829
  init_clock();
23741
23830
  OPPORTUNITY_SCAN_INTERVAL_MS = 6 * 60 * 6e4;
23831
+ PROFILE_RE = /今后|以后|将来|往后|再次遇到|下次遇到|会一直|会习惯|会继续|会优先|会避免|会主动|会定期|会倾向于|会按|会先|时会|倾向于|更愿意|养成了?.*习惯|将养成|将优先|将避免|习惯性地|will always|will prefer|will avoid|will continue|will keep|will strictly|tends? to|is likely to (?:always|keep|continue)/i;
23742
23832
  foresightCache = /* @__PURE__ */ new Map();
23743
23833
  FORESIGHT_CACHE_TTL_MS = 30 * 6e4;
23744
23834
  FORESIGHT_TEXT_MAX = 150;
@@ -24747,48 +24837,91 @@ var init_intent = __esm({
24747
24837
 
24748
24838
  // src/memory/proactive/sweep.ts
24749
24839
  import { readFileSync as readFileSync32 } from "node:fs";
24840
+ function sharesTerm(a, b) {
24841
+ const x = normalizeTitle(a);
24842
+ const y = normalizeTitle(b);
24843
+ if (x.length < 2 || y.length < 2) return false;
24844
+ const grams = new Set(Array.from({ length: x.length - 1 }, (_, i) => x.slice(i, i + 2)));
24845
+ for (let i = 0; i < y.length - 1; i++) if (grams.has(y.slice(i, i + 2))) return true;
24846
+ return false;
24847
+ }
24848
+ function isSameCommitment(a, b) {
24849
+ const x = normalizeTitle(a);
24850
+ const y = normalizeTitle(b);
24851
+ if (!x || !y) return false;
24852
+ if (x === y) return true;
24853
+ if (x.length >= 4 && y.includes(x)) return true;
24854
+ if (y.length >= 4 && x.includes(y)) return true;
24855
+ return false;
24856
+ }
24750
24857
  function actionPayload(a, now, recentEvidence) {
24751
24858
  let suggestedTask = null;
24859
+ let scheduledLocal = null;
24752
24860
  try {
24753
- const { dateStr, timeExact } = toBeijingParts(a.scheduledTime);
24861
+ const { dateStr, timeExact } = toCalendarWallParts(a.scheduledTime);
24754
24862
  suggestedTask = { date: dateStr, time: timeExact };
24863
+ scheduledLocal = formatForLlm(a.scheduledTime);
24864
+ } catch {
24865
+ }
24866
+ let generatedLocal = null;
24867
+ try {
24868
+ generatedLocal = a.createdAt ? formatForLlm(a.createdAt) : null;
24755
24869
  } catch {
24756
24870
  }
24757
24871
  const ageHours = (now.getTime() - Date.parse(a.scheduledTime)) / 36e5;
24758
- const staleHint = ageHours > 36 ? "\u53EF\u80FD\u8FC7\u65F6\uFF0C\u6CE8\u610F\u6838\u5BF9\u2014\u2014\u751F\u6210\u4E8E\u5F88\u4E45\u4E4B\u524D\uFF0C\u4E8B\u6001\u53EF\u80FD\u5DF2\u53D8\u5316" : void 0;
24872
+ const staleHint = ageHours > STALE_HINT_HOURS ? `\u8FD9\u6761\u5DF2\u8FC7\u6392\u671F ${Math.round(ageHours)} \u5C0F\u65F6\u2014\u2014\u6B63\u6587\u91CC\u7684"\u73B0\u5728/\u4ECA\u5929"\u6307\u7684\u662F**\u751F\u6210\u65F6\u523B**\uFF0C\u4E0D\u662F\u6B64\u523B\uFF0C\u5148\u6838\u5BF9\u518D\u5224` : void 0;
24759
24873
  return {
24760
24874
  action_id: a.id,
24761
24875
  intent_id: a.intentId,
24762
24876
  title: a.title,
24763
24877
  description: a.description,
24764
24878
  scheduled_time: a.scheduledTime,
24879
+ // UTC 绝对时刻(机器坐标)
24880
+ scheduled_time_local: scheduledLocal,
24881
+ // 本地可读 —— 与上一行是同一个时刻,别当两件事
24882
+ suggested_task: suggestedTask,
24883
+ // { date, time } 本地墙上时间,calendar add-task 参数
24765
24884
  urgency: a.urgency,
24766
24885
  duration_minutes: a.durationMinutes,
24767
24886
  calendar_id: a.calendarId,
24768
24887
  status: a.status,
24769
- suggested_task: suggestedTask,
24888
+ generated_at_local: generatedLocal,
24889
+ // 这条 action 是**几点生成的**(正文里的「现在」指这个点)
24770
24890
  stale_hint: staleHint,
24771
- // 0906 anti-hindsight:近期关联证据(最新在前)——"后续对话已做完"的马后炮当场现形
24772
- recent_evidence: recentEvidence && recentEvidence.length > 0 ? recentEvidence.map((e) => `[${e.createdAt.slice(5, 16).replace("T", " ")}] ${e.title}${e.description ? `\uFF1A${e.description}` : ""}`) : void 0,
24891
+ // 0906 anti-hindsight:近期关联证据(最新在前)——"后续对话已做完"的马后炮当场现形。
24892
+ // 时间戳走 formatShort(本地);此前是 createdAt.slice(5,16) —— 那是 **UTC**,
24893
+ // 混在 suggested_task(本地)旁边且无标注。
24894
+ recent_evidence: recentEvidence && recentEvidence.length > 0 ? recentEvidence.map((e) => `[${formatShort(e.createdAt)}] ${e.title}${e.description ? `\uFF1A${e.description}` : ""}`) : void 0,
24773
24895
  // 机械可判的过期事实标记(不靠 LLM):action 创建之后 graph 又来了新证据 →
24774
24896
  // action 基于过期事实生成,先核对新证据再决定(生成侧 Time-Evidence Gate 的兜底)
24775
24897
  stale_facts: recentEvidence && recentEvidence.length > 0 && recentEvidence.some((e) => Date.parse(e.createdAt) > Date.parse(a.createdAt)) ? "\u6B64 action \u751F\u6210\u540E\u53C8\u6709\u65B0\u8BC1\u636E\uFF08\u89C1 recent_evidence \u6700\u65B0\u4E00\u6761\uFF09\u2014\u2014\u5148\u6838\u5BF9\u518D\u51B3\u5B9A\uFF0C\u591A\u534A\u8BE5 cancelled" : void 0,
24776
24898
  _received_at: now.toISOString()
24777
24899
  };
24778
24900
  }
24779
- var DEFAULT_PROMPT, Sweep;
24901
+ var BACKLOG_ALERT_RUNS, STALE_HINT_HOURS, DEFAULT_PROMPT, Sweep;
24780
24902
  var init_sweep = __esm({
24781
24903
  "src/memory/proactive/sweep.ts"() {
24782
24904
  "use strict";
24783
24905
  init_calendar_bridge();
24906
+ init_intent();
24907
+ init_clock();
24908
+ BACKLOG_ALERT_RUNS = 12;
24909
+ STALE_HINT_HOURS = 8;
24784
24910
  DEFAULT_PROMPT = `[proactive] \u6709 action \u5230\u671F\u4E86\u3002\u5904\u7406\u89C4\u5219\uFF1A
24785
24911
 
24786
- 1. **\u6279\u91CF\u5904\u7406**\uFF1A\u5148\u5168\u90E8\u5224\u65AD\u5B8C\uFF0C\u518D\u7EDF\u4E00\u6807\u8BB0\uFF08\u4E0D\u8981\u6BCF\u6761\u6807\u4E00\u6B21\uFF09
24787
- 2. **status \u53EA\u6709**\uFF1Acompleted\uFF08\u5DF2\u5904\u7406\uFF0C\u542B\u5DF2\u544A\u8BC9 TA\uFF09/ cancelled\uFF08\u8DF3\u8FC7\uFF1A\u6280\u672F\u7C7B/\u5DF2\u8FC7\u65F6/\u91CD\u590D\uFF09/ failed\uFF08\u6267\u884C\u5931\u8D25\uFF09
24912
+ 1. **\u5FC5\u987B\u8C03\u5DE5\u5177\u843D\u72B6\u6001**\uFF08\u786C\u8981\u6C42\uFF0C\u4E0D\u662F\u5EFA\u8BAE\uFF09\uFF1A
24913
+ - \u5904\u7406\u5B8C \u2192 \`proactive_done\`\uFF08action_id\uFF0C\u843D\u8FDB\u65E5\u5386\u65F6\u5E26 calendar_id\uFF09
24914
+ - \u8DF3\u8FC7 \u2192 \`proactive_cancel\`\uFF08action_id + reason\uFF09
24915
+ \u26A0\uFE0F **\u5728\u56DE\u590D\u6B63\u6587\u91CC\u5199 \`act-xxx: cancelled\` \u4E0D\u7B97\u6807\u8BB0** \u2014\u2014 \u90A3\u662F\u7559\u75D5\uFF0C\u4E0D\u843D\u5E93\u3002
24916
+ \u7CFB\u7EDF\u6709\u4E00\u9053\u515C\u5E95\u89E3\u6790\u4F1A\u635E\u6B63\u6587\uFF0C\u4F46\u5DE5\u5177\u8DEF\u5F84\u624D\u662F\u6B63\u8DEF\uFF08\u7406\u7531\u66F4\u6743\u5A01\u3001\u80FD\u56DE\u586B calendar\uFF09\u3002
24917
+ 2. **\u6279\u91CF\u5904\u7406**\uFF1A\u5148\u5168\u90E8\u5224\u65AD\u5B8C\uFF0C\u518D\u7EDF\u4E00\u8C03\u5DE5\u5177\uFF08\u4E0D\u8981\u6BCF\u6761\u8C03\u4E00\u6B21\uFF09
24918
+ 3. **status \u53EA\u6709**\uFF1Acompleted\uFF08\u5DF2\u5904\u7406\uFF0C\u542B\u5DF2\u544A\u8BC9 TA\uFF09/ cancelled\uFF08\u8DF3\u8FC7\uFF1A\u6280\u672F\u7C7B/\u5DF2\u8FC7\u65F6/\u91CD\u590D\uFF09/ failed\uFF08\u6267\u884C\u5931\u8D25\uFF09
24788
24919
  \u26A0\uFE0F \u6CA1\u6709 notified\u3001\u6CA1\u6709 skipped
24789
- 3. **\u91CD\u590D\u68C0\u6D4B**\uFF1A\u591A\u6761 title \u9AD8\u5EA6\u76F8\u4F3C\u65F6\u53EA\u5904\u7406\u4E00\u6761\uFF0C\u5176\u4F59 cancelled
24790
- 4. **urgency \u662F\u53C2\u8003**\uFF08urgent=\u51E0\u5C0F\u65F6/high=24h/medium=48h/low=\u4E00\u5468\uFF09\uFF0C\u6309\u5B9E\u9645\u60C5\u51B5\u5224\u65AD
24791
- 5. \u51B3\u5B9A\u544A\u8BC9 TA \u65F6\u7528\u4F60\u81EA\u5DF1\u7684\u8BED\u6C14\u2014\u2014\u8FD9\u662F\u4F60\u7684\u4E3B\u52A8\u5173\u6000\uFF0C\u4E0D\u662F\u7CFB\u7EDF\u901A\u77E5`;
24920
+ 4. **\u91CD\u590D\u68C0\u6D4B**\uFF1A\u591A\u6761 title \u9AD8\u5EA6\u76F8\u4F3C\u65F6\u53EA\u5904\u7406\u4E00\u6761\uFF0C\u5176\u4F59 cancelled
24921
+ 5. **\u5148\u6838\u5BF9\u518D\u5224**\uFF1A\u6B63\u6587\u91CC\u7684"\u73B0\u5728/\u4ECA\u5929"\u662F**\u751F\u6210\u65F6\u523B**\u7684\uFF0C\u4E0D\u662F\u6B64\u523B\u2014\u2014
24922
+ \u62FF \`generated_at_local\` \u8DDF \`_received_at\` \u5BF9\u4E00\u4E0B\uFF1B\u518D\u8DDF calendar/goal \u91CC\u5DF2\u6709\u7684\u5BF9\u4E00\u904D\u3002
24923
+ 6. **urgency \u662F\u53C2\u8003**\uFF08urgent=\u51E0\u5C0F\u65F6/high=24h/medium=48h/low=\u4E00\u5468\uFF09\uFF0C\u6309\u5B9E\u9645\u60C5\u51B5\u5224\u65AD
24924
+ 7. \u51B3\u5B9A\u544A\u8BC9 TA \u65F6\u7528\u4F60\u81EA\u5DF1\u7684\u8BED\u6C14\u2014\u2014\u8FD9\u662F\u4F60\u7684\u4E3B\u52A8\u5173\u6000\uFF0C\u4E0D\u662F\u7CFB\u7EDF\u901A\u77E5`;
24792
24925
  Sweep = class {
24793
24926
  constructor(deps, intervalMs = 3e5) {
24794
24927
  this.deps = deps;
@@ -24797,6 +24930,9 @@ var init_sweep = __esm({
24797
24930
  deps;
24798
24931
  intervalMs;
24799
24932
  timer = null;
24933
+ /** executing 连续增长的轮数(对账告警用) */
24934
+ backlogStreak = 0;
24935
+ lastExecuting = 0;
24800
24936
  start() {
24801
24937
  if (this.timer) return;
24802
24938
  this.timer = setInterval(() => {
@@ -24819,8 +24955,20 @@ var init_sweep = __esm({
24819
24955
  }
24820
24956
  /** 单轮:收割超时 executing → due → 标记 → 合并注入一次。返回注入条数 */
24821
24957
  runOnce(now) {
24822
- const reaped = this.deps.persist.reapStaleExecuting(now.toISOString());
24958
+ const reaped = this.deps.persist.reapStaleExecuting(now.toISOString(), this.deps.staleHours ?? 36);
24823
24959
  if (reaped > 0) console.log(`[proactive] reaped ${reaped} stale executing action(s)`);
24960
+ const execNow = this.deps.persist.countByStatus("executing");
24961
+ if (execNow > 0 && execNow >= this.lastExecuting) this.backlogStreak++;
24962
+ else this.backlogStreak = 0;
24963
+ this.lastExecuting = execNow;
24964
+ if (this.backlogStreak >= BACKLOG_ALERT_RUNS) {
24965
+ this.backlogStreak = 0;
24966
+ try {
24967
+ this.deps.onUnclosedBacklog?.(execNow);
24968
+ } catch (err) {
24969
+ console.warn(`[proactive] backlog alert failed: ${err.message}`);
24970
+ }
24971
+ }
24824
24972
  let due = this.deps.persist.dueActions(now.toISOString());
24825
24973
  if (due.length === 0) return 0;
24826
24974
  const resolvedActions = [];
@@ -24834,13 +24982,58 @@ var init_sweep = __esm({
24834
24982
  });
24835
24983
  if (resolvedActions.length > 0) {
24836
24984
  for (const { id, intentStatus } of resolvedActions) {
24837
- this.deps.persist.updateActionStatus(id, "cancelled", { cancelReason: `intent ${intentStatus}\u2014\u2014R2 sweep \u518D\u9A8C\u8BC1` });
24985
+ this.deps.persist.updateActionStatus(id, "cancelled", {
24986
+ completedAt: now.toISOString(),
24987
+ cancelReason: `intent ${intentStatus}\u2014\u2014R2 sweep \u518D\u9A8C\u8BC1`
24988
+ });
24838
24989
  }
24839
24990
  console.log(`[proactive] R2: ${resolvedActions.length} due action(s) cancelled (intent resolved)`);
24840
24991
  }
24992
+ const gatedItems = [];
24993
+ const gateOff = (a, reason) => {
24994
+ gatedItems.push({ id: a.id, title: a.title, reason });
24995
+ return false;
24996
+ };
24997
+ const coveredTitles = this.deps.coveredTitles ?? [];
24998
+ const coveredEvents = this.deps.coveredEvents ?? [];
24999
+ if (coveredTitles.length > 0 || coveredEvents.length > 0) {
25000
+ due = due.filter((a) => {
25001
+ if (coveredTitles.some((c) => isSameCommitment(a.title, c))) {
25002
+ return gateOff(a, "calendar/goal \u91CC\u5DF2\u7ECF\u6709\u540C\u4E00\u4EF6\u4E8B\u2014\u2014\u4E0D\u91CD\u590D\u63D0\u9192");
25003
+ }
25004
+ const parts = toCalendarWallParts(a.scheduledTime);
25005
+ const sameMoment = coveredEvents.some((ev) => ev.date === parts.dateStr && ev.time === parts.timeExact);
25006
+ if (sameMoment && coveredEvents.some((ev) => ev.date === parts.dateStr && ev.time === parts.timeExact && sharesTerm(a.title, ev.title))) {
25007
+ return gateOff(a, "\u540C\u4E00\u65F6\u523B calendar \u91CC\u5DF2\u6709\u540C\u4E00\u4EF6\u4E8B\uFF08\u65F6\u95F4\u951A\u70B9\u5224\u91CD\uFF09\u2014\u2014\u4E0D\u91CD\u590D\u63D0\u9192");
25008
+ }
25009
+ return true;
25010
+ });
25011
+ }
25012
+ const minLeadDays = this.deps.minLeadDays ?? 0;
25013
+ if (minLeadDays > 0) {
25014
+ due = due.filter((a) => {
25015
+ const leadMs = Date.parse(a.scheduledTime) - Date.parse(a.createdAt);
25016
+ if (leadMs >= minLeadDays * 864e5) return true;
25017
+ return gateOff(a, `\u63D0\u524D\u91CF\u4E0D\u8DB3 ${minLeadDays} \u5929\uFF08\u751F\u6210\u65F6\u79BB\u4E8B\u53D1\u592A\u8FD1\uFF09\u2014\u2014proactive \u53EA\u505A\u5468\u7EA7\u63D0\u524D\u91CF`);
25018
+ });
25019
+ }
25020
+ for (const g of gatedItems) {
25021
+ this.deps.persist.updateActionStatus(g.id, "cancelled", {
25022
+ completedAt: now.toISOString(),
25023
+ cancelReason: g.reason
25024
+ });
25025
+ }
25026
+ if (gatedItems.length > 0) {
25027
+ console.log(`[proactive] gated ${gatedItems.length} action(s): ${gatedItems.map((g) => g.reason.slice(0, 12)).join(" / ")}`);
25028
+ try {
25029
+ this.deps.onGated?.(gatedItems);
25030
+ } catch (err) {
25031
+ console.warn(`[proactive] onGated failed: ${err.message}`);
25032
+ }
25033
+ }
24841
25034
  if (due.length === 0) return 0;
24842
25035
  for (const act of due) {
24843
- this.deps.persist.markActionReminded(act.id);
25036
+ this.deps.persist.markActionReminded(act.id, now.toISOString());
24844
25037
  }
24845
25038
  const payload = due.map((a) => actionPayload(a, now, this.deps.persist.getIntentRecentEvents(a.intentId)));
24846
25039
  const blocks = [
@@ -24851,7 +25044,12 @@ ${JSON.stringify(payload, null, 2)}
24851
25044
  \`\`\`` },
24852
25045
  { type: "text", text: this.loadPrompt() }
24853
25046
  ];
24854
- this.deps.submit(blocks);
25047
+ const delivered = this.deps.submit(blocks);
25048
+ if (!delivered) {
25049
+ const back = this.deps.persist.rollbackReminded(due.map((a) => a.id));
25050
+ console.warn(`[proactive] injection dropped (no main session / guard busy) \u2014 rolled back ${back} action(s) to queued`);
25051
+ return 0;
25052
+ }
24855
25053
  console.log(`[proactive] injected ${payload.length} due action(s) to main session`);
24856
25054
  return payload.length;
24857
25055
  }
@@ -24932,7 +25130,7 @@ function registerProactiveTools() {
24932
25130
  completedAt: nowUtc(),
24933
25131
  cancelReason: String(args.reason || "\u672A\u6CE8\u660E")
24934
25132
  });
24935
- return { content: `\u2713 action ${a.id} \u5DF2\u53D6\u6D88\uFF08${String(args.reason || "\u672A\u6CE8\u660E")}\uFF09` };
25133
+ return { content: `\u2713 action ${a.id} \u6807\u8BB0\u4E3A cancelled\uFF08${String(args.reason || "\u672A\u6CE8\u660E")}\uFF09` };
24936
25134
  });
24937
25135
  return out ?? { content: "proactive \u672A\u542F\u7528", isError: true };
24938
25136
  }
@@ -25053,6 +25251,78 @@ var init_completion_evidence = __esm({
25053
25251
  }
25054
25252
  });
25055
25253
 
25254
+ // src/memory/proactive/verdict-parse.ts
25255
+ function parseVerdicts(text) {
25256
+ if (!text) return [];
25257
+ const hits = [];
25258
+ VERDICT_RE.lastIndex = 0;
25259
+ let m;
25260
+ while ((m = VERDICT_RE.exec(text)) !== null) {
25261
+ hits.push({
25262
+ id: m[1],
25263
+ status: m[2],
25264
+ start: m.index,
25265
+ end: m.index + m[0].length
25266
+ });
25267
+ }
25268
+ const byId = /* @__PURE__ */ new Map();
25269
+ for (let i = 0; i < hits.length; i++) {
25270
+ const h = hits[i];
25271
+ const segEnd = Math.min(hits[i + 1]?.start ?? text.length, h.end + SEGMENT_MAX);
25272
+ const seg = text.slice(h.end, segEnd);
25273
+ let reason = "";
25274
+ const inline = seg.match(INLINE_REASON_RE);
25275
+ if (inline) reason = inline[1] ?? inline[2] ?? inline[3] ?? "";
25276
+ if (!reason) {
25277
+ const r = seg.match(LABELED_REASON_RE);
25278
+ if (r) reason = (r[1] ?? r[2] ?? r[3] ?? r[4] ?? "").trim();
25279
+ }
25280
+ byId.set(h.id, { id: h.id, status: h.status, reason: reason.trim().slice(0, SEGMENT_MAX) });
25281
+ }
25282
+ return [...byId.values()];
25283
+ }
25284
+ function applyProseVerdicts(persist, text) {
25285
+ const verdicts = parseVerdicts(text);
25286
+ const skipped = [];
25287
+ let applied = 0;
25288
+ for (const v of verdicts) {
25289
+ const a = persist.getAction(v.id);
25290
+ if (!a) {
25291
+ skipped.push(`${v.id}(\u4E0D\u5B58\u5728)`);
25292
+ continue;
25293
+ }
25294
+ if (a.status !== "executing") {
25295
+ skipped.push(`${v.id}(${a.status})`);
25296
+ continue;
25297
+ }
25298
+ const at = nowUtc();
25299
+ if (v.status === "completed") {
25300
+ persist.updateActionStatus(v.id, "completed", {
25301
+ completedAt: at,
25302
+ ...v.reason ? { note: v.reason } : {}
25303
+ });
25304
+ } else {
25305
+ persist.updateActionStatus(v.id, v.status, {
25306
+ completedAt: at,
25307
+ cancelReason: v.reason || "\u672A\u6CE8\u660E\uFF08\u6B63\u6587\u5224\u5B9A\uFF09"
25308
+ });
25309
+ }
25310
+ applied++;
25311
+ }
25312
+ return { applied, skipped };
25313
+ }
25314
+ var VERDICT_RE, LABELED_REASON_RE, INLINE_REASON_RE, SEGMENT_MAX;
25315
+ var init_verdict_parse = __esm({
25316
+ "src/memory/proactive/verdict-parse.ts"() {
25317
+ "use strict";
25318
+ init_clock();
25319
+ VERDICT_RE = /(act-[0-9a-f]{12})\s*[::]\s*(completed|cancelled|failed)\b/g;
25320
+ LABELED_REASON_RE = /(?:cancel_reason|note|reason)\s*[::]\s*(?:"([^"]*)"|'([^']*)'|“([^”]*)”|([^\n]*))/i;
25321
+ INLINE_REASON_RE = /^\s*[,,]\s*(?:"([^"]*)"|'([^']*)'|“([^”]*)”)/;
25322
+ SEGMENT_MAX = 500;
25323
+ }
25324
+ });
25325
+
25056
25326
  // src/memory/proactive/plugin.ts
25057
25327
  var plugin_exports = {};
25058
25328
  __export(plugin_exports, {
@@ -25060,7 +25330,7 @@ __export(plugin_exports, {
25060
25330
  feedMem7Cell: () => feedMem7Cell
25061
25331
  });
25062
25332
  import { join as join41 } from "node:path";
25063
- import { existsSync as existsSync27, readFileSync as readFileSync33 } from "node:fs";
25333
+ import { existsSync as existsSync27, readFileSync as readFileSync33, appendFileSync as appendFileSync2 } from "node:fs";
25064
25334
  function isFiller(text) {
25065
25335
  const t = text.trim();
25066
25336
  if (t.length >= 5) return false;
@@ -25133,7 +25403,10 @@ var init_plugin = __esm({
25133
25403
  init_proactive_tools();
25134
25404
  init_prompts();
25135
25405
  init_completion_evidence();
25406
+ init_verdict_parse();
25407
+ init_task_manager();
25136
25408
  init_opportunity();
25409
+ init_clock();
25137
25410
  init_embedding();
25138
25411
  init_filters();
25139
25412
  init_store2();
@@ -25295,22 +25568,76 @@ ${String(input.tool_output ?? "")}`
25295
25568
  return { continue: true };
25296
25569
  }
25297
25570
  });
25571
+ registerCallbackHook("Stop", {
25572
+ type: "callback",
25573
+ id: "proactive-verdict-parse",
25574
+ callback: async (input) => {
25575
+ if (input?.channel !== "proactive") return { continue: true };
25576
+ try {
25577
+ const r = applyProseVerdicts(this.persist, String(input?.last_assistant_message ?? ""));
25578
+ if (r.applied > 0) {
25579
+ console.log(`[proactive] \u6B63\u6587\u5224\u5B9A\u56DE\u6536 ${r.applied} \u6761\uFF08\u8DF3\u8FC7 ${r.skipped.length}\uFF09`);
25580
+ }
25581
+ } catch (err) {
25582
+ console.warn(`[proactive] verdict parse failed: ${err.message}`);
25583
+ }
25584
+ return { continue: true };
25585
+ }
25586
+ });
25298
25587
  this.hookInstalled = true;
25299
25588
  activeInstance = this;
25300
25589
  console.log(`[proactive] mem7-fed ingest ready (model=${this.config.modelRef}, graphInput=${liveConfig.get("mem7.graphInput") ?? "memcell"})`);
25301
25590
  registerProactiveTools();
25302
25591
  setProactiveWorkspace(workspace);
25592
+ const pluginCalDb = this.calDb;
25303
25593
  this.sweep = new Sweep({
25304
25594
  persist: this.persist,
25305
25595
  promptFile: join41(workspace, "prompts", "proactive.md"),
25596
+ // 三个都走 getter **每轮现读** —— 热加载规则(config 改了立刻生效)+
25597
+ // calendar 会变,都不能在 start() 时快照。
25598
+ get minLeadDays() {
25599
+ return parseProactiveConfig(liveConfig.get("proactive")).leadGateDays;
25600
+ },
25601
+ get staleHours() {
25602
+ return parseProactiveConfig(liveConfig.get("proactive")).staleHours;
25603
+ },
25604
+ get coveredTitles() {
25605
+ return buildPlanningContext(pluginCalDb, workspace).coveredTitles;
25606
+ },
25607
+ // 带日期时刻的版本 —— 供给"时间锚点判重"(张小媒 0925 review 补的维度)
25608
+ get coveredEvents() {
25609
+ return pluginCalDb ? getCalendarCoverage(pluginCalDb).map((c) => ({ title: c.title, date: c.date, time: c.time })) : [];
25610
+ },
25611
+ // 闸掉的必须留痕:她要能"扫一眼标题,真有冤案我喊"。
25612
+ onGated: (items) => {
25613
+ const gatedPath = join41(workspace, ".proactive", "gated.jsonl");
25614
+ try {
25615
+ appendFileSync2(
25616
+ gatedPath,
25617
+ items.map((i) => JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), ...i })).join("\n") + "\n",
25618
+ "utf8"
25619
+ );
25620
+ } catch (err) {
25621
+ console.warn(`[proactive] gated.jsonl \u5199\u5165\u5931\u8D25: ${err.message}`);
25622
+ }
25623
+ enqueueNotification(
25624
+ buildNudgeNotification(
25625
+ "stuck",
25626
+ `proactive \u4ECA\u5929\u95F8\u6389\u4E86 ${items.length} \u6761\uFF08\u6CA1\u8FDB\u6CE8\u5165\u3001\u5DF2\u6807 cancelled\uFF09\uFF1A` + items.map((i) => `
25627
+ \xB7 ${i.title.slice(0, 28)} \u2190 ${i.reason.slice(0, 22)}`).join("") + `
25628
+ \u5168\u91CF\u6E05\u5355\uFF1A${gatedPath}\u3002\u89C9\u5F97\u54EA\u6761\u662F\u51A4\u6848\u76F4\u63A5\u8BF4\uFF0C\u6211\u8C03\u9608\u503C\u6216\u6539\u5224\u636E\u3002`
25629
+ ),
25630
+ void 0,
25631
+ `proactive-gated-${todayKey()}`
25632
+ );
25633
+ },
25306
25634
  submit: (blocks) => {
25307
25635
  const mainSessionId = ctx.sessions?.getSessionId?.("scope:main");
25308
25636
  if (!mainSessionId) {
25309
- console.warn("[proactive] no main session \u2014 dropping sweep injection");
25310
- return;
25637
+ console.warn("[proactive] no main session \u2014 sweep injection dropped");
25638
+ return false;
25311
25639
  }
25312
- ;
25313
- ctx.dispatcher.submitMessage({
25640
+ const ok = ctx.dispatcher.submitMessage({
25314
25641
  text: blocks,
25315
25642
  sessionId: mainSessionId,
25316
25643
  channelName: "proactive",
@@ -25319,6 +25646,21 @@ ${String(input.tool_output ?? "")}`
25319
25646
  skipRecall: true,
25320
25647
  deps: ctx.deps
25321
25648
  });
25649
+ if (ok === false) console.warn("[proactive] dispatcher rejected sweep injection (guard busy) \u2014 rolling back");
25650
+ return ok !== false;
25651
+ },
25652
+ // 对账告警:注入的判定连续多轮不闭环 → 直接告诉她。她本人明确要过这条
25653
+ //("能发现只增不减的人是我,但前提是我收到这个信号")——躺日志里等于没有。
25654
+ onUnclosedBacklog: (count) => {
25655
+ enqueueNotification(
25656
+ buildNudgeNotification(
25657
+ "stuck",
25658
+ `proactive \u6709 ${count} \u6761 action \u6302\u7740 executing\u3001\u8FDE\u7EED 30 \u5206\u949F\u53EA\u589E\u4E0D\u51CF\u2014\u2014\u8BF4\u660E\u6CE8\u5165\u7684\u5224\u5B9A\u6CA1\u95ED\u73AF\u3002\u5904\u7406\u5B8C\u8BF7**\u8C03 proactive_done / proactive_cancel \u5DE5\u5177**\uFF08\u5728\u6B63\u6587\u91CC\u5199 act-<id>: cancelled \u53EA\u4F5C\u515C\u5E95\uFF0C\u5DE5\u5177\u8DEF\u5F84\u7684\u7406\u7531\u66F4\u6743\u5A01\uFF09\u3002\u7528 proactive_status \u770B\u5F53\u524D\u6302\u7740\u7684\u5217\u8868\u3002`
25659
+ ),
25660
+ void 0,
25661
+ "proactive-unclosed-backlog"
25662
+ );
25663
+ console.warn(`[proactive] \u5BF9\u8D26\u544A\u8B66\u5DF2\u6CE8\u5165\uFF1Aexecuting=${count} \u8FDE\u7EED ${BACKLOG_ALERT_RUNS} \u8F6E\u53EA\u589E\u4E0D\u51CF`);
25322
25664
  }
25323
25665
  });
25324
25666
  this.sweep.start();
@@ -25344,8 +25686,12 @@ ${String(input.tool_output ?? "")}`
25344
25686
  const nowDate = /* @__PURE__ */ new Date();
25345
25687
  const stateDir = String(liveConfig.get("stateDir") ?? "");
25346
25688
  if (stateDir) {
25347
- const mem7Entries = loadForesightEntries(join41(stateDir, "mem7"), nowDate);
25348
- if (mem7Entries.length > 0) console.log(`[proactive] mem7 foresight: ${mem7Entries.length} open-window predictions`);
25689
+ const all = loadForesightEntries(join41(stateDir, "mem7"), nowDate);
25690
+ const events = all.filter((e) => e.kind !== "profile");
25691
+ const mem7Entries = capForesightEntries(events, todayKey(nowDate));
25692
+ if (all.length > 0) {
25693
+ console.log(`[proactive] mem7 foresight: ${all.length} open-window \u2192 \u5254\u753B\u50CF ${all.length - events.length} \u2192 \u9650\u989D\u540E ${mem7Entries.length} \u8FDB prompt`);
25694
+ }
25349
25695
  for (const e of mem7Entries) {
25350
25696
  const key = e.text.replace(/\s+/g, "");
25351
25697
  if (seen.has(key)) continue;
@@ -25353,7 +25699,6 @@ ${String(input.tool_output ?? "")}`
25353
25699
  foresightLines.push(`[mem7] [\u7A97\u53E3 ${e.start}~${e.end}] ${e.text}${e.evidence ? `\uFF08\u4F9D\u636E\uFF1A${e.evidence}\uFF09` : ""}`);
25354
25700
  }
25355
25701
  }
25356
- if (foresightLines.length > 0) console.log(`[proactive] mem7 foresight total: ${foresightLines.length}`);
25357
25702
  } catch (err) {
25358
25703
  console.warn(`[proactive] foresight load failed: ${err.message}`);
25359
25704
  }
@@ -25377,6 +25722,7 @@ ${String(input.tool_output ?? "")}`
25377
25722
  this.hookInstalled = false;
25378
25723
  if (activeInstance === this) activeInstance = null;
25379
25724
  unregisterCallbackHook("PostToolUse", "proactive-completion-evidence");
25725
+ unregisterCallbackHook("Stop", "proactive-verdict-parse");
25380
25726
  await this.waitForIdle();
25381
25727
  this.sweep?.stop();
25382
25728
  this.sweep = null;
@@ -29676,6 +30022,15 @@ function buildDirTree(v) {
29676
30022
  content: readTemplate("workspace/prompts/contacts.md").replace("{{DISCORD_USER_ID}}", v.discordUserId || "YOUR_DISCORD_ID").replace("{{FEISHU_OPEN_ID}}", v.feishuOpenId || "YOUR_FEISHU_OPEN_ID")
29677
30023
  },
29678
30024
  { path: path34.join(w, "prompts", "auto-memory-instructions.md"), type: "file", content: readTemplate("workspace/prompts/auto-memory-instructions.md") },
30025
+ {
30026
+ // 0925:proactive 到期处理规则。此前仓内**零模板**——那份规则只在私有部署的
30027
+ // stateDir 里手工存在,装机用户吃的是引擎内置兜底文案(那份也没点名工具)。
30028
+ // 结果:agent 用正文写判定、不调工具,判定全部不落库。
30029
+ // 注意 readTemplate 找不到模板会 **throw**,所以本文件与 templates/ 下那份必须成对存在。
30030
+ path: path34.join(w, "prompts", "proactive.md"),
30031
+ type: "file",
30032
+ content: readTemplate("workspace/prompts/proactive.md")
30033
+ },
29679
30034
  {
29680
30035
  // 0917:inner-voice 定制 prompt 按人设装(templates/workspace/inner-voice/<persona>.md)
29681
30036
  path: path34.join(w, "prompts", "my-inner-voice.md"),