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.
@@ -7419,6 +7419,10 @@ function formatLog(i) {
7419
7419
  function formatWallLabel(i) {
7420
7420
  return formatForLlm(i).replace(/\s*(.*)$/, "");
7421
7421
  }
7422
+ function formatShort(i) {
7423
+ const p = wallParts(i ?? /* @__PURE__ */ new Date());
7424
+ return `${p.mo}/${p.d} ${p2(p.hh)}:${p2(p.mm)}`;
7425
+ }
7422
7426
  function warnOnce(callSite, shape, sample) {
7423
7427
  const key = `${callSite}:${shape}`;
7424
7428
  if (warned.has(key)) return;
@@ -10115,7 +10119,7 @@ CREATE TABLE IF NOT EXISTS watermarks (
10115
10119
  blocks.push(block);
10116
10120
  count++;
10117
10121
  }
10118
- head = head.replace(/entry_count: \d+/, `entry_count: ${count}`).replace(/last_appended_at: '[^']*'/, `last_appended_at: '${(/* @__PURE__ */ new Date()).toISOString()}'`);
10122
+ head = head.replace(/entry_count: \d+/, `entry_count: ${count}`).replace(/last_appended_at: '[^']*'/, `last_appended_at: '${formatMdTimestamp()}'`);
10119
10123
  writeFileSync6(file2, head + "\n\n" + blocks.join("\n\n") + "\n", "utf8");
10120
10124
  }
10121
10125
  /**
@@ -10535,7 +10539,7 @@ function calcDurationDays(startDate, endDate) {
10535
10539
  const ms = Date.parse(endDate + "T00:00:00Z") - Date.parse(startDate + "T00:00:00Z");
10536
10540
  return Number.isNaN(ms) ? null : Math.round(ms / 864e5);
10537
10541
  }
10538
- function cellDateCst(cell) {
10542
+ function cellDateKey(cell) {
10539
10543
  return todayKey(cell.startTs);
10540
10544
  }
10541
10545
  async function extractForesights(cell, ownerId, provider, model) {
@@ -10564,7 +10568,7 @@ async function extractForesights(cell, ownerId, provider, model) {
10564
10568
  }
10565
10569
  }
10566
10570
  if (!raw) return [];
10567
- const startFallback = cellDateCst(cell);
10571
+ const startFallback = cellDateKey(cell);
10568
10572
  const out = [];
10569
10573
  for (const r of raw) {
10570
10574
  if (!r || typeof r !== "object") continue;
@@ -10605,7 +10609,7 @@ var init_foresight = __esm({
10605
10609
  });
10606
10610
 
10607
10611
  // src/mem7/core/pipeline.ts
10608
- function dateCst(tsMs) {
10612
+ function dateKey(tsMs) {
10609
10613
  return todayKey(tsMs);
10610
10614
  }
10611
10615
  async function runMem7Pipeline(args) {
@@ -10640,7 +10644,7 @@ async function runMem7Pipeline(args) {
10640
10644
  store.saveTail(sessionId, tail);
10641
10645
  for (const cell of cells) {
10642
10646
  const userSenders = [...new Set(cell.messages.filter((m) => m.role === "user").map((m) => m.senderId))];
10643
- const dateStr = dateCst(cell.startTs);
10647
+ const dateStr = dateKey(cell.startTs);
10644
10648
  const cellTimeIso = new Date(cell.startTs).toISOString();
10645
10649
  let episodeOut = null;
10646
10650
  let factsOut = [];
@@ -10813,11 +10817,18 @@ function parseProactiveConfig(raw) {
10813
10817
  console.warn(`[proactive] model "${modelRef}" \u975E\u6CD5\uFF08${err.message}\uFF09\u2014\u2014 \u6539\u7528\u4E3B\u6A21\u578B`);
10814
10818
  }
10815
10819
  }
10820
+ const num = (v, dflt) => {
10821
+ const n = typeof v === "number" ? v : Number(v);
10822
+ return Number.isFinite(n) && n >= 0 ? n : dflt;
10823
+ };
10816
10824
  return {
10817
10825
  enabled: obj.enabled === true || obj.enabled === "yes" || obj.enabled === 1,
10818
10826
  modelRef: parsed ? `${parsed.providerId}/${parsed.modelId}` : "",
10819
10827
  providerId: parsed?.providerId ?? "",
10820
- modelId: parsed?.modelId ?? ""
10828
+ modelId: parsed?.modelId ?? "",
10829
+ // 默认 3 天 = 翀哥的"以周为单位";staleHours 默认 36 = 与旧行为一致
10830
+ leadGateDays: num(obj.leadGateDays, 3),
10831
+ staleHours: num(obj.staleHours, 36)
10821
10832
  };
10822
10833
  }
10823
10834
  function proactiveDbPath(workspace) {
@@ -11181,6 +11192,8 @@ function rowToAction(r) {
11181
11192
  createdAt: r.created_at,
11182
11193
  completedAt: r.completed_at ?? void 0,
11183
11194
  cancelReason: r.cancel_reason ?? void 0,
11195
+ injectedAt: r.injected_at ?? void 0,
11196
+ note: r.note ?? void 0,
11184
11197
  provenance: r.provenance ? JSON.parse(r.provenance) : void 0
11185
11198
  };
11186
11199
  }
@@ -11230,6 +11243,8 @@ CREATE TABLE IF NOT EXISTS actions (
11230
11243
  created_at TEXT NOT NULL,
11231
11244
  completed_at TEXT,
11232
11245
  cancel_reason TEXT,
11246
+ injected_at TEXT,
11247
+ note TEXT,
11233
11248
  provenance TEXT
11234
11249
  );
11235
11250
  CREATE INDEX IF NOT EXISTS idx_actions_due ON actions(status, scheduled_time);
@@ -11269,6 +11284,8 @@ CREATE INDEX IF NOT EXISTS idx_proactive_decisions_created ON proactive_decision
11269
11284
  this.db.exec(SCHEMA2);
11270
11285
  const actionCols = new Set(this.db.prepare("PRAGMA table_info(actions)").all().map((r) => r.name));
11271
11286
  if (!actionCols.has("provenance")) this.db.exec("ALTER TABLE actions ADD COLUMN provenance TEXT");
11287
+ if (!actionCols.has("injected_at")) this.db.exec("ALTER TABLE actions ADD COLUMN injected_at TEXT");
11288
+ if (!actionCols.has("note")) this.db.exec("ALTER TABLE actions ADD COLUMN note TEXT");
11272
11289
  const decisionCols = new Set(this.db.prepare("PRAGMA table_info(proactive_decisions)").all().map((r) => r.name));
11273
11290
  if (!decisionCols.has("coverage_matched")) this.db.exec("ALTER TABLE proactive_decisions ADD COLUMN coverage_matched INTEGER NOT NULL DEFAULT 0");
11274
11291
  this.migrateNakedLocalTimes();
@@ -11532,16 +11549,51 @@ CREATE INDEX IF NOT EXISTS idx_proactive_decisions_created ON proactive_decision
11532
11549
  ).run(reason, nowIso2, ...intentIds);
11533
11550
  return Number(res.changes);
11534
11551
  }
11552
+ /**
11553
+ * 改状态。**只更新显式传入的列**——此前是「任一字段有值就把两列都写」,
11554
+ * 未传的那列被写成 NULL:sweep 的 R2 复检只传 cancelReason,会把已存在的
11555
+ * completed_at 清空(0925 修)。
11556
+ */
11535
11557
  updateActionStatus(id, status, extra) {
11536
- if (extra?.completedAt !== void 0 || extra?.cancelReason !== void 0) {
11537
- this.db.prepare("UPDATE actions SET status=?, completed_at=?, cancel_reason=? WHERE id=?").run(status, extra?.completedAt ?? null, extra?.cancelReason ?? null, id);
11538
- } else {
11539
- this.db.prepare("UPDATE actions SET status=? WHERE id=?").run(status, id);
11558
+ const sets = ["status=?"];
11559
+ const vals = [status];
11560
+ if (extra?.completedAt !== void 0) {
11561
+ sets.push("completed_at=?");
11562
+ vals.push(extra.completedAt);
11563
+ }
11564
+ if (extra?.cancelReason !== void 0) {
11565
+ sets.push("cancel_reason=?");
11566
+ vals.push(extra.cancelReason);
11540
11567
  }
11568
+ if (extra?.note !== void 0) {
11569
+ sets.push("note=?");
11570
+ vals.push(extra.note);
11571
+ }
11572
+ vals.push(id);
11573
+ this.db.prepare(`UPDATE actions SET ${sets.join(", ")} WHERE id=?`).run(...vals);
11541
11574
  }
11542
- /** sweep 注入前标记(防重复注入;复刻 due→推送的原子语义) */
11543
- markActionReminded(id) {
11544
- this.db.prepare(`UPDATE actions SET status='executing' WHERE id=? AND status='queued'`).run(id);
11575
+ /** sweep 注入前标记(防重复注入;复刻 due→推送的原子语义)。
11576
+ * 0925:同时记 injected_at —— 收割机判"超时"必须拿**注入时刻**,不能拿排期/创建时刻。 */
11577
+ markActionReminded(id, injectedAt) {
11578
+ this.db.prepare(`UPDATE actions SET status='executing', injected_at=? WHERE id=? AND status='queued'`).run(injectedAt, id);
11579
+ }
11580
+ /**
11581
+ * 注入失败回滚(0925):submitMessage 被 dispatcher guard 拒了、或主 session 不在——
11582
+ * 此时 action 已被标 executing 却从没进过 agent 的视野,就是一条"谁也没见过"的僵尸。
11583
+ * 退回去重排,下一轮 sweep 还能再试。返回回滚条数。
11584
+ */
11585
+ rollbackReminded(ids) {
11586
+ if (ids.length === 0) return 0;
11587
+ const ph = ids.map(() => "?").join(",");
11588
+ const res = this.db.prepare(
11589
+ `UPDATE actions SET status='queued', injected_at=NULL WHERE status='executing' AND id IN (${ph})`
11590
+ ).run(...ids);
11591
+ return Number(res.changes);
11592
+ }
11593
+ /** 某状态的条数(对账告警用:executing 只增不减 = 闭环断了) */
11594
+ countByStatus(status) {
11595
+ const r = this.db.prepare("SELECT COUNT(*) AS c FROM actions WHERE status=?").get(status);
11596
+ return Number(r?.c ?? 0);
11545
11597
  }
11546
11598
  /**
11547
11599
  * 0915:executing 超时收割。sweep 注入时标 executing,agent 处理后标终态;
@@ -11550,10 +11602,12 @@ CREATE INDEX IF NOT EXISTS idx_proactive_decisions_created ON proactive_decision
11550
11602
  * scheduled_time 或 created_at 距今超 staleHours 的 executing → cancelled。返回收割条数。
11551
11603
  */
11552
11604
  reapStaleExecuting(nowIso2, staleHours = 36) {
11605
+ if (!(staleHours > 0)) return 0;
11553
11606
  const cutoff = new Date(Date.parse(nowIso2) - staleHours * 36e5).toISOString();
11554
11607
  const res = this.db.prepare(
11555
- `UPDATE actions SET status='cancelled', completed_at=?, cancel_reason=? WHERE status='executing' AND (scheduled_time < ? OR created_at < ?)`
11556
- ).run(nowIso2, `executing \u8D85\u65F6\u6536\u5272\uFF08\u6CE8\u5165\u540E ${staleHours}h \u672A\u95ED\u73AF\uFF09`, cutoff, cutoff);
11608
+ `UPDATE actions SET status='cancelled', completed_at=?, cancel_reason=?
11609
+ WHERE status='executing' AND COALESCE(injected_at, created_at) < ?`
11610
+ ).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);
11557
11611
  return Number(res.changes);
11558
11612
  }
11559
11613
  /**
@@ -12123,12 +12177,17 @@ ${D.intent_node_example}
12123
12177
  \`\`\`
12124
12178
 
12125
12179
  **Intent fields:**
12126
- - \`suggested_time\`: When to surface/execute this intent
12127
- - \`expiry\`: When this intent is no longer relevant
12180
+ - \`suggested_time\`: When to surface/execute this intent\uFF08\u89C1\u4E0B\u65B9\u65F6\u95F4\u683C\u5F0F\uFF09
12181
+ - \`expiry\`: When this intent is no longer relevant\uFF08\u89C1\u4E0B\u65B9\u65F6\u95F4\u683C\u5F0F\uFF09
12128
12182
  - \`priority\`: "low", "medium", "high", or "urgent"
12129
12183
  - \`status\`: "pending", "action_scheduled", or "resolved"
12130
12184
  - \`related_time_node\`: Link to a TIME node for urgency tracking
12131
- - \`pattern_source\`: The concept ID that triggered this intent (for pattern-based intents)`;
12185
+ - \`pattern_source\`: The concept ID that triggered this intent (for pattern-based intents)
12186
+
12187
+ **Time field format (applies to every *_time field below):**
12188
+ - Write **local wall time with NO timezone suffix** \u2014 \`"2026-09-21T09:00:00"\` means 9am in ${tz()}. This is the only accepted form.
12189
+ - **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.
12190
+ - Anchor on the \`## Current Time\` line when there is one, and convert relative phrasing ("\u660E\u65E99\u70B9") into that local clock.`;
12132
12191
  const SECTION_INTENTS_PATTERNS = `
12133
12192
  ## Pattern-Based Intents
12134
12193
 
@@ -12515,7 +12574,7 @@ ${contextText}
12515
12574
 
12516
12575
  ## Current Time
12517
12576
 
12518
- ${args.now.toISOString()}
12577
+ ${formatForLlm(args.now)} \u2190 use THIS as "now" (local wall time, NOT UTC)
12519
12578
 
12520
12579
  ## Existing Commitments (calendar / current work state)
12521
12580
 
@@ -12622,6 +12681,7 @@ var DEFAULT_DOMAIN, registryCache, DEFAULT_SECTION_ORDER, MODE_PROMPTS, OPPORTUN
12622
12681
  var init_prompts = __esm({
12623
12682
  "src/memory/proactive/prompts.ts"() {
12624
12683
  "use strict";
12684
+ init_clock();
12625
12685
  DEFAULT_DOMAIN = {
12626
12686
  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",
12627
12687
  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",
@@ -12643,12 +12703,12 @@ var init_prompts = __esm({
12643
12703
  time_node_example: JSON.stringify({
12644
12704
  op: "ADD_NODE",
12645
12705
  node_type: "time",
12646
- data: { id: "t-example", title: "\u4F5C\u54C1\u63D0\u4EA4\u622A\u6B62", scheduled_time: "2026-09-21T23:59:00Z", recurrence: null, urgency_window_hours: 24 }
12706
+ data: { id: "t-example", title: "\u4F5C\u54C1\u63D0\u4EA4\u622A\u6B62", scheduled_time: "2026-09-21T23:59:00", recurrence: null, urgency_window_hours: 24 }
12647
12707
  }, null, 2),
12648
12708
  intent_node_example: JSON.stringify({
12649
12709
  op: "ADD_NODE",
12650
12710
  node_type: "intent",
12651
- 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" }
12711
+ 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" }
12652
12712
  }, null, 2),
12653
12713
  pattern_intent_examples: [
12654
12714
  "**\u6A21\u5F0F**\uFF1ATA \u6BCF\u5468\u4E09\u665A\u6709\u56FA\u5B9A\u5B89\u6392\uFF08\u6982\u5FF5 c-ta-schedule\uFF0Cstrength 0.7\uFF09",
@@ -12781,7 +12841,7 @@ Return valid JSON with this structure:
12781
12841
  {
12782
12842
  "title": "Short action title (max 50 chars)",
12783
12843
  "description": "Detailed description of what to do",
12784
- "scheduled_time": "ISO 8601 datetime when to execute",
12844
+ "scheduled_time": "Local wall time, NO timezone suffix - '2026-09-21T09:00:00' means 9am ${tz()}",
12785
12845
  "estimated_duration_minutes": 30,
12786
12846
  "urgency": "low|medium|high|urgent"
12787
12847
  }
@@ -13525,7 +13585,7 @@ var init_commands = __esm({
13525
13585
  });
13526
13586
 
13527
13587
  // src/memory/proactive/calendar-bridge.ts
13528
- function toBeijingParts(iso) {
13588
+ function toCalendarWallParts(iso) {
13529
13589
  const b = wallParts(iso);
13530
13590
  const dateStr = `${b.mo}/${b.d}`;
13531
13591
  const timeExact = `${String(b.hh).padStart(2, "0")}:${String(b.mm).padStart(2, "0")}`;
@@ -13538,13 +13598,19 @@ function parseCalendarDateMs(dateStr, now) {
13538
13598
  const day = parseInt(parts[1], 10);
13539
13599
  if (!Number.isFinite(month) || !Number.isFinite(day)) return null;
13540
13600
  let year = wallParts(now).y;
13541
- let candidate = new Date(year, month - 1, day).getTime();
13601
+ let candidate = wallMidnightMs(year, month, day);
13602
+ if (candidate === null) return null;
13542
13603
  if (candidate < now.getTime() - 7 * 864e5) {
13543
13604
  year += 1;
13544
- candidate = new Date(year, month - 1, day).getTime();
13605
+ candidate = wallMidnightMs(year, month, day);
13545
13606
  }
13546
13607
  return candidate;
13547
13608
  }
13609
+ function wallMidnightMs(year, month, day) {
13610
+ const p22 = (n) => String(n).padStart(2, "0");
13611
+ const ms = Date.parse(parseWall(`${year}-${p22(month)}-${p22(day)}T00:00:00`));
13612
+ return Number.isNaN(ms) ? null : ms;
13613
+ }
13548
13614
  function injectCalendarEvents(store, calDb, days = 30, now = /* @__PURE__ */ new Date()) {
13549
13615
  const cutoffMs = now.getTime() + days * 864e5;
13550
13616
  const rows = listEventsForInjection(calDb, 50);
@@ -13751,6 +13817,16 @@ function enqueueOpportunity(persist, opp, anchor, planningContext, previousTitle
13751
13817
  console.log(`[proactive] opportunity enqueued (${anchor.label}): ${opp.title}`);
13752
13818
  return true;
13753
13819
  }
13820
+ function isProfilePrediction(text) {
13821
+ return PROFILE_RE.test(text);
13822
+ }
13823
+ function capForesightEntries(entries2, today2, openCap = 12, upcomingCap = 8) {
13824
+ const isOpen = (e) => e.start <= today2;
13825
+ 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)));
13826
+ const open = sorted.filter(isOpen).slice(0, openCap);
13827
+ const upcoming = sorted.filter((e) => !isOpen(e)).slice(0, upcomingCap);
13828
+ return [...open, ...upcoming];
13829
+ }
13754
13830
  function foresightMatchKey(text) {
13755
13831
  return text.replace(/\n+/g, " ").slice(0, FORESIGHT_TEXT_MAX).replace(/\s+/g, "");
13756
13832
  }
