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/main.mjs CHANGED
@@ -295,6 +295,10 @@ function formatLog(i) {
295
295
  function formatWallLabel(i) {
296
296
  return formatForLlm(i).replace(/\s*(.*)$/, "");
297
297
  }
298
+ function formatShort(i) {
299
+ const p = wallParts(i ?? /* @__PURE__ */ new Date());
300
+ return `${p.mo}/${p.d} ${p2(p.hh)}:${p2(p.mm)}`;
301
+ }
298
302
  function warnOnce(callSite, shape, sample) {
299
303
  const key = `${callSite}:${shape}`;
300
304
  if (warned.has(key)) return;
@@ -10253,7 +10257,7 @@ CREATE TABLE IF NOT EXISTS watermarks (
10253
10257
  blocks.push(block);
10254
10258
  count++;
10255
10259
  }
10256
- head = head.replace(/entry_count: \d+/, `entry_count: ${count}`).replace(/last_appended_at: '[^']*'/, `last_appended_at: '${(/* @__PURE__ */ new Date()).toISOString()}'`);
10260
+ head = head.replace(/entry_count: \d+/, `entry_count: ${count}`).replace(/last_appended_at: '[^']*'/, `last_appended_at: '${formatMdTimestamp()}'`);
10257
10261
  writeFileSync7(file2, head + "\n\n" + blocks.join("\n\n") + "\n", "utf8");
10258
10262
  }
10259
10263
  /**
@@ -10673,7 +10677,7 @@ function calcDurationDays(startDate, endDate) {
10673
10677
  const ms = Date.parse(endDate + "T00:00:00Z") - Date.parse(startDate + "T00:00:00Z");
10674
10678
  return Number.isNaN(ms) ? null : Math.round(ms / 864e5);
10675
10679
  }
10676
- function cellDateCst(cell) {
10680
+ function cellDateKey(cell) {
10677
10681
  return todayKey(cell.startTs);
10678
10682
  }
10679
10683
  async function extractForesights(cell, ownerId, provider, model) {
@@ -10702,7 +10706,7 @@ async function extractForesights(cell, ownerId, provider, model) {
10702
10706
  }
10703
10707
  }
10704
10708
  if (!raw) return [];
10705
- const startFallback = cellDateCst(cell);
10709
+ const startFallback = cellDateKey(cell);
10706
10710
  const out = [];
10707
10711
  for (const r of raw) {
10708
10712
  if (!r || typeof r !== "object") continue;
@@ -10743,7 +10747,7 @@ var init_foresight = __esm({
10743
10747
  });
10744
10748
 
10745
10749
  // src/mem7/core/pipeline.ts
10746
- function dateCst(tsMs) {
10750
+ function dateKey(tsMs) {
10747
10751
  return todayKey(tsMs);
10748
10752
  }
10749
10753
  async function runMem7Pipeline(args2) {
@@ -10778,7 +10782,7 @@ async function runMem7Pipeline(args2) {
10778
10782
  store.saveTail(sessionId, tail);
10779
10783
  for (const cell of cells) {
10780
10784
  const userSenders = [...new Set(cell.messages.filter((m) => m.role === "user").map((m) => m.senderId))];
10781
- const dateStr = dateCst(cell.startTs);
10785
+ const dateStr = dateKey(cell.startTs);
10782
10786
  const cellTimeIso = new Date(cell.startTs).toISOString();
10783
10787
  let episodeOut = null;
10784
10788
  let factsOut = [];
@@ -10951,11 +10955,18 @@ function parseProactiveConfig(raw) {
10951
10955
  console.warn(`[proactive] model "${modelRef}" \u975E\u6CD5\uFF08${err.message}\uFF09\u2014\u2014 \u6539\u7528\u4E3B\u6A21\u578B`);
10952
10956
  }
10953
10957
  }
10958
+ const num = (v, dflt) => {
10959
+ const n = typeof v === "number" ? v : Number(v);
10960
+ return Number.isFinite(n) && n >= 0 ? n : dflt;
10961
+ };
10954
10962
  return {
10955
10963
  enabled: obj.enabled === true || obj.enabled === "yes" || obj.enabled === 1,
10956
10964
  modelRef: parsed ? `${parsed.providerId}/${parsed.modelId}` : "",
10957
10965
  providerId: parsed?.providerId ?? "",
10958
- modelId: parsed?.modelId ?? ""
10966
+ modelId: parsed?.modelId ?? "",
10967
+ // 默认 3 天 = 翀哥的"以周为单位";staleHours 默认 36 = 与旧行为一致
10968
+ leadGateDays: num(obj.leadGateDays, 3),
10969
+ staleHours: num(obj.staleHours, 36)
10959
10970
  };
10960
10971
  }
10961
10972
  function proactiveDbPath(workspace) {
@@ -11319,6 +11330,8 @@ function rowToAction(r) {
11319
11330
  createdAt: r.created_at,
11320
11331
  completedAt: r.completed_at ?? void 0,
11321
11332
  cancelReason: r.cancel_reason ?? void 0,
11333
+ injectedAt: r.injected_at ?? void 0,
11334
+ note: r.note ?? void 0,
11322
11335
  provenance: r.provenance ? JSON.parse(r.provenance) : void 0
11323
11336
  };
11324
11337
  }
@@ -11368,6 +11381,8 @@ CREATE TABLE IF NOT EXISTS actions (
11368
11381
  created_at TEXT NOT NULL,
11369
11382
  completed_at TEXT,
11370
11383
  cancel_reason TEXT,
11384
+ injected_at TEXT,
11385
+ note TEXT,
11371
11386
  provenance TEXT
11372
11387
  );
11373
11388
  CREATE INDEX IF NOT EXISTS idx_actions_due ON actions(status, scheduled_time);
@@ -11407,6 +11422,8 @@ CREATE INDEX IF NOT EXISTS idx_proactive_decisions_created ON proactive_decision
11407
11422
  this.db.exec(SCHEMA2);
11408
11423
  const actionCols = new Set(this.db.prepare("PRAGMA table_info(actions)").all().map((r) => r.name));
11409
11424
  if (!actionCols.has("provenance")) this.db.exec("ALTER TABLE actions ADD COLUMN provenance TEXT");
11425
+ if (!actionCols.has("injected_at")) this.db.exec("ALTER TABLE actions ADD COLUMN injected_at TEXT");
11426
+ if (!actionCols.has("note")) this.db.exec("ALTER TABLE actions ADD COLUMN note TEXT");
11410
11427
  const decisionCols = new Set(this.db.prepare("PRAGMA table_info(proactive_decisions)").all().map((r) => r.name));
11411
11428
  if (!decisionCols.has("coverage_matched")) this.db.exec("ALTER TABLE proactive_decisions ADD COLUMN coverage_matched INTEGER NOT NULL DEFAULT 0");
11412
11429
  this.migrateNakedLocalTimes();
@@ -11670,16 +11687,51 @@ CREATE INDEX IF NOT EXISTS idx_proactive_decisions_created ON proactive_decision
11670
11687
  ).run(reason, nowIso2, ...intentIds);
11671
11688
  return Number(res.changes);
11672
11689
  }
11690
+ /**
11691
+ * 改状态。**只更新显式传入的列**——此前是「任一字段有值就把两列都写」,
11692
+ * 未传的那列被写成 NULL:sweep 的 R2 复检只传 cancelReason,会把已存在的
11693
+ * completed_at 清空(0925 修)。
11694
+ */
11673
11695
  updateActionStatus(id, status, extra) {
11674
- if (extra?.completedAt !== void 0 || extra?.cancelReason !== void 0) {
11675
- this.db.prepare("UPDATE actions SET status=?, completed_at=?, cancel_reason=? WHERE id=?").run(status, extra?.completedAt ?? null, extra?.cancelReason ?? null, id);
11676
- } else {
11677
- this.db.prepare("UPDATE actions SET status=? WHERE id=?").run(status, id);
11696
+ const sets = ["status=?"];
11697
+ const vals = [status];
11698
+ if (extra?.completedAt !== void 0) {
11699
+ sets.push("completed_at=?");
11700
+ vals.push(extra.completedAt);
11701
+ }
11702
+ if (extra?.cancelReason !== void 0) {
11703
+ sets.push("cancel_reason=?");
11704
+ vals.push(extra.cancelReason);
11678
11705
  }
11706
+ if (extra?.note !== void 0) {
11707
+ sets.push("note=?");
11708
+ vals.push(extra.note);
11709
+ }
11710
+ vals.push(id);
11711
+ this.db.prepare(`UPDATE actions SET ${sets.join(", ")} WHERE id=?`).run(...vals);
11679
11712
  }
11680
- /** sweep 注入前标记(防重复注入;复刻 due→推送的原子语义) */
11681
- markActionReminded(id) {
11682
- this.db.prepare(`UPDATE actions SET status='executing' WHERE id=? AND status='queued'`).run(id);
11713
+ /** sweep 注入前标记(防重复注入;复刻 due→推送的原子语义)。
11714
+ * 0925:同时记 injected_at —— 收割机判"超时"必须拿**注入时刻**,不能拿排期/创建时刻。 */
11715
+ markActionReminded(id, injectedAt) {
11716
+ this.db.prepare(`UPDATE actions SET status='executing', injected_at=? WHERE id=? AND status='queued'`).run(injectedAt, id);
11717
+ }
11718
+ /**
11719
+ * 注入失败回滚(0925):submitMessage 被 dispatcher guard 拒了、或主 session 不在——
11720
+ * 此时 action 已被标 executing 却从没进过 agent 的视野,就是一条"谁也没见过"的僵尸。
11721
+ * 退回去重排,下一轮 sweep 还能再试。返回回滚条数。
11722
+ */
11723
+ rollbackReminded(ids) {
11724
+ if (ids.length === 0) return 0;
11725
+ const ph = ids.map(() => "?").join(",");
11726
+ const res = this.db.prepare(
11727
+ `UPDATE actions SET status='queued', injected_at=NULL WHERE status='executing' AND id IN (${ph})`
11728
+ ).run(...ids);
11729
+ return Number(res.changes);
11730
+ }
11731
+ /** 某状态的条数(对账告警用:executing 只增不减 = 闭环断了) */
11732
+ countByStatus(status) {
11733
+ const r = this.db.prepare("SELECT COUNT(*) AS c FROM actions WHERE status=?").get(status);
11734
+ return Number(r?.c ?? 0);
11683
11735
  }
11684
11736
  /**
11685
11737
  * 0915:executing 超时收割。sweep 注入时标 executing,agent 处理后标终态;
@@ -11688,10 +11740,12 @@ CREATE INDEX IF NOT EXISTS idx_proactive_decisions_created ON proactive_decision
11688
11740
  * scheduled_time 或 created_at 距今超 staleHours 的 executing → cancelled。返回收割条数。
11689
11741
  */
11690
11742
  reapStaleExecuting(nowIso2, staleHours = 36) {
11743
+ if (!(staleHours > 0)) return 0;
11691
11744
  const cutoff = new Date(Date.parse(nowIso2) - staleHours * 36e5).toISOString();
11692
11745
  const res = this.db.prepare(
11693
- `UPDATE actions SET status='cancelled', completed_at=?, cancel_reason=? WHERE status='executing' AND (scheduled_time < ? OR created_at < ?)`
11694
- ).run(nowIso2, `executing \u8D85\u65F6\u6536\u5272\uFF08\u6CE8\u5165\u540E ${staleHours}h \u672A\u95ED\u73AF\uFF09`, cutoff, cutoff);
11746
+ `UPDATE actions SET status='cancelled', completed_at=?, cancel_reason=?
11747
+ WHERE status='executing' AND COALESCE(injected_at, created_at) < ?`
11748
+ ).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);
11695
11749
  return Number(res.changes);
11696
11750
  }
11697
11751
  /**
@@ -12261,12 +12315,17 @@ ${D.intent_node_example}
12261
12315
  \`\`\`
12262
12316
 
12263
12317
  **Intent fields:**
12264
- - \`suggested_time\`: When to surface/execute this intent
12265
- - \`expiry\`: When this intent is no longer relevant
12318
+ - \`suggested_time\`: When to surface/execute this intent\uFF08\u89C1\u4E0B\u65B9\u65F6\u95F4\u683C\u5F0F\uFF09
12319
+ - \`expiry\`: When this intent is no longer relevant\uFF08\u89C1\u4E0B\u65B9\u65F6\u95F4\u683C\u5F0F\uFF09
12266
12320
  - \`priority\`: "low", "medium", "high", or "urgent"
12267
12321
  - \`status\`: "pending", "action_scheduled", or "resolved"
12268
12322
  - \`related_time_node\`: Link to a TIME node for urgency tracking
12269
- - \`pattern_source\`: The concept ID that triggered this intent (for pattern-based intents)`;
12323
+ - \`pattern_source\`: The concept ID that triggered this intent (for pattern-based intents)
12324
+
12325
+ **Time field format (applies to every *_time field below):**
12326
+ - Write **local wall time with NO timezone suffix** \u2014 \`"2026-09-21T09:00:00"\` means 9am in ${tz()}. This is the only accepted form.
12327
+ - **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.
12328
+ - Anchor on the \`## Current Time\` line when there is one, and convert relative phrasing ("\u660E\u65E99\u70B9") into that local clock.`;
12270
12329
  const SECTION_INTENTS_PATTERNS = `
12271
12330
  ## Pattern-Based Intents
12272
12331
 
@@ -12653,7 +12712,7 @@ ${contextText}
12653
12712
 
12654
12713
  ## Current Time
12655
12714
 
12656
- ${args2.now.toISOString()}
12715
+ ${formatForLlm(args2.now)} \u2190 use THIS as "now" (local wall time, NOT UTC)
12657
12716
 
12658
12717
  ## Existing Commitments (calendar / current work state)
12659
12718
 
@@ -12760,6 +12819,7 @@ var DEFAULT_DOMAIN, registryCache, DEFAULT_SECTION_ORDER, MODE_PROMPTS, OPPORTUN
12760
12819
  var init_prompts = __esm({
12761
12820
  "src/memory/proactive/prompts.ts"() {
12762
12821
  "use strict";
12822
+ init_clock();
12763
12823
  DEFAULT_DOMAIN = {
12764
12824
  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",
12765
12825
  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",
@@ -12781,12 +12841,12 @@ var init_prompts = __esm({
12781
12841
  time_node_example: JSON.stringify({
12782
12842
  op: "ADD_NODE",
12783
12843
  node_type: "time",
12784
- data: { id: "t-example", title: "\u4F5C\u54C1\u63D0\u4EA4\u622A\u6B62", scheduled_time: "2026-09-21T23:59:00Z", recurrence: null, urgency_window_hours: 24 }
12844
+ data: { id: "t-example", title: "\u4F5C\u54C1\u63D0\u4EA4\u622A\u6B62", scheduled_time: "2026-09-21T23:59:00", recurrence: null, urgency_window_hours: 24 }
12785
12845
  }, null, 2),
12786
12846
  intent_node_example: JSON.stringify({
12787
12847
  op: "ADD_NODE",
12788
12848
  node_type: "intent",
12789
- 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" }
12849
+ 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" }
12790
12850
  }, null, 2),
12791
12851
  pattern_intent_examples: [
12792
12852
  "**\u6A21\u5F0F**\uFF1ATA \u6BCF\u5468\u4E09\u665A\u6709\u56FA\u5B9A\u5B89\u6392\uFF08\u6982\u5FF5 c-ta-schedule\uFF0Cstrength 0.7\uFF09",
@@ -12919,7 +12979,7 @@ Return valid JSON with this structure:
12919
12979
  {
12920
12980
  "title": "Short action title (max 50 chars)",
12921
12981
  "description": "Detailed description of what to do",
12922
- "scheduled_time": "ISO 8601 datetime when to execute",
12982
+ "scheduled_time": "Local wall time, NO timezone suffix - '2026-09-21T09:00:00' means 9am ${tz()}",
12923
12983
  "estimated_duration_minutes": 30,
12924
12984
  "urgency": "low|medium|high|urgent"
12925
12985
  }
@@ -13663,7 +13723,7 @@ var init_commands = __esm({
13663
13723
  });
13664
13724
 
13665
13725
  // src/memory/proactive/calendar-bridge.ts
13666
- function toBeijingParts(iso) {
13726
+ function toCalendarWallParts(iso) {
13667
13727
  const b = wallParts(iso);
13668
13728
  const dateStr = `${b.mo}/${b.d}`;
13669
13729
  const timeExact = `${String(b.hh).padStart(2, "0")}:${String(b.mm).padStart(2, "0")}`;
@@ -13676,13 +13736,19 @@ function parseCalendarDateMs(dateStr, now) {
13676
13736
  const day = parseInt(parts[1], 10);
13677
13737
  if (!Number.isFinite(month) || !Number.isFinite(day)) return null;
13678
13738
  let year = wallParts(now).y;
13679
- let candidate = new Date(year, month - 1, day).getTime();
13739
+ let candidate = wallMidnightMs(year, month, day);
13740
+ if (candidate === null) return null;
13680
13741
  if (candidate < now.getTime() - 7 * 864e5) {
13681
13742
  year += 1;
13682
- candidate = new Date(year, month - 1, day).getTime();
13743
+ candidate = wallMidnightMs(year, month, day);
13683
13744
  }
13684
13745
  return candidate;
13685
13746
  }
13747
+ function wallMidnightMs(year, month, day) {
13748
+ const p22 = (n) => String(n).padStart(2, "0");
13749
+ const ms = Date.parse(parseWall(`${year}-${p22(month)}-${p22(day)}T00:00:00`));
13750
+ return Number.isNaN(ms) ? null : ms;
13751
+ }
13686
13752
  function injectCalendarEvents(store, calDb, days = 30, now = /* @__PURE__ */ new Date()) {
13687
13753
  const cutoffMs = now.getTime() + days * 864e5;
13688
13754
  const rows = listEventsForInjection(calDb, 50);
@@ -13889,6 +13955,16 @@ function enqueueOpportunity(persist, opp, anchor, planningContext, previousTitle
13889
13955
  console.log(`[proactive] opportunity enqueued (${anchor.label}): ${opp.title}`);
13890
13956
  return true;
13891
13957
  }
13958
+ function isProfilePrediction(text) {
13959
+ return PROFILE_RE.test(text);
13960
+ }
13961
+ function capForesightEntries(entries2, today2, openCap = 12, upcomingCap = 8) {
13962
+ const isOpen = (e) => e.start <= today2;
13963
+ 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)));
13964
+ const open = sorted.filter(isOpen).slice(0, openCap);
13965
+ const upcoming = sorted.filter((e) => !isOpen(e)).slice(0, upcomingCap);
13966
+ return [...open, ...upcoming];
13967
+ }
13892
13968
  function foresightMatchKey(text) {
13893
13969
  return text.replace(/\n+/g, " ").slice(0, FORESIGHT_TEXT_MAX).replace(/\s+/g, "");
13894
13970
  }