@@ -13800,7 +13876,16 @@ function parseForesightEntries(everosRoot, now, userId) {
13800
13876
  const key = text.replace(/\s+/g, "");
13801
13877
  if (seen.has(key)) continue;
13802
13878
  seen.add(key);
13803
- out.push({ start, end, text: text.replace(/\n+/g, " ").slice(0, FORESIGHT_TEXT_MAX), evidence: evidence.replace(/\n+/g, " ").slice(0, 80), ownerId: blockOwner });
13879
+ const textFlat = text.replace(/\n+/g, " ");
13880
+ out.push({
13881
+ start,
13882
+ end,
13883
+ text: textFlat.slice(0, FORESIGHT_TEXT_MAX),
13884
+ evidence: evidence.replace(/\n+/g, " ").slice(0, 80),
13885
+ ownerId: blockOwner,
13886
+ // 画像型在这里就标出来,扫描路径据此剔除(见 isProfilePrediction 的注释)
13887
+ kind: isProfilePrediction(textFlat) ? "profile" : "event"
13888
+ });
13804
13889
  }
13805
13890
  }
13806
13891
  return out;
@@ -13854,7 +13939,10 @@ async function runOpportunityScan(deps) {
13854
13939
  return ms !== null && ms >= now.getTime() - 864e5 && ms <= now.getTime() + 31 * 864e5;
13855
13940
  }).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}`);
13856
13941
  const userPrompt = [
13857
- `\u5F53\u524D\u65F6\u95F4\uFF1A${now.toISOString()}`,
13942
+ // 这一行必须与上面的 calendar 行同坐标:calendar 的 date/time 是**配置时区**墙上时间,
13943
+ // 而这里此前给的是 UTC Z —— 同一个 prompt 两套坐标、且都没标注,模型无从判断"现在"。
13944
+ `\u5F53\u524D\u65F6\u95F4\uFF1A${formatForLlm(now)}`,
13945
+ `\uFF08\u4E0B\u9762 calendar \u884C\u7684 date/time \u4E5F\u662F ${tz()} \u672C\u5730\u65F6\u523B\uFF0C\u4E0E\u4E4B\u540C\u4E00\u5750\u6807\uFF09`,
13858
13946
  "",
13859
13947
  "## \u6302\u7740\u7684 intent\uFF08\u672A\u5B8C\u7ED3\uFF09",
13860
13948
  activeIntents.length > 0 ? activeIntents.join("\n") : "\uFF08\u65E0\uFF09",
@@ -13862,7 +13950,8 @@ async function runOpportunityScan(deps) {
13862
13950
  "## \u672A\u6765\u65E5\u7A0B\uFF08\u672A\u6765 30 \u5929 calendar\uFF09",
13863
13951
  calendarLines.length > 0 ? calendarLines.join("\n") : "\uFF08\u65E0\uFF09",
13864
13952
  "",
13865
- "## \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",
13953
+ // 0919 删轨 B 后源头已是 mem7(945be93c 漏改的措辞)
13954
+ "## \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",
13866
13955
  (deps.foresightLines ?? []).length > 0 ? (deps.foresightLines ?? []).join("\n") : "\uFF08\u65E0\uFF09",
13867
13956
  "",
13868
13957
  "## \u8FD1\u671F\u4E8B\u4EF6\uFF08\u6700\u65B0 15 \u6761\uFF09",
@@ -13942,7 +14031,7 @@ async function runOpportunityScan(deps) {
13942
14031
  }
13943
14032
  return enqueued;
13944
14033
  }
13945
- var OPPORTUNITY_SCAN_INTERVAL_MS, foresightCache, FORESIGHT_CACHE_TTL_MS, FORESIGHT_TEXT_MAX, FS_MATCH_STOP;
14034
+ var OPPORTUNITY_SCAN_INTERVAL_MS, PROFILE_RE, foresightCache, FORESIGHT_CACHE_TTL_MS, FORESIGHT_TEXT_MAX, FS_MATCH_STOP;
13946
14035
  var init_opportunity = __esm({
13947
14036
  "src/memory/proactive/opportunity.ts"() {
13948
14037
  "use strict";
@@ -13952,6 +14041,7 @@ var init_opportunity = __esm({
13952
14041
  init_recall_score();
13953
14042
  init_clock();
13954
14043
  OPPORTUNITY_SCAN_INTERVAL_MS = 6 * 60 * 6e4;
14044
+ PROFILE_RE = /今后|以后|将来|往后|再次遇到|下次遇到|会一直|会习惯|会继续|会优先|会避免|会主动|会定期|会倾向于|会按|会先|时会|倾向于|更愿意|养成了?.*习惯|将养成|将优先|将避免|习惯性地|will always|will prefer|will avoid|will continue|will keep|will strictly|tends? to|is likely to (?:always|keep|continue)/i;
13955
14045
  foresightCache = /* @__PURE__ */ new Map();
13956
14046
  FORESIGHT_CACHE_TTL_MS = 30 * 6e4;
13957
14047
  FORESIGHT_TEXT_MAX = 150;
@@ -14960,48 +15050,91 @@ var init_intent = __esm({
14960
15050
 
14961
15051
  // src/memory/proactive/sweep.ts
14962
15052
  import { readFileSync as readFileSync22 } from "node:fs";
15053
+ function sharesTerm(a, b) {
15054
+ const x = normalizeTitle(a);
15055
+ const y = normalizeTitle(b);
15056
+ if (x.length < 2 || y.length < 2) return false;
15057
+ const grams = new Set(Array.from({ length: x.length - 1 }, (_, i) => x.slice(i, i + 2)));
15058
+ for (let i = 0; i < y.length - 1; i++) if (grams.has(y.slice(i, i + 2))) return true;
15059
+ return false;
15060
+ }
15061
+ function isSameCommitment(a, b) {
15062
+ const x = normalizeTitle(a);
15063
+ const y = normalizeTitle(b);
15064
+ if (!x || !y) return false;
15065
+ if (x === y) return true;
15066
+ if (x.length >= 4 && y.includes(x)) return true;
15067
+ if (y.length >= 4 && x.includes(y)) return true;
15068
+ return false;
15069
+ }
14963
15070
  function actionPayload(a, now, recentEvidence) {
14964
15071
  let suggestedTask = null;
15072
+ let scheduledLocal = null;
14965
15073
  try {
14966
- const { dateStr, timeExact } = toBeijingParts(a.scheduledTime);
15074
+ const { dateStr, timeExact } = toCalendarWallParts(a.scheduledTime);
14967
15075
  suggestedTask = { date: dateStr, time: timeExact };
15076
+ scheduledLocal = formatForLlm(a.scheduledTime);
15077
+ } catch {
15078
+ }
15079
+ let generatedLocal = null;
15080
+ try {
15081
+ generatedLocal = a.createdAt ? formatForLlm(a.createdAt) : null;
14968
15082
  } catch {
14969
15083
  }
14970
15084
  const ageHours = (now.getTime() - Date.parse(a.scheduledTime)) / 36e5;
14971
- 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;
15085
+ 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;
14972
15086
  return {
14973
15087
  action_id: a.id,
14974
15088
  intent_id: a.intentId,
14975
15089
  title: a.title,
14976
15090
  description: a.description,
14977
15091
  scheduled_time: a.scheduledTime,
15092
+ // UTC 绝对时刻(机器坐标)
15093
+ scheduled_time_local: scheduledLocal,
15094
+ // 本地可读 —— 与上一行是同一个时刻,别当两件事
15095
+ suggested_task: suggestedTask,
15096
+ // { date, time } 本地墙上时间,calendar add-task 参数
14978
15097
  urgency: a.urgency,
14979
15098
  duration_minutes: a.durationMinutes,
14980
15099
  calendar_id: a.calendarId,
14981
15100
  status: a.status,
14982
- suggested_task: suggestedTask,
15101
+ generated_at_local: generatedLocal,
15102
+ // 这条 action 是**几点生成的**(正文里的「现在」指这个点)
14983
15103
  stale_hint: staleHint,
14984
- // 0906 anti-hindsight:近期关联证据(最新在前)——"后续对话已做完"的马后炮当场现形
14985
- 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,
15104
+ // 0906 anti-hindsight:近期关联证据(最新在前)——"后续对话已做完"的马后炮当场现形。
15105
+ // 时间戳走 formatShort(本地);此前是 createdAt.slice(5,16) —— 那是 **UTC**,
15106
+ // 混在 suggested_task(本地)旁边且无标注。
15107
+ recent_evidence: recentEvidence && recentEvidence.length > 0 ? recentEvidence.map((e) => `[${formatShort(e.createdAt)}] ${e.title}${e.description ? `\uFF1A${e.description}` : ""}`) : void 0,
14986
15108
  // 机械可判的过期事实标记(不靠 LLM):action 创建之后 graph 又来了新证据 →
14987
15109
  // action 基于过期事实生成,先核对新证据再决定(生成侧 Time-Evidence Gate 的兜底)
14988
15110
  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,
14989
15111
  _received_at: now.toISOString()
14990
15112
  };
14991
15113
  }
14992
- var DEFAULT_PROMPT, Sweep;
15114
+ var BACKLOG_ALERT_RUNS, STALE_HINT_HOURS, DEFAULT_PROMPT, Sweep;
14993
15115
  var init_sweep = __esm({
14994
15116
  "src/memory/proactive/sweep.ts"() {
14995
15117
  "use strict";
14996
15118
  init_calendar_bridge();
15119
+ init_intent();
15120
+ init_clock();
15121
+ BACKLOG_ALERT_RUNS = 12;
15122
+ STALE_HINT_HOURS = 8;
14997
15123
  DEFAULT_PROMPT = `[proactive] \u6709 action \u5230\u671F\u4E86\u3002\u5904\u7406\u89C4\u5219\uFF1A