@@ -13938,7 +14014,16 @@ function parseForesightEntries(everosRoot, now, userId) {
13938
14014
  const key = text.replace(/\s+/g, "");
13939
14015
  if (seen.has(key)) continue;
13940
14016
  seen.add(key);
13941
- out.push({ start, end, text: text.replace(/\n+/g, " ").slice(0, FORESIGHT_TEXT_MAX), evidence: evidence.replace(/\n+/g, " ").slice(0, 80), ownerId: blockOwner });
14017
+ const textFlat = text.replace(/\n+/g, " ");
14018
+ out.push({
14019
+ start,
14020
+ end,
14021
+ text: textFlat.slice(0, FORESIGHT_TEXT_MAX),
14022
+ evidence: evidence.replace(/\n+/g, " ").slice(0, 80),
14023
+ ownerId: blockOwner,
14024
+ // 画像型在这里就标出来,扫描路径据此剔除(见 isProfilePrediction 的注释)
14025
+ kind: isProfilePrediction(textFlat) ? "profile" : "event"
14026
+ });
13942
14027
  }
13943
14028
  }
13944
14029
  return out;
@@ -13992,7 +14077,10 @@ async function runOpportunityScan(deps) {
13992
14077
  return ms !== null && ms >= now.getTime() - 864e5 && ms <= now.getTime() + 31 * 864e5;
13993
14078
  }).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}`);
13994
14079
  const userPrompt = [
13995
- `\u5F53\u524D\u65F6\u95F4\uFF1A${now.toISOString()}`,
14080
+ // 这一行必须与上面的 calendar 行同坐标:calendar 的 date/time 是**配置时区**墙上时间,
14081
+ // 而这里此前给的是 UTC Z —— 同一个 prompt 两套坐标、且都没标注,模型无从判断"现在"。
14082
+ `\u5F53\u524D\u65F6\u95F4\uFF1A${formatForLlm(now)}`,
14083
+ `\uFF08\u4E0B\u9762 calendar \u884C\u7684 date/time \u4E5F\u662F ${tz()} \u672C\u5730\u65F6\u523B\uFF0C\u4E0E\u4E4B\u540C\u4E00\u5750\u6807\uFF09`,
13996
14084
  "",
13997
14085
  "## \u6302\u7740\u7684 intent\uFF08\u672A\u5B8C\u7ED3\uFF09",
13998
14086
  activeIntents.length > 0 ? activeIntents.join("\n") : "\uFF08\u65E0\uFF09",
@@ -14000,7 +14088,8 @@ async function runOpportunityScan(deps) {
14000
14088
  "## \u672A\u6765\u65E5\u7A0B\uFF08\u672A\u6765 30 \u5929 calendar\uFF09",
14001
14089
  calendarLines.length > 0 ? calendarLines.join("\n") : "\uFF08\u65E0\uFF09",
14002
14090
  "",
14003
- "## \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",
14091
+ // 0919 删轨 B 后源头已是 mem7(945be93c 漏改的措辞)
14092
+ "## \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",
14004
14093
  (deps.foresightLines ?? []).length > 0 ? (deps.foresightLines ?? []).join("\n") : "\uFF08\u65E0\uFF09",
14005
14094
  "",
14006
14095
  "## \u8FD1\u671F\u4E8B\u4EF6\uFF08\u6700\u65B0 15 \u6761\uFF09",
@@ -14080,7 +14169,7 @@ async function runOpportunityScan(deps) {
14080
14169
  }
14081
14170
  return enqueued;
14082
14171
  }
14083
- var OPPORTUNITY_SCAN_INTERVAL_MS, foresightCache, FORESIGHT_CACHE_TTL_MS, FORESIGHT_TEXT_MAX, FS_MATCH_STOP;
14172
+ var OPPORTUNITY_SCAN_INTERVAL_MS, PROFILE_RE, foresightCache, FORESIGHT_CACHE_TTL_MS, FORESIGHT_TEXT_MAX, FS_MATCH_STOP;
14084
14173
  var init_opportunity = __esm({
14085
14174
  "src/memory/proactive/opportunity.ts"() {
14086
14175
  "use strict";
@@ -14090,6 +14179,7 @@ var init_opportunity = __esm({
14090
14179
  init_recall_score();
14091
14180
  init_clock();
14092
14181
  OPPORTUNITY_SCAN_INTERVAL_MS = 6 * 60 * 6e4;
14182
+ PROFILE_RE = /今后|以后|将来|往后|再次遇到|下次遇到|会一直|会习惯|会继续|会优先|会避免|会主动|会定期|会倾向于|会按|会先|时会|倾向于|更愿意|养成了?.*习惯|将养成|将优先|将避免|习惯性地|will always|will prefer|will avoid|will continue|will keep|will strictly|tends? to|is likely to (?:always|keep|continue)/i;
14093
14183
  foresightCache = /* @__PURE__ */ new Map();
14094
14184
  FORESIGHT_CACHE_TTL_MS = 30 * 6e4;
14095
14185
  FORESIGHT_TEXT_MAX = 150;
@@ -15098,48 +15188,91 @@ var init_intent = __esm({
15098
15188
 
15099
15189
  // src/memory/proactive/sweep.ts
15100
15190
  import { readFileSync as readFileSync23 } from "node:fs";
15191
+ function sharesTerm(a, b) {
15192
+ const x = normalizeTitle(a);
15193
+ const y = normalizeTitle(b);
15194
+ if (x.length < 2 || y.length < 2) return false;
15195
+ const grams = new Set(Array.from({ length: x.length - 1 }, (_, i) => x.slice(i, i + 2)));
15196
+ for (let i = 0; i < y.length - 1; i++) if (grams.has(y.slice(i, i + 2))) return true;
15197
+ return false;
15198
+ }
15199
+ function isSameCommitment(a, b) {
15200
+ const x = normalizeTitle(a);
15201
+ const y = normalizeTitle(b);
15202
+ if (!x || !y) return false;
15203
+ if (x === y) return true;
15204
+ if (x.length >= 4 && y.includes(x)) return true;
15205
+ if (y.length >= 4 && x.includes(y)) return true;
15206
+ return false;
15207
+ }
15101
15208
  function actionPayload(a, now, recentEvidence) {
15102
15209
  let suggestedTask = null;
15210
+ let scheduledLocal = null;
15103
15211
  try {
15104
- const { dateStr, timeExact } = toBeijingParts(a.scheduledTime);
15212
+ const { dateStr, timeExact } = toCalendarWallParts(a.scheduledTime);
15105
15213
  suggestedTask = { date: dateStr, time: timeExact };
15214
+ scheduledLocal = formatForLlm(a.scheduledTime);
15215
+ } catch {
15216
+ }
15217
+ let generatedLocal = null;
15218
+ try {
15219
+ generatedLocal = a.createdAt ? formatForLlm(a.createdAt) : null;
15106
15220
  } catch {
15107
15221
  }
15108
15222
  const ageHours = (now.getTime() - Date.parse(a.scheduledTime)) / 36e5;
15109
- 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;
15223
+ 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;
15110
15224
  return {
15111
15225
  action_id: a.id,
15112
15226
  intent_id: a.intentId,
15113
15227
  title: a.title,
15114
15228
  description: a.description,
15115
15229
  scheduled_time: a.scheduledTime,
15230
+ // UTC 绝对时刻(机器坐标)
15231
+ scheduled_time_local: scheduledLocal,
15232
+ // 本地可读 —— 与上一行是同一个时刻,别当两件事
15233
+ suggested_task: suggestedTask,
15234
+ // { date, time } 本地墙上时间,calendar add-task 参数
15116
15235
  urgency: a.urgency,
15117
15236
  duration_minutes: a.durationMinutes,
15118
15237
  calendar_id: a.calendarId,
15119
15238
  status: a.status,
15120
- suggested_task: suggestedTask,
15239
+ generated_at_local: generatedLocal,
15240
+ // 这条 action 是**几点生成的**(正文里的「现在」指这个点)
15121
15241
  stale_hint: staleHint,
15122
- // 0906 anti-hindsight:近期关联证据(最新在前)——"后续对话已做完"的马后炮当场现形
15123
- 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,
15242
+ // 0906 anti-hindsight:近期关联证据(最新在前)——"后续对话已做完"的马后炮当场现形。
15243
+ // 时间戳走 formatShort(本地);此前是 createdAt.slice(5,16) —— 那是 **UTC**,
15244
+ // 混在 suggested_task(本地)旁边且无标注。
15245
+ recent_evidence: recentEvidence && recentEvidence.length > 0 ? recentEvidence.map((e) => `[${formatShort(e.createdAt)}] ${e.title}${e.description ? `\uFF1A${e.description}` : ""}`) : void 0,
15124
15246
  // 机械可判的过期事实标记(不靠 LLM):action 创建之后 graph 又来了新证据 →
15125
15247
  // action 基于过期事实生成,先核对新证据再决定(生成侧 Time-Evidence Gate 的兜底)
15126
15248
  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,
15127
15249
  _received_at: now.toISOString()
15128
15250
  };
15129
15251
  }
15130
- var DEFAULT_PROMPT, Sweep;
15252
+ var BACKLOG_ALERT_RUNS, STALE_HINT_HOURS, DEFAULT_PROMPT, Sweep;
15131
15253
  var init_sweep = __esm({
15132
15254
  "src/memory/proactive/sweep.ts"() {
15133
15255
  "use strict";
15134
15256
  init_calendar_bridge();
15257
+ init_intent();
15258
+ init_clock();
15259
+ BACKLOG_ALERT_RUNS = 12;
15260
+ STALE_HINT_HOURS = 8;
15135
15261
  DEFAULT_PROMPT = `[proactive] \u6709 action \u5230\u671F\u4E86\u3002\u5904\u7406\u89C4\u5219\uFF1A