14998
15124
 
14999
- 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
15000
- 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
15125
+ 1. **\u5FC5\u987B\u8C03\u5DE5\u5177\u843D\u72B6\u6001**\uFF08\u786C\u8981\u6C42\uFF0C\u4E0D\u662F\u5EFA\u8BAE\uFF09\uFF1A
15126
+ - \u5904\u7406\u5B8C \u2192 \`proactive_done\`\uFF08action_id\uFF0C\u843D\u8FDB\u65E5\u5386\u65F6\u5E26 calendar_id\uFF09
15127
+ - \u8DF3\u8FC7 \u2192 \`proactive_cancel\`\uFF08action_id + reason\uFF09
15128
+ \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
15129
+ \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
15130
+ 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
15131
+ 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
15001
15132
  \u26A0\uFE0F \u6CA1\u6709 notified\u3001\u6CA1\u6709 skipped
15002
- 3. **\u91CD\u590D\u68C0\u6D4B**\uFF1A\u591A\u6761 title \u9AD8\u5EA6\u76F8\u4F3C\u65F6\u53EA\u5904\u7406\u4E00\u6761\uFF0C\u5176\u4F59 cancelled
15003
- 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
15004
- 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`;
15133
+ 4. **\u91CD\u590D\u68C0\u6D4B**\uFF1A\u591A\u6761 title \u9AD8\u5EA6\u76F8\u4F3C\u65F6\u53EA\u5904\u7406\u4E00\u6761\uFF0C\u5176\u4F59 cancelled
15134
+ 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
15135
+ \u62FF \`generated_at_local\` \u8DDF \`_received_at\` \u5BF9\u4E00\u4E0B\uFF1B\u518D\u8DDF calendar/goal \u91CC\u5DF2\u6709\u7684\u5BF9\u4E00\u904D\u3002
15136
+ 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
15137
+ 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`;
15005
15138
  Sweep = class {
15006
15139
  constructor(deps, intervalMs = 3e5) {
15007
15140
  this.deps = deps;
@@ -15010,6 +15143,9 @@ var init_sweep = __esm({
15010
15143
  deps;
15011
15144
  intervalMs;
15012
15145
  timer = null;
15146
+ /** executing 连续增长的轮数(对账告警用) */
15147
+ backlogStreak = 0;
15148
+ lastExecuting = 0;
15013
15149
  start() {
15014
15150
  if (this.timer) return;
15015
15151
  this.timer = setInterval(() => {
@@ -15032,8 +15168,20 @@ var init_sweep = __esm({
15032
15168
  }
15033
15169
  /** 单轮:收割超时 executing → due → 标记 → 合并注入一次。返回注入条数 */
15034
15170
  runOnce(now) {
15035
- const reaped = this.deps.persist.reapStaleExecuting(now.toISOString());
15171
+ const reaped = this.deps.persist.reapStaleExecuting(now.toISOString(), this.deps.staleHours ?? 36);
15036
15172
  if (reaped > 0) console.log(`[proactive] reaped ${reaped} stale executing action(s)`);
15173
+ const execNow = this.deps.persist.countByStatus("executing");
15174
+ if (execNow > 0 && execNow >= this.lastExecuting) this.backlogStreak++;
15175
+ else this.backlogStreak = 0;
15176
+ this.lastExecuting = execNow;
15177
+ if (this.backlogStreak >= BACKLOG_ALERT_RUNS) {
15178
+ this.backlogStreak = 0;
15179
+ try {
15180
+ this.deps.onUnclosedBacklog?.(execNow);
15181
+ } catch (err) {
15182
+ console.warn(`[proactive] backlog alert failed: ${err.message}`);
15183
+ }
15184
+ }
15037
15185
  let due = this.deps.persist.dueActions(now.toISOString());
15038
15186
  if (due.length === 0) return 0;
15039
15187
  const resolvedActions = [];
@@ -15047,13 +15195,58 @@ var init_sweep = __esm({
15047
15195
  });
15048
15196
  if (resolvedActions.length > 0) {
15049
15197
  for (const { id, intentStatus } of resolvedActions) {
15050
- this.deps.persist.updateActionStatus(id, "cancelled", { cancelReason: `intent ${intentStatus}\u2014\u2014R2 sweep \u518D\u9A8C\u8BC1` });
15198
+ this.deps.persist.updateActionStatus(id, "cancelled", {
15199
+ completedAt: now.toISOString(),
15200
+ cancelReason: `intent ${intentStatus}\u2014\u2014R2 sweep \u518D\u9A8C\u8BC1`
15201
+ });
15051
15202
  }
15052
15203
  console.log(`[proactive] R2: ${resolvedActions.length} due action(s) cancelled (intent resolved)`);
15053
15204
  }
15205
+ const gatedItems = [];
15206
+ const gateOff = (a, reason) => {
15207
+ gatedItems.push({ id: a.id, title: a.title, reason });
15208
+ return false;
15209
+ };
15210
+ const coveredTitles = this.deps.coveredTitles ?? [];
15211
+ const coveredEvents = this.deps.coveredEvents ?? [];
15212
+ if (coveredTitles.length > 0 || coveredEvents.length > 0) {
15213
+ due = due.filter((a) => {
15214
+ if (coveredTitles.some((c) => isSameCommitment(a.title, c))) {
15215
+ return gateOff(a, "calendar/goal \u91CC\u5DF2\u7ECF\u6709\u540C\u4E00\u4EF6\u4E8B\u2014\u2014\u4E0D\u91CD\u590D\u63D0\u9192");
15216
+ }
15217
+ const parts = toCalendarWallParts(a.scheduledTime);
15218
+ const sameMoment = coveredEvents.some((ev) => ev.date === parts.dateStr && ev.time === parts.timeExact);
15219
+ if (sameMoment && coveredEvents.some((ev) => ev.date === parts.dateStr && ev.time === parts.timeExact && sharesTerm(a.title, ev.title))) {
15220
+ 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");
15221
+ }
15222
+ return true;
15223
+ });
15224
+ }
15225
+ const minLeadDays = this.deps.minLeadDays ?? 0;
15226
+ if (minLeadDays > 0) {
15227
+ due = due.filter((a) => {
15228
+ const leadMs = Date.parse(a.scheduledTime) - Date.parse(a.createdAt);
15229
+ if (leadMs >= minLeadDays * 864e5) return true;
15230
+ 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`);
15231
+ });
15232
+ }
15233
+ for (const g of gatedItems) {
15234
+ this.deps.persist.updateActionStatus(g.id, "cancelled", {
15235
+ completedAt: now.toISOString(),
15236
+ cancelReason: g.reason
15237
+ });
15238
+ }
15239
+ if (gatedItems.length > 0) {
15240
+ console.log(`[proactive] gated ${gatedItems.length} action(s): ${gatedItems.map((g) => g.reason.slice(0, 12)).join(" / ")}`);
15241
+ try {
15242
+ this.deps.onGated?.(gatedItems);
15243
+ } catch (err) {
15244
+ console.warn(`[proactive] onGated failed: ${err.message}`);
15245
+ }
15246
+ }
15054
15247
  if (due.length === 0) return 0;
15055
15248
  for (const act of due) {
15056
- this.deps.persist.markActionReminded(act.id);
15249
+ this.deps.persist.markActionReminded(act.id, now.toISOString());
15057
15250
  }
15058
15251
  const payload = due.map((a) => actionPayload(a, now, this.deps.persist.getIntentRecentEvents(a.intentId)));
15059
15252
  const blocks = [
@@ -15064,7 +15257,12 @@ ${JSON.stringify(payload, null, 2)}
15064
15257
  \`\`\`` },
15065
15258
  { type: "text", text: this.loadPrompt() }
15066
15259
  ];
15067
- this.deps.submit(blocks);
15260
+ const delivered = this.deps.submit(blocks);
15261
+ if (!delivered) {
15262
+ const back = this.deps.persist.rollbackReminded(due.map((a) => a.id));
15263
+ console.warn(`[proactive] injection dropped (no main session / guard busy) \u2014 rolled back ${back} action(s) to queued`);
15264
+ return 0;
15265
+ }
15068
15266
  console.log(`[proactive] injected ${payload.length} due action(s) to main session`);
15069
15267
  return payload.length;
15070
15268
  }
@@ -15145,7 +15343,7 @@ function registerProactiveTools() {
15145
15343
  completedAt: nowUtc(),
15146
15344
  cancelReason: String(args.reason || "\u672A\u6CE8\u660E")
15147
15345
  });
15148
- return { content: `\u2713 action ${a.id} \u5DF2\u53D6\u6D88\uFF08${String(args.reason || "\u672A\u6CE8\u660E")}\uFF09` };
15346
+ return { content: `\u2713 action ${a.id} \u6807\u8BB0\u4E3A cancelled\uFF08${String(args.reason || "\u672A\u6CE8\u660E")}\uFF09` };
15149
15347
  });
15150
15348
  return out ?? { content: "proactive \u672A\u542F\u7528", isError: true };
15151
15349
  }
@@ -15266,6 +15464,78 @@ var init_completion_evidence = __esm({
15266
15464
  }
15267
15465
  });
15268
15466
 
15467
+ // src/memory/proactive/verdict-parse.ts
15468
+ function parseVerdicts(text) {
15469
+ if (!text) return [];
15470
+ const hits = [];
15471
+ VERDICT_RE.lastIndex = 0;
15472
+ let m;
15473
+ while ((m = VERDICT_RE.exec(text)) !== null) {
15474
+ hits.push({
15475
+ id: m[1],
15476
+ status: m[2],
15477
+ start: m.index,
15478
+ end: m.index + m[0].length
15479
+ });
15480
+ }
15481
+ const byId = /* @__PURE__ */ new Map();
15482
+ for (let i = 0; i < hits.length; i++) {
15483
+ const h = hits[i];
15484
+ const segEnd = Math.min(hits[i + 1]?.start ?? text.length, h.end + SEGMENT_MAX);
15485
+ const seg = text.slice(h.end, segEnd);
15486
+ let reason = "";
15487
+ const inline = seg.match(INLINE_REASON_RE);
15488
+ if (inline) reason = inline[1] ?? inline[2] ?? inline[3] ?? "";
15489
+ if (!reason) {
15490
+ const r = seg.match(LABELED_REASON_RE);
15491
+ if (r) reason = (r[1] ?? r[2] ?? r[3] ?? r[4] ?? "").trim();
15492
+ }
15493
+ byId.set(h.id, { id: h.id, status: h.status, reason: reason.trim().slice(0, SEGMENT_MAX) });
15494
+ }
15495
+ return [...byId.values()];
15496
+ }
15497
+ function applyProseVerdicts(persist, text) {
15498
+ const verdicts = parseVerdicts(text);
15499
+ const skipped = [];
15500
+ let applied = 0;
15501
+ for (const v of verdicts) {
15502
+ const a = persist.getAction(v.id);
15503
+ if (!a) {
15504
+ skipped.push(`${v.id}(\u4E0D\u5B58\u5728)`);
15505
+ continue;
15506
+ }
15507
+ if (a.status !== "executing") {
15508
+ skipped.push(`${v.id}(${a.status})`);
15509
+ continue;
15510
+ }
15511
+ const at = nowUtc();
15512
+ if (v.status === "completed") {
15513
+ persist.updateActionStatus(v.id, "completed", {
15514
+ completedAt: at,
15515
+ ...v.reason ? { note: v.reason } : {}
15516
+ });
15517
+ } else {
15518
+ persist.updateActionStatus(v.id, v.status, {
15519
+ completedAt: at,
15520
+ cancelReason: v.reason || "\u672A\u6CE8\u660E\uFF08\u6B63\u6587\u5224\u5B9A\uFF09"
15521
+ });
15522
+ }
15523
+ applied++;
15524
+ }
15525
+ return { applied, skipped };
15526
+ }
15527
+ var VERDICT_RE, LABELED_REASON_RE, INLINE_REASON_RE, SEGMENT_MAX;
15528
+ var init_verdict_parse = __esm({
15529
+ "src/memory/proactive/verdict-parse.ts"() {
15530
+ "use strict";
15531
+ init_clock();
15532
+ VERDICT_RE = /(act-[0-9a-f]{12})\s*[::]\s*(completed|cancelled|failed)\b/g;
15533
+ LABELED_REASON_RE = /(?:cancel_reason|note|reason)\s*[::]\s*(?:"([^"]*)"|'([^']*)'|“([^”]*)”|([^\n]*))/i;
15534
+ INLINE_REASON_RE = /^\s*[,,]\s*(?:"([^"]*)"|'([^']*)'|“([^”]*)”)/;
15535
+ SEGMENT_MAX = 500;
15536
+ }
15537
+ });
15538
+
15269
15539
  // src/memory/proactive/plugin.ts
15270
15540
  var plugin_exports = {};
15271
15541
  __export(plugin_exports, {
@@ -15273,7 +15543,7 @@ __export(plugin_exports, {
15273
15543
  feedMem7Cell: () => feedMem7Cell
15274
15544
  });
15275
15545
  import { join as join31 } from "node:path";
15276
- import { existsSync as existsSync17, readFileSync as readFileSync23 } from "node:fs";
15546
+ import { existsSync as existsSync17, readFileSync as readFileSync23, appendFileSync as appendFileSync3 } from "node:fs";
15277
15547
  function isFiller(text) {
15278
15548
  const t = text.trim();
15279
15549
  if (t.length >= 5) return false;
@@ -15346,7 +15616,10 @@ var init_plugin = __esm({
15346
15616
  init_proactive_tools();
15347
15617
  init_prompts();
15348
15618
  init_completion_evidence();
15619
+ init_verdict_parse();
15620
+ init_task_manager();
15349
15621
  init_opportunity();
15622
+ init_clock();
15350
15623
  init_embedding();
15351
15624
  init_filters();
15352
15625
  init_store2();
@@ -15508,22 +15781,76 @@ ${String(input.tool_output ?? "")}`
15508
15781
  return { continue: true };
15509
15782
  }
15510
15783
  });
15784
+ registerCallbackHook("Stop", {
15785
+ type: "callback",
15786
+ id: "proactive-verdict-parse",
15787
+ callback: async (input) => {
15788
+ if (input?.channel !== "proactive") return { continue: true };
15789
+ try {
15790
+ const r = applyProseVerdicts(this.persist, String(input?.last_assistant_message ?? ""));
15791
+ if (r.applied > 0) {
15792
+ console.log(`[proactive] \u6B63\u6587\u5224\u5B9A\u56DE\u6536 ${r.applied} \u6761\uFF08\u8DF3\u8FC7 ${r.skipped.length}\uFF09`);
15793
+ }
15794
+ } catch (err) {
15795
+ console.warn(`[proactive] verdict parse failed: ${err.message}`);
15796
+ }
15797
+ return { continue: true };
15798
+ }
15799
+ });
15511
15800
  this.hookInstalled = true;
15512
15801
  activeInstance = this;
15513
15802
  console.log(`[proactive] mem7-fed ingest ready (model=${this.config.modelRef}, graphInput=${liveConfig.get("mem7.graphInput") ?? "memcell"})`);
15514
15803
  registerProactiveTools();
15515
15804
  setProactiveWorkspace(workspace);
15805
+ const pluginCalDb = this.calDb;
15516
15806
  this.sweep = new Sweep({
15517
15807
  persist: this.persist,
15518
15808
  promptFile: join31(workspace, "prompts", "proactive.md"),
15809
+ // 三个都走 getter **每轮现读** —— 热加载规则(config 改了立刻生效)+
15810
+ // calendar 会变,都不能在 start() 时快照。
15811
+ get minLeadDays() {
15812
+ return parseProactiveConfig(liveConfig.get("proactive")).leadGateDays;
15813
+ },
15814
+ get staleHours() {
15815
+ return parseProactiveConfig(liveConfig.get("proactive")).staleHours;
15816
+ },
15817
+ get coveredTitles() {
15818
+ return buildPlanningContext(pluginCalDb, workspace).coveredTitles;
15819
+ },
15820
+ // 带日期时刻的版本 —— 供给"时间锚点判重"(张小媒 0925 review 补的维度)
15821
+ get coveredEvents() {
15822
+ return pluginCalDb ? getCalendarCoverage(pluginCalDb).map((c) => ({ title: c.title, date: c.date, time: c.time })) : [];
15823
+ },
15824
+ // 闸掉的必须留痕:她要能"扫一眼标题,真有冤案我喊"。
15825
+ onGated: (items) => {
15826
+ const gatedPath = join31(workspace, ".proactive", "gated.jsonl");
15827
+ try {
15828
+ appendFileSync3(
15829
+ gatedPath,
15830
+ items.map((i) => JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), ...i })).join("\n") + "\n",
15831
+ "utf8"
15832
+ );
15833
+ } catch (err) {
15834
+ console.warn(`[proactive] gated.jsonl \u5199\u5165\u5931\u8D25: ${err.message}`);
15835
+ }
15836
+ enqueueNotification(
15837
+ buildNudgeNotification(
15838
+ "stuck",
15839
+ `proactive \u4ECA\u5929\u95F8\u6389\u4E86 ${items.length} \u6761\uFF08\u6CA1\u8FDB\u6CE8\u5165\u3001\u5DF2\u6807 cancelled\uFF09\uFF1A` + items.map((i) => `
15840
+ \xB7 ${i.title.slice(0, 28)} \u2190 ${i.reason.slice(0, 22)}`).join("") + `
15841
+ \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`
15842
+ ),
15843
+ void 0,
15844
+ `proactive-gated-${todayKey()}`
15845
+ );
15846
+ },
15519
15847
  submit: (blocks) => {
15520
15848
  const mainSessionId = ctx.sessions?.getSessionId?.("scope:main");
15521
15849
  if (!mainSessionId) {
15522
- console.warn("[proactive] no main session \u2014 dropping sweep injection");
15523
- return;
15850
+ console.warn("[proactive] no main session \u2014 sweep injection dropped");
15851
+ return false;
15524
15852
  }
15525
- ;
15526
- ctx.dispatcher.submitMessage({
15853
+ const ok = ctx.dispatcher.submitMessage({
15527
15854
  text: blocks,
15528
15855
  sessionId: mainSessionId,
15529
15856
  channelName: "proactive",
@@ -15532,6 +15859,21 @@ ${String(input.tool_output ?? "")}`
15532
15859
  skipRecall: true,
15533
15860
  deps: ctx.deps
15534
15861
  });
15862
+ if (ok === false) console.warn("[proactive] dispatcher rejected sweep injection (guard busy) \u2014 rolling back");
15863
+ return ok !== false;
15864
+ },
15865
+ // 对账告警:注入的判定连续多轮不闭环 → 直接告诉她。她本人明确要过这条
15866
+ //("能发现只增不减的人是我,但前提是我收到这个信号")——躺日志里等于没有。
15867
+ onUnclosedBacklog: (count) => {
15868
+ enqueueNotification(
15869
+ buildNudgeNotification(
15870
+ "stuck",
15871
+ `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`
15872
+ ),
15873
+ void 0,
15874
+ "proactive-unclosed-backlog"
15875
+ );
15876
+ console.warn(`[proactive] \u5BF9\u8D26\u544A\u8B66\u5DF2\u6CE8\u5165\uFF1Aexecuting=${count} \u8FDE\u7EED ${BACKLOG_ALERT_RUNS} \u8F6E\u53EA\u589E\u4E0D\u51CF`);
15535
15877
  }
15536
15878
  });
15537
15879
  this.sweep.start();
@@ -15557,8 +15899,12 @@ ${String(input.tool_output ?? "")}`
15557
15899
  const nowDate = /* @__PURE__ */ new Date();
15558
15900
  const stateDir = String(liveConfig.get("stateDir") ?? "");
15559
15901
  if (stateDir) {
15560
- const mem7Entries = loadForesightEntries(join31(stateDir, "mem7"), nowDate);
15561
- if (mem7Entries.length > 0) console.log(`[proactive] mem7 foresight: ${mem7Entries.length} open-window predictions`);
15902
+ const all = loadForesightEntries(join31(stateDir, "mem7"), nowDate);
15903
+ const events = all.filter((e) => e.kind !== "profile");
15904
+ const mem7Entries = capForesightEntries(events, todayKey(nowDate));
15905
+ if (all.length > 0) {
15906
+ 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`);
15907
+ }
15562
15908
  for (const e of mem7Entries) {
15563
15909
  const key = e.text.replace(/\s+/g, "");
15564
15910
  if (seen.has(key)) continue;
@@ -15566,7 +15912,6 @@ ${String(input.tool_output ?? "")}`
15566
15912
  foresightLines.push(`[mem7] [\u7A97\u53E3 ${e.start}~${e.end}] ${e.text}${e.evidence ? `\uFF08\u4F9D\u636E\uFF1A${e.evidence}\uFF09` : ""}`);
15567
15913
  }
15568
15914
  }
15569
- if (foresightLines.length > 0) console.log(`[proactive] mem7 foresight total: ${foresightLines.length}`);
15570
15915
  } catch (err) {
15571
15916
  console.warn(`[proactive] foresight load failed: ${err.message}`);
15572
15917
  }
@@ -15590,6 +15935,7 @@ ${String(input.tool_output ?? "")}`
15590
15935
  this.hookInstalled = false;
15591
15936
  if (activeInstance === this) activeInstance = null;
15592
15937
  unregisterCallbackHook("PostToolUse", "proactive-completion-evidence");
15938
+ unregisterCallbackHook("Stop", "proactive-verdict-parse");
15593
15939
  await this.waitForIdle();
15594
15940
  this.sweep?.stop();
15595
15941
  this.sweep = null;
@@ -28867,7 +29213,7 @@ var NEGATIVE_PATTERNS = [
28867
29213
  var COLD_PATTERN = /^[嗯哦额喔]+$/;
28868
29214
  var INJECTED_MARKERS = ["\u3010\u5B9A\u65F6\u5FC3\u8DF3\u3011", "[\u5185\u5FC3\u5BF9\u8BDD\u6D4B\u8BD5]", "[inner-voice]"];
28869
29215
  function initialState() {
28870
- return { version: 1, mood: NEUTRAL, trend: "stable", updatedAt: nowIsoBj(), events: [] };
29216
+ return { version: 1, mood: NEUTRAL, trend: "stable", updatedAt: nowLocalIso(), events: [] };
28871
29217
  }
28872
29218
  async function updateEmotionalState(workspace, sessions) {
28873
29219
  const stateFile = path29.join(workspace, "inner-voice", "emotional-state.json");
@@ -28892,7 +29238,7 @@ async function updateEmotionalState(workspace, sessions) {
28892
29238
  refreshHoursAgo(state2.events);
28893
29239
  const recentV = state2.events.slice(-5).filter((e) => e.hoursAgo < 4).reduce((s, e) => s + e.valence, 0);
28894
29240
  state2.trend = recentV > 0.2 ? "rising" : recentV < -0.2 ? "falling" : "stable";
28895
- state2.updatedAt = nowIsoBj();
29241
+ state2.updatedAt = nowLocalIso();
28896
29242
  saveJson(stateFile, state2);
28897
29243
  const recent = state2.events.filter((e) => e.hoursAgo < 4).slice(-3);
28898
29244
  const summary = recent.length ? recent.map((e) => `${e.summary}(${e.valence >= 0 ? "+" : ""}${e.valence.toFixed(1)})`).join(" | ") : "\u65E0\u8FD1\u671F\u60C5\u611F\u4E8B\u4EF6";
@@ -29022,7 +29368,7 @@ function refreshHoursAgo(events) {
29022
29368
  function appendMoodLog(workspace, state2, summary) {
29023
29369
  try {
29024
29370
  const logPath = path29.join(workspace, "mood-history.log");
29025
- const ts = formatBj(/* @__PURE__ */ new Date(), false);
29371
+ const ts = formatLocal(/* @__PURE__ */ new Date(), false);
29026
29372
  fs29.appendFileSync(logPath, `${ts} mood=${state2.mood.toFixed(2)} trend=${state2.trend} ${summary}
29027
29373
  `);
29028
29374
  } catch (err) {
@@ -29073,10 +29419,10 @@ function clamp(x, lo, hi) {
29073
29419
  function clamp01(x) {
29074
29420
  return clamp(x, 0, 1);
29075
29421
  }
29076
- function nowIsoBj() {
29422
+ function nowLocalIso() {
29077
29423
  return `${toWall().replace(" ", "T")}${localOffsetSuffix()}`;
29078
29424
  }
29079
- function formatBj(d, withSec) {
29425
+ function formatLocal(d, withSec) {
29080
29426
  const p = (n) => String(n).padStart(2, "0");
29081
29427
  const t = wallParts(d);
29082
29428
  return `${t.y}-${p(t.mo)}-${p(t.d)} ${p(t.hh)}:${p(t.mm)}${withSec ? ":" + p(t.ss) : ""}`;
@@ -33265,6 +33611,9 @@ function buildReportPayload(opts) {
33265
33611
  msgsToday: opts.counters.msgsToday,
33266
33612
  msgsTotal: opts.counters.msgsTotal
33267
33613
  };
33614
+ if (opts.counters.lastMsgAt > 0 || opts.counters.msgsTotal === 0) {
33615
+ p.lastMsgAt = opts.counters.lastMsgAt;
33616
+ }
33268
33617
  if (opts.identity.contactEmail) p.contactEmail = opts.identity.contactEmail;
33269
33618
  return p;
33270
33619
  }
@@ -33276,6 +33625,7 @@ var nowFn = () => /* @__PURE__ */ new Date();
33276
33625
  var today = "";
33277
33626
  var msgsToday = 0;
33278
33627
  var msgsTotal = 0;
33628
+ var lastMsgAt = 0;
33279
33629
  function rolloverIfNeeded(now) {
33280
33630
  const d = todayKey(now);
33281
33631
  if (d !== today) {
@@ -33289,15 +33639,17 @@ function noteUserMessage(source) {
33289
33639
  rolloverIfNeeded(now);
33290
33640
  msgsToday++;
33291
33641
  msgsTotal++;
33642
+ lastMsgAt = Math.floor(now.getTime() / 1e3);
33292
33643
  }
33293
33644
  function snapshotCounters() {
33294
33645
  rolloverIfNeeded(nowFn());
33295
- return { day: today, msgsToday, msgsTotal };
33646
+ return { day: today, msgsToday, msgsTotal, lastMsgAt };
33296
33647
  }
33297
33648
  function restoreCounters(s) {
33298
33649
  today = todayKey(nowFn());
33299
33650
  msgsToday = s.day === today && typeof s.msgsToday === "number" && s.msgsToday > 0 ? s.msgsToday : 0;
33300
33651
  msgsTotal = typeof s.msgsTotal === "number" && s.msgsTotal > 0 ? s.msgsTotal : 0;
33652
+ lastMsgAt = typeof s.lastMsgAt === "number" && Number.isFinite(s.lastMsgAt) && s.lastMsgAt > 0 ? s.lastMsgAt : 0;
33301
33653
  }
33302
33654
 
33303
33655
  // src/optional/reporting/reporter.ts