15136
15262
 
15137
- 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
15138
- 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
15263
+ 1. **\u5FC5\u987B\u8C03\u5DE5\u5177\u843D\u72B6\u6001**\uFF08\u786C\u8981\u6C42\uFF0C\u4E0D\u662F\u5EFA\u8BAE\uFF09\uFF1A
15264
+ - \u5904\u7406\u5B8C \u2192 \`proactive_done\`\uFF08action_id\uFF0C\u843D\u8FDB\u65E5\u5386\u65F6\u5E26 calendar_id\uFF09
15265
+ - \u8DF3\u8FC7 \u2192 \`proactive_cancel\`\uFF08action_id + reason\uFF09
15266
+ \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
15267
+ \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
15268
+ 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
15269
+ 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
15139
15270
  \u26A0\uFE0F \u6CA1\u6709 notified\u3001\u6CA1\u6709 skipped
15140
- 3. **\u91CD\u590D\u68C0\u6D4B**\uFF1A\u591A\u6761 title \u9AD8\u5EA6\u76F8\u4F3C\u65F6\u53EA\u5904\u7406\u4E00\u6761\uFF0C\u5176\u4F59 cancelled
15141
- 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
15142
- 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`;
15271
+ 4. **\u91CD\u590D\u68C0\u6D4B**\uFF1A\u591A\u6761 title \u9AD8\u5EA6\u76F8\u4F3C\u65F6\u53EA\u5904\u7406\u4E00\u6761\uFF0C\u5176\u4F59 cancelled
15272
+ 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
15273
+ \u62FF \`generated_at_local\` \u8DDF \`_received_at\` \u5BF9\u4E00\u4E0B\uFF1B\u518D\u8DDF calendar/goal \u91CC\u5DF2\u6709\u7684\u5BF9\u4E00\u904D\u3002
15274
+ 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
15275
+ 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`;
15143
15276
  Sweep = class {
15144
15277
  constructor(deps, intervalMs = 3e5) {
15145
15278
  this.deps = deps;
@@ -15148,6 +15281,9 @@ var init_sweep = __esm({
15148
15281
  deps;
15149
15282
  intervalMs;
15150
15283
  timer = null;
15284
+ /** executing 连续增长的轮数(对账告警用) */
15285
+ backlogStreak = 0;
15286
+ lastExecuting = 0;
15151
15287
  start() {
15152
15288
  if (this.timer) return;
15153
15289
  this.timer = setInterval(() => {
@@ -15170,8 +15306,20 @@ var init_sweep = __esm({
15170
15306
  }
15171
15307
  /** 单轮:收割超时 executing → due → 标记 → 合并注入一次。返回注入条数 */
15172
15308
  runOnce(now) {
15173
- const reaped = this.deps.persist.reapStaleExecuting(now.toISOString());
15309
+ const reaped = this.deps.persist.reapStaleExecuting(now.toISOString(), this.deps.staleHours ?? 36);
15174
15310
  if (reaped > 0) console.log(`[proactive] reaped ${reaped} stale executing action(s)`);
15311
+ const execNow = this.deps.persist.countByStatus("executing");
15312
+ if (execNow > 0 && execNow >= this.lastExecuting) this.backlogStreak++;
15313
+ else this.backlogStreak = 0;
15314
+ this.lastExecuting = execNow;
15315
+ if (this.backlogStreak >= BACKLOG_ALERT_RUNS) {
15316
+ this.backlogStreak = 0;
15317
+ try {
15318
+ this.deps.onUnclosedBacklog?.(execNow);
15319
+ } catch (err) {
15320
+ console.warn(`[proactive] backlog alert failed: ${err.message}`);
15321
+ }
15322
+ }
15175
15323
  let due = this.deps.persist.dueActions(now.toISOString());
15176
15324
  if (due.length === 0) return 0;
15177
15325
  const resolvedActions = [];
@@ -15185,13 +15333,58 @@ var init_sweep = __esm({
15185
15333
  });
15186
15334
  if (resolvedActions.length > 0) {
15187
15335
  for (const { id, intentStatus } of resolvedActions) {
15188
- this.deps.persist.updateActionStatus(id, "cancelled", { cancelReason: `intent ${intentStatus}\u2014\u2014R2 sweep \u518D\u9A8C\u8BC1` });
15336
+ this.deps.persist.updateActionStatus(id, "cancelled", {
15337
+ completedAt: now.toISOString(),
15338
+ cancelReason: `intent ${intentStatus}\u2014\u2014R2 sweep \u518D\u9A8C\u8BC1`
15339
+ });
15189
15340
  }
15190
15341
  console.log(`[proactive] R2: ${resolvedActions.length} due action(s) cancelled (intent resolved)`);
15191
15342
  }
15343
+ const gatedItems = [];
15344
+ const gateOff = (a, reason) => {
15345
+ gatedItems.push({ id: a.id, title: a.title, reason });
15346
+ return false;
15347
+ };
15348
+ const coveredTitles = this.deps.coveredTitles ?? [];
15349
+ const coveredEvents = this.deps.coveredEvents ?? [];
15350
+ if (coveredTitles.length > 0 || coveredEvents.length > 0) {
15351
+ due = due.filter((a) => {
15352
+ if (coveredTitles.some((c) => isSameCommitment(a.title, c))) {
15353
+ return gateOff(a, "calendar/goal \u91CC\u5DF2\u7ECF\u6709\u540C\u4E00\u4EF6\u4E8B\u2014\u2014\u4E0D\u91CD\u590D\u63D0\u9192");
15354
+ }
15355
+ const parts = toCalendarWallParts(a.scheduledTime);
15356
+ const sameMoment = coveredEvents.some((ev) => ev.date === parts.dateStr && ev.time === parts.timeExact);
15357
+ if (sameMoment && coveredEvents.some((ev) => ev.date === parts.dateStr && ev.time === parts.timeExact && sharesTerm(a.title, ev.title))) {
15358
+ 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");
15359
+ }
15360
+ return true;
15361
+ });
15362
+ }
15363
+ const minLeadDays = this.deps.minLeadDays ?? 0;
15364
+ if (minLeadDays > 0) {
15365
+ due = due.filter((a) => {
15366
+ const leadMs = Date.parse(a.scheduledTime) - Date.parse(a.createdAt);
15367
+ if (leadMs >= minLeadDays * 864e5) return true;
15368
+ 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`);
15369
+ });
15370
+ }
15371
+ for (const g of gatedItems) {
15372
+ this.deps.persist.updateActionStatus(g.id, "cancelled", {
15373
+ completedAt: now.toISOString(),
15374
+ cancelReason: g.reason
15375
+ });
15376
+ }
15377
+ if (gatedItems.length > 0) {
15378
+ console.log(`[proactive] gated ${gatedItems.length} action(s): ${gatedItems.map((g) => g.reason.slice(0, 12)).join(" / ")}`);
15379
+ try {
15380
+ this.deps.onGated?.(gatedItems);
15381
+ } catch (err) {
15382
+ console.warn(`[proactive] onGated failed: ${err.message}`);
15383
+ }
15384
+ }
15192
15385
  if (due.length === 0) return 0;
15193
15386
  for (const act of due) {
15194
- this.deps.persist.markActionReminded(act.id);
15387
+ this.deps.persist.markActionReminded(act.id, now.toISOString());
15195
15388
  }
15196
15389
  const payload = due.map((a) => actionPayload(a, now, this.deps.persist.getIntentRecentEvents(a.intentId)));
15197
15390
  const blocks = [
@@ -15202,7 +15395,12 @@ ${JSON.stringify(payload, null, 2)}
15202
15395
  \`\`\`` },
15203
15396
  { type: "text", text: this.loadPrompt() }
15204
15397
  ];
15205
- this.deps.submit(blocks);
15398
+ const delivered = this.deps.submit(blocks);
15399
+ if (!delivered) {
15400
+ const back = this.deps.persist.rollbackReminded(due.map((a) => a.id));
15401
+ console.warn(`[proactive] injection dropped (no main session / guard busy) \u2014 rolled back ${back} action(s) to queued`);
15402
+ return 0;
15403
+ }
15206
15404
  console.log(`[proactive] injected ${payload.length} due action(s) to main session`);
15207
15405
  return payload.length;
15208
15406
  }
@@ -15283,7 +15481,7 @@ function registerProactiveTools() {
15283
15481
  completedAt: nowUtc(),
15284
15482
  cancelReason: String(args2.reason || "\u672A\u6CE8\u660E")
15285
15483
  });
15286
- return { content: `\u2713 action ${a.id} \u5DF2\u53D6\u6D88\uFF08${String(args2.reason || "\u672A\u6CE8\u660E")}\uFF09` };
15484
+ return { content: `\u2713 action ${a.id} \u6807\u8BB0\u4E3A cancelled\uFF08${String(args2.reason || "\u672A\u6CE8\u660E")}\uFF09` };
15287
15485
  });
15288
15486
  return out ?? { content: "proactive \u672A\u542F\u7528", isError: true };
15289
15487
  }
@@ -15404,6 +15602,78 @@ var init_completion_evidence = __esm({
15404
15602
  }
15405
15603
  });
15406
15604
 
15605
+ // src/memory/proactive/verdict-parse.ts
15606
+ function parseVerdicts(text) {
15607
+ if (!text) return [];
15608
+ const hits = [];
15609
+ VERDICT_RE.lastIndex = 0;
15610
+ let m;
15611
+ while ((m = VERDICT_RE.exec(text)) !== null) {
15612
+ hits.push({
15613
+ id: m[1],
15614
+ status: m[2],
15615
+ start: m.index,
15616
+ end: m.index + m[0].length
15617
+ });
15618
+ }
15619
+ const byId = /* @__PURE__ */ new Map();
15620
+ for (let i = 0; i < hits.length; i++) {
15621
+ const h = hits[i];
15622
+ const segEnd = Math.min(hits[i + 1]?.start ?? text.length, h.end + SEGMENT_MAX);
15623
+ const seg = text.slice(h.end, segEnd);
15624
+ let reason = "";
15625
+ const inline = seg.match(INLINE_REASON_RE);
15626
+ if (inline) reason = inline[1] ?? inline[2] ?? inline[3] ?? "";
15627
+ if (!reason) {
15628
+ const r = seg.match(LABELED_REASON_RE);
15629
+ if (r) reason = (r[1] ?? r[2] ?? r[3] ?? r[4] ?? "").trim();
15630
+ }
15631
+ byId.set(h.id, { id: h.id, status: h.status, reason: reason.trim().slice(0, SEGMENT_MAX) });
15632
+ }
15633
+ return [...byId.values()];
15634
+ }
15635
+ function applyProseVerdicts(persist, text) {
15636
+ const verdicts = parseVerdicts(text);
15637
+ const skipped = [];
15638
+ let applied = 0;
15639
+ for (const v of verdicts) {
15640
+ const a = persist.getAction(v.id);
15641
+ if (!a) {
15642
+ skipped.push(`${v.id}(\u4E0D\u5B58\u5728)`);
15643
+ continue;
15644
+ }
15645
+ if (a.status !== "executing") {
15646
+ skipped.push(`${v.id}(${a.status})`);
15647
+ continue;
15648
+ }
15649
+ const at = nowUtc();
15650
+ if (v.status === "completed") {
15651
+ persist.updateActionStatus(v.id, "completed", {
15652
+ completedAt: at,
15653
+ ...v.reason ? { note: v.reason } : {}
15654
+ });
15655
+ } else {
15656
+ persist.updateActionStatus(v.id, v.status, {
15657
+ completedAt: at,
15658
+ cancelReason: v.reason || "\u672A\u6CE8\u660E\uFF08\u6B63\u6587\u5224\u5B9A\uFF09"
15659
+ });
15660
+ }
15661
+ applied++;
15662
+ }
15663
+ return { applied, skipped };
15664
+ }
15665
+ var VERDICT_RE, LABELED_REASON_RE, INLINE_REASON_RE, SEGMENT_MAX;
15666
+ var init_verdict_parse = __esm({
15667
+ "src/memory/proactive/verdict-parse.ts"() {
15668
+ "use strict";
15669
+ init_clock();
15670
+ VERDICT_RE = /(act-[0-9a-f]{12})\s*[::]\s*(completed|cancelled|failed)\b/g;
15671
+ LABELED_REASON_RE = /(?:cancel_reason|note|reason)\s*[::]\s*(?:"([^"]*)"|'([^']*)'|“([^”]*)”|([^\n]*))/i;
15672
+ INLINE_REASON_RE = /^\s*[,,]\s*(?:"([^"]*)"|'([^']*)'|“([^”]*)”)/;
15673
+ SEGMENT_MAX = 500;
15674
+ }
15675
+ });
15676
+
15407
15677
  // src/memory/proactive/plugin.ts
15408
15678
  var plugin_exports = {};
15409
15679
  __export(plugin_exports, {
@@ -15411,7 +15681,7 @@ __export(plugin_exports, {
15411
15681
  feedMem7Cell: () => feedMem7Cell
15412
15682
  });
15413
15683
  import { join as join32 } from "node:path";
15414
- import { existsSync as existsSync18, readFileSync as readFileSync24 } from "node:fs";
15684
+ import { existsSync as existsSync18, readFileSync as readFileSync24, appendFileSync as appendFileSync3 } from "node:fs";
15415
15685
  function isFiller(text) {
15416
15686
  const t = text.trim();
15417
15687
  if (t.length >= 5) return false;
@@ -15484,7 +15754,10 @@ var init_plugin = __esm({
15484
15754
  init_proactive_tools();
15485
15755
  init_prompts();
15486
15756
  init_completion_evidence();
15757
+ init_verdict_parse();
15758
+ init_task_manager();
15487
15759
  init_opportunity();
15760
+ init_clock();
15488
15761
  init_embedding();
15489
15762
  init_filters();
15490
15763
  init_store2();
@@ -15646,22 +15919,76 @@ ${String(input.tool_output ?? "")}`
15646
15919
  return { continue: true };
15647
15920
  }
15648
15921
  });
15922
+ registerCallbackHook("Stop", {
15923
+ type: "callback",
15924
+ id: "proactive-verdict-parse",
15925
+ callback: async (input) => {
15926
+ if (input?.channel !== "proactive") return { continue: true };
15927
+ try {
15928
+ const r = applyProseVerdicts(this.persist, String(input?.last_assistant_message ?? ""));
15929
+ if (r.applied > 0) {
15930
+ console.log(`[proactive] \u6B63\u6587\u5224\u5B9A\u56DE\u6536 ${r.applied} \u6761\uFF08\u8DF3\u8FC7 ${r.skipped.length}\uFF09`);
15931
+ }
15932
+ } catch (err) {
15933
+ console.warn(`[proactive] verdict parse failed: ${err.message}`);
15934
+ }
15935
+ return { continue: true };
15936
+ }
15937
+ });
15649
15938
  this.hookInstalled = true;
15650
15939
  activeInstance = this;
15651
15940
  console.log(`[proactive] mem7-fed ingest ready (model=${this.config.modelRef}, graphInput=${liveConfig.get("mem7.graphInput") ?? "memcell"})`);
15652
15941
  registerProactiveTools();
15653
15942
  setProactiveWorkspace(workspace);
15943
+ const pluginCalDb = this.calDb;
15654
15944
  this.sweep = new Sweep({
15655
15945
  persist: this.persist,
15656
15946
  promptFile: join32(workspace, "prompts", "proactive.md"),
15947
+ // 三个都走 getter **每轮现读** —— 热加载规则(config 改了立刻生效)+
15948
+ // calendar 会变,都不能在 start() 时快照。
15949
+ get minLeadDays() {
15950
+ return parseProactiveConfig(liveConfig.get("proactive")).leadGateDays;
15951
+ },
15952
+ get staleHours() {
15953
+ return parseProactiveConfig(liveConfig.get("proactive")).staleHours;
15954
+ },
15955
+ get coveredTitles() {
15956
+ return buildPlanningContext(pluginCalDb, workspace).coveredTitles;
15957
+ },
15958
+ // 带日期时刻的版本 —— 供给"时间锚点判重"(张小媒 0925 review 补的维度)
15959
+ get coveredEvents() {
15960
+ return pluginCalDb ? getCalendarCoverage(pluginCalDb).map((c) => ({ title: c.title, date: c.date, time: c.time })) : [];
15961
+ },
15962
+ // 闸掉的必须留痕:她要能"扫一眼标题,真有冤案我喊"。
15963
+ onGated: (items) => {
15964
+ const gatedPath = join32(workspace, ".proactive", "gated.jsonl");
15965
+ try {
15966
+ appendFileSync3(
15967
+ gatedPath,
15968
+ items.map((i) => JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), ...i })).join("\n") + "\n",
15969
+ "utf8"
15970
+ );
15971
+ } catch (err) {
15972
+ console.warn(`[proactive] gated.jsonl \u5199\u5165\u5931\u8D25: ${err.message}`);
15973
+ }
15974
+ enqueueNotification(
15975
+ buildNudgeNotification(
15976
+ "stuck",
15977
+ `proactive \u4ECA\u5929\u95F8\u6389\u4E86 ${items.length} \u6761\uFF08\u6CA1\u8FDB\u6CE8\u5165\u3001\u5DF2\u6807 cancelled\uFF09\uFF1A` + items.map((i) => `
15978
+ \xB7 ${i.title.slice(0, 28)} \u2190 ${i.reason.slice(0, 22)}`).join("") + `
15979
+ \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`
15980
+ ),
15981
+ void 0,
15982
+ `proactive-gated-${todayKey()}`
15983
+ );
15984
+ },
15657
15985
  submit: (blocks) => {
15658
15986
  const mainSessionId = ctx.sessions?.getSessionId?.("scope:main");
15659
15987
  if (!mainSessionId) {
15660
- console.warn("[proactive] no main session \u2014 dropping sweep injection");
15661
- return;
15988
+ console.warn("[proactive] no main session \u2014 sweep injection dropped");
15989
+ return false;
15662
15990
  }
15663
- ;
15664
- ctx.dispatcher.submitMessage({
15991
+ const ok = ctx.dispatcher.submitMessage({
15665
15992
  text: blocks,
15666
15993
  sessionId: mainSessionId,
15667
15994
  channelName: "proactive",
@@ -15670,6 +15997,21 @@ ${String(input.tool_output ?? "")}`
15670
15997
  skipRecall: true,
15671
15998
  deps: ctx.deps
15672
15999
  });
16000
+ if (ok === false) console.warn("[proactive] dispatcher rejected sweep injection (guard busy) \u2014 rolling back");
16001
+ return ok !== false;
16002
+ },
16003
+ // 对账告警:注入的判定连续多轮不闭环 → 直接告诉她。她本人明确要过这条
16004
+ //("能发现只增不减的人是我,但前提是我收到这个信号")——躺日志里等于没有。
16005
+ onUnclosedBacklog: (count) => {
16006
+ enqueueNotification(
16007
+ buildNudgeNotification(
16008
+ "stuck",
16009
+ `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`
16010
+ ),
16011
+ void 0,
16012
+ "proactive-unclosed-backlog"
16013
+ );
16014
+ console.warn(`[proactive] \u5BF9\u8D26\u544A\u8B66\u5DF2\u6CE8\u5165\uFF1Aexecuting=${count} \u8FDE\u7EED ${BACKLOG_ALERT_RUNS} \u8F6E\u53EA\u589E\u4E0D\u51CF`);
15673
16015
  }
15674
16016
  });
15675
16017
  this.sweep.start();
@@ -15695,8 +16037,12 @@ ${String(input.tool_output ?? "")}`
15695
16037
  const nowDate = /* @__PURE__ */ new Date();
15696
16038
  const stateDir = String(liveConfig.get("stateDir") ?? "");
15697
16039
  if (stateDir) {
15698
- const mem7Entries = loadForesightEntries(join32(stateDir, "mem7"), nowDate);
15699
- if (mem7Entries.length > 0) console.log(`[proactive] mem7 foresight: ${mem7Entries.length} open-window predictions`);
16040
+ const all = loadForesightEntries(join32(stateDir, "mem7"), nowDate);
16041
+ const events = all.filter((e) => e.kind !== "profile");
16042
+ const mem7Entries = capForesightEntries(events, todayKey(nowDate));
16043
+ if (all.length > 0) {
16044
+ 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`);
16045
+ }
15700
16046
  for (const e of mem7Entries) {
15701
16047
  const key = e.text.replace(/\s+/g, "");
15702
16048
  if (seen.has(key)) continue;
@@ -15704,7 +16050,6 @@ ${String(input.tool_output ?? "")}`
15704
16050
  foresightLines.push(`[mem7] [\u7A97\u53E3 ${e.start}~${e.end}] ${e.text}${e.evidence ? `\uFF08\u4F9D\u636E\uFF1A${e.evidence}\uFF09` : ""}`);
15705
16051
  }
15706
16052
  }
15707
- if (foresightLines.length > 0) console.log(`[proactive] mem7 foresight total: ${foresightLines.length}`);
15708
16053
  } catch (err) {
15709
16054
  console.warn(`[proactive] foresight load failed: ${err.message}`);
15710
16055
  }
@@ -15728,6 +16073,7 @@ ${String(input.tool_output ?? "")}`
15728
16073
  this.hookInstalled = false;
15729
16074
  if (activeInstance === this) activeInstance = null;
15730
16075
  unregisterCallbackHook("PostToolUse", "proactive-completion-evidence");
16076
+ unregisterCallbackHook("Stop", "proactive-verdict-parse");
15731
16077
  await this.waitForIdle();
15732
16078
  this.sweep?.stop();
15733
16079
  this.sweep = null;
@@ -29164,7 +29510,7 @@ var NEGATIVE_PATTERNS = [
29164
29510
  var COLD_PATTERN = /^[嗯哦额喔]+$/;
29165
29511
  var INJECTED_MARKERS = ["\u3010\u5B9A\u65F6\u5FC3\u8DF3\u3011", "[\u5185\u5FC3\u5BF9\u8BDD\u6D4B\u8BD5]", "[inner-voice]"];
29166
29512
  function initialState() {
29167
- return { version: 1, mood: NEUTRAL, trend: "stable", updatedAt: nowIsoBj(), events: [] };
29513
+ return { version: 1, mood: NEUTRAL, trend: "stable", updatedAt: nowLocalIso(), events: [] };
29168
29514
  }
29169
29515
  async function updateEmotionalState(workspace, sessions) {
29170
29516
  const stateFile = path29.join(workspace, "inner-voice", "emotional-state.json");
@@ -29189,7 +29535,7 @@ async function updateEmotionalState(workspace, sessions) {
29189
29535
  refreshHoursAgo(state2.events);
29190
29536
  const recentV = state2.events.slice(-5).filter((e) => e.hoursAgo < 4).reduce((s, e) => s + e.valence, 0);
29191
29537
  state2.trend = recentV > 0.2 ? "rising" : recentV < -0.2 ? "falling" : "stable";
29192
- state2.updatedAt = nowIsoBj();
29538
+ state2.updatedAt = nowLocalIso();
29193
29539
  saveJson(stateFile, state2);
29194
29540
  const recent = state2.events.filter((e) => e.hoursAgo < 4).slice(-3);
29195
29541
  const summary = recent.length ? recent.map((e) => `${e.summary}(${e.valence >= 0 ? "+" : ""}${e.valence.toFixed(1)})`).join(" | ") : "\u65E0\u8FD1\u671F\u60C5\u611F\u4E8B\u4EF6";
@@ -29319,7 +29665,7 @@ function refreshHoursAgo(events) {
29319
29665
  function appendMoodLog(workspace, state2, summary) {
29320
29666
  try {
29321
29667
  const logPath = path29.join(workspace, "mood-history.log");
29322
- const ts = formatBj(/* @__PURE__ */ new Date(), false);
29668
+ const ts = formatLocal(/* @__PURE__ */ new Date(), false);
29323
29669
  fs29.appendFileSync(logPath, `${ts} mood=${state2.mood.toFixed(2)} trend=${state2.trend} ${summary}
29324
29670
  `);
29325
29671
  } catch (err) {
@@ -29370,10 +29716,10 @@ function clamp(x, lo, hi) {
29370
29716
  function clamp01(x) {
29371
29717
  return clamp(x, 0, 1);
29372
29718
  }
29373
- function nowIsoBj() {
29719
+ function nowLocalIso() {
29374
29720
  return `${toWall().replace(" ", "T")}${localOffsetSuffix()}`;
29375
29721
  }
29376
- function formatBj(d, withSec) {
29722
+ function formatLocal(d, withSec) {
29377
29723
  const p = (n) => String(n).padStart(2, "0");
29378
29724
  const t = wallParts(d);
29379
29725
  return `${t.y}-${p(t.mo)}-${p(t.d)} ${p(t.hh)}:${p(t.mm)}${withSec ? ":" + p(t.ss) : ""}`;
@@ -33562,6 +33908,9 @@ function buildReportPayload(opts) {
33562
33908
  msgsToday: opts.counters.msgsToday,
33563
33909
  msgsTotal: opts.counters.msgsTotal
33564
33910
  };
33911
+ if (opts.counters.lastMsgAt > 0 || opts.counters.msgsTotal === 0) {
33912
+ p.lastMsgAt = opts.counters.lastMsgAt;
33913
+ }
33565
33914
  if (opts.identity.contactEmail) p.contactEmail = opts.identity.contactEmail;
33566
33915
  return p;
33567
33916
  }
@@ -33573,6 +33922,7 @@ var nowFn = () => /* @__PURE__ */ new Date();
33573
33922
  var today = "";
33574
33923
  var msgsToday = 0;
33575
33924
  var msgsTotal = 0;
33925
+ var lastMsgAt = 0;
33576
33926
  function rolloverIfNeeded(now) {
33577
33927
  const d = todayKey(now);
33578
33928
  if (d !== today) {
@@ -33586,15 +33936,17 @@ function noteUserMessage(source) {
33586
33936
  rolloverIfNeeded(now);
33587
33937
  msgsToday++;
33588
33938
  msgsTotal++;
33939
+ lastMsgAt = Math.floor(now.getTime() / 1e3);
33589
33940
  }
33590
33941
  function snapshotCounters() {
33591
33942
  rolloverIfNeeded(nowFn());
33592
- return { day: today, msgsToday, msgsTotal };
33943
+ return { day: today, msgsToday, msgsTotal, lastMsgAt };
33593
33944
  }
33594
33945
  function restoreCounters(s) {
33595
33946
  today = todayKey(nowFn());
33596
33947
  msgsToday = s.day === today && typeof s.msgsToday === "number" && s.msgsToday > 0 ? s.msgsToday : 0;
33597
33948
  msgsTotal = typeof s.msgsTotal === "number" && s.msgsTotal > 0 ? s.msgsTotal : 0;
33949
+ lastMsgAt = typeof s.lastMsgAt === "number" && Number.isFinite(s.lastMsgAt) && s.lastMsgAt > 0 ? s.lastMsgAt : 0;
33598
33950
  }
33599
33951
 
33600
33952
  // src/optional/reporting/reporter.ts